- 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
This commit is contained in:
Elwador 2025-09-06 17:39:20 +02:00
parent 5cda547e22
commit 15c62193ca
25 changed files with 952 additions and 311 deletions

View file

@ -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");

View file

@ -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<string, CookieCollection> 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<CrToken>(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<string, string>{
{ "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<CrToken>(content, crunInstance.SettingsJsonSerializerSettings);
Token = Helpers.Deserialize<CrToken>(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<string, string>{
{ "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<CrProfile>(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<Subscription>(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<string, string>{
{ "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<string, string>{
{ "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<string, string>{
{ "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<string, string>{
{ "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);

View file

@ -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<CrBrowseEpisodeBase?> 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);

View file

@ -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);

View file

@ -106,7 +106,7 @@ public class CrMusic{
public async Task<CrArtist> 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<CrunchyMusicVideoList> 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);

View file

@ -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<CrunchySeriesList?> 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<CrSeriesSearch?> 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<CrSeriesBase?> 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<CrSearchSeriesBase?> 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);

View file

@ -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<CrDownloadOptions> _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<string> 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<CrToken>(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<DownloadResponse> 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<DownloadedMedia>(),
@ -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<string, string> 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<string, string> authDataDict = new Dictionary<string, string>
{ { "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<string, string> authDataDict = new Dictionary<string, string>
{ { "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<string, string> authDataDict = new Dictionary<string, string>
{ { "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<string, StreamDetails>()
};
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<PlaybackData> ProcessPlaybackResponseAsync(string responseContent, string mediaId, string mediaGuidId){
private async Task<PlaybackData> ProcessPlaybackResponseAsync(string responseContent, string mediaId, string mediaGuidId, CrAuth authEndpoint){
var temppbData = new PlaybackData{
Total = 0,
Data = new Dictionary<string, StreamDetails>()
@ -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<string> 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();

View file

@ -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<ComboBoxItem> 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<string> dubLangs = new List<string>();
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<StringItemWithDisplayName> GetAvailableHWAccelOptions(){
try{
using (var process = new Process()){

View file

@ -110,7 +110,7 @@
<CheckBox IsChecked="{Binding SubsDownloadDuplicate}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Add ScaledBorderAndShadow ">
<controls:SettingsExpanderItem.Footer>
<StackPanel Orientation="Horizontal">
@ -239,16 +239,73 @@
</ComboBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Stream Endpoint Secondary" IsEnabled="False" IsVisible="False">
<controls:SettingsExpanderItem Content="Stream Endpoint Secondary" Description="Changes to these settings require you to log in again.">
<controls:SettingsExpanderItem.Footer>
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
ItemsSource="{Binding StreamEndpointsSecondary}"
SelectedItem="{Binding SelectedStreamEndpointSecondary}">
</ComboBox>
<StackPanel>
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
ItemsSource="{Binding StreamEndpointsSecondary}"
SelectedItem="{Binding SelectedStreamEndpointSecondary}">
</ComboBox>
<StackPanel Margin="0,5">
<TextBlock Text="Authorization" />
<TextBox Name="AuthorizationTextBox" HorizontalAlignment="Left" MinWidth="250"
Text="{Binding EndpointAuthorization}" />
</StackPanel>
<StackPanel Margin="0,5">
<TextBlock Text="User Agent" />
<TextBox Name="UserAgentTextBox" HorizontalAlignment="Left" MinWidth="250"
Text="{Binding EndpointUserAgent}" />
</StackPanel>
<StackPanel Margin="0,5">
<TextBlock Text="Device Type" />
<TextBox Name="DeviceTypeTextBox" HorizontalAlignment="Left" MinWidth="250"
Text="{Binding EndpointDeviceType}" />
</StackPanel>
<StackPanel Margin="0,5">
<TextBlock Text="Device Name" />
<TextBox Name="DeviceNameTextBox" HorizontalAlignment="Left" MinWidth="250"
Text="{Binding EndpointDeviceName}" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<Button Margin="5 10" VerticalAlignment="Center"
Command="{Binding ResetEndpointSettings}">
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
<TextBlock Text="Reset" HorizontalAlignment="Center" TextWrapping="Wrap" FontSize="12" />
</StackPanel>
</Button>
<Button Margin="5 10" VerticalAlignment="Center"
Command="{Binding Login}">
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
<TextBlock Text="Login" HorizontalAlignment="Center" TextWrapping="Wrap" FontSize="12" />
</StackPanel>
</Button>
<controls:SymbolIcon Symbol="CloudOff"
IsVisible="{Binding EndpointNotSignedWarning}"
Foreground="OrangeRed"
FontSize="30"
VerticalAlignment="Center">
<ToolTip.Tip>
<TextBlock Text="Signin for this endpoint failed. Check logs for more details."
TextWrapping="Wrap"
MaxWidth="250" />
</ToolTip.Tip>
</controls:SymbolIcon>
</StackPanel>
</StackPanel>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Video">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding DownloadVideo}"> </CheckBox>
@ -325,7 +382,7 @@
HorizontalAlignment="Stretch" />
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="FileName Whitespace Substitute"
Description="Character used to replace whitespace in file name variables like ${seriesTitle}">
<controls:SettingsExpanderItem.Footer>
@ -364,7 +421,7 @@
<CheckBox IsChecked="{Binding MuxToMp4}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="MP3" Description="Outputs an MP3 instead of an MKV/MP4 if only audio streams were downloaded">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding MuxToMp3}"> </CheckBox>
@ -411,7 +468,7 @@
<CheckBox IsChecked="{Binding MuxFonts}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="Include episode thumbnail" Description="Embeds the episode thumbnail into the MKV as cover">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding MuxCover}"> </CheckBox>
@ -446,7 +503,7 @@
<StackPanel HorizontalAlignment="Right">
<CheckBox HorizontalAlignment="Right" IsChecked="{Binding SyncTimings}"> </CheckBox>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" IsVisible="{Binding SyncTimings}">
<TextBlock VerticalAlignment="Center" Text="Video Processing Method" Margin=" 0 0 5 0" ></TextBlock>
<TextBlock VerticalAlignment="Center" Text="Video Processing Method" Margin=" 0 0 5 0"></TextBlock>
<ComboBox HorizontalAlignment="Right"
ItemsSource="{Binding FFmpegHWAccel}"
SelectedItem="{Binding SelectedFFmpegHWAccel}">
@ -457,7 +514,7 @@
</ItemsControl.ItemTemplate>
</ComboBox>
</StackPanel>
</StackPanel>
</controls:SettingsExpanderItem.Footer>

View file

@ -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);

View file

@ -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);

View file

@ -33,16 +33,10 @@ public class HttpClientReq{
}
#endregion
private HttpClient client;
private Dictionary<string, CookieCollection> cookieStore;
private HttpClientHandler handler;
public HttpClientReq(){
cookieStore = new Dictionary<string, CookieCollection>();
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<string, CookieCollection>? 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<string, CookieCollection>? 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<string, CookieCollection>? 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=";

View file

@ -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; }

View file

@ -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{

View file

@ -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<string?> Url{ get; set; }
public List<UrlWithAuth> 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<string?> Url{ get; set; }
public List<UrlWithAuth> Url{ get; set; }
public required LanguageItem HardsubLang{ get; set; }
public bool IsHardsubbed{ get; set; }

View file

@ -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; }

View file

@ -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();
}
}

View file

@ -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){

View file

@ -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){

View file

@ -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<SeriesDetailsImage> _imagesListPosterTall = new();
[ObservableProperty]
private ObservableCollection<SeriesDetailsImage> _imagesListPosterWide = new();
[ObservableProperty]
private ObservableCollection<SeriesDetailsImage> _imagesListPromoImage = new();
[ObservableProperty]
private ObservableCollection<SeriesDetailsImage> _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<Image> images{ get; set; }
public Bitmap? imagePreview{ get; set; }
}

View file

@ -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;

View file

@ -10,14 +10,14 @@
xmlns:ui="clr-namespace:CRD.Utils.UI">
<UserControl.Resources>
<ui:UiValueConverter x:Key="UiValueConverter"/>
<ui:UiValueConverter x:Key="UiValueConverter" />
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
@ -28,7 +28,7 @@
<TextBlock Text="Automatically shut down the PC when the queue is empty" FontSize="16" HorizontalAlignment="Center" VerticalAlignment="Center" TextWrapping="Wrap"></TextBlock>
</ToolTip.Tip>
</ToggleSwitch>
<Button BorderThickness="0"
HorizontalAlignment="Right"
Margin="0 0 10 0 "
@ -42,20 +42,20 @@
<TextBlock Text="Retry failed" HorizontalAlignment="Center" VerticalAlignment="Center" TextWrapping="Wrap"></TextBlock>
</ToolTip.Tip>
</Button>
<Button BorderThickness="0"
HorizontalAlignment="Right"
Margin="0 0 10 0 "
VerticalAlignment="Center"
Command="{Binding ClearQueue}">
<Button BorderThickness="0"
HorizontalAlignment="Right"
Margin="0 0 10 0 "
VerticalAlignment="Center"
Command="{Binding ClearQueue}">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
<controls:SymbolIcon Symbol="Delete" FontSize="22" />
</StackPanel>
<ToolTip.Tip>
<TextBlock Text="Clear Queue" HorizontalAlignment="Center" VerticalAlignment="Center" TextWrapping="Wrap" ></TextBlock>
<TextBlock Text="Clear Queue" HorizontalAlignment="Center" VerticalAlignment="Center" TextWrapping="Wrap"></TextBlock>
</ToolTip.Tip>
</Button>
</StackPanel>
@ -69,16 +69,16 @@
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid>
<Image HorizontalAlignment="Center" Width="208" Height="117" Source="../Assets/coming_soon_ep.jpg" />
<Image Grid.Column="0" Width="208" Height="117" Source="{Binding ImageBitmap}"
Stretch="Fill" />
</Grid>
<!-- Text Content -->
<Grid Grid.Column="1" Margin="10" >
<Grid Grid.Column="1" Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <!-- Takes up most space for the title -->
<ColumnDefinition Width="Auto" />
@ -90,13 +90,16 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" MaxHeight="117" Text="{Binding Title}" FontWeight="Bold" FontSize="16"
<TextBlock Grid.Row="0" Grid.Column="0" IsVisible="{Binding !epMeta.HighlightAllAvailable}" MaxHeight="117" Text="{Binding Title}" FontWeight="Bold" FontSize="16"
TextWrapping="Wrap" VerticalAlignment="Top" />
<TextBlock Grid.Row="1" Grid.Column="0" MaxHeight="117" Text="{Binding InfoText}" Opacity="0.8"
<TextBlock Grid.Row="0" Grid.Column="0" Foreground="Orange" IsVisible="{Binding epMeta.HighlightAllAvailable}" MaxHeight="117" Text="{Binding Title}" FontWeight="Bold" FontSize="16"
TextWrapping="Wrap" VerticalAlignment="Top" />
<TextBlock Grid.Row="1" Grid.Column="0" MaxHeight="117" Text="{Binding InfoText}" Opacity="0.8"
TextWrapping="Wrap" VerticalAlignment="Center" />
<Button Grid.Row="0" Grid.Column="1" Margin="0 0 5 0" IsVisible="{Binding !Error}" Command="{Binding ToggleIsDownloading}" FontStyle="Italic"
HorizontalAlignment="Right" VerticalAlignment="Top">
<StackPanel Orientation="Horizontal">
@ -104,8 +107,8 @@
!Paused, Converter={StaticResource UiValueConverter}}" FontSize="18" />
</StackPanel>
</Button>
<Button Grid.Row="0" Grid.Column="1" Margin="0 0 5 0" IsVisible="{Binding Error}" Command="{Binding ToggleIsDownloading}" FontStyle="Italic"
<Button Grid.Row="0" Grid.Column="1" Margin="0 0 5 0" IsVisible="{Binding Error}" Command="{Binding ToggleIsDownloading}" FontStyle="Italic"
HorizontalAlignment="Right" VerticalAlignment="Top">
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Refresh" FontSize="18" />
@ -118,12 +121,12 @@
<controls:SymbolIcon Symbol="Delete" FontSize="18" />
</StackPanel>
</Button>
<ProgressBar Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3" VerticalAlignment="Bottom" Margin="0 0 0 10" Height="5" Value="{Binding Percent}"></ProgressBar>
<Grid Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="3" VerticalAlignment="Bottom" >
<ProgressBar Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3" VerticalAlignment="Bottom" Margin="0 0 0 10" Height="5" Value="{Binding Percent}"></ProgressBar>
<Grid Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="3" VerticalAlignment="Bottom">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
@ -132,15 +135,15 @@
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Bottom" Text="{Binding DoingWhat}"
Opacity="1" TextWrapping="NoWrap" />
<TextBlock Grid.Row="0" Grid.Column="1" VerticalAlignment="Bottom" Margin="0 0 10 0" Text="{Binding Time}"
Opacity="0.8" TextWrapping="Wrap" />
<TextBlock Grid.Row="0" Grid.Column="2" VerticalAlignment="Bottom" Text="{Binding DownloadSpeed}"
Opacity="0.8" TextWrapping="Wrap" />
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Bottom" Text="{Binding DoingWhat}"
Opacity="1" TextWrapping="NoWrap" />
<TextBlock Grid.Row="0" Grid.Column="1" VerticalAlignment="Bottom" Margin="0 0 10 0" Text="{Binding Time}"
Opacity="0.8" TextWrapping="Wrap" />
<TextBlock Grid.Row="0" Grid.Column="2" VerticalAlignment="Bottom" Text="{Binding DownloadSpeed}"
Opacity="0.8" TextWrapping="Wrap" />
</Grid>
</Grid>
</Grid>

View file

@ -42,9 +42,13 @@
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image Grid.Column="0" Margin="10" Source="{Binding SelectedSeries.ThumbnailImage}" Width="240"
Height="360">
</Image>
<Button Grid.Column="0" Margin="10" Command="{Binding OpenSeriesDetails}"
BorderThickness="0"
Background="Transparent"
Padding="0">
<Image Source="{Binding SelectedSeries.ThumbnailImage}"
Width="240" Height="360"/>
</Button>
<Grid Grid.Column="1">
@ -341,31 +345,36 @@
</StackPanel>
<StackPanel IsVisible="{Binding EditMode}">
<Button Width="30" Height="30" Margin="0 0 10 0"
<Button Height="30" Margin="0 0 5 0"
BorderThickness="0"
IsVisible="{Binding SonarrConnected}"
Command="{Binding MatchSonarrSeries_Button}">
<Grid>
<StackPanel Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
<controls:ImageIcon Source="../Assets/sonarr.png" Width="25" Height="25" />
<ToolTip.Tip>
<TextBlock Text="Match Sonarr Series" FontSize="15" />
</ToolTip.Tip>
</Grid>
<TextBlock Text="Match" VerticalAlignment="Center" />
</StackPanel>
<ToolTip.Tip>
<TextBlock Text="Match Sonarr Series" FontSize="15" />
</ToolTip.Tip>
</Button>
</StackPanel>
<StackPanel IsVisible="{Binding EditMode}">
<Button Width="30" Height="30" Margin="0 0 10 0"
<Button Height="30" Margin="0 0 10 0"
BorderThickness="0"
IsVisible="{Binding SonarrConnected}"
Command="{Binding RefreshSonarrEpisodeMatch}">
<Grid>
<StackPanel Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
<controls:ImageIcon Source="../Assets/sonarr.png" Width="25" Height="25" />
<ToolTip.Tip>
<TextBlock Text="Refresh all Sonarr Episode matches" FontSize="15" />
</ToolTip.Tip>
</Grid>
<TextBlock Text="Refresh" VerticalAlignment="Center" />
</StackPanel>
<ToolTip.Tip>
<TextBlock Text="Refresh all Sonarr Episode matches" FontSize="15" />
</ToolTip.Tip>
</Button>
</StackPanel>

View file

@ -0,0 +1,205 @@
<ui:CustomContentDialog xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:CRD.ViewModels.Utils"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:ui="clr-namespace:CRD.Utils.UI"
x:DataType="vm:ContentDialogSeriesDetailsViewModel"
x:Class="CRD.Views.Utils.ContentDialogSeriesDetailsView">
<!-- Local styles for headers & cards -->
<ui:CustomContentDialog.Styles>
<!-- Section header text -->
<Style Selector="TextBlock.sectionHeader">
<Setter Property="FontSize" Value="18" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Margin" Value="0,16,0,8" />
</Style>
<!-- Image card border -->
<Style Selector="Border.imageCard">
<Setter Property="CornerRadius" Value="8" />
<Setter Property="BorderBrush" Value="#22000000" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Margin" Value="8" />
</Style>
</ui:CustomContentDialog.Styles>
<Grid>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="12">
<!-- Poster Tall -->
<TextBlock
Classes="sectionHeader"
Text="{Binding ImagesListPosterTall.Count,
StringFormat='Poster Tall ({0})'}" />
<ItemsControl ItemsSource="{Binding ImagesListPosterTall}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<!-- Left align, add spacing between items -->
<WrapPanel Orientation="Horizontal"
HorizontalAlignment="Left" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Classes="imageCard">
<Grid Width="240" Height="360" x:Name="RootGrid">
<!-- Placeholder -->
<Image Source="/Assets/coming_soon_ep.jpg"
Stretch="Fill" />
<!-- Actual preview -->
<Image Source="{Binding imagePreview}"
Stretch="UniformToFill" />
<Border Background="#80000000"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsVisible="{Binding #RootGrid.IsPointerOver}">
<Grid>
<Button x:Name="DownloadToggleTall"
Background="Transparent"
BorderThickness="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Command="{Binding $parent[UserControl].((vm:ContentDialogSeriesDetailsViewModel)DataContext).ToggleButtonTallCommand}">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="8">
<controls:SymbolIcon Symbol="Download" FontSize="24" />
<TextBlock Text="Choose resolution" />
</StackPanel>
</Button>
<Popup PlacementTarget="{Binding #DownloadToggleTall}"
Placement="Center"
IsOpen="{Binding $parent[UserControl].((vm:ContentDialogSeriesDetailsViewModel)DataContext).IsResolutionPopupOpenTall, ElementName=DownloadToggleTall, Mode=TwoWay}"
IsLightDismissEnabled="True">
<Border Background="{DynamicResource SolidBackgroundFillColorBaseBrush}"
BorderBrush="{DynamicResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="6"
Padding="6">
<ListBox ItemsSource="{Binding images}"
MinWidth="180"
MaxHeight="240"
PointerWheelChanged="ListBox_PointerWheelChanged"
SelectionChanged="ImageSelectionChanged"
SelectedItem="{Binding $parent[UserControl].((vm:ContentDialogSeriesDetailsViewModel)DataContext).SelectedImage}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Spacing="8">
<controls:SymbolIcon Symbol="Image" FontSize="16" />
<TextBlock Text="{Binding Width}" />
<TextBlock Text="×" />
<TextBlock Text="{Binding Height}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
</Popup>
</Grid>
</Border>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Separator Margin="0,12" />
<!-- Poster Wide -->
<TextBlock
Classes="sectionHeader"
Text="{Binding ImagesListPosterWide.Count,
StringFormat='Poster Wide ({0})'}" />
<ItemsControl ItemsSource="{Binding ImagesListPosterWide}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal"
HorizontalAlignment="Left" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Classes="imageCard">
<Grid Width="640" Height="360" x:Name="RootGrid">
<Image Source="/Assets/coming_soon_ep.jpg" Stretch="Fill" />
<Image Source="{Binding imagePreview}" Stretch="UniformToFill" />
<Border Background="#80000000"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsVisible="{Binding #RootGrid.IsPointerOver}">
<Grid>
<Button x:Name="DownloadToggleWide"
Background="Transparent"
BorderThickness="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Command="{Binding $parent[UserControl].((vm:ContentDialogSeriesDetailsViewModel)DataContext).ToggleButtonWideCommand}">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="8">
<controls:SymbolIcon Symbol="Download" FontSize="24" />
<TextBlock Text="Choose resolution" />
</StackPanel>
</Button>
<Popup PlacementTarget="{Binding #DownloadToggleWide}"
Placement="Center"
IsOpen="{Binding $parent[UserControl].((vm:ContentDialogSeriesDetailsViewModel)DataContext).IsResolutionPopupOpenWide, ElementName=DownloadToggleWide, Mode=TwoWay}"
IsLightDismissEnabled="True">
<Border Background="{DynamicResource SolidBackgroundFillColorBaseBrush}"
BorderBrush="{DynamicResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="6"
Padding="6">
<ListBox ItemsSource="{Binding images}"
MinWidth="180"
MaxHeight="240"
PointerWheelChanged="ListBox_PointerWheelChanged"
SelectionChanged="ImageSelectionChanged"
SelectedItem="{Binding $parent[UserControl].((vm:ContentDialogSeriesDetailsViewModel)DataContext).SelectedImage}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Spacing="8">
<controls:SymbolIcon Symbol="Image" FontSize="16" />
<TextBlock Text="{Binding Width}" />
<TextBlock Text="×" />
<TextBlock Text="{Binding Height}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
</Popup>
</Grid>
</Border>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>
</Grid>
</ui:CustomContentDialog>

View file

@ -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<ScrollViewer>().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
}
}
}
}