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