mirror of
https://github.com/Crunchy-DL/Crunchy-Downloader.git
synced 2026-05-19 00:11:48 +00:00
- 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:
parent
5cda547e22
commit
15c62193ca
25 changed files with 952 additions and 311 deletions
|
|
@ -68,8 +68,8 @@ public class CalendarManager{
|
||||||
}
|
}
|
||||||
|
|
||||||
var request = calendarLanguage.ContainsKey(CrunchyrollManager.Instance.CrunOptions.SelectedCalendarLanguage ?? "en-us")
|
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[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, false, null);
|
: 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");
|
request.Headers.Accept.ParseAdd("text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8");
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CRD.Utils;
|
using CRD.Utils;
|
||||||
|
|
@ -13,13 +14,60 @@ using ReactiveUI;
|
||||||
|
|
||||||
namespace CRD.Downloader.Crunchyroll;
|
namespace CRD.Downloader.Crunchyroll;
|
||||||
|
|
||||||
public class CrAuth{
|
public class CrAuth(CrunchyrollManager crunInstance, CrAuthSettings authSettings){
|
||||||
private readonly CrunchyrollManager crunInstance = CrunchyrollManager.Instance;
|
|
||||||
|
public CrToken? Token;
|
||||||
|
public CrProfile Profile = new();
|
||||||
|
|
||||||
private readonly string authorization = ApiUrls.authBasicMob;
|
public CrAuthSettings AuthSettings = authSettings;
|
||||||
private readonly string userAgent = ApiUrls.MobileUserAgent;
|
|
||||||
private readonly string deviceType = "OnePlus CPH2449";
|
public Dictionary<string, CookieCollection> cookieStore = new();
|
||||||
private readonly string deviceName = "CPH2449";
|
|
||||||
|
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(){
|
public async Task AuthAnonymous(){
|
||||||
string uuid = Guid.NewGuid().ToString();
|
string uuid = Guid.NewGuid().ToString();
|
||||||
|
|
@ -28,18 +76,18 @@ public class CrAuth{
|
||||||
{ "grant_type", "client_id" },
|
{ "grant_type", "client_id" },
|
||||||
{ "scope", "offline_access" },
|
{ "scope", "offline_access" },
|
||||||
{ "device_id", uuid },
|
{ "device_id", uuid },
|
||||||
{ "device_type", deviceType },
|
{ "device_type", AuthSettings.Device_type },
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(deviceName)){
|
if (!string.IsNullOrEmpty(AuthSettings.Device_name)){
|
||||||
formData.Add("device_name", deviceName);
|
formData.Add("device_name", AuthSettings.Device_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
var requestContent = new FormUrlEncodedContent(formData);
|
var requestContent = new FormUrlEncodedContent(formData);
|
||||||
|
|
||||||
var crunchyAuthHeaders = new Dictionary<string, string>{
|
var crunchyAuthHeaders = new Dictionary<string, string>{
|
||||||
{ "Authorization", authorization },
|
{ "Authorization", AuthSettings.Authorization },
|
||||||
{ "User-Agent", userAgent }
|
{ "User-Agent", AuthSettings.UserAgent }
|
||||||
};
|
};
|
||||||
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Auth){
|
var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Auth){
|
||||||
|
|
@ -58,7 +106,7 @@ public class CrAuth{
|
||||||
Console.Error.WriteLine("Anonymous login failed");
|
Console.Error.WriteLine("Anonymous login failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
crunInstance.Profile = new CrProfile{
|
Profile = new CrProfile{
|
||||||
Username = "???",
|
Username = "???",
|
||||||
Avatar = "crbrand_avatars_logo_marks_mangagirl_taupe.png",
|
Avatar = "crbrand_avatars_logo_marks_mangagirl_taupe.png",
|
||||||
PreferredContentAudioLanguage = "ja-JP",
|
PreferredContentAudioLanguage = "ja-JP",
|
||||||
|
|
@ -67,13 +115,13 @@ public class CrAuth{
|
||||||
}
|
}
|
||||||
|
|
||||||
private void JsonTokenToFileAndVariable(string content, string deviceId){
|
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 }){
|
if (Token is{ expires_in: not null }){
|
||||||
crunInstance.Token.device_id = deviceId;
|
Token.device_id = deviceId;
|
||||||
crunInstance.Token.expires = DateTime.Now.AddSeconds((double)crunInstance.Token.expires_in);
|
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" },
|
{ "grant_type", "password" },
|
||||||
{ "scope", "offline_access" },
|
{ "scope", "offline_access" },
|
||||||
{ "device_id", uuid },
|
{ "device_id", uuid },
|
||||||
{ "device_type", deviceType },
|
{ "device_type", AuthSettings.Device_type },
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(deviceName)){
|
if (!string.IsNullOrEmpty(AuthSettings.Device_name)){
|
||||||
formData.Add("device_name", deviceName);
|
formData.Add("device_name", AuthSettings.Device_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
var requestContent = new FormUrlEncodedContent(formData);
|
var requestContent = new FormUrlEncodedContent(formData);
|
||||||
|
|
||||||
var crunchyAuthHeaders = new Dictionary<string, string>{
|
var crunchyAuthHeaders = new Dictionary<string, string>{
|
||||||
{ "Authorization", authorization },
|
{ "Authorization", AuthSettings.Authorization },
|
||||||
{ "User-Agent", userAgent }
|
{ "User-Agent", AuthSettings.UserAgent }
|
||||||
};
|
};
|
||||||
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Auth){
|
var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Auth){
|
||||||
|
|
@ -128,20 +176,20 @@ public class CrAuth{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (crunInstance.Token?.refresh_token != null){
|
if (Token?.refresh_token != null){
|
||||||
HttpClientReq.Instance.SetETPCookie(crunInstance.Token.refresh_token);
|
SetETPCookie(Token.refresh_token);
|
||||||
|
|
||||||
await GetProfile();
|
await GetProfile();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task GetProfile(){
|
public async Task GetProfile(){
|
||||||
if (crunInstance.Token?.access_token == null){
|
if (Token?.access_token == null){
|
||||||
Console.Error.WriteLine("Missing Access Token");
|
Console.Error.WriteLine("Missing Access Token");
|
||||||
return;
|
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);
|
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||||
|
|
||||||
|
|
@ -149,42 +197,42 @@ public class CrAuth{
|
||||||
var profileTemp = Helpers.Deserialize<CrProfile>(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
|
var profileTemp = Helpers.Deserialize<CrProfile>(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
|
||||||
|
|
||||||
if (profileTemp != null){
|
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);
|
var responseSubs = await HttpClientReq.Instance.SendHttpRequest(requestSubs);
|
||||||
|
|
||||||
if (responseSubs.IsOk){
|
if (responseSubs.IsOk){
|
||||||
var subsc = Helpers.Deserialize<Subscription>(responseSubs.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
|
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 }){
|
if (subsc is{ SubscriptionProducts:{ Count: 0 }, ThirdPartySubscriptionProducts.Count: > 0 }){
|
||||||
var thirdPartySub = subsc.ThirdPartySubscriptionProducts.First();
|
var thirdPartySub = subsc.ThirdPartySubscriptionProducts.First();
|
||||||
var expiration = thirdPartySub.InGrace ? thirdPartySub.InGraceExpirationDate : thirdPartySub.ExpirationDate;
|
var expiration = thirdPartySub.InGrace ? thirdPartySub.InGraceExpirationDate : thirdPartySub.ExpirationDate;
|
||||||
var remaining = expiration - DateTime.Now;
|
var remaining = expiration - DateTime.Now;
|
||||||
crunInstance.Profile.HasPremium = true;
|
Profile.HasPremium = true;
|
||||||
if (crunInstance.Profile.Subscription != null){
|
if (Profile.Subscription != null){
|
||||||
crunInstance.Profile.Subscription.IsActive = remaining > TimeSpan.Zero;
|
Profile.Subscription.IsActive = remaining > TimeSpan.Zero;
|
||||||
crunInstance.Profile.Subscription.NextRenewalDate = expiration;
|
Profile.Subscription.NextRenewalDate = expiration;
|
||||||
}
|
}
|
||||||
} else if (subsc is{ SubscriptionProducts:{ Count: 0 }, NonrecurringSubscriptionProducts.Count: > 0 }){
|
} else if (subsc is{ SubscriptionProducts:{ Count: 0 }, NonrecurringSubscriptionProducts.Count: > 0 }){
|
||||||
var nonRecurringSub = subsc.NonrecurringSubscriptionProducts.First();
|
var nonRecurringSub = subsc.NonrecurringSubscriptionProducts.First();
|
||||||
var remaining = nonRecurringSub.EndDate - DateTime.Now;
|
var remaining = nonRecurringSub.EndDate - DateTime.Now;
|
||||||
crunInstance.Profile.HasPremium = true;
|
Profile.HasPremium = true;
|
||||||
if (crunInstance.Profile.Subscription != null){
|
if (Profile.Subscription != null){
|
||||||
crunInstance.Profile.Subscription.IsActive = remaining > TimeSpan.Zero;
|
Profile.Subscription.IsActive = remaining > TimeSpan.Zero;
|
||||||
crunInstance.Profile.Subscription.NextRenewalDate = nonRecurringSub.EndDate;
|
Profile.Subscription.NextRenewalDate = nonRecurringSub.EndDate;
|
||||||
}
|
}
|
||||||
} else if (subsc is{ SubscriptionProducts:{ Count: 0 }, FunimationSubscriptions.Count: > 0 }){
|
} else if (subsc is{ SubscriptionProducts:{ Count: 0 }, FunimationSubscriptions.Count: > 0 }){
|
||||||
crunInstance.Profile.HasPremium = true;
|
Profile.HasPremium = true;
|
||||||
} else if (subsc is{ SubscriptionProducts.Count: > 0 }){
|
} else if (subsc is{ SubscriptionProducts.Count: > 0 }){
|
||||||
crunInstance.Profile.HasPremium = true;
|
Profile.HasPremium = true;
|
||||||
} else{
|
} else{
|
||||||
crunInstance.Profile.HasPremium = false;
|
Profile.HasPremium = false;
|
||||||
Console.Error.WriteLine($"No subscription available:\n {JsonConvert.SerializeObject(subsc, Formatting.Indented)} ");
|
Console.Error.WriteLine($"No subscription available:\n {JsonConvert.SerializeObject(subsc, Formatting.Indented)} ");
|
||||||
}
|
}
|
||||||
} else{
|
} else{
|
||||||
crunInstance.Profile.HasPremium = false;
|
Profile.HasPremium = false;
|
||||||
Console.Error.WriteLine("Failed to check premium subscription status");
|
Console.Error.WriteLine("Failed to check premium subscription status");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -192,31 +240,31 @@ public class CrAuth{
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task LoginWithToken(){
|
public async Task LoginWithToken(){
|
||||||
if (crunInstance.Token?.refresh_token == null){
|
if (Token?.refresh_token == null){
|
||||||
Console.Error.WriteLine("Missing Refresh Token");
|
Console.Error.WriteLine("Missing Refresh Token");
|
||||||
await AuthAnonymous();
|
await AuthAnonymous();
|
||||||
return;
|
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>{
|
var formData = new Dictionary<string, string>{
|
||||||
{ "refresh_token", crunInstance.Token.refresh_token },
|
{ "refresh_token", Token.refresh_token },
|
||||||
{ "scope", "offline_access" },
|
{ "scope", "offline_access" },
|
||||||
{ "device_id", uuid },
|
{ "device_id", uuid },
|
||||||
{ "grant_type", "refresh_token" },
|
{ "grant_type", "refresh_token" },
|
||||||
{ "device_type", deviceType },
|
{ "device_type", AuthSettings.Device_type },
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(deviceName)){
|
if (!string.IsNullOrEmpty(AuthSettings.Device_name)){
|
||||||
formData.Add("device_name", deviceName);
|
formData.Add("device_name", AuthSettings.Device_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
var requestContent = new FormUrlEncodedContent(formData);
|
var requestContent = new FormUrlEncodedContent(formData);
|
||||||
|
|
||||||
var crunchyAuthHeaders = new Dictionary<string, string>{
|
var crunchyAuthHeaders = new Dictionary<string, string>{
|
||||||
{ "Authorization", authorization },
|
{ "Authorization", AuthSettings.Authorization },
|
||||||
{ "User-Agent", userAgent }
|
{ "User-Agent", AuthSettings.UserAgent }
|
||||||
};
|
};
|
||||||
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Auth){
|
var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Auth){
|
||||||
|
|
@ -227,7 +275,7 @@ public class CrAuth{
|
||||||
request.Headers.Add(header.Key, header.Value);
|
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);
|
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||||
|
|
||||||
|
|
@ -243,8 +291,8 @@ public class CrAuth{
|
||||||
if (response.IsOk){
|
if (response.IsOk){
|
||||||
JsonTokenToFileAndVariable(response.ResponseContent, uuid);
|
JsonTokenToFileAndVariable(response.ResponseContent, uuid);
|
||||||
|
|
||||||
if (crunInstance.Token?.refresh_token != null){
|
if (Token?.refresh_token != null){
|
||||||
HttpClientReq.Instance.SetETPCookie(crunInstance.Token.refresh_token);
|
SetETPCookie(Token.refresh_token);
|
||||||
|
|
||||||
await GetProfile();
|
await GetProfile();
|
||||||
}
|
}
|
||||||
|
|
@ -258,38 +306,38 @@ public class CrAuth{
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RefreshToken(bool needsToken){
|
public async Task RefreshToken(bool needsToken){
|
||||||
if (crunInstance.Token?.access_token == null && crunInstance.Token?.refresh_token == null ||
|
if (Token?.access_token == null && Token?.refresh_token == null ||
|
||||||
crunInstance.Token.access_token != null && crunInstance.Token.refresh_token == null){
|
Token.access_token != null && Token.refresh_token == null){
|
||||||
await AuthAnonymous();
|
await AuthAnonymous();
|
||||||
} else{
|
} else{
|
||||||
if (!(DateTime.Now > crunInstance.Token.expires) && needsToken){
|
if (!(DateTime.Now > Token.expires) && needsToken){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (crunInstance.Profile.Username == "???"){
|
if (Profile.Username == "???"){
|
||||||
return;
|
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>{
|
var formData = new Dictionary<string, string>{
|
||||||
{ "refresh_token", crunInstance.Token?.refresh_token ?? "" },
|
{ "refresh_token", Token?.refresh_token ?? "" },
|
||||||
{ "grant_type", "refresh_token" },
|
{ "grant_type", "refresh_token" },
|
||||||
{ "scope", "offline_access" },
|
{ "scope", "offline_access" },
|
||||||
{ "device_id", uuid },
|
{ "device_id", uuid },
|
||||||
{ "device_type", deviceType },
|
{ "device_type", AuthSettings.Device_type },
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(deviceName)){
|
if (!string.IsNullOrEmpty(AuthSettings.Device_name)){
|
||||||
formData.Add("device_name", deviceName);
|
formData.Add("device_name", AuthSettings.Device_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
var requestContent = new FormUrlEncodedContent(formData);
|
var requestContent = new FormUrlEncodedContent(formData);
|
||||||
|
|
||||||
var crunchyAuthHeaders = new Dictionary<string, string>{
|
var crunchyAuthHeaders = new Dictionary<string, string>{
|
||||||
{ "Authorization", authorization },
|
{ "Authorization", AuthSettings.Authorization },
|
||||||
{ "User-Agent", userAgent }
|
{ "User-Agent", AuthSettings.UserAgent }
|
||||||
};
|
};
|
||||||
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Auth){
|
var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Auth){
|
||||||
|
|
@ -300,7 +348,7 @@ public class CrAuth{
|
||||||
request.Headers.Add(header.Key, header.Value);
|
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);
|
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||||
|
|
||||||
|
|
@ -51,7 +51,7 @@ public class CrEpisode(){
|
||||||
//guid for episode id
|
//guid for episode id
|
||||||
foreach (var episodeVersionse in list){
|
foreach (var episodeVersionse in list){
|
||||||
foreach (var version in episodeVersionse){
|
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);
|
var checkResponse = await HttpClientReq.Instance.SendHttpRequest(checkRequest, true);
|
||||||
if (!checkResponse.IsOk){
|
if (!checkResponse.IsOk){
|
||||||
epsidoe.Data.First().Versions?.Remove(version);
|
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){
|
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();
|
CrBrowseEpisodeBase? complete = new CrBrowseEpisodeBase();
|
||||||
complete.Data =[];
|
complete.Data =[];
|
||||||
|
|
||||||
|
|
@ -257,7 +257,7 @@ public class CrEpisode(){
|
||||||
query["sort_by"] = "newly_added";
|
query["sort_by"] = "newly_added";
|
||||||
query["type"] = "episode";
|
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);
|
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||||
|
|
||||||
|
|
@ -290,7 +290,7 @@ public class CrEpisode(){
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task MarkAsWatched(string episodeId){
|
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);
|
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@ public class CrMusic{
|
||||||
|
|
||||||
public async Task<CrArtist> ParseArtistByIdAsync(string id, string crLocale, bool forcedLang = false){
|
public async Task<CrArtist> ParseArtistByIdAsync(string id, string crLocale, bool forcedLang = false){
|
||||||
var query = CreateQueryParameters(crLocale, forcedLang);
|
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);
|
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){
|
private async Task<CrunchyMusicVideoList> FetchMediaListAsync(string url, string crLocale, bool forcedLang){
|
||||||
var query = CreateQueryParameters(crLocale, 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);
|
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ public class CrSeries{
|
||||||
for (int index = 0; index < episode.Items.Count; index++){
|
for (int index = 0; index < episode.Items.Count; index++){
|
||||||
var item = episode.Items[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));
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -120,7 +120,7 @@ public class CrSeries{
|
||||||
|
|
||||||
|
|
||||||
public async Task<CrunchySeriesList?> ListSeriesId(string id, string crLocale, CrunchyMultiDownload? data, bool forcedLocale = false){
|
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;
|
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);
|
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);
|
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){
|
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);
|
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
|
||||||
|
|
||||||
query["preferred_audio_language"] = "ja-JP";
|
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);
|
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){
|
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);
|
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
|
||||||
|
|
||||||
query["preferred_audio_language"] = "ja-JP";
|
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);
|
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){
|
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);
|
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(crLocale)){
|
if (!string.IsNullOrEmpty(crLocale)){
|
||||||
|
|
@ -423,7 +423,7 @@ public class CrSeries{
|
||||||
query["n"] = "6";
|
query["n"] = "6";
|
||||||
query["type"] = "series";
|
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);
|
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||||
|
|
||||||
|
|
@ -468,7 +468,7 @@ public class CrSeries{
|
||||||
query["n"] = "50";
|
query["n"] = "50";
|
||||||
query["sort_by"] = "alphabetical";
|
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);
|
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||||
|
|
||||||
|
|
@ -503,7 +503,7 @@ public class CrSeries{
|
||||||
query["seasonal_tag"] = season.ToLower() + "-" + year;
|
query["seasonal_tag"] = season.ToLower() + "-" + year;
|
||||||
query["n"] = "100";
|
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);
|
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,9 +33,6 @@ using LanguageItem = CRD.Utils.Structs.LanguageItem;
|
||||||
namespace CRD.Downloader.Crunchyroll;
|
namespace CRD.Downloader.Crunchyroll;
|
||||||
|
|
||||||
public class CrunchyrollManager{
|
public class CrunchyrollManager{
|
||||||
public CrToken? Token;
|
|
||||||
|
|
||||||
public CrProfile Profile = new();
|
|
||||||
private readonly Lazy<CrDownloadOptions> _optionsLazy;
|
private readonly Lazy<CrDownloadOptions> _optionsLazy;
|
||||||
public CrDownloadOptions CrunOptions => _optionsLazy.Value;
|
public CrDownloadOptions CrunOptions => _optionsLazy.Value;
|
||||||
|
|
||||||
|
|
@ -60,7 +57,9 @@ public class CrunchyrollManager{
|
||||||
|
|
||||||
private Widevine _widevine = Widevine.Instance;
|
private Widevine _widevine = Widevine.Instance;
|
||||||
|
|
||||||
public CrAuth CrAuth;
|
public CrAuth CrAuthEndpoint1;
|
||||||
|
public CrAuth CrAuthEndpoint2;
|
||||||
|
|
||||||
public CrEpisode CrEpisode;
|
public CrEpisode CrEpisode;
|
||||||
public CrSeries CrSeries;
|
public CrSeries CrSeries;
|
||||||
public CrMovies CrMovies;
|
public CrMovies CrMovies;
|
||||||
|
|
@ -153,20 +152,16 @@ public class CrunchyrollManager{
|
||||||
public void InitOptions(){
|
public void InitOptions(){
|
||||||
_widevine = Widevine.Instance;
|
_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();
|
CrEpisode = new CrEpisode();
|
||||||
CrSeries = new CrSeries();
|
CrSeries = new CrSeries();
|
||||||
CrMovies = new CrMovies();
|
CrMovies = new CrMovies();
|
||||||
CrMusic = new CrMusic();
|
CrMusic = new CrMusic();
|
||||||
History = new History();
|
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(){
|
public static async Task<string> GetBase64EncodedTokenAsync(){
|
||||||
|
|
@ -200,9 +195,34 @@ public class CrunchyrollManager{
|
||||||
}
|
}
|
||||||
|
|
||||||
CrunOptions.StreamEndpoint = "tv/android_tv";
|
CrunOptions.StreamEndpoint = "tv/android_tv";
|
||||||
CrunOptions.StreamEndpointSecondary = "";
|
CrAuthEndpoint1.AuthSettings = new CrAuthSettings(){
|
||||||
CfgManager.WriteCrSettings();
|
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();
|
// var token = await GetBase64EncodedTokenAsync();
|
||||||
//
|
//
|
||||||
// if (!string.IsNullOrEmpty(token)){
|
// 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 (CrunOptions.History){
|
||||||
if (File.Exists(CfgManager.PathCrHistory)){
|
if (File.Exists(CfgManager.PathCrHistory)){
|
||||||
|
|
@ -680,7 +693,7 @@ public class CrunchyrollManager{
|
||||||
CcSubsMuxingFlag = options.CcSubsMuxingFlag,
|
CcSubsMuxingFlag = options.CcSubsMuxingFlag,
|
||||||
SignsSubsAsForced = options.SignsSubsAsForced,
|
SignsSubsAsForced = options.SignsSubsAsForced,
|
||||||
Description = muxDesc ? data.Where(a => a.Type == DownloadMediaType.Description).Select(a => new MergerInput{ Path = a.Path ?? string.Empty }).ToList() :[],
|
Description = muxDesc ? data.Where(a => a.Type == DownloadMediaType.Description).Select(a => new MergerInput{ Path = a.Path ?? string.Empty }).ToList() :[],
|
||||||
Cover = options.MuxCover ? data.Where(a => a.Type == DownloadMediaType.Cover).Select(a => new MergerInput{ Path = a.Path ?? string.Empty }).ToList() : [],
|
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)){
|
if (!File.Exists(CfgManager.PathFFMPEG)){
|
||||||
|
|
@ -760,7 +773,7 @@ public class CrunchyrollManager{
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<DownloadResponse> DownloadMediaList(CrunchyEpMeta data, CrDownloadOptions options){
|
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?");
|
MainWindow.Instance.ShowError($"User Account not recognized - are you signed in?");
|
||||||
return new DownloadResponse{
|
return new DownloadResponse{
|
||||||
Data = new List<DownloadedMedia>(),
|
Data = new List<DownloadedMedia>(),
|
||||||
|
|
@ -886,7 +899,7 @@ public class CrunchyrollManager{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
await CrAuth.RefreshToken(true);
|
await CrAuthEndpoint1.RefreshToken(true);
|
||||||
|
|
||||||
EpisodeVersion currentVersion = new EpisodeVersion();
|
EpisodeVersion currentVersion = new EpisodeVersion();
|
||||||
EpisodeVersion primaryVersion = new EpisodeVersion();
|
EpisodeVersion primaryVersion = new EpisodeVersion();
|
||||||
|
|
@ -947,10 +960,10 @@ public class CrunchyrollManager{
|
||||||
|
|
||||||
#endregion
|
#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;
|
(bool IsOk, PlaybackData pbData, string error) fetchPlaybackData2 = default;
|
||||||
if (!string.IsNullOrEmpty(options.StreamEndpointSecondary) && !(options.StreamEndpoint ?? "web/firefox").Equals(options.StreamEndpointSecondary)){
|
if (CrAuthEndpoint2.Profile.Username != "???"){
|
||||||
fetchPlaybackData2 = await FetchPlaybackData(options.StreamEndpointSecondary, mediaId, mediaGuid, data.Music);
|
fetchPlaybackData2 = await FetchPlaybackData(CrAuthEndpoint2, mediaId, mediaGuid, data.Music);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fetchPlaybackData.IsOk){
|
if (!fetchPlaybackData.IsOk){
|
||||||
|
|
@ -1004,13 +1017,13 @@ public class CrunchyrollManager{
|
||||||
foreach (var keyValuePair in fetchPlaybackData2.pbData.Data){
|
foreach (var keyValuePair in fetchPlaybackData2.pbData.Data){
|
||||||
var pbDataFirstEndpoint = fetchPlaybackData.pbData?.Data;
|
var pbDataFirstEndpoint = fetchPlaybackData.pbData?.Data;
|
||||||
if (pbDataFirstEndpoint != null && pbDataFirstEndpoint.TryGetValue(keyValuePair.Key, out var value)){
|
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 match = Regex.Match(secondEndpoint.Url ?? "", @"(https?:\/\/.*?\/(?:dash\/|\.urlset\/))");
|
||||||
var shortendUrl = match.Success ? match.Value : urlSecondEndpoint;
|
var shortendUrl = match.Success ? match.Value : secondEndpoint.Url;
|
||||||
|
|
||||||
if (!value.Url.Any(arrayUrl => arrayUrl != null && arrayUrl.Contains(shortendUrl))){
|
if (!string.IsNullOrEmpty(shortendUrl) && !value.Url.Any(arrayUrl => arrayUrl.Url != null && arrayUrl.Url.Contains(shortendUrl))){
|
||||||
value.Url.Add(urlSecondEndpoint);
|
value.Url.Add(secondEndpoint);
|
||||||
}
|
}
|
||||||
} else{
|
} else{
|
||||||
if (pbDataFirstEndpoint != null){
|
if (pbDataFirstEndpoint != null){
|
||||||
|
|
@ -1189,7 +1202,7 @@ public class CrunchyrollManager{
|
||||||
Dictionary<string, string> streamPlaylistsReqResponseList =[];
|
Dictionary<string, string> streamPlaylistsReqResponseList =[];
|
||||||
|
|
||||||
foreach (var streamUrl in curStream.Url){
|
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);
|
var streamPlaylistsReqResponse = await HttpClientReq.Instance.SendHttpRequest(streamPlaylistsReq);
|
||||||
|
|
||||||
if (!streamPlaylistsReqResponse.IsOk){
|
if (!streamPlaylistsReqResponse.IsOk){
|
||||||
|
|
@ -1203,7 +1216,7 @@ public class CrunchyrollManager{
|
||||||
}
|
}
|
||||||
|
|
||||||
if (streamPlaylistsReqResponse.ResponseContent.Contains("MPD")){
|
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){
|
} else if (options.Novids){
|
||||||
Console.WriteLine("Skipping video download...");
|
Console.WriteLine("Skipping video download...");
|
||||||
} else{
|
} else{
|
||||||
await CrAuth.RefreshToken(true);
|
await CrAuthEndpoint1.RefreshToken(true);
|
||||||
|
|
||||||
Dictionary<string, string> authDataDict = new Dictionary<string, string>
|
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);
|
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){
|
if (chosenAudioSegments.segments.Count > 0 && !options.Noaudio && !dlFailed){
|
||||||
await CrAuth.RefreshToken(true);
|
await CrAuthEndpoint1.RefreshToken(true);
|
||||||
|
|
||||||
if (chosenVideoSegments.encryptionKeys.Count == 0){
|
if (chosenVideoSegments.encryptionKeys.Count == 0){
|
||||||
Dictionary<string, string> authDataDict = new Dictionary<string, string>
|
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);
|
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>
|
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;
|
var encryptionKeys = chosenVideoSegments.encryptionKeys;
|
||||||
|
|
||||||
|
|
@ -1899,7 +1912,7 @@ public class CrunchyrollManager{
|
||||||
|
|
||||||
Console.WriteLine($"{fileName}.xml has been created with the description.");
|
Console.WriteLine($"{fileName}.xml has been created with the description.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.MuxCover){
|
if (options.MuxCover){
|
||||||
if (!string.IsNullOrEmpty(data.ImageBig) && !File.Exists(fileDir + "cover.png")){
|
if (!string.IsNullOrEmpty(data.ImageBig) && !File.Exists(fileDir + "cover.png")){
|
||||||
var bitmap = await Helpers.LoadImage(data.ImageBig);
|
var bitmap = await Helpers.LoadImage(data.ImageBig);
|
||||||
|
|
@ -1909,14 +1922,14 @@ public class CrunchyrollManager{
|
||||||
await using (var fs = File.OpenWrite(coverPath)){
|
await using (var fs = File.OpenWrite(coverPath)){
|
||||||
bitmap.Save(fs); // always saves PNG
|
bitmap.Save(fs); // always saves PNG
|
||||||
}
|
}
|
||||||
|
|
||||||
bitmap.Dispose();
|
bitmap.Dispose();
|
||||||
|
|
||||||
files.Add(new DownloadedMedia{
|
files.Add(new DownloadedMedia{
|
||||||
Type = DownloadMediaType.Cover,
|
Type = DownloadMediaType.Cover,
|
||||||
Lang = Languages.DEFAULT_lang,
|
Lang = Languages.DEFAULT_lang,
|
||||||
Path = coverPath
|
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")));
|
!(data.DownloadSubs.Count == 1 && !data.DownloadSubs.Contains("all")));
|
||||||
sxData.Path = Path.Combine(fileDir, sxData.File);
|
sxData.Path = Path.Combine(fileDir, sxData.File);
|
||||||
|
|
||||||
|
|
@ -2015,7 +2029,7 @@ public class CrunchyrollManager{
|
||||||
continue;
|
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);
|
var subsAssReqResponse = await HttpClientReq.Instance.SendHttpRequest(subsAssReq);
|
||||||
|
|
||||||
|
|
@ -2249,32 +2263,34 @@ public class CrunchyrollManager{
|
||||||
|
|
||||||
#region Fetch Playback Data
|
#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{
|
var temppbData = new PlaybackData{
|
||||||
Total = 0,
|
Total = 0,
|
||||||
Data = new Dictionary<string, StreamDetails>()
|
Data = new Dictionary<string, StreamDetails>()
|
||||||
};
|
};
|
||||||
|
|
||||||
var playbackEndpoint = $"{ApiUrls.Playback}/{(music ? "music/" : "")}{mediaGuidId}/{streamEndpoint}/play";
|
await authEndpoint.RefreshToken(true);
|
||||||
var playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint);
|
|
||||||
|
var playbackEndpoint = $"{ApiUrls.Playback}/{(music ? "music/" : "")}{mediaGuidId}/{authEndpoint.AuthSettings.Endpoint}/play?queue=false";
|
||||||
|
var playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint, authEndpoint);
|
||||||
|
|
||||||
if (!playbackRequestResponse.IsOk){
|
if (!playbackRequestResponse.IsOk){
|
||||||
playbackRequestResponse = await HandleStreamErrorsAsync(playbackRequestResponse, playbackEndpoint);
|
playbackRequestResponse = await HandleStreamErrorsAsync(playbackRequestResponse, playbackEndpoint, authEndpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playbackRequestResponse.IsOk){
|
if (playbackRequestResponse.IsOk){
|
||||||
temppbData = await ProcessPlaybackResponseAsync(playbackRequestResponse.ResponseContent, mediaId, mediaGuidId);
|
temppbData = await ProcessPlaybackResponseAsync(playbackRequestResponse.ResponseContent, mediaId, mediaGuidId, authEndpoint);
|
||||||
} else{
|
} else{
|
||||||
Console.WriteLine("Request Stream URLs FAILED! Attempting fallback");
|
Console.WriteLine("Request Stream URLs FAILED! Attempting fallback");
|
||||||
playbackEndpoint = $"{ApiUrls.Playback}/{(music ? "music/" : "")}{mediaGuidId}/web/firefox/play";
|
playbackEndpoint = $"{ApiUrls.Playback}/{(music ? "music/" : "")}{mediaGuidId}/web/firefox/play";
|
||||||
playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint);
|
playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint, authEndpoint);
|
||||||
|
|
||||||
if (!playbackRequestResponse.IsOk){
|
if (!playbackRequestResponse.IsOk){
|
||||||
playbackRequestResponse = await HandleStreamErrorsAsync(playbackRequestResponse, playbackEndpoint);
|
playbackRequestResponse = await HandleStreamErrorsAsync(playbackRequestResponse, playbackEndpoint, authEndpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playbackRequestResponse.IsOk){
|
if (playbackRequestResponse.IsOk){
|
||||||
temppbData = await ProcessPlaybackResponseAsync(playbackRequestResponse.ResponseContent, mediaId, mediaGuidId);
|
temppbData = await ProcessPlaybackResponseAsync(playbackRequestResponse.ResponseContent, mediaId, mediaGuidId, authEndpoint);
|
||||||
} else{
|
} else{
|
||||||
Console.Error.WriteLine("Fallback Request Stream URLs FAILED!");
|
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);
|
return (playbackRequestResponse.IsOk, pbData: temppbData, error: playbackRequestResponse.IsOk ? "" : playbackRequestResponse.ResponseContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<(bool IsOk, string ResponseContent, string error)> SendPlaybackRequestAsync(string endpoint){
|
private async Task<(bool IsOk, string ResponseContent, string error)> SendPlaybackRequestAsync(string endpoint, CrAuth authEndpoint){
|
||||||
var request = HttpClientReq.CreateRequestMessage(endpoint, HttpMethod.Get, true, false, null);
|
var request = HttpClientReq.CreateRequestMessage(endpoint, HttpMethod.Get, true, authEndpoint.Token?.access_token, null);
|
||||||
request.Headers.UserAgent.ParseAdd("ANDROIDTV/3.42.1_22267 Android/16");
|
request.Headers.UserAgent.ParseAdd(authEndpoint.AuthSettings.UserAgent);
|
||||||
return await HttpClientReq.Instance.SendHttpRequest(request);
|
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;
|
if (response.IsOk || string.IsNullOrEmpty(response.ResponseContent)) return response;
|
||||||
|
|
||||||
var error = StreamError.FromJson(response.ResponseContent);
|
var error = StreamError.FromJson(response.ResponseContent);
|
||||||
if (error?.IsTooManyActiveStreamsError() == true){
|
if (error?.IsTooManyActiveStreamsError() == true){
|
||||||
foreach (var errorActiveStream in error.ActiveStreams){
|
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;
|
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{
|
var temppbData = new PlaybackData{
|
||||||
Total = 0,
|
Total = 0,
|
||||||
Data = new Dictionary<string, StreamDetails>()
|
Data = new Dictionary<string, StreamDetails>()
|
||||||
|
|
@ -2314,7 +2330,7 @@ public class CrunchyrollManager{
|
||||||
if (playStream == null) return temppbData;
|
if (playStream == null) return temppbData;
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(playStream.Token)){
|
if (!string.IsNullOrEmpty(playStream.Token)){
|
||||||
await HttpClientReq.DeAuthVideo(mediaGuidId, playStream.Token);
|
await Instance.DeAuthVideo(mediaGuidId, playStream.Token, authEndpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
var derivedPlayCrunchyStreams = new CrunchyStreams();
|
var derivedPlayCrunchyStreams = new CrunchyStreams();
|
||||||
|
|
@ -2323,7 +2339,7 @@ public class CrunchyrollManager{
|
||||||
foreach (var hardsub in playStream.HardSubs){
|
foreach (var hardsub in playStream.HardSubs){
|
||||||
var stream = hardsub.Value;
|
var stream = hardsub.Value;
|
||||||
derivedPlayCrunchyStreams[hardsub.Key] = new StreamDetails{
|
derivedPlayCrunchyStreams[hardsub.Key] = new StreamDetails{
|
||||||
Url =[stream.Url],
|
Url =[new UrlWithAuth(){ Url = stream.Url, CrAuth = authEndpoint }],
|
||||||
IsHardsubbed = true,
|
IsHardsubbed = true,
|
||||||
HardsubLocale = stream.Hlang,
|
HardsubLocale = stream.Hlang,
|
||||||
HardsubLang = Languages.FixAndFindCrLc((stream.Hlang ?? Locale.DefaulT).GetEnumMemberValue())
|
HardsubLang = Languages.FixAndFindCrLc((stream.Hlang ?? Locale.DefaulT).GetEnumMemberValue())
|
||||||
|
|
@ -2332,7 +2348,7 @@ public class CrunchyrollManager{
|
||||||
}
|
}
|
||||||
|
|
||||||
derivedPlayCrunchyStreams[""] = new StreamDetails{
|
derivedPlayCrunchyStreams[""] = new StreamDetails{
|
||||||
Url =[playStream.Url],
|
Url =[new UrlWithAuth(){ Url = playStream.Url, CrAuth = authEndpoint }],
|
||||||
IsHardsubbed = false,
|
IsHardsubbed = false,
|
||||||
HardsubLocale = Locale.DefaulT,
|
HardsubLocale = Locale.DefaulT,
|
||||||
HardsubLang = Languages.DEFAULT_lang
|
HardsubLang = Languages.DEFAULT_lang
|
||||||
|
|
@ -2368,7 +2384,7 @@ public class CrunchyrollManager{
|
||||||
|
|
||||||
|
|
||||||
private async Task ParseChapters(string currentMediaId, List<string> compiledChapters){
|
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);
|
var showRequestResponse = await HttpClientReq.Instance.SendHttpRequest(showRequest, true);
|
||||||
|
|
||||||
|
|
@ -2455,7 +2471,7 @@ public class CrunchyrollManager{
|
||||||
} else{
|
} else{
|
||||||
Console.WriteLine("Chapter request failed, attempting old API ");
|
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);
|
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) =>
|
private static string FormatKey(byte[] keyBytes) =>
|
||||||
BitConverter.ToString(keyBytes).Replace("-", "").ToLower();
|
BitConverter.ToString(keyBytes).Replace("-", "").ToLower();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,22 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
private ComboBoxItem _selectedStreamEndpoint;
|
private ComboBoxItem _selectedStreamEndpoint;
|
||||||
|
|
||||||
[ObservableProperty]
|
[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]
|
[ObservableProperty]
|
||||||
private ComboBoxItem _selectedDefaultDubLang;
|
private ComboBoxItem _selectedDefaultDubLang;
|
||||||
|
|
@ -228,14 +243,14 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
|
|
||||||
public ObservableCollection<ComboBoxItem> StreamEndpointsSecondary{ get; } =[
|
public ObservableCollection<ComboBoxItem> StreamEndpointsSecondary{ get; } =[
|
||||||
new(){ Content = "" },
|
new(){ Content = "" },
|
||||||
new(){ Content = "web/firefox" },
|
// new(){ Content = "web/firefox" },
|
||||||
new(){ Content = "console/switch" },
|
new(){ Content = "console/switch" },
|
||||||
new(){ Content = "console/ps4" },
|
new(){ Content = "console/ps4" },
|
||||||
new(){ Content = "console/ps5" },
|
new(){ Content = "console/ps5" },
|
||||||
new(){ Content = "console/xbox_one" },
|
new(){ Content = "console/xbox_one" },
|
||||||
new(){ Content = "web/edge" },
|
// new(){ Content = "web/edge" },
|
||||||
new(){ Content = "web/chrome" },
|
// new(){ Content = "web/chrome" },
|
||||||
new(){ Content = "web/fallback" },
|
// new(){ Content = "web/fallback" },
|
||||||
new(){ Content = "android/phone" },
|
new(){ Content = "android/phone" },
|
||||||
new(){ Content = "android/tablet" },
|
new(){ Content = "android/tablet" },
|
||||||
new(){ Content = "tv/samsung" },
|
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;
|
ComboBoxItem? streamEndpoint = StreamEndpoints.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpoint ?? "")) ?? null;
|
||||||
SelectedStreamEndpoint = streamEndpoint ?? StreamEndpoints[0];
|
SelectedStreamEndpoint = streamEndpoint ?? StreamEndpoints[0];
|
||||||
|
|
||||||
ComboBoxItem? streamEndpointSecondary = StreamEndpointsSecondary.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpointSecondary ?? "")) ?? null;
|
ComboBoxItem? streamEndpointSecondar = StreamEndpointsSecondary.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpointSecondSettings?.Endpoint ?? "")) ?? null;
|
||||||
SelectedStreamEndpointSecondary = streamEndpointSecondary ?? StreamEndpointsSecondary[0];
|
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());
|
FFmpegHWAccel.AddRange(GetAvailableHWAccelOptions());
|
||||||
|
|
||||||
|
|
@ -473,7 +497,16 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
|
|
||||||
|
|
||||||
CrunchyrollManager.Instance.CrunOptions.StreamEndpoint = SelectedStreamEndpoint.Content + "";
|
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>();
|
List<string> dubLangs = new List<string>();
|
||||||
foreach (var listBoxItem in SelectedDubLang){
|
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(){
|
private List<StringItemWithDisplayName> GetAvailableHWAccelOptions(){
|
||||||
try{
|
try{
|
||||||
using (var process = new Process()){
|
using (var process = new Process()){
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@
|
||||||
<CheckBox IsChecked="{Binding SubsDownloadDuplicate}"> </CheckBox>
|
<CheckBox IsChecked="{Binding SubsDownloadDuplicate}"> </CheckBox>
|
||||||
</controls:SettingsExpanderItem.Footer>
|
</controls:SettingsExpanderItem.Footer>
|
||||||
</controls:SettingsExpanderItem>
|
</controls:SettingsExpanderItem>
|
||||||
|
|
||||||
<controls:SettingsExpanderItem Content="Add ScaledBorderAndShadow ">
|
<controls:SettingsExpanderItem Content="Add ScaledBorderAndShadow ">
|
||||||
<controls:SettingsExpanderItem.Footer>
|
<controls:SettingsExpanderItem.Footer>
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
|
|
@ -239,16 +239,73 @@
|
||||||
</ComboBox>
|
</ComboBox>
|
||||||
</controls:SettingsExpanderItem.Footer>
|
</controls:SettingsExpanderItem.Footer>
|
||||||
</controls:SettingsExpanderItem>
|
</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>
|
<controls:SettingsExpanderItem.Footer>
|
||||||
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
|
<StackPanel>
|
||||||
ItemsSource="{Binding StreamEndpointsSecondary}"
|
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
|
||||||
SelectedItem="{Binding SelectedStreamEndpointSecondary}">
|
ItemsSource="{Binding StreamEndpointsSecondary}"
|
||||||
</ComboBox>
|
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.Footer>
|
||||||
</controls:SettingsExpanderItem>
|
</controls:SettingsExpanderItem>
|
||||||
|
|
||||||
<controls:SettingsExpanderItem Content="Video">
|
<controls:SettingsExpanderItem Content="Video">
|
||||||
<controls:SettingsExpanderItem.Footer>
|
<controls:SettingsExpanderItem.Footer>
|
||||||
<CheckBox IsChecked="{Binding DownloadVideo}"> </CheckBox>
|
<CheckBox IsChecked="{Binding DownloadVideo}"> </CheckBox>
|
||||||
|
|
@ -325,7 +382,7 @@
|
||||||
HorizontalAlignment="Stretch" />
|
HorizontalAlignment="Stretch" />
|
||||||
</controls:SettingsExpanderItem.Footer>
|
</controls:SettingsExpanderItem.Footer>
|
||||||
</controls:SettingsExpanderItem>
|
</controls:SettingsExpanderItem>
|
||||||
|
|
||||||
<controls:SettingsExpanderItem Content="FileName Whitespace Substitute"
|
<controls:SettingsExpanderItem Content="FileName Whitespace Substitute"
|
||||||
Description="Character used to replace whitespace in file name variables like ${seriesTitle}">
|
Description="Character used to replace whitespace in file name variables like ${seriesTitle}">
|
||||||
<controls:SettingsExpanderItem.Footer>
|
<controls:SettingsExpanderItem.Footer>
|
||||||
|
|
@ -364,7 +421,7 @@
|
||||||
<CheckBox IsChecked="{Binding MuxToMp4}"> </CheckBox>
|
<CheckBox IsChecked="{Binding MuxToMp4}"> </CheckBox>
|
||||||
</controls:SettingsExpanderItem.Footer>
|
</controls:SettingsExpanderItem.Footer>
|
||||||
</controls:SettingsExpanderItem>
|
</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 IsVisible="{Binding !SkipMuxing}" Content="MP3" Description="Outputs an MP3 instead of an MKV/MP4 if only audio streams were downloaded">
|
||||||
<controls:SettingsExpanderItem.Footer>
|
<controls:SettingsExpanderItem.Footer>
|
||||||
<CheckBox IsChecked="{Binding MuxToMp3}"> </CheckBox>
|
<CheckBox IsChecked="{Binding MuxToMp3}"> </CheckBox>
|
||||||
|
|
@ -411,7 +468,7 @@
|
||||||
<CheckBox IsChecked="{Binding MuxFonts}"> </CheckBox>
|
<CheckBox IsChecked="{Binding MuxFonts}"> </CheckBox>
|
||||||
</controls:SettingsExpanderItem.Footer>
|
</controls:SettingsExpanderItem.Footer>
|
||||||
</controls:SettingsExpanderItem>
|
</controls:SettingsExpanderItem>
|
||||||
|
|
||||||
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="Include episode thumbnail" Description="Embeds the episode thumbnail into the MKV as cover">
|
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="Include episode thumbnail" Description="Embeds the episode thumbnail into the MKV as cover">
|
||||||
<controls:SettingsExpanderItem.Footer>
|
<controls:SettingsExpanderItem.Footer>
|
||||||
<CheckBox IsChecked="{Binding MuxCover}"> </CheckBox>
|
<CheckBox IsChecked="{Binding MuxCover}"> </CheckBox>
|
||||||
|
|
@ -446,7 +503,7 @@
|
||||||
<StackPanel HorizontalAlignment="Right">
|
<StackPanel HorizontalAlignment="Right">
|
||||||
<CheckBox HorizontalAlignment="Right" IsChecked="{Binding SyncTimings}"> </CheckBox>
|
<CheckBox HorizontalAlignment="Right" IsChecked="{Binding SyncTimings}"> </CheckBox>
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" IsVisible="{Binding SyncTimings}">
|
<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"
|
<ComboBox HorizontalAlignment="Right"
|
||||||
ItemsSource="{Binding FFmpegHWAccel}"
|
ItemsSource="{Binding FFmpegHWAccel}"
|
||||||
SelectedItem="{Binding SelectedFFmpegHWAccel}">
|
SelectedItem="{Binding SelectedFFmpegHWAccel}">
|
||||||
|
|
@ -457,7 +514,7 @@
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ComboBox>
|
</ComboBox>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
</controls:SettingsExpanderItem.Footer>
|
</controls:SettingsExpanderItem.Footer>
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ public class History{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
await crunInstance.CrAuth.RefreshToken(true);
|
await crunInstance.CrAuthEndpoint1.RefreshToken(true);
|
||||||
|
|
||||||
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
|
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -99,13 +99,13 @@ public partial class QueueManager : ObservableObject{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await CrunchyrollManager.Instance.CrAuth.RefreshToken(true);
|
await CrunchyrollManager.Instance.CrAuthEndpoint1.RefreshToken(true);
|
||||||
|
|
||||||
var episodeL = await CrunchyrollManager.Instance.CrEpisode.ParseEpisodeById(epId, crLocale);
|
var episodeL = await CrunchyrollManager.Instance.CrEpisode.ParseEpisodeById(epId, crLocale);
|
||||||
|
|
||||||
|
|
||||||
if (episodeL != null){
|
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));
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -197,6 +197,12 @@ public partial class QueueManager : ObservableObject{
|
||||||
break;
|
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;
|
newOptions.DubLang = dubLang;
|
||||||
|
|
||||||
selected.DownloadSettings = newOptions;
|
selected.DownloadSettings = newOptions;
|
||||||
|
|
@ -293,7 +299,7 @@ public partial class QueueManager : ObservableObject{
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CrAddMusicVideoToQueue(string epId){
|
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, "");
|
var musicVideo = await CrunchyrollManager.Instance.CrMusic.ParseMusicVideoByIdAsync(epId, "");
|
||||||
|
|
||||||
|
|
@ -317,7 +323,7 @@ public partial class QueueManager : ObservableObject{
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CrAddConcertToQueue(string epId){
|
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, "");
|
var concert = await CrunchyrollManager.Instance.CrMusic.ParseConcertByIdAsync(epId, "");
|
||||||
|
|
||||||
|
|
@ -410,6 +416,12 @@ public partial class QueueManager : ObservableObject{
|
||||||
|
|
||||||
crunchyEpMeta.DownloadSettings = newOptions;
|
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);
|
Queue.Add(crunchyEpMeta);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,16 +33,10 @@ public class HttpClientReq{
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
private HttpClient client;
|
private HttpClient client;
|
||||||
private Dictionary<string, CookieCollection> cookieStore;
|
|
||||||
|
|
||||||
private HttpClientHandler handler;
|
|
||||||
|
|
||||||
public HttpClientReq(){
|
public HttpClientReq(){
|
||||||
cookieStore = new Dictionary<string, CookieCollection>();
|
|
||||||
|
|
||||||
IWebProxy systemProxy = WebRequest.DefaultWebProxy;
|
IWebProxy systemProxy = WebRequest.DefaultWebProxy;
|
||||||
|
|
||||||
HttpClientHandler handler = new HttpClientHandler();
|
HttpClientHandler handler = new HttpClientHandler();
|
||||||
|
|
@ -130,43 +124,10 @@ public class HttpClientReq{
|
||||||
return handler;
|
return handler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<(bool IsOk, string ResponseContent, string error)> SendHttpRequest(HttpRequestMessage request, bool suppressError = false, Dictionary<string, CookieCollection>? cookieStore = null){
|
||||||
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){
|
|
||||||
string content = string.Empty;
|
string content = string.Empty;
|
||||||
try{
|
try{
|
||||||
AttachCookies(request);
|
AttachCookies(request, cookieStore);
|
||||||
|
|
||||||
HttpResponseMessage response = await client.SendAsync(request);
|
HttpResponseMessage response = await client.SendAsync(request);
|
||||||
|
|
||||||
|
|
@ -178,18 +139,22 @@ public class HttpClientReq{
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
return (IsOk: true, ResponseContent: content,error:"");
|
return (IsOk: true, ResponseContent: content, error: "");
|
||||||
} catch (Exception e){
|
} catch (Exception e){
|
||||||
// Console.Error.WriteLine($"Error: {e} \n Response: {(content.Length < 500 ? content : "error to long")}");
|
// Console.Error.WriteLine($"Error: {e} \n Response: {(content.Length < 500 ? content : "error to long")}");
|
||||||
if (!suppressError){
|
if (!suppressError){
|
||||||
Console.Error.WriteLine($"Error: {e} \n Response: {(content.Length < 500 ? content : "error to long")}");
|
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();
|
var cookieHeader = new StringBuilder();
|
||||||
|
|
||||||
if (request.Headers.TryGetValues("Cookie", out var existingCookies)){
|
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)){
|
if (string.IsNullOrEmpty(uri)){
|
||||||
Console.Error.WriteLine($" Request URI is empty");
|
Console.Error.WriteLine($" Request URI is empty");
|
||||||
return new HttpRequestMessage(HttpMethod.Get, "about:blank");
|
return new HttpRequestMessage(HttpMethod.Get, "about:blank");
|
||||||
}
|
}
|
||||||
|
|
||||||
UriBuilder uriBuilder = new UriBuilder(uri);
|
UriBuilder uriBuilder = new UriBuilder(uri);
|
||||||
|
|
||||||
if (query != null){
|
if (query != null){
|
||||||
|
|
@ -230,20 +213,13 @@ public class HttpClientReq{
|
||||||
var request = new HttpRequestMessage(requestMethod, uriBuilder.ToString());
|
var request = new HttpRequestMessage(requestMethod, uriBuilder.ToString());
|
||||||
|
|
||||||
if (authHeader){
|
if (authHeader){
|
||||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", CrunchyrollManager.Instance.Token?.access_token);
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||||
}
|
|
||||||
|
|
||||||
if (disableDrmHeader){
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return request;
|
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(){
|
public HttpClient GetHttpClient(){
|
||||||
return client;
|
return client;
|
||||||
|
|
@ -266,17 +242,17 @@ public static class ApiUrls{
|
||||||
public static string Playback => "https://cr-play-service.prd.crunchyrollsvc.com/v2";
|
public static string Playback => "https://cr-play-service.prd.crunchyrollsvc.com/v2";
|
||||||
//https://www.crunchyroll.com/playback/v2
|
//https://www.crunchyroll.com/playback/v2
|
||||||
//https://cr-play-service.prd.crunchyrollsvc.com/v2
|
//https://cr-play-service.prd.crunchyrollsvc.com/v2
|
||||||
|
|
||||||
public static string Subscription => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/subs/v3/subscriptions/";
|
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 BetaBrowse = ApiBeta + "/content/v1/browse";
|
||||||
public static readonly string BetaCms = ApiBeta + "/cms/v2";
|
public static readonly string BetaCms = ApiBeta + "/cms/v2";
|
||||||
public static readonly string DRM = ApiBeta + "/drm/v1/auth";
|
public static readonly string DRM = ApiBeta + "/drm/v1/auth";
|
||||||
|
|
||||||
public static readonly string WidevineLicenceUrl = "https://www.crunchyroll.com/license/v1/license/widevine";
|
public static readonly string WidevineLicenceUrl = "https://www.crunchyroll.com/license/v1/license/widevine";
|
||||||
//https://lic.drmtoday.com/license-proxy-widevine/cenc/
|
//https://lic.drmtoday.com/license-proxy-widevine/cenc/
|
||||||
//https://lic.staging.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 djV3YnNsdGJueG5oeXk3cDN4ZmI6cFdKWkZMaHVTM0I2NFhPbk81bWVlWXpiTlBtZWsyRVU=";
|
||||||
public static string authBasicMob = "Basic Ym1icmt4eXgzZDd1NmpzZnlsYTQ6QUlONEQ1VkVfY3Awd1Z6Zk5vUDBZcUhVcllGcDloU2c=";
|
public static string authBasicMob = "Basic Ym1icmt4eXgzZDd1NmpzZnlsYTQ6QUlONEQ1VkVfY3Awd1Z6Zk5vUDBZcUhVcllGcDloU2c=";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -292,8 +292,8 @@ public class CrDownloadOptions{
|
||||||
[JsonProperty("stream_endpoint")]
|
[JsonProperty("stream_endpoint")]
|
||||||
public string? StreamEndpoint{ get; set; }
|
public string? StreamEndpoint{ get; set; }
|
||||||
|
|
||||||
[JsonProperty("stream_endpoint_secondary")]
|
[JsonProperty("stream_endpoint_secondary_settings")]
|
||||||
public string? StreamEndpointSecondary { get; set; }
|
public CrAuthSettings? StreamEndpointSecondSettings { get; set; }
|
||||||
|
|
||||||
[JsonProperty("search_fetch_featured_music")]
|
[JsonProperty("search_fetch_featured_music")]
|
||||||
public bool SearchFetchFeaturedMusic{ get; set; }
|
public bool SearchFetchFeaturedMusic{ get; set; }
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using Avalonia.Media.Imaging;
|
||||||
using CRD.Utils.Structs.Crunchyroll;
|
using CRD.Utils.Structs.Crunchyroll;
|
||||||
using CRD.Utils.Structs.History;
|
using CRD.Utils.Structs.History;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
@ -389,6 +390,8 @@ public class CrunchyEpMeta{
|
||||||
public bool OnlySubs{ get; set; }
|
public bool OnlySubs{ get; set; }
|
||||||
|
|
||||||
public CrDownloadOptions? DownloadSettings;
|
public CrDownloadOptions? DownloadSettings;
|
||||||
|
|
||||||
|
public bool HighlightAllAvailable{ get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DownloadProgress{
|
public class DownloadProgress{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using CRD.Downloader.Crunchyroll;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace CRD.Utils.Structs.Crunchyroll;
|
namespace CRD.Utils.Structs.Crunchyroll;
|
||||||
|
|
@ -13,7 +14,7 @@ public class StreamDetails{
|
||||||
[JsonProperty("hardsub_locale")]
|
[JsonProperty("hardsub_locale")]
|
||||||
public Locale? HardsubLocale{ get; set; }
|
public Locale? HardsubLocale{ get; set; }
|
||||||
|
|
||||||
public List<string?> Url{ get; set; }
|
public List<UrlWithAuth> Url{ get; set; }
|
||||||
|
|
||||||
[JsonProperty("hardsub_lang")]
|
[JsonProperty("hardsub_lang")]
|
||||||
public required LanguageItem HardsubLang{ get; set; }
|
public required LanguageItem HardsubLang{ get; set; }
|
||||||
|
|
@ -26,9 +27,17 @@ public class StreamDetails{
|
||||||
public string? Type{ get; set; }
|
public string? Type{ get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class UrlWithAuth{
|
||||||
|
|
||||||
|
public CrAuth? CrAuth{ get; set; }
|
||||||
|
|
||||||
|
public string? Url{ get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public class StreamDetailsPop{
|
public class StreamDetailsPop{
|
||||||
public Locale? HardsubLocale{ get; set; }
|
public Locale? HardsubLocale{ get; set; }
|
||||||
public List<string?> Url{ get; set; }
|
public List<UrlWithAuth> Url{ get; set; }
|
||||||
public required LanguageItem HardsubLang{ get; set; }
|
public required LanguageItem HardsubLang{ get; set; }
|
||||||
|
|
||||||
public bool IsHardsubbed{ get; set; }
|
public bool IsHardsubbed{ get; set; }
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,14 @@ public class AuthData{
|
||||||
public string Password{ get; set; }
|
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{
|
public class DrmAuthData{
|
||||||
[JsonProperty("custom_data")]
|
[JsonProperty("custom_data")]
|
||||||
public string? CustomData{ get; set; }
|
public string? CustomData{ get; set; }
|
||||||
|
|
|
||||||
|
|
@ -50,8 +50,8 @@ public partial class AccountPageViewModel : ViewModelBase{
|
||||||
RemainingTime = "Subscription maybe ended";
|
RemainingTime = "Subscription maybe ended";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CrunchyrollManager.Instance.Profile.Subscription != null){
|
if (CrunchyrollManager.Instance.CrAuthEndpoint1.Profile.Subscription != null){
|
||||||
Console.Error.WriteLine(JsonConvert.SerializeObject(CrunchyrollManager.Instance.Profile.Subscription, Formatting.Indented));
|
Console.Error.WriteLine(JsonConvert.SerializeObject(CrunchyrollManager.Instance.CrAuthEndpoint1.Profile.Subscription, Formatting.Indented));
|
||||||
}
|
}
|
||||||
} else{
|
} else{
|
||||||
RemainingTime = $"{(IsCancelled ? "Subscription ending in: " : "Subscription refreshing in: ")}{remaining:dd\\:hh\\:mm\\:ss}";
|
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(){
|
public void UpdatetProfile(){
|
||||||
ProfileName = CrunchyrollManager.Instance.Profile.Username ?? CrunchyrollManager.Instance.Profile.ProfileName ?? "???"; // Default or fetched user name
|
ProfileName = CrunchyrollManager.Instance.CrAuthEndpoint1.Profile.Username ?? CrunchyrollManager.Instance.CrAuthEndpoint1.Profile.ProfileName ?? "???"; // Default or fetched user name
|
||||||
LoginLogoutText = CrunchyrollManager.Instance.Profile.Username == "???" ? "Login" : "Logout"; // Default state
|
LoginLogoutText = CrunchyrollManager.Instance.CrAuthEndpoint1.Profile.Username == "???" ? "Login" : "Logout"; // Default state
|
||||||
LoadProfileImage("https://static.crunchyroll.com/assets/avatar/170x170/" +
|
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 != null){
|
||||||
if (subscriptions.SubscriptionProducts is{ Count: >= 1 }){
|
if (subscriptions.SubscriptionProducts is{ Count: >= 1 }){
|
||||||
|
|
@ -84,8 +84,8 @@ public partial class AccountPageViewModel : ViewModelBase{
|
||||||
UnknownEndDate = true;
|
UnknownEndDate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CrunchyrollManager.Instance.Profile.Subscription?.NextRenewalDate != null && !UnknownEndDate){
|
if (CrunchyrollManager.Instance.CrAuthEndpoint1.Profile.Subscription?.NextRenewalDate != null && !UnknownEndDate){
|
||||||
_targetTime = CrunchyrollManager.Instance.Profile.Subscription.NextRenewalDate;
|
_targetTime = CrunchyrollManager.Instance.CrAuthEndpoint1.Profile.Subscription.NextRenewalDate;
|
||||||
_timer = new DispatcherTimer{
|
_timer = new DispatcherTimer{
|
||||||
Interval = TimeSpan.FromSeconds(1)
|
Interval = TimeSpan.FromSeconds(1)
|
||||||
};
|
};
|
||||||
|
|
@ -101,8 +101,8 @@ public partial class AccountPageViewModel : ViewModelBase{
|
||||||
|
|
||||||
RaisePropertyChanged(nameof(RemainingTime));
|
RaisePropertyChanged(nameof(RemainingTime));
|
||||||
|
|
||||||
if (CrunchyrollManager.Instance.Profile.Subscription != null){
|
if (CrunchyrollManager.Instance.CrAuthEndpoint1.Profile.Subscription != null){
|
||||||
Console.Error.WriteLine(JsonConvert.SerializeObject(CrunchyrollManager.Instance.Profile.Subscription, Formatting.Indented));
|
Console.Error.WriteLine(JsonConvert.SerializeObject(CrunchyrollManager.Instance.CrAuthEndpoint1.Profile.Subscription, Formatting.Indented));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -131,7 +131,8 @@ public partial class AccountPageViewModel : ViewModelBase{
|
||||||
|
|
||||||
_ = await dialog.ShowAsync();
|
_ = await dialog.ShowAsync();
|
||||||
} else{
|
} else{
|
||||||
await CrunchyrollManager.Instance.CrAuth.AuthAnonymous();
|
await CrunchyrollManager.Instance.CrAuthEndpoint1.AuthAnonymous();
|
||||||
|
await CrunchyrollManager.Instance.CrAuthEndpoint2.AuthAnonymous();
|
||||||
UpdatetProfile();
|
UpdatetProfile();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -324,6 +324,30 @@ public partial class SeriesPageViewModel : ViewModelBase{
|
||||||
Console.Error.WriteLine($"An error occurred while opening the folder: {ex.Message}");
|
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){
|
partial void OnSelectedDownloadModeChanged(EpisodeDownloadMode value){
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ public partial class ContentDialogInputLoginViewModel : ViewModelBase{
|
||||||
|
|
||||||
private AccountPageViewModel accountPageViewModel;
|
private AccountPageViewModel accountPageViewModel;
|
||||||
|
|
||||||
public ContentDialogInputLoginViewModel(ContentDialog dialog, AccountPageViewModel accountPageViewModel){
|
public ContentDialogInputLoginViewModel(ContentDialog dialog, AccountPageViewModel accountPageViewModel = null){
|
||||||
if (dialog is null){
|
if (dialog is null){
|
||||||
throw new ArgumentNullException(nameof(dialog));
|
throw new ArgumentNullException(nameof(dialog));
|
||||||
}
|
}
|
||||||
|
|
@ -30,8 +30,15 @@ public partial class ContentDialogInputLoginViewModel : ViewModelBase{
|
||||||
|
|
||||||
private async void LoginButton(ContentDialog sender, ContentDialogButtonClickEventArgs args){
|
private async void LoginButton(ContentDialog sender, ContentDialogButtonClickEventArgs args){
|
||||||
dialog.PrimaryButtonClick -= LoginButton;
|
dialog.PrimaryButtonClick -= LoginButton;
|
||||||
await CrunchyrollManager.Instance.CrAuth.Auth(new AuthData{Password = Password,Username = Email});
|
await CrunchyrollManager.Instance.CrAuthEndpoint1.Auth(new AuthData{Password = Password,Username = Email});
|
||||||
accountPageViewModel.UpdatetProfile();
|
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){
|
private void DialogOnClosed(ContentDialog sender, ContentDialogClosedEventArgs args){
|
||||||
|
|
|
||||||
157
CRD/ViewModels/Utils/ContentDialogSeriesDetailsViewModel.cs
Normal file
157
CRD/ViewModels/Utils/ContentDialogSeriesDetailsViewModel.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
|
@ -608,7 +608,7 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
public async void CheckIp(){
|
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);
|
Console.Error.WriteLine("Your IP: " + result.ResponseContent);
|
||||||
if (result.IsOk){
|
if (result.IsOk){
|
||||||
CurrentIp = result.ResponseContent;
|
CurrentIp = result.ResponseContent;
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,14 @@
|
||||||
xmlns:ui="clr-namespace:CRD.Utils.UI">
|
xmlns:ui="clr-namespace:CRD.Utils.UI">
|
||||||
|
|
||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
<ui:UiValueConverter x:Key="UiValueConverter"/>
|
<ui:UiValueConverter x:Key="UiValueConverter" />
|
||||||
</UserControl.Resources>
|
</UserControl.Resources>
|
||||||
|
|
||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<StackPanel Grid.Row="0" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
|
<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>
|
<TextBlock Text="Automatically shut down the PC when the queue is empty" FontSize="16" HorizontalAlignment="Center" VerticalAlignment="Center" TextWrapping="Wrap"></TextBlock>
|
||||||
</ToolTip.Tip>
|
</ToolTip.Tip>
|
||||||
</ToggleSwitch>
|
</ToggleSwitch>
|
||||||
|
|
||||||
<Button BorderThickness="0"
|
<Button BorderThickness="0"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
Margin="0 0 10 0 "
|
Margin="0 0 10 0 "
|
||||||
|
|
@ -42,20 +42,20 @@
|
||||||
<TextBlock Text="Retry failed" HorizontalAlignment="Center" VerticalAlignment="Center" TextWrapping="Wrap"></TextBlock>
|
<TextBlock Text="Retry failed" HorizontalAlignment="Center" VerticalAlignment="Center" TextWrapping="Wrap"></TextBlock>
|
||||||
</ToolTip.Tip>
|
</ToolTip.Tip>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button BorderThickness="0"
|
<Button BorderThickness="0"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
Margin="0 0 10 0 "
|
Margin="0 0 10 0 "
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Command="{Binding ClearQueue}">
|
Command="{Binding ClearQueue}">
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||||
<controls:SymbolIcon Symbol="Delete" FontSize="22" />
|
<controls:SymbolIcon Symbol="Delete" FontSize="22" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<ToolTip.Tip>
|
<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>
|
</ToolTip.Tip>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -69,16 +69,16 @@
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
<Image HorizontalAlignment="Center" Width="208" Height="117" Source="../Assets/coming_soon_ep.jpg" />
|
<Image HorizontalAlignment="Center" Width="208" Height="117" Source="../Assets/coming_soon_ep.jpg" />
|
||||||
<Image Grid.Column="0" Width="208" Height="117" Source="{Binding ImageBitmap}"
|
<Image Grid.Column="0" Width="208" Height="117" Source="{Binding ImageBitmap}"
|
||||||
Stretch="Fill" />
|
Stretch="Fill" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
|
||||||
<!-- Text Content -->
|
<!-- Text Content -->
|
||||||
<Grid Grid.Column="1" Margin="10" >
|
<Grid Grid.Column="1" Margin="10">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*" /> <!-- Takes up most space for the title -->
|
<ColumnDefinition Width="*" /> <!-- Takes up most space for the title -->
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
|
|
@ -90,13 +90,16 @@
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</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" />
|
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" />
|
TextWrapping="Wrap" VerticalAlignment="Center" />
|
||||||
|
|
||||||
<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">
|
HorizontalAlignment="Right" VerticalAlignment="Top">
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
|
|
@ -104,8 +107,8 @@
|
||||||
!Paused, Converter={StaticResource UiValueConverter}}" FontSize="18" />
|
!Paused, Converter={StaticResource UiValueConverter}}" FontSize="18" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Button>
|
</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">
|
HorizontalAlignment="Right" VerticalAlignment="Top">
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<controls:SymbolIcon Symbol="Refresh" FontSize="18" />
|
<controls:SymbolIcon Symbol="Refresh" FontSize="18" />
|
||||||
|
|
@ -118,12 +121,12 @@
|
||||||
<controls:SymbolIcon Symbol="Delete" FontSize="18" />
|
<controls:SymbolIcon Symbol="Delete" FontSize="18" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|
||||||
<ProgressBar Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3" VerticalAlignment="Bottom" Margin="0 0 0 10" Height="5" Value="{Binding Percent}"></ProgressBar>
|
<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 Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="3" VerticalAlignment="Bottom">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
|
|
@ -132,15 +135,15 @@
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Bottom" Text="{Binding DoingWhat}"
|
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Bottom" Text="{Binding DoingWhat}"
|
||||||
Opacity="1" TextWrapping="NoWrap" />
|
Opacity="1" TextWrapping="NoWrap" />
|
||||||
|
|
||||||
<TextBlock Grid.Row="0" Grid.Column="1" VerticalAlignment="Bottom" Margin="0 0 10 0" Text="{Binding Time}"
|
<TextBlock Grid.Row="0" Grid.Column="1" VerticalAlignment="Bottom" Margin="0 0 10 0" Text="{Binding Time}"
|
||||||
Opacity="0.8" TextWrapping="Wrap" />
|
Opacity="0.8" TextWrapping="Wrap" />
|
||||||
|
|
||||||
<TextBlock Grid.Row="0" Grid.Column="2" VerticalAlignment="Bottom" Text="{Binding DownloadSpeed}"
|
<TextBlock Grid.Row="0" Grid.Column="2" VerticalAlignment="Bottom" Text="{Binding DownloadSpeed}"
|
||||||
Opacity="0.8" TextWrapping="Wrap" />
|
Opacity="0.8" TextWrapping="Wrap" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,13 @@
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<Image Grid.Column="0" Margin="10" Source="{Binding SelectedSeries.ThumbnailImage}" Width="240"
|
<Button Grid.Column="0" Margin="10" Command="{Binding OpenSeriesDetails}"
|
||||||
Height="360">
|
BorderThickness="0"
|
||||||
</Image>
|
Background="Transparent"
|
||||||
|
Padding="0">
|
||||||
|
<Image Source="{Binding SelectedSeries.ThumbnailImage}"
|
||||||
|
Width="240" Height="360"/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
|
||||||
<Grid Grid.Column="1">
|
<Grid Grid.Column="1">
|
||||||
|
|
@ -341,31 +345,36 @@
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel IsVisible="{Binding EditMode}">
|
<StackPanel IsVisible="{Binding EditMode}">
|
||||||
<Button Width="30" Height="30" Margin="0 0 10 0"
|
<Button Height="30" Margin="0 0 5 0"
|
||||||
BorderThickness="0"
|
BorderThickness="0"
|
||||||
IsVisible="{Binding SonarrConnected}"
|
IsVisible="{Binding SonarrConnected}"
|
||||||
Command="{Binding MatchSonarrSeries_Button}">
|
Command="{Binding MatchSonarrSeries_Button}">
|
||||||
<Grid>
|
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
|
||||||
<controls:ImageIcon Source="../Assets/sonarr.png" Width="25" Height="25" />
|
<controls:ImageIcon Source="../Assets/sonarr.png" Width="25" Height="25" />
|
||||||
<ToolTip.Tip>
|
<TextBlock Text="Match" VerticalAlignment="Center" />
|
||||||
<TextBlock Text="Match Sonarr Series" FontSize="15" />
|
</StackPanel>
|
||||||
</ToolTip.Tip>
|
<ToolTip.Tip>
|
||||||
</Grid>
|
<TextBlock Text="Match Sonarr Series" FontSize="15" />
|
||||||
|
</ToolTip.Tip>
|
||||||
|
|
||||||
</Button>
|
</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel IsVisible="{Binding EditMode}">
|
<StackPanel IsVisible="{Binding EditMode}">
|
||||||
<Button Width="30" Height="30" Margin="0 0 10 0"
|
<Button Height="30" Margin="0 0 10 0"
|
||||||
BorderThickness="0"
|
BorderThickness="0"
|
||||||
IsVisible="{Binding SonarrConnected}"
|
IsVisible="{Binding SonarrConnected}"
|
||||||
Command="{Binding RefreshSonarrEpisodeMatch}">
|
Command="{Binding RefreshSonarrEpisodeMatch}">
|
||||||
<Grid>
|
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
|
||||||
<controls:ImageIcon Source="../Assets/sonarr.png" Width="25" Height="25" />
|
<controls:ImageIcon Source="../Assets/sonarr.png" Width="25" Height="25" />
|
||||||
<ToolTip.Tip>
|
<TextBlock Text="Refresh" VerticalAlignment="Center" />
|
||||||
<TextBlock Text="Refresh all Sonarr Episode matches" FontSize="15" />
|
</StackPanel>
|
||||||
</ToolTip.Tip>
|
<ToolTip.Tip>
|
||||||
</Grid>
|
<TextBlock Text="Refresh all Sonarr Episode matches" FontSize="15" />
|
||||||
|
</ToolTip.Tip>
|
||||||
|
|
||||||
</Button>
|
</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
|
|
||||||
205
CRD/Views/Utils/ContentDialogSeriesDetailsView.axaml
Normal file
205
CRD/Views/Utils/ContentDialogSeriesDetailsView.axaml
Normal 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>
|
||||||
35
CRD/Views/Utils/ContentDialogSeriesDetailsView.axaml.cs
Normal file
35
CRD/Views/Utils/ContentDialogSeriesDetailsView.axaml.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue