mirror of
https://github.com/Crunchy-DL/Crunchy-Downloader.git
synced 2026-03-11 17:45:39 +00:00
Compare commits
3 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
199ff9f96c | ||
|
|
985fd9c00f | ||
|
|
973c45ce5c |
68 changed files with 2424 additions and 808 deletions
|
|
@ -4,12 +4,18 @@ using Avalonia.Controls.ApplicationLifetimes;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using CRD.ViewModels;
|
using CRD.ViewModels;
|
||||||
using MainWindow = CRD.Views.MainWindow;
|
using MainWindow = CRD.Views.MainWindow;
|
||||||
using System.Linq;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Platform;
|
||||||
using CRD.Downloader;
|
using CRD.Downloader;
|
||||||
|
using CRD.Downloader.Crunchyroll;
|
||||||
|
|
||||||
namespace CRD;
|
namespace CRD;
|
||||||
|
|
||||||
public partial class App : Application{
|
public class App : Application{
|
||||||
|
|
||||||
|
private TrayIcon? trayIcon;
|
||||||
|
private bool exitRequested;
|
||||||
|
|
||||||
public override void Initialize(){
|
public override void Initialize(){
|
||||||
AvaloniaXamlLoader.Load(this);
|
AvaloniaXamlLoader.Load(this);
|
||||||
}
|
}
|
||||||
|
|
@ -21,11 +27,21 @@ public partial class App : Application{
|
||||||
var manager = ProgramManager.Instance;
|
var manager = ProgramManager.Instance;
|
||||||
|
|
||||||
if (!isHeadless){
|
if (!isHeadless){
|
||||||
desktop.MainWindow = new MainWindow{
|
desktop.ShutdownMode = ShutdownMode.OnExplicitShutdown;
|
||||||
|
|
||||||
|
var mainWindow = new MainWindow{
|
||||||
DataContext = new MainWindowViewModel(manager),
|
DataContext = new MainWindowViewModel(manager),
|
||||||
};
|
};
|
||||||
|
|
||||||
desktop.MainWindow.Opened += (_, _) => { manager.SetBackgroundImage(); };
|
mainWindow.Opened += (_, _) => { manager.SetBackgroundImage(); };
|
||||||
|
desktop.Exit += (_, _) => { manager.StopBackgroundTasks(); };
|
||||||
|
|
||||||
|
if (!CrunchyrollManager.Instance.CrunOptions.StartMinimizedToTray){
|
||||||
|
desktop.MainWindow = mainWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetupTrayIcon(desktop, mainWindow, manager);
|
||||||
|
SetupMinimizeToTray(desktop,mainWindow,manager);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -35,5 +51,80 @@ public partial class App : Application{
|
||||||
base.OnFrameworkInitializationCompleted();
|
base.OnFrameworkInitializationCompleted();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void SetupTrayIcon(IClassicDesktopStyleApplicationLifetime desktop, Window mainWindow, ProgramManager programManager){
|
||||||
|
trayIcon = new TrayIcon{
|
||||||
|
ToolTipText = "CRD",
|
||||||
|
Icon = new WindowIcon(AssetLoader.Open(new Uri("avares://CRD/Assets/app_icon.ico"))),
|
||||||
|
IsVisible = CrunchyrollManager.Instance.CrunOptions.TrayIconEnabled,
|
||||||
|
};
|
||||||
|
|
||||||
|
var menu = new NativeMenu();
|
||||||
|
|
||||||
|
var exitItem = new NativeMenuItem("Exit");
|
||||||
|
exitItem.Click += (_, _) => {
|
||||||
|
exitRequested = true;
|
||||||
|
trayIcon?.Dispose();
|
||||||
|
desktop.Shutdown();
|
||||||
|
};
|
||||||
|
menu.Items.Add(exitItem);
|
||||||
|
|
||||||
|
trayIcon.Menu = menu;
|
||||||
|
|
||||||
|
trayIcon.Clicked += (_, _) => ShowFromTray(desktop, mainWindow);
|
||||||
|
|
||||||
|
TrayIcon.SetIcons(this, new TrayIcons{ trayIcon });
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetupMinimizeToTray(IClassicDesktopStyleApplicationLifetime desktop, Window window , ProgramManager programManager){
|
||||||
|
window.Closing += (_, e) => {
|
||||||
|
if (exitRequested)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (CrunchyrollManager.Instance.CrunOptions is{ MinimizeToTrayOnClose: true, TrayIconEnabled: true }){
|
||||||
|
HideToTray(window);
|
||||||
|
e.Cancel = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
exitRequested = true;
|
||||||
|
trayIcon?.Dispose();
|
||||||
|
desktop.Shutdown();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.GetObservable(Window.WindowStateProperty).Subscribe(state => {
|
||||||
|
if (CrunchyrollManager.Instance.CrunOptions is{ TrayIconEnabled: true, MinimizeToTray: true } && state == WindowState.Minimized)
|
||||||
|
HideToTray(window);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void HideToTray(Window window){
|
||||||
|
window.ShowInTaskbar = false;
|
||||||
|
window.Hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowFromTray(IClassicDesktopStyleApplicationLifetime desktop, Window mainWindow){
|
||||||
|
desktop.MainWindow ??= mainWindow;
|
||||||
|
RestoreFromTray(mainWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RestoreFromTray(Window window){
|
||||||
|
window.ShowInTaskbar = true;
|
||||||
|
window.Show();
|
||||||
|
|
||||||
|
if (window.WindowState == WindowState.Minimized)
|
||||||
|
window.WindowState = WindowState.Normal;
|
||||||
|
|
||||||
|
window.Activate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetTrayIconVisible(bool enabled){
|
||||||
|
trayIcon?.IsVisible = enabled;
|
||||||
|
|
||||||
|
if (!enabled && ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop){
|
||||||
|
if (desktop.MainWindow is{ } w)
|
||||||
|
RestoreFromTray(w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -10,6 +10,7 @@ using System.Threading.Tasks;
|
||||||
using CRD.Downloader.Crunchyroll;
|
using CRD.Downloader.Crunchyroll;
|
||||||
using CRD.Downloader.Crunchyroll.Utils;
|
using CRD.Downloader.Crunchyroll.Utils;
|
||||||
using CRD.Utils;
|
using CRD.Utils;
|
||||||
|
using CRD.Utils.Files;
|
||||||
using CRD.Utils.Structs;
|
using CRD.Utils.Structs;
|
||||||
using CRD.Utils.Structs.History;
|
using CRD.Utils.Structs.History;
|
||||||
using CRD.Views;
|
using CRD.Views;
|
||||||
|
|
@ -234,8 +235,17 @@ public class CalendarManager{
|
||||||
var newEpisodesBase = await CrunchyrollManager.Instance.CrEpisode.GetNewEpisodes(CrunchyrollManager.Instance.CrunOptions.HistoryLang, 2000, null, true);
|
var newEpisodesBase = await CrunchyrollManager.Instance.CrEpisode.GetNewEpisodes(CrunchyrollManager.Instance.CrunOptions.HistoryLang, 2000, null, true);
|
||||||
|
|
||||||
if (newEpisodesBase is{ Data.Count: > 0 }){
|
if (newEpisodesBase is{ Data.Count: > 0 }){
|
||||||
var newEpisodes = newEpisodesBase.Data;
|
var newEpisodes = newEpisodesBase.Data ?? [];
|
||||||
|
|
||||||
|
if (CrunchyrollManager.Instance.CrunOptions.UpdateHistoryFromCalendar){
|
||||||
|
try{
|
||||||
|
await CrunchyrollManager.Instance.History.UpdateWithEpisode(newEpisodes);
|
||||||
|
CfgManager.UpdateHistoryFile();
|
||||||
|
} catch (Exception e){
|
||||||
|
Console.Error.WriteLine("Failed to update History from calendar");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//EpisodeAirDate
|
//EpisodeAirDate
|
||||||
foreach (var crBrowseEpisode in newEpisodes){
|
foreach (var crBrowseEpisode in newEpisodes){
|
||||||
bool filtered = false;
|
bool filtered = false;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,15 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Specialized;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Web;
|
||||||
using CRD.Utils;
|
using CRD.Utils;
|
||||||
using CRD.Utils.Files;
|
using CRD.Utils.Files;
|
||||||
using CRD.Utils.Structs;
|
using CRD.Utils.Structs;
|
||||||
|
|
@ -15,9 +21,12 @@ using ReactiveUI;
|
||||||
namespace CRD.Downloader.Crunchyroll;
|
namespace CRD.Downloader.Crunchyroll;
|
||||||
|
|
||||||
public class CrAuth(CrunchyrollManager crunInstance, CrAuthSettings authSettings){
|
public class CrAuth(CrunchyrollManager crunInstance, CrAuthSettings authSettings){
|
||||||
|
|
||||||
public CrToken? Token;
|
public CrToken? Token;
|
||||||
public CrProfile Profile = new();
|
public CrProfile Profile = new();
|
||||||
|
public Subscription? Subscription{ get; set; }
|
||||||
|
public CrMultiProfile MultiProfile = new();
|
||||||
|
|
||||||
|
public CrunchyrollEndpoints EndpointEnum = CrunchyrollEndpoints.Unknown;
|
||||||
|
|
||||||
public CrAuthSettings AuthSettings = authSettings;
|
public CrAuthSettings AuthSettings = authSettings;
|
||||||
|
|
||||||
|
|
@ -32,7 +41,6 @@ public class CrAuth(CrunchyrollManager crunInstance, CrAuthSettings authSettings
|
||||||
PreferredContentSubtitleLanguage = crunInstance.DefaultLocale,
|
PreferredContentSubtitleLanguage = crunInstance.DefaultLocale,
|
||||||
HasPremium = false,
|
HasPremium = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetTokenFilePath(){
|
private string GetTokenFilePath(){
|
||||||
|
|
@ -49,9 +57,10 @@ public class CrAuth(CrunchyrollManager crunInstance, CrAuthSettings authSettings
|
||||||
case "console/ps5":
|
case "console/ps5":
|
||||||
case "console/xbox_one":
|
case "console/xbox_one":
|
||||||
return CfgManager.PathCrToken.Replace(".json", "_console.json");
|
return CfgManager.PathCrToken.Replace(".json", "_console.json");
|
||||||
|
case "---":
|
||||||
|
return CfgManager.PathCrToken.Replace(".json", "_guest.json");
|
||||||
default:
|
default:
|
||||||
return CfgManager.PathCrToken;
|
return CfgManager.PathCrToken;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,12 +74,14 @@ public class CrAuth(CrunchyrollManager crunInstance, CrAuthSettings authSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetETPCookie(string refreshToken){
|
public void SetETPCookie(string refreshToken){
|
||||||
HttpClientReq.Instance.AddCookie(".crunchyroll.com", new Cookie("etp_rt", refreshToken),cookieStore);
|
HttpClientReq.Instance.AddCookie(".crunchyroll.com", new Cookie("etp_rt", refreshToken), cookieStore);
|
||||||
HttpClientReq.Instance.AddCookie(".crunchyroll.com", new Cookie("c_locale", "en-US"),cookieStore);
|
HttpClientReq.Instance.AddCookie(".crunchyroll.com", new Cookie("c_locale", "en-US"), cookieStore);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AuthAnonymous(){
|
public async Task AuthAnonymous(){
|
||||||
string uuid = Guid.NewGuid().ToString();
|
string uuid = string.IsNullOrEmpty(Token?.device_id) ? Guid.NewGuid().ToString() : Token.device_id;
|
||||||
|
|
||||||
|
Subscription = new Subscription();
|
||||||
|
|
||||||
var formData = new Dictionary<string, string>{
|
var formData = new Dictionary<string, string>{
|
||||||
{ "grant_type", "client_id" },
|
{ "grant_type", "client_id" },
|
||||||
|
|
@ -163,10 +174,10 @@ public class CrAuth(CrunchyrollManager crunInstance, CrAuthSettings authSettings
|
||||||
} else{
|
} else{
|
||||||
if (response.ResponseContent.Contains("invalid_credentials")){
|
if (response.ResponseContent.Contains("invalid_credentials")){
|
||||||
MessageBus.Current.SendMessage(new ToastMessage($"Failed to login - because of invalid login credentials", ToastType.Error, 5));
|
MessageBus.Current.SendMessage(new ToastMessage($"Failed to login - because of invalid login credentials", ToastType.Error, 5));
|
||||||
} else if (response.ResponseContent.Contains("<title>Just a moment...</title>") ||
|
} else if (response.ResponseContent.Contains("<title>Just a moment...</title>") ||
|
||||||
response.ResponseContent.Contains("<title>Access denied</title>") ||
|
response.ResponseContent.Contains("<title>Access denied</title>") ||
|
||||||
response.ResponseContent.Contains("<title>Attention Required! | Cloudflare</title>") ||
|
response.ResponseContent.Contains("<title>Attention Required! | Cloudflare</title>") ||
|
||||||
response.ResponseContent.Trim().Equals("error code: 1020") ||
|
response.ResponseContent.Trim().Equals("error code: 1020") ||
|
||||||
response.ResponseContent.IndexOf("<title>DDOS-GUARD</title>", StringComparison.OrdinalIgnoreCase) > -1){
|
response.ResponseContent.IndexOf("<title>DDOS-GUARD</title>", StringComparison.OrdinalIgnoreCase) > -1){
|
||||||
MessageBus.Current.SendMessage(new ToastMessage($"Failed to login - Cloudflare error try to change to BetaAPI in settings", ToastType.Error, 5));
|
MessageBus.Current.SendMessage(new ToastMessage($"Failed to login - Cloudflare error try to change to BetaAPI in settings", ToastType.Error, 5));
|
||||||
} else{
|
} else{
|
||||||
|
|
@ -179,7 +190,64 @@ public class CrAuth(CrunchyrollManager crunInstance, CrAuthSettings authSettings
|
||||||
if (Token?.refresh_token != null){
|
if (Token?.refresh_token != null){
|
||||||
SetETPCookie(Token.refresh_token);
|
SetETPCookie(Token.refresh_token);
|
||||||
|
|
||||||
await GetProfile();
|
await GetMultiProfile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ChangeProfile(string profileId){
|
||||||
|
if (Token?.access_token == null && Token?.refresh_token == null ||
|
||||||
|
Token.access_token != null && Token.refresh_token == null){
|
||||||
|
await AuthAnonymous();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Profile.Username == "???"){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(profileId) || Token?.refresh_token == null){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string uuid = string.IsNullOrEmpty(Token.device_id) ? Guid.NewGuid().ToString() : Token.device_id;
|
||||||
|
|
||||||
|
SetETPCookie(Token.refresh_token);
|
||||||
|
|
||||||
|
var formData = new Dictionary<string, string>{
|
||||||
|
{ "grant_type", "refresh_token_profile_id" },
|
||||||
|
{ "profile_id", profileId },
|
||||||
|
{ "device_id", uuid },
|
||||||
|
{ "device_type", AuthSettings.Device_type },
|
||||||
|
};
|
||||||
|
|
||||||
|
var requestContent = new FormUrlEncodedContent(formData);
|
||||||
|
|
||||||
|
var crunchyAuthHeaders = new Dictionary<string, string>{
|
||||||
|
{ "Authorization", AuthSettings.Authorization },
|
||||||
|
{ "User-Agent", AuthSettings.UserAgent }
|
||||||
|
};
|
||||||
|
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Auth){
|
||||||
|
Content = requestContent
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var header in crunchyAuthHeaders){
|
||||||
|
request.Headers.Add(header.Key, header.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Token?.refresh_token != null) SetETPCookie(Token.refresh_token);
|
||||||
|
|
||||||
|
var response = await HttpClientReq.Instance.SendHttpRequest(request, false, cookieStore);
|
||||||
|
|
||||||
|
if (response.IsOk){
|
||||||
|
JsonTokenToFileAndVariable(response.ResponseContent, uuid);
|
||||||
|
if (Token?.refresh_token != null){
|
||||||
|
SetETPCookie(Token.refresh_token);
|
||||||
|
}
|
||||||
|
|
||||||
|
await GetMultiProfile();
|
||||||
|
|
||||||
|
} else{
|
||||||
|
Console.Error.WriteLine("Refresh Token Auth Failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -199,42 +267,69 @@ public class CrAuth(CrunchyrollManager crunInstance, CrAuthSettings authSettings
|
||||||
if (profileTemp != null){
|
if (profileTemp != null){
|
||||||
Profile = profileTemp;
|
Profile = profileTemp;
|
||||||
|
|
||||||
var requestSubs = HttpClientReq.CreateRequestMessage(ApiUrls.Subscription + Token.account_id, HttpMethod.Get, true, Token.access_token, null);
|
await GetSubscription();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var responseSubs = await HttpClientReq.Instance.SendHttpRequest(requestSubs);
|
private async Task GetSubscription(){
|
||||||
|
var requestSubs = HttpClientReq.CreateRequestMessage(ApiUrls.Subscription + Token.account_id, HttpMethod.Get, true, Token.access_token, null);
|
||||||
|
|
||||||
if (responseSubs.IsOk){
|
var responseSubs = await HttpClientReq.Instance.SendHttpRequest(requestSubs);
|
||||||
var subsc = Helpers.Deserialize<Subscription>(responseSubs.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
|
|
||||||
Profile.Subscription = subsc;
|
if (responseSubs.IsOk){
|
||||||
if (subsc is{ SubscriptionProducts:{ Count: 0 }, ThirdPartySubscriptionProducts.Count: > 0 }){
|
var subsc = Helpers.Deserialize<Subscription>(responseSubs.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
|
||||||
var thirdPartySub = subsc.ThirdPartySubscriptionProducts.First();
|
Subscription = subsc;
|
||||||
var expiration = thirdPartySub.InGrace ? thirdPartySub.InGraceExpirationDate : thirdPartySub.ExpirationDate;
|
if (subsc is{ SubscriptionProducts:{ Count: 0 }, ThirdPartySubscriptionProducts.Count: > 0 }){
|
||||||
var remaining = expiration - DateTime.Now;
|
var thirdPartySub = subsc.ThirdPartySubscriptionProducts.First();
|
||||||
Profile.HasPremium = true;
|
var expiration = thirdPartySub.InGrace ? thirdPartySub.InGraceExpirationDate : thirdPartySub.ExpirationDate;
|
||||||
if (Profile.Subscription != null){
|
var remaining = expiration - DateTime.Now;
|
||||||
Profile.Subscription.IsActive = remaining > TimeSpan.Zero;
|
Profile.HasPremium = true;
|
||||||
Profile.Subscription.NextRenewalDate = expiration;
|
if (Subscription != null){
|
||||||
}
|
Subscription.IsActive = remaining > TimeSpan.Zero;
|
||||||
} else if (subsc is{ SubscriptionProducts:{ Count: 0 }, NonrecurringSubscriptionProducts.Count: > 0 }){
|
Subscription.NextRenewalDate = expiration;
|
||||||
var nonRecurringSub = subsc.NonrecurringSubscriptionProducts.First();
|
|
||||||
var remaining = nonRecurringSub.EndDate - DateTime.Now;
|
|
||||||
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 }){
|
|
||||||
Profile.HasPremium = true;
|
|
||||||
} else if (subsc is{ SubscriptionProducts.Count: > 0 }){
|
|
||||||
Profile.HasPremium = true;
|
|
||||||
} else{
|
|
||||||
Profile.HasPremium = false;
|
|
||||||
Console.Error.WriteLine($"No subscription available:\n {JsonConvert.SerializeObject(subsc, Formatting.Indented)} ");
|
|
||||||
}
|
|
||||||
} else{
|
|
||||||
Profile.HasPremium = false;
|
|
||||||
Console.Error.WriteLine("Failed to check premium subscription status");
|
|
||||||
}
|
}
|
||||||
|
} else if (subsc is{ SubscriptionProducts:{ Count: 0 }, NonrecurringSubscriptionProducts.Count: > 0 }){
|
||||||
|
var nonRecurringSub = subsc.NonrecurringSubscriptionProducts.First();
|
||||||
|
var remaining = nonRecurringSub.EndDate - DateTime.Now;
|
||||||
|
Profile.HasPremium = true;
|
||||||
|
if (Subscription != null){
|
||||||
|
Subscription.IsActive = remaining > TimeSpan.Zero;
|
||||||
|
Subscription.NextRenewalDate = nonRecurringSub.EndDate;
|
||||||
|
}
|
||||||
|
} else if (subsc is{ SubscriptionProducts:{ Count: 0 }, FunimationSubscriptions.Count: > 0 }){
|
||||||
|
Profile.HasPremium = true;
|
||||||
|
} else if (subsc is{ SubscriptionProducts.Count: > 0 }){
|
||||||
|
Profile.HasPremium = true;
|
||||||
|
} else{
|
||||||
|
Profile.HasPremium = false;
|
||||||
|
Console.Error.WriteLine($"No subscription available:\n {JsonConvert.SerializeObject(subsc, Formatting.Indented)} ");
|
||||||
|
}
|
||||||
|
} else{
|
||||||
|
Profile.HasPremium = false;
|
||||||
|
Console.Error.WriteLine("Failed to check premium subscription status");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task GetMultiProfile(){
|
||||||
|
if (Token?.access_token == null){
|
||||||
|
Console.Error.WriteLine("Missing Access Token");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = HttpClientReq.CreateRequestMessage(ApiUrls.MultiProfile, HttpMethod.Get, true, Token?.access_token);
|
||||||
|
|
||||||
|
var response = await HttpClientReq.Instance.SendHttpRequest(request, false, cookieStore);
|
||||||
|
|
||||||
|
if (response.IsOk){
|
||||||
|
MultiProfile = Helpers.Deserialize<CrMultiProfile>(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings) ?? new CrMultiProfile();
|
||||||
|
|
||||||
|
var selectedProfile = MultiProfile.Profiles.FirstOrDefault( e => e.IsSelected);
|
||||||
|
if (selectedProfile != null) Profile = selectedProfile;
|
||||||
|
|
||||||
|
await GetSubscription();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -279,33 +374,41 @@ public class CrAuth(CrunchyrollManager crunInstance, CrAuthSettings authSettings
|
||||||
|
|
||||||
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||||
|
|
||||||
if (response.ResponseContent.Contains("<title>Just a moment...</title>") ||
|
if (response.ResponseContent.Contains("<title>Just a moment...</title>") ||
|
||||||
response.ResponseContent.Contains("<title>Access denied</title>") ||
|
response.ResponseContent.Contains("<title>Access denied</title>") ||
|
||||||
response.ResponseContent.Contains("<title>Attention Required! | Cloudflare</title>") ||
|
response.ResponseContent.Contains("<title>Attention Required! | Cloudflare</title>") ||
|
||||||
response.ResponseContent.Trim().Equals("error code: 1020") ||
|
response.ResponseContent.Trim().Equals("error code: 1020") ||
|
||||||
response.ResponseContent.IndexOf("<title>DDOS-GUARD</title>", StringComparison.OrdinalIgnoreCase) > -1){
|
response.ResponseContent.IndexOf("<title>DDOS-GUARD</title>", StringComparison.OrdinalIgnoreCase) > -1){
|
||||||
MessageBus.Current.SendMessage(new ToastMessage($"Failed to login - Cloudflare error try to change to BetaAPI in settings", ToastType.Error, 5));
|
MessageBus.Current.SendMessage(new ToastMessage($"Failed to login - Cloudflare error try to change to BetaAPI in settings", ToastType.Error, 5));
|
||||||
Console.Error.WriteLine($"Failed to login - Cloudflare error try to change to BetaAPI in settings");
|
Console.Error.WriteLine($"Failed to login - Cloudflare error try to change to BetaAPI in settings");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.IsOk){
|
if (response.IsOk){
|
||||||
JsonTokenToFileAndVariable(response.ResponseContent, uuid);
|
JsonTokenToFileAndVariable(response.ResponseContent, uuid);
|
||||||
|
|
||||||
if (Token?.refresh_token != null){
|
if (Token?.refresh_token != null){
|
||||||
SetETPCookie(Token.refresh_token);
|
SetETPCookie(Token.refresh_token);
|
||||||
|
|
||||||
await GetProfile();
|
await GetMultiProfile();
|
||||||
}
|
}
|
||||||
} else{
|
} else{
|
||||||
Console.Error.WriteLine("Token Auth Failed");
|
Console.Error.WriteLine("Token Auth Failed");
|
||||||
await AuthAnonymous();
|
await AuthAnonymous();
|
||||||
|
|
||||||
MainWindow.Instance.ShowError("Login failed. Please check the log for more details.");
|
MainWindow.Instance.ShowError("Login failed. Please check the log for more details.");
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RefreshToken(bool needsToken){
|
public async Task RefreshToken(bool needsToken){
|
||||||
|
if (EndpointEnum == CrunchyrollEndpoints.Guest){
|
||||||
|
if (Token != null && !(DateTime.Now > Token.expires)){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await AuthAnonymousFoxy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (Token?.access_token == null && Token?.refresh_token == null ||
|
if (Token?.access_token == null && Token?.refresh_token == null ||
|
||||||
Token.access_token != null && Token.refresh_token == null){
|
Token.access_token != null && Token.refresh_token == null){
|
||||||
await AuthAnonymous();
|
await AuthAnonymous();
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ using System.Net.Http;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Web;
|
using System.Web;
|
||||||
|
using CRD.Downloader.Crunchyroll.Utils;
|
||||||
using CRD.Utils;
|
using CRD.Utils;
|
||||||
using CRD.Utils.Files;
|
using CRD.Utils.Files;
|
||||||
using CRD.Utils.Structs;
|
using CRD.Utils.Structs;
|
||||||
|
|
@ -73,92 +74,102 @@ public class CrEpisode(){
|
||||||
|
|
||||||
public async Task<CrunchyRollEpisodeData> EpisodeData(CrunchyEpisode dlEpisode, bool updateHistory = false){
|
public async Task<CrunchyRollEpisodeData> EpisodeData(CrunchyEpisode dlEpisode, bool updateHistory = false){
|
||||||
bool serieshasversions = true;
|
bool serieshasversions = true;
|
||||||
|
var episode = new CrunchyRollEpisodeData();
|
||||||
// Dictionary<string, EpisodeAndLanguage> episodes = new Dictionary<string, EpisodeAndLanguage>();
|
|
||||||
|
|
||||||
CrunchyRollEpisodeData episode = new CrunchyRollEpisodeData();
|
|
||||||
|
|
||||||
if (crunInstance.CrunOptions.History && updateHistory){
|
if (crunInstance.CrunOptions.History && updateHistory){
|
||||||
await crunInstance.History.UpdateWithEpisodeList([dlEpisode]);
|
await crunInstance.History.UpdateWithEpisodeList([dlEpisode]);
|
||||||
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == dlEpisode.SeriesId);
|
var historySeries = crunInstance.HistoryList
|
||||||
|
.FirstOrDefault(series => series.SeriesId == dlEpisode.SeriesId);
|
||||||
|
|
||||||
if (historySeries != null){
|
if (historySeries != null){
|
||||||
CrunchyrollManager.Instance.History.MatchHistorySeriesWithSonarr(false);
|
crunInstance.History.MatchHistorySeriesWithSonarr(false);
|
||||||
await CrunchyrollManager.Instance.History.MatchHistoryEpisodesWithSonarr(false, historySeries);
|
await crunInstance.History.MatchHistoryEpisodesWithSonarr(false, historySeries);
|
||||||
CfgManager.UpdateHistoryFile();
|
CfgManager.UpdateHistoryFile();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var seasonIdentifier = !string.IsNullOrEmpty(dlEpisode.Identifier) ? dlEpisode.Identifier.Split('|')[1] : $"S{dlEpisode.SeasonNumber}";
|
// initial key
|
||||||
episode.Key = $"{seasonIdentifier}E{dlEpisode.Episode ?? (dlEpisode.EpisodeNumber + "")}";
|
var seasonIdentifier = !string.IsNullOrEmpty(dlEpisode.Identifier)
|
||||||
episode.EpisodeAndLanguages = new EpisodeAndLanguage{
|
? dlEpisode.Identifier.Split('|')[1]
|
||||||
Items = new List<CrunchyEpisode>(),
|
: $"S{dlEpisode.SeasonNumber}";
|
||||||
Langs = new List<LanguageItem>()
|
|
||||||
};
|
|
||||||
|
|
||||||
|
episode.Key = $"{seasonIdentifier}E{dlEpisode.Episode ?? (dlEpisode.EpisodeNumber + "")}";
|
||||||
|
|
||||||
|
episode.EpisodeAndLanguages = new EpisodeAndLanguage();
|
||||||
|
|
||||||
|
// Build Variants
|
||||||
if (dlEpisode.Versions != null){
|
if (dlEpisode.Versions != null){
|
||||||
foreach (var version in dlEpisode.Versions){
|
foreach (var version in dlEpisode.Versions){
|
||||||
// Ensure there is only one of the same language
|
var lang = Array.Find(Languages.languages, a => a.CrLocale == version.AudioLocale)
|
||||||
if (episode.EpisodeAndLanguages.Langs.All(a => a.CrLocale != version.AudioLocale)){
|
?? Languages.DEFAULT_lang;
|
||||||
// Push to arrays if there are no duplicates of the same language
|
|
||||||
episode.EpisodeAndLanguages.Items.Add(dlEpisode);
|
episode.EpisodeAndLanguages.AddUnique(dlEpisode, lang);
|
||||||
episode.EpisodeAndLanguages.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == version.AudioLocale) ?? Languages.DEFAULT_lang);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else{
|
} else{
|
||||||
// Episode didn't have versions, mark it as such to be logged.
|
|
||||||
serieshasversions = false;
|
serieshasversions = false;
|
||||||
// Ensure there is only one of the same language
|
|
||||||
if (episode.EpisodeAndLanguages.Langs.All(a => a.CrLocale != dlEpisode.AudioLocale)){
|
var lang = Array.Find(Languages.languages, a => a.CrLocale == dlEpisode.AudioLocale)
|
||||||
// Push to arrays if there are no duplicates of the same language
|
?? Languages.DEFAULT_lang;
|
||||||
episode.EpisodeAndLanguages.Items.Add(dlEpisode);
|
|
||||||
episode.EpisodeAndLanguages.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == dlEpisode.AudioLocale) ?? Languages.DEFAULT_lang);
|
episode.EpisodeAndLanguages.AddUnique(dlEpisode, lang);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (episode.EpisodeAndLanguages.Variants.Count == 0)
|
||||||
|
return episode;
|
||||||
|
|
||||||
int specialIndex = 1;
|
var baseEp = episode.EpisodeAndLanguages.Variants[0].Item;
|
||||||
int epIndex = 1;
|
|
||||||
|
|
||||||
|
var isSpecial = baseEp.IsSpecialEpisode();
|
||||||
|
|
||||||
var isSpecial = !Regex.IsMatch(episode.EpisodeAndLanguages.Items[0].Episode ?? string.Empty, @"^\d+(\.\d+)?$"); // Checking if the episode is not a number (i.e., special).
|
|
||||||
string newKey;
|
string newKey;
|
||||||
if (isSpecial && !string.IsNullOrEmpty(episode.EpisodeAndLanguages.Items[0].Episode)){
|
if (isSpecial && !string.IsNullOrEmpty(baseEp.Episode)){
|
||||||
newKey = episode.EpisodeAndLanguages.Items[0].Episode ?? "SP" + (specialIndex + " " + episode.EpisodeAndLanguages.Items[0].Id);
|
newKey = baseEp.Episode;
|
||||||
} else{
|
} else{
|
||||||
newKey = $"{(isSpecial ? "SP" : 'E')}{(isSpecial ? (specialIndex + " " + episode.EpisodeAndLanguages.Items[0].Id) : episode.EpisodeAndLanguages.Items[0].Episode ?? epIndex + "")}";
|
var epPart = baseEp.Episode ?? (baseEp.EpisodeNumber?.ToString() ?? "1");
|
||||||
|
newKey = isSpecial
|
||||||
|
? $"SP{epPart} {baseEp.Id}"
|
||||||
|
: $"E{epPart}";
|
||||||
}
|
}
|
||||||
|
|
||||||
episode.Key = newKey;
|
episode.Key = newKey;
|
||||||
|
|
||||||
var seasonTitle = episode.EpisodeAndLanguages.Items.FirstOrDefault(a => !Regex.IsMatch(a.SeasonTitle, @"\(\w+ Dub\)"))?.SeasonTitle
|
var seasonTitle =
|
||||||
?? Regex.Replace(episode.EpisodeAndLanguages.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd();
|
episode.EpisodeAndLanguages.Variants
|
||||||
|
.Select(v => v.Item.SeasonTitle)
|
||||||
|
.FirstOrDefault(t => !DownloadQueueItemFactory.HasDubSuffix(t))
|
||||||
|
?? DownloadQueueItemFactory.StripDubSuffix(baseEp.SeasonTitle);
|
||||||
|
|
||||||
var title = episode.EpisodeAndLanguages.Items[0].Title;
|
var title = baseEp.Title;
|
||||||
var seasonNumber = Helpers.ExtractNumberAfterS(episode.EpisodeAndLanguages.Items[0].Identifier) ?? episode.EpisodeAndLanguages.Items[0].SeasonNumber.ToString();
|
var seasonNumber = baseEp.GetSeasonNum();
|
||||||
|
|
||||||
var languages = episode.EpisodeAndLanguages.Items.Select((a, index) =>
|
var languages = episode.EpisodeAndLanguages.Variants
|
||||||
$"{(a.IsPremiumOnly ? "+ " : "")}{episode.EpisodeAndLanguages.Langs.ElementAtOrDefault(index)?.Name ?? "Unknown"}").ToArray(); //☆
|
.Select(v => $"{(v.Item.IsPremiumOnly ? "+ " : "")}{v.Lang?.Name ?? "Unknown"}")
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
Console.WriteLine($"[{episode.Key}] {seasonTitle} - Season {seasonNumber} - {title} [{string.Join(", ", languages)}]");
|
Console.WriteLine($"[{episode.Key}] {seasonTitle} - Season {seasonNumber} - {title} [{string.Join(", ", languages)}]");
|
||||||
|
|
||||||
|
if (!serieshasversions)
|
||||||
if (!serieshasversions){
|
|
||||||
Console.WriteLine("Couldn\'t find versions on episode, added languages with language array.");
|
Console.WriteLine("Couldn\'t find versions on episode, added languages with language array.");
|
||||||
}
|
|
||||||
|
|
||||||
return episode;
|
return episode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public CrunchyEpMeta EpisodeMeta(CrunchyRollEpisodeData episodeP, List<string> dubLang){
|
public CrunchyEpMeta EpisodeMeta(CrunchyRollEpisodeData episodeP, List<string> dubLang){
|
||||||
// var ret = new Dictionary<string, CrunchyEpMeta>();
|
CrunchyEpMeta? retMeta = null;
|
||||||
|
|
||||||
var retMeta = new CrunchyEpMeta();
|
var epNum = episodeP.Key.StartsWith('E') ? episodeP.Key[1..] : episodeP.Key;
|
||||||
|
var hslang = crunInstance.CrunOptions.Hslang;
|
||||||
|
|
||||||
|
var selectedDubs = dubLang
|
||||||
|
.Where(d => episodeP.EpisodeAndLanguages.Variants.Any(v => v.Lang.CrLocale == d))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
for (int index = 0; index < episodeP.EpisodeAndLanguages.Items.Count; index++){
|
foreach (var v in episodeP.EpisodeAndLanguages.Variants){
|
||||||
var item = episodeP.EpisodeAndLanguages.Items[index];
|
var item = v.Item;
|
||||||
|
var lang = v.Lang;
|
||||||
|
|
||||||
if (!dubLang.Contains(episodeP.EpisodeAndLanguages.Langs[index].CrLocale))
|
if (!dubLang.Contains(lang.CrLocale))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
item.HideSeasonTitle = true;
|
item.HideSeasonTitle = true;
|
||||||
|
|
@ -173,67 +184,54 @@ public class CrEpisode(){
|
||||||
item.SeriesTitle = "NO_TITLE";
|
item.SeriesTitle = "NO_TITLE";
|
||||||
}
|
}
|
||||||
|
|
||||||
var epNum = episodeP.Key.StartsWith('E') ? episodeP.Key[1..] : episodeP.Key;
|
|
||||||
var images = (item.Images?.Thumbnail ?? [new List<Image>{ new(){ Source = "/notFound.jpg" } }]);
|
|
||||||
|
|
||||||
Regex dubPattern = new Regex(@"\(\w+ Dub\)");
|
|
||||||
|
|
||||||
var epMeta = new CrunchyEpMeta();
|
|
||||||
epMeta.Data = new List<CrunchyEpMetaData>{ new(){ MediaId = item.Id, Versions = item.Versions, IsSubbed = item.IsSubbed, IsDubbed = item.IsDubbed } };
|
|
||||||
epMeta.SeriesTitle = episodeP.EpisodeAndLanguages.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeriesTitle))?.SeriesTitle ??
|
|
||||||
Regex.Replace(episodeP.EpisodeAndLanguages.Items[0].SeriesTitle, @"\(\w+ Dub\)", "").TrimEnd();
|
|
||||||
epMeta.SeasonTitle = episodeP.EpisodeAndLanguages.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeasonTitle))?.SeasonTitle ??
|
|
||||||
Regex.Replace(episodeP.EpisodeAndLanguages.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd();
|
|
||||||
epMeta.EpisodeNumber = item.Episode;
|
|
||||||
epMeta.EpisodeTitle = item.Title;
|
|
||||||
epMeta.SeasonId = item.SeasonId;
|
|
||||||
epMeta.Season = Helpers.ExtractNumberAfterS(item.Identifier) ?? item.SeasonNumber + "";
|
|
||||||
epMeta.SeriesId = item.SeriesId;
|
|
||||||
epMeta.AbsolutEpisodeNumberE = epNum;
|
|
||||||
epMeta.Image = images.FirstOrDefault()?.FirstOrDefault()?.Source ?? string.Empty;
|
|
||||||
epMeta.ImageBig = images.FirstOrDefault()?.LastOrDefault()?.Source ?? string.Empty;
|
|
||||||
epMeta.DownloadProgress = new DownloadProgress(){
|
|
||||||
IsDownloading = false,
|
|
||||||
Done = false,
|
|
||||||
Error = false,
|
|
||||||
Percent = 0,
|
|
||||||
Time = 0,
|
|
||||||
DownloadSpeedBytes = 0
|
|
||||||
};
|
|
||||||
epMeta.AvailableSubs = item.SubtitleLocales;
|
|
||||||
epMeta.Description = item.Description;
|
|
||||||
epMeta.Hslang = CrunchyrollManager.Instance.CrunOptions.Hslang;
|
|
||||||
|
|
||||||
if (episodeP.EpisodeAndLanguages.Langs.Count > 0){
|
|
||||||
epMeta.SelectedDubs = dubLang
|
|
||||||
.Where(language => episodeP.EpisodeAndLanguages.Langs.Any(epLang => epLang.CrLocale == language))
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
var epMetaData = epMeta.Data[0];
|
|
||||||
if (!string.IsNullOrEmpty(item.StreamsLink)){
|
|
||||||
epMetaData.Playback = item.StreamsLink;
|
|
||||||
if (string.IsNullOrEmpty(item.Playback)){
|
|
||||||
item.Playback = item.StreamsLink;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (retMeta.Data is{ Count: > 0 }){
|
|
||||||
epMetaData.Lang = episodeP.EpisodeAndLanguages.Langs[index];
|
|
||||||
retMeta.Data.Add(epMetaData);
|
|
||||||
} else{
|
|
||||||
epMetaData.Lang = episodeP.EpisodeAndLanguages.Langs[index];
|
|
||||||
epMeta.Data[0] = epMetaData;
|
|
||||||
retMeta = epMeta;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// show ep
|
|
||||||
item.SeqId = epNum;
|
item.SeqId = epNum;
|
||||||
|
|
||||||
|
if (retMeta == null){
|
||||||
|
var seriesTitle = DownloadQueueItemFactory.CanonicalTitle(
|
||||||
|
episodeP.EpisodeAndLanguages.Variants.Select(x => (string?)x.Item.SeriesTitle));
|
||||||
|
|
||||||
|
var seasonTitle = DownloadQueueItemFactory.CanonicalTitle(
|
||||||
|
episodeP.EpisodeAndLanguages.Variants.Select(x => (string?)x.Item.SeasonTitle));
|
||||||
|
|
||||||
|
var (img, imgBig) = DownloadQueueItemFactory.GetThumbSmallBig(item.Images);
|
||||||
|
|
||||||
|
retMeta = DownloadQueueItemFactory.CreateShell(
|
||||||
|
service: StreamingService.Crunchyroll,
|
||||||
|
seriesTitle: seriesTitle,
|
||||||
|
seasonTitle: seasonTitle,
|
||||||
|
episodeNumber: item.Episode,
|
||||||
|
episodeTitle: item.GetEpisodeTitle(),
|
||||||
|
description: item.Description,
|
||||||
|
seriesId: item.SeriesId,
|
||||||
|
seasonId: item.SeasonId,
|
||||||
|
season: item.GetSeasonNum(),
|
||||||
|
absolutEpisodeNumberE: epNum,
|
||||||
|
image: img,
|
||||||
|
imageBig: imgBig,
|
||||||
|
hslang: hslang,
|
||||||
|
availableSubs: item.SubtitleLocales,
|
||||||
|
selectedDubs: selectedDubs
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var playback = item.Playback;
|
||||||
|
if (!string.IsNullOrEmpty(item.StreamsLink)){
|
||||||
|
playback = item.StreamsLink;
|
||||||
|
if (string.IsNullOrEmpty(item.Playback))
|
||||||
|
item.Playback = item.StreamsLink;
|
||||||
|
}
|
||||||
|
|
||||||
|
retMeta.Data.Add(DownloadQueueItemFactory.CreateVariant(
|
||||||
|
mediaId: item.Id,
|
||||||
|
lang: lang,
|
||||||
|
playback: playback,
|
||||||
|
versions: item.Versions,
|
||||||
|
isSubbed: item.IsSubbed,
|
||||||
|
isDubbed: item.IsDubbed
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return retMeta ?? new CrunchyEpMeta();
|
||||||
return retMeta;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<CrBrowseEpisodeBase?> GetNewEpisodes(string? crLocale, int requestAmount, DateTime? firstWeekDay = null, bool forcedLang = false){
|
public async Task<CrBrowseEpisodeBase?> GetNewEpisodes(string? crLocale, int requestAmount, DateTime? firstWeekDay = null, bool forcedLang = false){
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ using System.Net.Http;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Web;
|
using System.Web;
|
||||||
|
using CRD.Downloader.Crunchyroll.Utils;
|
||||||
using CRD.Utils;
|
using CRD.Utils;
|
||||||
using CRD.Utils.Files;
|
using CRD.Utils.Files;
|
||||||
using CRD.Utils.Structs;
|
using CRD.Utils.Structs;
|
||||||
|
|
@ -17,32 +18,44 @@ namespace CRD.Downloader.Crunchyroll;
|
||||||
public class CrSeries{
|
public class CrSeries{
|
||||||
private readonly CrunchyrollManager crunInstance = CrunchyrollManager.Instance;
|
private readonly CrunchyrollManager crunInstance = CrunchyrollManager.Instance;
|
||||||
|
|
||||||
public Dictionary<string, CrunchyEpMeta> ItemSelectMultiDub(Dictionary<string, EpisodeAndLanguage> eps, List<string> dubLang, bool? but, bool? all, List<string>? e){
|
public Dictionary<string, CrunchyEpMeta> ItemSelectMultiDub(Dictionary<string, EpisodeAndLanguage> eps, List<string> dubLang, bool? all, List<string>? e){
|
||||||
var ret = new Dictionary<string, CrunchyEpMeta>();
|
var ret = new Dictionary<string, CrunchyEpMeta>();
|
||||||
|
|
||||||
|
var hasPremium = crunInstance.CrAuthEndpoint1.Profile.HasPremium;
|
||||||
|
|
||||||
foreach (var kvp in eps){
|
var hslang = crunInstance.CrunOptions.Hslang;
|
||||||
var key = kvp.Key;
|
|
||||||
var episode = kvp.Value;
|
|
||||||
|
|
||||||
for (int index = 0; index < episode.Items.Count; index++){
|
bool ShouldInclude(string epNum) =>
|
||||||
var item = episode.Items[index];
|
all is true || (e != null && e.Contains(epNum));
|
||||||
|
|
||||||
if (item.IsPremiumOnly && !crunInstance.CrAuthEndpoint1.Profile.HasPremium){
|
foreach (var (key, episode) in eps){
|
||||||
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));
|
var epNum = key.StartsWith('E') ? key[1..] : key;
|
||||||
|
|
||||||
|
foreach (var v in episode.Variants){
|
||||||
|
var item = v.Item;
|
||||||
|
var lang = v.Lang;
|
||||||
|
|
||||||
|
item.SeqId = epNum;
|
||||||
|
|
||||||
|
if (item.IsPremiumOnly && !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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// history override
|
||||||
|
var effectiveDubs = dubLang;
|
||||||
if (crunInstance.CrunOptions.History){
|
if (crunInstance.CrunOptions.History){
|
||||||
var dubLangList = crunInstance.History.GetDubList(item.SeriesId, item.SeasonId);
|
var dubLangList = crunInstance.History.GetDubList(item.SeriesId, item.SeasonId);
|
||||||
if (dubLangList.Count > 0){
|
if (dubLangList.Count > 0)
|
||||||
dubLang = dubLangList;
|
effectiveDubs = dubLangList;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!dubLang.Contains(episode.Langs[index].CrLocale))
|
if (!effectiveDubs.Contains(lang.CrLocale))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
// season title fallbacks (same behavior)
|
||||||
item.HideSeasonTitle = true;
|
item.HideSeasonTitle = true;
|
||||||
if (string.IsNullOrEmpty(item.SeasonTitle) && !string.IsNullOrEmpty(item.SeriesTitle)){
|
if (string.IsNullOrEmpty(item.SeasonTitle) && !string.IsNullOrEmpty(item.SeriesTitle)){
|
||||||
item.SeasonTitle = item.SeriesTitle;
|
item.SeasonTitle = item.SeriesTitle;
|
||||||
|
|
@ -55,66 +68,65 @@ public class CrSeries{
|
||||||
item.SeriesTitle = "NO_TITLE";
|
item.SeriesTitle = "NO_TITLE";
|
||||||
}
|
}
|
||||||
|
|
||||||
var epNum = key.StartsWith('E') ? key[1..] : key;
|
// selection gate
|
||||||
var images = (item.Images?.Thumbnail ??[new List<Image>{ new(){ Source = "/notFound.jpg" } }]);
|
if (!ShouldInclude(epNum))
|
||||||
|
continue;
|
||||||
|
|
||||||
Regex dubPattern = new Regex(@"\(\w+ Dub\)");
|
// Create base queue item once per "key"
|
||||||
|
if (!ret.TryGetValue(key, out var qItem)){
|
||||||
|
var seriesTitle = DownloadQueueItemFactory.CanonicalTitle(
|
||||||
|
episode.Variants.Select(x => (string?)x.Item.SeriesTitle));
|
||||||
|
|
||||||
var epMeta = new CrunchyEpMeta();
|
var seasonTitle = DownloadQueueItemFactory.CanonicalTitle(
|
||||||
epMeta.Data = new List<CrunchyEpMetaData>{ new(){ MediaId = item.Id, Versions = item.Versions, IsSubbed = item.IsSubbed, IsDubbed = item.IsDubbed } };
|
episode.Variants.Select(x => (string?)x.Item.SeasonTitle));
|
||||||
epMeta.SeriesTitle = episode.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeriesTitle))?.SeriesTitle ?? Regex.Replace(episode.Items[0].SeriesTitle, @"\(\w+ Dub\)", "").TrimEnd();
|
|
||||||
epMeta.SeasonTitle = episode.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeasonTitle))?.SeasonTitle ?? Regex.Replace(episode.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd();
|
var (img, imgBig) = DownloadQueueItemFactory.GetThumbSmallBig(item.Images);
|
||||||
epMeta.EpisodeNumber = item.Episode;
|
|
||||||
epMeta.EpisodeTitle = item.Title;
|
var selectedDubs = effectiveDubs
|
||||||
epMeta.SeasonId = item.SeasonId;
|
.Where(d => episode.Variants.Any(x => x.Lang.CrLocale == d))
|
||||||
epMeta.Season = Helpers.ExtractNumberAfterS(item.Identifier) ?? item.SeasonNumber + "";
|
|
||||||
epMeta.SeriesId = item.SeriesId;
|
|
||||||
epMeta.AbsolutEpisodeNumberE = epNum;
|
|
||||||
epMeta.Image = images.FirstOrDefault()?.FirstOrDefault()?.Source ?? string.Empty;
|
|
||||||
epMeta.ImageBig = images.FirstOrDefault()?.LastOrDefault()?.Source ?? string.Empty;
|
|
||||||
epMeta.DownloadProgress = new DownloadProgress(){
|
|
||||||
IsDownloading = false,
|
|
||||||
Done = false,
|
|
||||||
Percent = 0,
|
|
||||||
Time = 0,
|
|
||||||
DownloadSpeedBytes = 0
|
|
||||||
};
|
|
||||||
epMeta.Hslang = CrunchyrollManager.Instance.CrunOptions.Hslang;
|
|
||||||
epMeta.Description = item.Description;
|
|
||||||
epMeta.AvailableSubs = item.SubtitleLocales;
|
|
||||||
if (episode.Langs.Count > 0){
|
|
||||||
epMeta.SelectedDubs = dubLang
|
|
||||||
.Where(language => episode.Langs.Any(epLang => epLang.CrLocale == language))
|
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
qItem = DownloadQueueItemFactory.CreateShell(
|
||||||
|
service: StreamingService.Crunchyroll,
|
||||||
|
seriesTitle: seriesTitle,
|
||||||
|
seasonTitle: seasonTitle,
|
||||||
|
episodeNumber: item.Episode,
|
||||||
|
episodeTitle: item.Title,
|
||||||
|
description: item.Description,
|
||||||
|
seriesId: item.SeriesId,
|
||||||
|
seasonId: item.SeasonId,
|
||||||
|
season: Helpers.ExtractNumberAfterS(item.Identifier) ?? item.SeasonNumber.ToString(),
|
||||||
|
absolutEpisodeNumberE: epNum,
|
||||||
|
image: img,
|
||||||
|
imageBig: imgBig,
|
||||||
|
hslang: hslang,
|
||||||
|
availableSubs: item.SubtitleLocales,
|
||||||
|
selectedDubs: selectedDubs
|
||||||
|
);
|
||||||
|
|
||||||
|
ret.Add(key, qItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// playback preference
|
||||||
var epMetaData = epMeta.Data[0];
|
var playback = item.Playback;
|
||||||
if (!string.IsNullOrEmpty(item.StreamsLink)){
|
if (!string.IsNullOrEmpty(item.StreamsLink)){
|
||||||
epMetaData.Playback = item.StreamsLink;
|
playback = item.StreamsLink;
|
||||||
if (string.IsNullOrEmpty(item.Playback)){
|
if (string.IsNullOrEmpty(item.Playback))
|
||||||
item.Playback = item.StreamsLink;
|
item.Playback = item.StreamsLink;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (all is true || e != null && e.Contains(epNum)){
|
// Add variant
|
||||||
if (ret.TryGetValue(key, out var epMe)){
|
ret[key].Data.Add(DownloadQueueItemFactory.CreateVariant(
|
||||||
epMetaData.Lang = episode.Langs[index];
|
mediaId: item.Id,
|
||||||
epMe.Data.Add(epMetaData);
|
lang: lang,
|
||||||
} else{
|
playback: playback,
|
||||||
epMetaData.Lang = episode.Langs[index];
|
versions: item.Versions,
|
||||||
epMeta.Data[0] = epMetaData;
|
isSubbed: item.IsSubbed,
|
||||||
ret.Add(key, epMeta);
|
isDubbed: item.IsDubbed
|
||||||
}
|
));
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// show ep
|
|
||||||
item.SeqId = epNum;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,64 +136,58 @@ public class CrSeries{
|
||||||
|
|
||||||
CrSeriesSearch? parsedSeries = await ParseSeriesById(id, crLocale, forcedLocale);
|
CrSeriesSearch? parsedSeries = await ParseSeriesById(id, crLocale, forcedLocale);
|
||||||
|
|
||||||
if (parsedSeries == null || parsedSeries.Data == null){
|
if (parsedSeries?.Data == null){
|
||||||
Console.Error.WriteLine("Parse Data Invalid");
|
Console.Error.WriteLine("Parse Data Invalid");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// var result = ParseSeriesResult(parsedSeries);
|
var episodes = new Dictionary<string, EpisodeAndLanguage>();
|
||||||
Dictionary<string, EpisodeAndLanguage> episodes = new Dictionary<string, EpisodeAndLanguage>();
|
|
||||||
|
|
||||||
if (crunInstance.CrunOptions.History){
|
if (crunInstance.CrunOptions.History)
|
||||||
_ = crunInstance.History.CrUpdateSeries(id, "");
|
_ = crunInstance.History.CrUpdateSeries(id, "");
|
||||||
}
|
|
||||||
|
|
||||||
var cachedSeasonId = "";
|
var cachedSeasonId = "";
|
||||||
var seasonData = new CrunchyEpisodeList();
|
var seasonData = new CrunchyEpisodeList();
|
||||||
|
|
||||||
foreach (var s in parsedSeries.Data){
|
foreach (var s in parsedSeries.Data){
|
||||||
if (data?.S != null && s.Id != data.S) continue;
|
if (data?.S != null && s.Id != data.S)
|
||||||
|
continue;
|
||||||
|
|
||||||
int fallbackIndex = 0;
|
int fallbackIndex = 0;
|
||||||
|
|
||||||
if (cachedSeasonId != s.Id){
|
if (cachedSeasonId != s.Id){
|
||||||
seasonData = await GetSeasonDataById(s.Id, forcedLocale ? crLocale : "");
|
seasonData = await GetSeasonDataById(s.Id, forcedLocale ? crLocale : "");
|
||||||
cachedSeasonId = s.Id;
|
cachedSeasonId = s.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (seasonData.Data != null){
|
if (seasonData.Data == null)
|
||||||
foreach (var episode in seasonData.Data){
|
continue;
|
||||||
// Prepare the episode array
|
|
||||||
EpisodeAndLanguage item;
|
|
||||||
|
|
||||||
|
foreach (var episode in seasonData.Data){
|
||||||
|
string episodeNum =
|
||||||
|
(episode.Episode != string.Empty ? episode.Episode : (episode.EpisodeNumber != null ? episode.EpisodeNumber + "" : $"F{fallbackIndex++}"))
|
||||||
|
?? string.Empty;
|
||||||
|
|
||||||
string episodeNum = (episode.Episode != String.Empty ? episode.Episode : (episode.EpisodeNumber != null ? episode.EpisodeNumber + "" : $"F{fallbackIndex++}")) ?? string.Empty;
|
var seasonIdentifier = !string.IsNullOrEmpty(s.Identifier)
|
||||||
|
? s.Identifier.Split('|')[1]
|
||||||
|
: $"S{episode.SeasonNumber}";
|
||||||
|
|
||||||
var seasonIdentifier = !string.IsNullOrEmpty(s.Identifier) ? s.Identifier.Split('|')[1] : $"S{episode.SeasonNumber}";
|
var episodeKey = $"{seasonIdentifier}E{episodeNum}";
|
||||||
var episodeKey = $"{seasonIdentifier}E{episodeNum}";
|
|
||||||
|
|
||||||
if (!episodes.ContainsKey(episodeKey)){
|
if (!episodes.TryGetValue(episodeKey, out var item)){
|
||||||
item = new EpisodeAndLanguage{
|
item = new EpisodeAndLanguage(); // must have Variants
|
||||||
Items = new List<CrunchyEpisode>(),
|
episodes[episodeKey] = item;
|
||||||
Langs = new List<LanguageItem>()
|
}
|
||||||
};
|
|
||||||
episodes[episodeKey] = item;
|
if (episode.Versions != null){
|
||||||
} else{
|
foreach (var version in episode.Versions){
|
||||||
item = episodes[episodeKey];
|
var lang = Array.Find(Languages.languages, a => a.CrLocale == version.AudioLocale) ?? new LanguageItem();
|
||||||
}
|
item.AddUnique(episode, lang); // must enforce uniqueness by CrLocale
|
||||||
|
|
||||||
if (episode.Versions != null){
|
|
||||||
foreach (var version in episode.Versions){
|
|
||||||
if (item.Langs.All(a => a.CrLocale != version.AudioLocale)){
|
|
||||||
item.Items.Add(episode);
|
|
||||||
item.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == version.AudioLocale) ?? new LanguageItem());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else{
|
|
||||||
serieshasversions = false;
|
|
||||||
if (item.Langs.All(a => a.CrLocale != episode.AudioLocale)){
|
|
||||||
item.Items.Add(episode);
|
|
||||||
item.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == episode.AudioLocale) ?? new LanguageItem());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} else{
|
||||||
|
serieshasversions = false;
|
||||||
|
var lang = Array.Find(Languages.languages, a => a.CrLocale == episode.AudioLocale) ?? new LanguageItem();
|
||||||
|
item.AddUnique(episode, lang);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -198,22 +204,25 @@ public class CrSeries{
|
||||||
int specialIndex = 1;
|
int specialIndex = 1;
|
||||||
int epIndex = 1;
|
int epIndex = 1;
|
||||||
|
|
||||||
var keys = new List<string>(episodes.Keys); // Copying the keys to a new list to avoid modifying the collection while iterating.
|
var keys = new List<string>(episodes.Keys);
|
||||||
|
|
||||||
foreach (var key in keys){
|
foreach (var key in keys){
|
||||||
EpisodeAndLanguage item = episodes[key];
|
var item = episodes[key];
|
||||||
var episode = item.Items[0].Episode;
|
if (item.Variants.Count == 0)
|
||||||
var isSpecial = episode != null && !Regex.IsMatch(episode, @"^\d+(\.\d+)?$"); // Checking if the episode is not a number (i.e., special).
|
continue;
|
||||||
// var newKey = $"{(isSpecial ? 'S' : 'E')}{(isSpecial ? specialIndex : epIndex).ToString()}";
|
|
||||||
|
var baseEp = item.Variants[0].Item;
|
||||||
|
|
||||||
|
var epStr = baseEp.Episode;
|
||||||
|
var isSpecial = epStr != null && !Regex.IsMatch(epStr, @"^\d+(\.\d+)?$");
|
||||||
|
|
||||||
string newKey;
|
string newKey;
|
||||||
if (isSpecial && !string.IsNullOrEmpty(item.Items[0].Episode)){
|
if (isSpecial && !string.IsNullOrEmpty(baseEp.Episode)){
|
||||||
newKey = $"SP{specialIndex}_" + item.Items[0].Episode;// ?? "SP" + (specialIndex + " " + item.Items[0].Id);
|
newKey = $"SP{specialIndex}_" + baseEp.Episode;
|
||||||
} else{
|
} else{
|
||||||
newKey = $"{(isSpecial ? "SP" : 'E')}{(isSpecial ? (specialIndex + " " + item.Items[0].Id) : epIndex + "")}";
|
newKey = $"{(isSpecial ? "SP" : 'E')}{(isSpecial ? (specialIndex + " " + baseEp.Id) : epIndex + "")}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
episodes.Remove(key);
|
episodes.Remove(key);
|
||||||
|
|
||||||
int counter = 1;
|
int counter = 1;
|
||||||
|
|
@ -225,63 +234,95 @@ public class CrSeries{
|
||||||
|
|
||||||
episodes.Add(newKey, item);
|
episodes.Add(newKey, item);
|
||||||
|
|
||||||
if (isSpecial){
|
if (isSpecial) specialIndex++;
|
||||||
specialIndex++;
|
else epIndex++;
|
||||||
} else{
|
|
||||||
epIndex++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var specials = episodes.Where(e => e.Key.StartsWith("S")).ToList();
|
var normal = episodes.Where(kvp => kvp.Key.StartsWith("E")).ToList();
|
||||||
var normal = episodes.Where(e => e.Key.StartsWith("E")).ToList();
|
var specials = episodes.Where(kvp => kvp.Key.StartsWith("SP")).ToList();
|
||||||
|
|
||||||
// Combining and sorting episodes with normal first, then specials.
|
|
||||||
var sortedEpisodes = new Dictionary<string, EpisodeAndLanguage>(normal.Concat(specials));
|
var sortedEpisodes = new Dictionary<string, EpisodeAndLanguage>(normal.Concat(specials));
|
||||||
|
|
||||||
foreach (var kvp in sortedEpisodes){
|
foreach (var kvp in sortedEpisodes){
|
||||||
var key = kvp.Key;
|
var key = kvp.Key;
|
||||||
var item = kvp.Value;
|
var item = kvp.Value;
|
||||||
|
|
||||||
var seasonTitle = item.Items.FirstOrDefault(a => !Regex.IsMatch(a.SeasonTitle, @"\(\w+ Dub\)"))?.SeasonTitle
|
if (item.Variants.Count == 0)
|
||||||
?? Regex.Replace(item.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd();
|
continue;
|
||||||
|
|
||||||
var title = item.Items[0].Title;
|
var baseEp = item.Variants[0].Item;
|
||||||
var seasonNumber = Helpers.ExtractNumberAfterS(item.Items[0].Identifier) ?? item.Items[0].SeasonNumber.ToString();
|
|
||||||
|
|
||||||
var languages = item.Items.Select((a, index) =>
|
var seasonTitle = DownloadQueueItemFactory.CanonicalTitle(
|
||||||
$"{(a.IsPremiumOnly ? "+ " : "")}{item.Langs.ElementAtOrDefault(index)?.Name ?? "Unknown"}").ToArray(); //☆
|
item.Variants.Select(string? (v) => v.Item.SeasonTitle)
|
||||||
|
);
|
||||||
|
|
||||||
|
var title = baseEp.Title;
|
||||||
|
var seasonNumber = Helpers.ExtractNumberAfterS(baseEp.Identifier) ?? baseEp.SeasonNumber.ToString();
|
||||||
|
|
||||||
|
var languages = item.Variants
|
||||||
|
.Select(v => $"{(v.Item.IsPremiumOnly ? "+ " : "")}{v.Lang?.Name ?? "Unknown"}")
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
Console.WriteLine($"[{key}] {seasonTitle} - Season {seasonNumber} - {title} [{string.Join(", ", languages)}]");
|
Console.WriteLine($"[{key}] {seasonTitle} - Season {seasonNumber} - {title} [{string.Join(", ", languages)}]");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!serieshasversions){
|
if (!serieshasversions)
|
||||||
Console.WriteLine("Couldn\'t find versions on some episodes, added languages with language array.");
|
Console.WriteLine("Couldn\'t find versions on some episodes, added languages with language array.");
|
||||||
}
|
|
||||||
|
|
||||||
CrunchySeriesList crunchySeriesList = new CrunchySeriesList();
|
var crunchySeriesList = new CrunchySeriesList{
|
||||||
crunchySeriesList.Data = sortedEpisodes;
|
Data = sortedEpisodes
|
||||||
|
};
|
||||||
|
|
||||||
crunchySeriesList.List = sortedEpisodes.Select(kvp => {
|
crunchySeriesList.List = sortedEpisodes.Select(kvp => {
|
||||||
var key = kvp.Key;
|
var key = kvp.Key;
|
||||||
var value = kvp.Value;
|
var value = kvp.Value;
|
||||||
var images = (value.Items.FirstOrDefault()?.Images?.Thumbnail ??[new List<Image>{ new(){ Source = "/notFound.jpg" } }]);
|
|
||||||
var seconds = (int)Math.Floor((value.Items.FirstOrDefault()?.DurationMs ?? 0) / 1000.0);
|
if (value.Variants.Count == 0){
|
||||||
var langList = value.Langs.Select(a => a.CrLocale).ToList();
|
return new Episode{
|
||||||
|
E = key.StartsWith("E") ? key.Substring(1) : key,
|
||||||
|
Lang = new List<string>(),
|
||||||
|
Name = string.Empty,
|
||||||
|
Season = string.Empty,
|
||||||
|
SeriesTitle = string.Empty,
|
||||||
|
SeasonTitle = string.Empty,
|
||||||
|
EpisodeNum = key,
|
||||||
|
Id = string.Empty,
|
||||||
|
Img = string.Empty,
|
||||||
|
Description = string.Empty,
|
||||||
|
EpisodeType = EpisodeType.Episode,
|
||||||
|
Time = "0:00"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var baseEp = value.Variants[0].Item;
|
||||||
|
|
||||||
|
var thumbRow = baseEp.Images.Thumbnail.FirstOrDefault();
|
||||||
|
var img = thumbRow?.FirstOrDefault()?.Source ?? "/notFound.jpg";
|
||||||
|
|
||||||
|
var seconds = (int)Math.Floor((baseEp.DurationMs) / 1000.0);
|
||||||
|
|
||||||
|
var langList = value.Variants
|
||||||
|
.Select(v => v.Lang.CrLocale)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
Languages.SortListByLangList(langList);
|
Languages.SortListByLangList(langList);
|
||||||
|
|
||||||
return new Episode{
|
return new Episode{
|
||||||
E = key.StartsWith("E") ? key.Substring(1) : key,
|
E = key.StartsWith("E") ? key.Substring(1) : key,
|
||||||
Lang = langList,
|
Lang = langList,
|
||||||
Name = value.Items.FirstOrDefault()?.Title ?? string.Empty,
|
Name = baseEp.Title ?? string.Empty,
|
||||||
Season = (Helpers.ExtractNumberAfterS(value.Items.FirstOrDefault()?.Identifier?? string.Empty) ?? value.Items.FirstOrDefault()?.SeasonNumber.ToString()) ?? string.Empty,
|
Season = (Helpers.ExtractNumberAfterS(baseEp.Identifier) ?? baseEp.SeasonNumber.ToString()) ?? string.Empty,
|
||||||
SeriesTitle = Regex.Replace(value.Items.FirstOrDefault()?.SeriesTitle?? string.Empty, @"\(\w+ Dub\)", "").TrimEnd(),
|
SeriesTitle = DownloadQueueItemFactory.StripDubSuffix(baseEp.SeriesTitle),
|
||||||
SeasonTitle = Regex.Replace(value.Items.FirstOrDefault()?.SeasonTitle?? string.Empty, @"\(\w+ Dub\)", "").TrimEnd(),
|
SeasonTitle = DownloadQueueItemFactory.StripDubSuffix(baseEp.SeasonTitle),
|
||||||
EpisodeNum = key.StartsWith("SP") ? key : value.Items.FirstOrDefault()?.EpisodeNumber?.ToString() ?? value.Items.FirstOrDefault()?.Episode ?? "?",
|
EpisodeNum = key.StartsWith("SP")
|
||||||
Id = value.Items.FirstOrDefault()?.SeasonId ?? string.Empty,
|
? key
|
||||||
Img = images.FirstOrDefault()?.FirstOrDefault()?.Source ?? string.Empty,
|
: (baseEp.EpisodeNumber?.ToString() ?? baseEp.Episode ?? "?"),
|
||||||
Description = value.Items.FirstOrDefault()?.Description ?? string.Empty,
|
Id = baseEp.SeasonId ?? string.Empty,
|
||||||
|
Img = img,
|
||||||
|
Description = baseEp.Description ?? string.Empty,
|
||||||
EpisodeType = EpisodeType.Episode,
|
EpisodeType = EpisodeType.Episode,
|
||||||
Time = $"{seconds / 60}:{seconds % 60:D2}" // Ensures two digits for seconds.
|
Time = $"{seconds / 60}:{seconds % 60:D2}"
|
||||||
};
|
};
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
|
|
@ -333,7 +374,7 @@ public class CrSeries{
|
||||||
Console.Error.WriteLine($"Episode List Request FAILED! uri: {episodeRequest.RequestUri}");
|
Console.Error.WriteLine($"Episode List Request FAILED! uri: {episodeRequest.RequestUri}");
|
||||||
} else{
|
} else{
|
||||||
episodeList = Helpers.Deserialize<CrunchyEpisodeList>(episodeRequestResponse.ResponseContent, crunInstance.SettingsJsonSerializerSettings) ??
|
episodeList = Helpers.Deserialize<CrunchyEpisodeList>(episodeRequestResponse.ResponseContent, crunInstance.SettingsJsonSerializerSettings) ??
|
||||||
new CrunchyEpisodeList(){ Data =[], Total = 0, Meta = new Meta() };
|
new CrunchyEpisodeList(){ Data = [], Total = 0, Meta = new Meta() };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (episodeList.Total < 1){
|
if (episodeList.Total < 1){
|
||||||
|
|
@ -377,8 +418,8 @@ public class CrSeries{
|
||||||
|
|
||||||
public async Task<CrSeriesBase?> SeriesById(string id, string? crLocale, bool forced = false){
|
public async Task<CrSeriesBase?> SeriesById(string id, string? crLocale, bool forced = false){
|
||||||
await crunInstance.CrAuthGuest.RefreshToken(true);
|
await crunInstance.CrAuthGuest.RefreshToken(true);
|
||||||
|
|
||||||
|
|
||||||
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
|
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
|
||||||
|
|
||||||
query["preferred_audio_language"] = "ja-JP";
|
query["preferred_audio_language"] = "ja-JP";
|
||||||
|
|
@ -411,7 +452,7 @@ public class CrSeries{
|
||||||
|
|
||||||
public async Task<CrSearchSeriesBase?> Search(string searchString, string? crLocale, bool forced = false){
|
public async Task<CrSearchSeriesBase?> Search(string searchString, string? crLocale, bool forced = false){
|
||||||
await crunInstance.CrAuthGuest.RefreshToken(true);
|
await crunInstance.CrAuthGuest.RefreshToken(true);
|
||||||
|
|
||||||
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
|
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(crLocale)){
|
if (!string.IsNullOrEmpty(crLocale)){
|
||||||
|
|
@ -456,7 +497,7 @@ public class CrSeries{
|
||||||
public async Task<CrBrowseSeriesBase?> GetAllSeries(string? crLocale){
|
public async Task<CrBrowseSeriesBase?> GetAllSeries(string? crLocale){
|
||||||
await crunInstance.CrAuthGuest.RefreshToken(true);
|
await crunInstance.CrAuthGuest.RefreshToken(true);
|
||||||
CrBrowseSeriesBase complete = new CrBrowseSeriesBase();
|
CrBrowseSeriesBase complete = new CrBrowseSeriesBase();
|
||||||
complete.Data =[];
|
complete.Data = [];
|
||||||
|
|
||||||
var i = 0;
|
var i = 0;
|
||||||
|
|
||||||
|
|
@ -495,7 +536,7 @@ public class CrSeries{
|
||||||
|
|
||||||
return complete;
|
return complete;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<CrBrowseSeriesBase?> GetSeasonalSeries(string season, string year, string? crLocale){
|
public async Task<CrBrowseSeriesBase?> GetSeasonalSeries(string season, string year, string? crLocale){
|
||||||
await crunInstance.CrAuthGuest.RefreshToken(true);
|
await crunInstance.CrAuthGuest.RefreshToken(true);
|
||||||
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
|
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
|
||||||
|
|
@ -503,7 +544,7 @@ public class CrSeries{
|
||||||
if (!string.IsNullOrEmpty(crLocale)){
|
if (!string.IsNullOrEmpty(crLocale)){
|
||||||
query["locale"] = crLocale;
|
query["locale"] = crLocale;
|
||||||
}
|
}
|
||||||
|
|
||||||
query["seasonal_tag"] = season.ToLower() + "-" + year;
|
query["seasonal_tag"] = season.ToLower() + "-" + year;
|
||||||
query["n"] = "100";
|
query["n"] = "100";
|
||||||
|
|
||||||
|
|
@ -520,5 +561,4 @@ public class CrSeries{
|
||||||
|
|
||||||
return series;
|
return series;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -151,6 +151,9 @@ public class CrunchyrollManager{
|
||||||
};
|
};
|
||||||
|
|
||||||
options.History = true;
|
options.History = true;
|
||||||
|
|
||||||
|
options.HistoryAutoRefreshMode = HistoryRefreshMode.FastNewReleases;
|
||||||
|
options.HistoryAutoRefreshIntervalMinutes = 0;
|
||||||
|
|
||||||
CfgManager.UpdateSettingsFromFile(options, CfgManager.PathCrDownloadOptions);
|
CfgManager.UpdateSettingsFromFile(options, CfgManager.PathCrDownloadOptions);
|
||||||
|
|
||||||
|
|
@ -581,23 +584,36 @@ public class CrunchyrollManager{
|
||||||
if (options.MarkAsWatched && data.Data is{ Count: > 0 }){
|
if (options.MarkAsWatched && data.Data is{ Count: > 0 }){
|
||||||
_ = CrEpisode.MarkAsWatched(data.Data.First().MediaId);
|
_ = CrEpisode.MarkAsWatched(data.Data.First().MediaId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (QueueManager.Instance.Queue.Count == 0 || QueueManager.Instance.Queue.All(e => e.DownloadProgress.Done)){
|
if (!QueueManager.Instance.Queue.Any(e => e.DownloadProgress is{ Done: false, Error: false })){
|
||||||
QueueManager.Instance.ResetDownloads();
|
if (CrunOptions.DownloadFinishedPlaySound){
|
||||||
try{
|
try{
|
||||||
var audioPath = CrunOptions.DownloadFinishedSoundPath;
|
var audioPath = CrunOptions.DownloadFinishedSoundPath;
|
||||||
if (!string.IsNullOrEmpty(audioPath)){
|
if (!string.IsNullOrEmpty(audioPath)){
|
||||||
var player = new AudioPlayer();
|
var player = new AudioPlayer();
|
||||||
player.Play(audioPath);
|
player.Play(audioPath);
|
||||||
|
}
|
||||||
|
} catch (Exception exception){
|
||||||
|
Console.Error.WriteLine("Failed to play sound: " + exception);
|
||||||
}
|
}
|
||||||
} catch (Exception exception){
|
|
||||||
Console.Error.WriteLine("Failed to play sound: " + exception);
|
|
||||||
}
|
}
|
||||||
|
if (CrunOptions.DownloadFinishedExecute){
|
||||||
|
try{
|
||||||
|
var filePath = CrunOptions.DownloadFinishedExecutePath;
|
||||||
|
if (!string.IsNullOrEmpty(filePath)){
|
||||||
|
Helpers.ExecuteFile(filePath);
|
||||||
|
}
|
||||||
|
} catch (Exception exception){
|
||||||
|
Console.Error.WriteLine("Failed to execute file: " + exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (CrunOptions.ShutdownWhenQueueEmpty){
|
if (CrunOptions.ShutdownWhenQueueEmpty){
|
||||||
Helpers.ShutdownComputer();
|
Helpers.ShutdownComputer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -1551,8 +1567,10 @@ public class CrunchyrollManager{
|
||||||
|
|
||||||
string qualityConsoleLog = sb.ToString();
|
string qualityConsoleLog = sb.ToString();
|
||||||
Console.WriteLine(qualityConsoleLog);
|
Console.WriteLine(qualityConsoleLog);
|
||||||
data.AvailableQualities = qualityConsoleLog;
|
if (!options.DlVideoOnce || string.IsNullOrEmpty(data.AvailableQualities)){
|
||||||
|
data.AvailableQualities = qualityConsoleLog;
|
||||||
|
}
|
||||||
|
|
||||||
Console.WriteLine("Stream URL:" + chosenVideoSegments.segments[0].uri.Split(new[]{ ",.urlset" }, StringSplitOptions.None)[0]);
|
Console.WriteLine("Stream URL:" + chosenVideoSegments.segments[0].uri.Split(new[]{ ",.urlset" }, StringSplitOptions.None)[0]);
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
89
CRD/Downloader/Crunchyroll/Utils/DownloadQueueItemFactory.cs
Normal file
89
CRD/Downloader/Crunchyroll/Utils/DownloadQueueItemFactory.cs
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using CRD.Utils;
|
||||||
|
using CRD.Utils.Structs;
|
||||||
|
|
||||||
|
namespace CRD.Downloader.Crunchyroll.Utils;
|
||||||
|
|
||||||
|
public static class DownloadQueueItemFactory{
|
||||||
|
private static readonly Regex DubSuffix = new(@"\(\w+ Dub\)", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
public static bool HasDubSuffix(string? s)
|
||||||
|
=> !string.IsNullOrWhiteSpace(s) && DubSuffix.IsMatch(s);
|
||||||
|
|
||||||
|
public static string StripDubSuffix(string? s)
|
||||||
|
=> string.IsNullOrWhiteSpace(s) ? "" : DubSuffix.Replace(s, "").TrimEnd();
|
||||||
|
|
||||||
|
public static string CanonicalTitle(IEnumerable<string?> candidates){
|
||||||
|
var noDub = candidates.FirstOrDefault(t => !HasDubSuffix(t));
|
||||||
|
return !string.IsNullOrWhiteSpace(noDub)
|
||||||
|
? noDub!
|
||||||
|
: StripDubSuffix(candidates.FirstOrDefault());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (string small, string big) GetThumbSmallBig(Images? images){
|
||||||
|
var firstRow = images?.Thumbnail?.FirstOrDefault();
|
||||||
|
var small = firstRow?.FirstOrDefault()?.Source ?? "/notFound.jpg";
|
||||||
|
var big = firstRow?.LastOrDefault()?.Source ?? small;
|
||||||
|
return (small, big);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CrunchyEpMeta CreateShell(
|
||||||
|
StreamingService service,
|
||||||
|
string? seriesTitle,
|
||||||
|
string? seasonTitle,
|
||||||
|
string? episodeNumber,
|
||||||
|
string? episodeTitle,
|
||||||
|
string? description,
|
||||||
|
string? seriesId,
|
||||||
|
string? seasonId,
|
||||||
|
string? season,
|
||||||
|
string? absolutEpisodeNumberE,
|
||||||
|
string? image,
|
||||||
|
string? imageBig,
|
||||||
|
string hslang,
|
||||||
|
List<string>? availableSubs = null,
|
||||||
|
List<string>? selectedDubs = null,
|
||||||
|
bool music = false){
|
||||||
|
return new CrunchyEpMeta(){
|
||||||
|
SeriesTitle = seriesTitle,
|
||||||
|
SeasonTitle = seasonTitle,
|
||||||
|
EpisodeNumber = episodeNumber,
|
||||||
|
EpisodeTitle = episodeTitle,
|
||||||
|
Description = description,
|
||||||
|
|
||||||
|
SeriesId = seriesId,
|
||||||
|
SeasonId = seasonId,
|
||||||
|
Season = season,
|
||||||
|
AbsolutEpisodeNumberE = absolutEpisodeNumberE,
|
||||||
|
|
||||||
|
Image = image,
|
||||||
|
ImageBig = imageBig,
|
||||||
|
|
||||||
|
Hslang = hslang,
|
||||||
|
AvailableSubs = availableSubs,
|
||||||
|
SelectedDubs = selectedDubs,
|
||||||
|
Music = music
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CrunchyEpMetaData CreateVariant(
|
||||||
|
string mediaId,
|
||||||
|
LanguageItem? lang,
|
||||||
|
string? playback,
|
||||||
|
List<EpisodeVersion>? versions,
|
||||||
|
bool isSubbed,
|
||||||
|
bool isDubbed,
|
||||||
|
bool isAudioRoleDescription = false){
|
||||||
|
return new CrunchyEpMetaData{
|
||||||
|
MediaId = mediaId,
|
||||||
|
Lang = lang,
|
||||||
|
Playback = playback,
|
||||||
|
Versions = versions,
|
||||||
|
IsSubbed = isSubbed,
|
||||||
|
IsDubbed = isDubbed,
|
||||||
|
IsAudioRoleDescription = isAudioRoleDescription
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
122
CRD/Downloader/Crunchyroll/Utils/EpisodeMapper.cs
Normal file
122
CRD/Downloader/Crunchyroll/Utils/EpisodeMapper.cs
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using CRD.Utils;
|
||||||
|
using CRD.Utils.Structs;
|
||||||
|
|
||||||
|
namespace CRD.Downloader.Crunchyroll.Utils;
|
||||||
|
|
||||||
|
public static class EpisodeMapper{
|
||||||
|
public static CrunchyEpisode ToCrunchyEpisode(this CrBrowseEpisode src){
|
||||||
|
if (src == null) throw new ArgumentNullException(nameof(src));
|
||||||
|
|
||||||
|
var meta = src.EpisodeMetadata ?? new CrBrowseEpisodeMetaData();
|
||||||
|
|
||||||
|
return new CrunchyEpisode{
|
||||||
|
|
||||||
|
Id = src.Id ?? string.Empty,
|
||||||
|
Slug = src.Slug ?? string.Empty,
|
||||||
|
SlugTitle = src.SlugTitle ?? string.Empty,
|
||||||
|
Title = src.Title ?? string.Empty,
|
||||||
|
Description = src.Description ?? src.PromoDescription ?? string.Empty,
|
||||||
|
MediaType = src.Type,
|
||||||
|
ChannelId = src.ChannelId,
|
||||||
|
StreamsLink = src.StreamsLink,
|
||||||
|
Images = src.Images ?? new Images(),
|
||||||
|
|
||||||
|
|
||||||
|
SeoTitle = src.PromoTitle ?? string.Empty,
|
||||||
|
SeoDescription = src.PromoDescription ?? string.Empty,
|
||||||
|
|
||||||
|
|
||||||
|
ProductionEpisodeId = src.ExternalId ?? string.Empty,
|
||||||
|
ListingId = src.LinkedResourceKey ?? string.Empty,
|
||||||
|
|
||||||
|
|
||||||
|
SeriesId = meta.SeriesId ?? string.Empty,
|
||||||
|
SeasonId = meta.SeasonId ?? string.Empty,
|
||||||
|
|
||||||
|
SeriesTitle = meta.SeriesTitle ?? string.Empty,
|
||||||
|
SeriesSlugTitle = meta.SeriesSlugTitle ?? string.Empty,
|
||||||
|
|
||||||
|
SeasonTitle = meta.SeasonTitle ?? string.Empty,
|
||||||
|
SeasonSlugTitle = meta.SeasonSlugTitle ?? string.Empty,
|
||||||
|
|
||||||
|
SeasonNumber = SafeInt(meta.SeasonNumber),
|
||||||
|
SequenceNumber = (float)meta.SequenceNumber,
|
||||||
|
|
||||||
|
Episode = meta.Episode,
|
||||||
|
EpisodeNumber = meta.EpisodeCount,
|
||||||
|
|
||||||
|
DurationMs = meta.DurationMs,
|
||||||
|
Identifier = meta.Identifier ?? string.Empty,
|
||||||
|
|
||||||
|
AvailabilityNotes = meta.AvailabilityNotes ?? string.Empty,
|
||||||
|
EligibleRegion = meta.EligibleRegion ?? string.Empty,
|
||||||
|
|
||||||
|
AvailabilityStarts = meta.AvailabilityStarts,
|
||||||
|
AvailabilityEnds = meta.AvailabilityEnds,
|
||||||
|
PremiumAvailableDate = meta.PremiumAvailableDate,
|
||||||
|
FreeAvailableDate = meta.FreeAvailableDate,
|
||||||
|
AvailableDate = meta.AvailableDate,
|
||||||
|
PremiumDate = meta.PremiumDate,
|
||||||
|
UploadDate = meta.UploadDate,
|
||||||
|
EpisodeAirDate = meta.EpisodeAirDate,
|
||||||
|
|
||||||
|
IsDubbed = meta.IsDubbed,
|
||||||
|
IsSubbed = meta.IsSubbed,
|
||||||
|
IsMature = meta.IsMature,
|
||||||
|
IsClip = meta.IsClip,
|
||||||
|
IsPremiumOnly = meta.IsPremiumOnly,
|
||||||
|
MatureBlocked = meta.MatureBlocked,
|
||||||
|
|
||||||
|
AvailableOffline = meta.AvailableOffline,
|
||||||
|
ClosedCaptionsAvailable = meta.ClosedCaptionsAvailable,
|
||||||
|
|
||||||
|
MaturityRatings = meta.MaturityRatings ?? new List<string>(),
|
||||||
|
|
||||||
|
|
||||||
|
AudioLocale = (meta.AudioLocale ?? Locale.DefaulT).GetEnumMemberValue(),
|
||||||
|
SubtitleLocales = (meta.SubtitleLocales ?? new List<Locale>())
|
||||||
|
.Select(l => l.GetEnumMemberValue())
|
||||||
|
.Where(s => !string.IsNullOrWhiteSpace(s))
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList(),
|
||||||
|
|
||||||
|
ExtendedMaturityRating = ToStringKeyDict(meta.ExtendedMaturityRating),
|
||||||
|
|
||||||
|
Versions = meta.versions?.Select(ToEpisodeVersion).ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EpisodeVersion ToEpisodeVersion(CrBrowseEpisodeVersion v){
|
||||||
|
return new EpisodeVersion{
|
||||||
|
AudioLocale = (v.AudioLocale ?? Locale.DefaulT).GetEnumMemberValue(),
|
||||||
|
Guid = v.Guid ?? string.Empty,
|
||||||
|
Original = v.Original,
|
||||||
|
Variant = v.Variant ?? string.Empty,
|
||||||
|
SeasonGuid = v.SeasonGuid ?? string.Empty,
|
||||||
|
MediaGuid = v.MediaGuid,
|
||||||
|
IsPremiumOnly = v.IsPremiumOnly,
|
||||||
|
roles = Array.Empty<string>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SafeInt(double value){
|
||||||
|
if (double.IsNaN(value) || double.IsInfinity(value)) return 0;
|
||||||
|
return (int)Math.Round(value, MidpointRounding.AwayFromZero);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, object> ToStringKeyDict(Dictionary<object, object>? dict){
|
||||||
|
var result = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
if (dict == null) return result;
|
||||||
|
|
||||||
|
foreach (var kv in dict){
|
||||||
|
var key = kv.Key?.ToString();
|
||||||
|
if (string.IsNullOrWhiteSpace(key)) continue;
|
||||||
|
result[key] = kv.Value ?? new object();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CRD.Downloader.Crunchyroll;
|
using CRD.Downloader.Crunchyroll;
|
||||||
|
using CRD.Downloader.Crunchyroll.Utils;
|
||||||
using CRD.Utils;
|
using CRD.Utils;
|
||||||
using CRD.Utils.Files;
|
using CRD.Utils.Files;
|
||||||
using CRD.Utils.Sonarr;
|
using CRD.Utils.Sonarr;
|
||||||
|
|
@ -38,7 +39,7 @@ public class History{
|
||||||
}
|
}
|
||||||
} else{
|
} else{
|
||||||
var matchingSeason = historySeries.Seasons.FirstOrDefault(historySeason => historySeason.SeasonId == seasonId);
|
var matchingSeason = historySeries.Seasons.FirstOrDefault(historySeason => historySeason.SeasonId == seasonId);
|
||||||
|
|
||||||
if (matchingSeason != null){
|
if (matchingSeason != null){
|
||||||
foreach (var historyEpisode in matchingSeason.EpisodesList){
|
foreach (var historyEpisode in matchingSeason.EpisodesList){
|
||||||
historyEpisode.IsEpisodeAvailableOnStreamingService = false;
|
historyEpisode.IsEpisodeAvailableOnStreamingService = false;
|
||||||
|
|
@ -129,6 +130,129 @@ public class History{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task UpdateWithEpisode(List<CrBrowseEpisode> episodes){
|
||||||
|
var historyIndex = crunInstance.HistoryList
|
||||||
|
.Where(h => !string.IsNullOrWhiteSpace(h.SeriesId))
|
||||||
|
.ToDictionary(
|
||||||
|
h => h.SeriesId!,
|
||||||
|
h => (h.Seasons)
|
||||||
|
.Where(s => !string.IsNullOrWhiteSpace(s.SeasonId))
|
||||||
|
.ToDictionary(
|
||||||
|
s => s.SeasonId ?? "UNKNOWN",
|
||||||
|
s => (s.EpisodesList)
|
||||||
|
.Select(ep => ep.EpisodeId)
|
||||||
|
.Where(id => !string.IsNullOrWhiteSpace(id))
|
||||||
|
.ToHashSet(StringComparer.Ordinal),
|
||||||
|
StringComparer.Ordinal
|
||||||
|
),
|
||||||
|
StringComparer.Ordinal
|
||||||
|
);
|
||||||
|
|
||||||
|
episodes = episodes
|
||||||
|
.Where(e => !string.IsNullOrWhiteSpace(e.EpisodeMetadata?.SeriesId) &&
|
||||||
|
historyIndex.ContainsKey(e.EpisodeMetadata!.SeriesId!))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var seriesGroup in episodes.GroupBy(e => e.EpisodeMetadata?.SeriesId ?? "UNKNOWN_SERIES")){
|
||||||
|
var seriesId = seriesGroup.Key;
|
||||||
|
|
||||||
|
var originalEntries = seriesGroup
|
||||||
|
.Select(e => new{ OriginalId = TryGetOriginalId(e), SeasonId = TryGetOriginalSeasonId(e) })
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x.OriginalId))
|
||||||
|
.GroupBy(x => x.OriginalId!, StringComparer.Ordinal)
|
||||||
|
.Select(g => new{
|
||||||
|
OriginalId = g.Key,
|
||||||
|
SeasonId = g.Select(x => x.SeasonId).FirstOrDefault(s => !string.IsNullOrWhiteSpace(s))
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var hasAnyOriginalInfo = originalEntries.Count > 0;
|
||||||
|
|
||||||
|
var allOriginalsInHistory =
|
||||||
|
hasAnyOriginalInfo
|
||||||
|
&& originalEntries.All(x => IsOriginalInHistory(historyIndex, seriesId, x.SeasonId, x.OriginalId));
|
||||||
|
|
||||||
|
var originalItems = seriesGroup.Where(IsOriginalItem).ToList();
|
||||||
|
|
||||||
|
if (originalItems.Count > 0){
|
||||||
|
if (allOriginalsInHistory){
|
||||||
|
var sT = seriesGroup.Select(e => e.EpisodeMetadata?.SeriesTitle)
|
||||||
|
.FirstOrDefault(t => !string.IsNullOrWhiteSpace(t)) ?? "";
|
||||||
|
// Console.WriteLine($"[INFO] Skipping SeriesId={seriesId} {sT} - all ORIGINAL episodes already in history.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var convertedList = originalItems.Select(crBrowseEpisode => crBrowseEpisode.ToCrunchyEpisode()).ToList();
|
||||||
|
|
||||||
|
await crunInstance.History.UpdateWithSeasonData(convertedList.ToList<IHistorySource>());
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var seriesTitle = seriesGroup.Select(e => e.EpisodeMetadata?.SeriesTitle)
|
||||||
|
.FirstOrDefault(t => !string.IsNullOrWhiteSpace(t)) ?? "";
|
||||||
|
|
||||||
|
if (allOriginalsInHistory){
|
||||||
|
// Console.WriteLine($"[INFO] Skipping SeriesId={seriesId} - originals implied by Versions already in history.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"[WARN] No original ITEM found for SeriesId={seriesId} {seriesTitle}");
|
||||||
|
|
||||||
|
if (HasAllSeriesEpisodesInHistory(historyIndex, seriesId, seriesGroup)){
|
||||||
|
Console.WriteLine($"[History] Skip (already in history): {seriesId}");
|
||||||
|
} else{
|
||||||
|
await CrUpdateSeries(seriesId, null);
|
||||||
|
Console.WriteLine($"[History] Updating (full series): {seriesId}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? TryGetOriginalId(CrBrowseEpisode e) =>
|
||||||
|
e.EpisodeMetadata?.versions?
|
||||||
|
.FirstOrDefault(v => v.Original && !string.IsNullOrWhiteSpace(v.Guid))
|
||||||
|
?.Guid;
|
||||||
|
|
||||||
|
private string? TryGetOriginalSeasonId(CrBrowseEpisode e) =>
|
||||||
|
e.EpisodeMetadata?.versions?
|
||||||
|
.FirstOrDefault(v => v.Original && !string.IsNullOrWhiteSpace(v.SeasonGuid))
|
||||||
|
?.SeasonGuid
|
||||||
|
?? e.EpisodeMetadata?.SeasonId;
|
||||||
|
|
||||||
|
private bool IsOriginalItem(CrBrowseEpisode e){
|
||||||
|
var originalId = TryGetOriginalId(e);
|
||||||
|
return !string.IsNullOrWhiteSpace(originalId)
|
||||||
|
&& !string.IsNullOrWhiteSpace(e.Id)
|
||||||
|
&& string.Equals(e.Id, originalId, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private bool IsOriginalInHistory(Dictionary<string, Dictionary<string, HashSet<string?>>> historyIndex, string seriesId, string? seasonId, string originalEpisodeId){
|
||||||
|
if (!historyIndex.TryGetValue(seriesId, out var seasons)) return false;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(seasonId))
|
||||||
|
return seasons.TryGetValue(seasonId, out var eps) && eps.Contains(originalEpisodeId);
|
||||||
|
|
||||||
|
return seasons.Values.Any(eps => eps.Contains(originalEpisodeId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool HasAllSeriesEpisodesInHistory(Dictionary<string, Dictionary<string, HashSet<string?>>> historyIndex, string seriesId, IEnumerable<CrBrowseEpisode> seriesEpisodes){
|
||||||
|
if (!historyIndex.TryGetValue(seriesId, out var seasons)) return false;
|
||||||
|
|
||||||
|
var allHistoryEpisodeIds = seasons.Values
|
||||||
|
.SelectMany(set => set)
|
||||||
|
.ToHashSet(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
foreach (var e in seriesEpisodes){
|
||||||
|
if (string.IsNullOrWhiteSpace(e.Id)) return false;
|
||||||
|
if (!allHistoryEpisodeIds.Contains(e.Id)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This method updates the History with a list of episodes. The episodes have to be from the same season.
|
/// This method updates the History with a list of episodes. The episodes have to be from the same season.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -206,7 +330,7 @@ public class History{
|
||||||
historySeries = new HistorySeries{
|
historySeries = new HistorySeries{
|
||||||
SeriesTitle = firstEpisode.GetSeriesTitle(),
|
SeriesTitle = firstEpisode.GetSeriesTitle(),
|
||||||
SeriesId = firstEpisode.GetSeriesId(),
|
SeriesId = firstEpisode.GetSeriesId(),
|
||||||
Seasons =[],
|
Seasons = [],
|
||||||
HistorySeriesAddDate = DateTime.Now,
|
HistorySeriesAddDate = DateTime.Now,
|
||||||
SeriesType = firstEpisode.GetSeriesType(),
|
SeriesType = firstEpisode.GetSeriesType(),
|
||||||
SeriesStreamingService = StreamingService.Crunchyroll
|
SeriesStreamingService = StreamingService.Crunchyroll
|
||||||
|
|
@ -302,8 +426,8 @@ public class History{
|
||||||
|
|
||||||
var downloadDirPath = "";
|
var downloadDirPath = "";
|
||||||
var videoQuality = "";
|
var videoQuality = "";
|
||||||
List<string> dublist =[];
|
List<string> dublist = [];
|
||||||
List<string> sublist =[];
|
List<string> sublist = [];
|
||||||
|
|
||||||
if (historySeries != null){
|
if (historySeries != null){
|
||||||
var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId);
|
var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId);
|
||||||
|
|
@ -353,7 +477,7 @@ public class History{
|
||||||
public List<string> GetDubList(string? seriesId, string? seasonId){
|
public List<string> GetDubList(string? seriesId, string? seasonId){
|
||||||
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
|
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
|
||||||
|
|
||||||
List<string> dublist =[];
|
List<string> dublist = [];
|
||||||
|
|
||||||
if (historySeries != null){
|
if (historySeries != null){
|
||||||
var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId);
|
var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId);
|
||||||
|
|
@ -372,7 +496,7 @@ public class History{
|
||||||
public (List<string> sublist, string videoQuality) GetSubList(string? seriesId, string? seasonId){
|
public (List<string> sublist, string videoQuality) GetSubList(string? seriesId, string? seasonId){
|
||||||
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
|
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
|
||||||
|
|
||||||
List<string> sublist =[];
|
List<string> sublist = [];
|
||||||
var videoQuality = "";
|
var videoQuality = "";
|
||||||
|
|
||||||
if (historySeries != null){
|
if (historySeries != null){
|
||||||
|
|
@ -430,8 +554,8 @@ public class History{
|
||||||
SeriesId = artisteData.Id,
|
SeriesId = artisteData.Id,
|
||||||
SeriesTitle = artisteData.Name ?? "",
|
SeriesTitle = artisteData.Name ?? "",
|
||||||
ThumbnailImageUrl = artisteData.Images.PosterTall.FirstOrDefault(e => e.Height == 360)?.Source ?? "",
|
ThumbnailImageUrl = artisteData.Images.PosterTall.FirstOrDefault(e => e.Height == 360)?.Source ?? "",
|
||||||
HistorySeriesAvailableDubLang =[],
|
HistorySeriesAvailableDubLang = [],
|
||||||
HistorySeriesAvailableSoftSubs =[]
|
HistorySeriesAvailableSoftSubs = []
|
||||||
};
|
};
|
||||||
|
|
||||||
historySeries.SeriesDescription = cachedSeries.SeriesDescription;
|
historySeries.SeriesDescription = cachedSeries.SeriesDescription;
|
||||||
|
|
@ -563,7 +687,7 @@ public class History{
|
||||||
SeasonTitle = firstEpisode.GetSeasonTitle(),
|
SeasonTitle = firstEpisode.GetSeasonTitle(),
|
||||||
SeasonId = firstEpisode.GetSeasonId(),
|
SeasonId = firstEpisode.GetSeasonId(),
|
||||||
SeasonNum = firstEpisode.GetSeasonNum(),
|
SeasonNum = firstEpisode.GetSeasonNum(),
|
||||||
EpisodesList =[],
|
EpisodesList = [],
|
||||||
SpecialSeason = firstEpisode.IsSpecialSeason()
|
SpecialSeason = firstEpisode.IsSpecialSeason()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -631,7 +755,7 @@ public class History{
|
||||||
|
|
||||||
historySeries.SonarrNextAirDate = GetNextAirDate(episodes);
|
historySeries.SonarrNextAirDate = GetNextAirDate(episodes);
|
||||||
|
|
||||||
List<HistoryEpisode> allHistoryEpisodes =[];
|
List<HistoryEpisode> allHistoryEpisodes = [];
|
||||||
|
|
||||||
foreach (var historySeriesSeason in historySeries.Seasons){
|
foreach (var historySeriesSeason in historySeries.Seasons){
|
||||||
allHistoryEpisodes.AddRange(historySeriesSeason.EpisodesList);
|
allHistoryEpisodes.AddRange(historySeriesSeason.EpisodesList);
|
||||||
|
|
@ -659,7 +783,7 @@ public class History{
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<HistoryEpisode> failedEpisodes =[];
|
List<HistoryEpisode> failedEpisodes = [];
|
||||||
|
|
||||||
Parallel.ForEach(allHistoryEpisodes, historyEpisode => {
|
Parallel.ForEach(allHistoryEpisodes, historyEpisode => {
|
||||||
if (string.IsNullOrEmpty(historyEpisode.SonarrEpisodeId)){
|
if (string.IsNullOrEmpty(historyEpisode.SonarrEpisodeId)){
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
|
|
@ -12,10 +13,12 @@ using Avalonia.Styling;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CRD.Downloader.Crunchyroll;
|
using CRD.Downloader.Crunchyroll;
|
||||||
using CRD.Utils;
|
using CRD.Utils;
|
||||||
|
using CRD.Utils.Files;
|
||||||
using CRD.Utils.Structs;
|
using CRD.Utils.Structs;
|
||||||
using CRD.Utils.Structs.History;
|
using CRD.Utils.Structs.History;
|
||||||
using CRD.Utils.Updater;
|
using CRD.Utils.Updater;
|
||||||
using FluentAvalonia.Styling;
|
using FluentAvalonia.Styling;
|
||||||
|
using ProtoBuf.Meta;
|
||||||
|
|
||||||
namespace CRD.Downloader;
|
namespace CRD.Downloader;
|
||||||
|
|
||||||
|
|
@ -75,10 +78,12 @@ public partial class ProgramManager : ObservableObject{
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
private readonly PeriodicWorkRunner checkForNewEpisodesRunner;
|
||||||
|
|
||||||
public IStorageProvider StorageProvider;
|
public IStorageProvider StorageProvider;
|
||||||
|
|
||||||
public ProgramManager(){
|
public ProgramManager(){
|
||||||
|
checkForNewEpisodesRunner = new PeriodicWorkRunner(async ct => { await CheckForDownloadsAsync(ct); });
|
||||||
_faTheme = Application.Current?.Styles[0] as FluentAvaloniaTheme;
|
_faTheme = Application.Current?.Styles[0] as FluentAvaloniaTheme;
|
||||||
|
|
||||||
foreach (var arg in Environment.GetCommandLineArgs()){
|
foreach (var arg in Environment.GetCommandLineArgs()){
|
||||||
|
|
@ -106,7 +111,7 @@ public partial class ProgramManager : ObservableObject{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Init();
|
_ = Init();
|
||||||
|
|
||||||
CleanUpOldUpdater();
|
CleanUpOldUpdater();
|
||||||
}
|
}
|
||||||
|
|
@ -172,12 +177,53 @@ public partial class ProgramManager : ObservableObject{
|
||||||
await Task.WhenAll(tasks);
|
await Task.WhenAll(tasks);
|
||||||
|
|
||||||
|
|
||||||
while (QueueManager.Instance.Queue.Any(e => e.DownloadProgress.Done != true)){
|
while (QueueManager.Instance.Queue.Any(e => e.DownloadProgress is{ Done: false, Error: false })){
|
||||||
Console.WriteLine("Waiting for downloads to complete...");
|
Console.WriteLine("Waiting for downloads to complete...");
|
||||||
await Task.Delay(2000);
|
await Task.Delay(2000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task CheckForDownloadsAsync(CancellationToken ct){
|
||||||
|
var crunchyManager = CrunchyrollManager.Instance;
|
||||||
|
var crunOptions = crunchyManager.CrunOptions;
|
||||||
|
|
||||||
|
if (!crunOptions.History){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (crunOptions.HistoryAutoRefreshMode){
|
||||||
|
case HistoryRefreshMode.DefaultAll:
|
||||||
|
await RefreshHistory(FilterType.All);
|
||||||
|
break;
|
||||||
|
case HistoryRefreshMode.DefaultActive:
|
||||||
|
await RefreshHistory(FilterType.Active);
|
||||||
|
break;
|
||||||
|
case HistoryRefreshMode.FastNewReleases:
|
||||||
|
var newEpisodesBase = await crunchyManager.CrEpisode.GetNewEpisodes(
|
||||||
|
string.IsNullOrEmpty(crunOptions.HistoryLang) ? crunchyManager.DefaultLocale : crunOptions.HistoryLang,
|
||||||
|
2000, null, true);
|
||||||
|
if (newEpisodesBase is{ Data.Count: > 0 }){
|
||||||
|
var newEpisodes = newEpisodesBase.Data ?? [];
|
||||||
|
|
||||||
|
try{
|
||||||
|
await crunchyManager.History.UpdateWithEpisode(newEpisodes);
|
||||||
|
CfgManager.UpdateHistoryFile();
|
||||||
|
} catch (Exception e){
|
||||||
|
Console.Error.WriteLine("Failed to update History");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tasks = crunchyManager.HistoryList
|
||||||
|
.Select(item => item.AddNewMissingToDownloads(true));
|
||||||
|
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
public void SetBackgroundImage(){
|
public void SetBackgroundImage(){
|
||||||
if (!string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.BackgroundImagePath)){
|
if (!string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.BackgroundImagePath)){
|
||||||
Helpers.SetBackgroundImage(CrunchyrollManager.Instance.CrunOptions.BackgroundImagePath, CrunchyrollManager.Instance.CrunOptions.BackgroundImageOpacity,
|
Helpers.SetBackgroundImage(CrunchyrollManager.Instance.CrunOptions.BackgroundImagePath, CrunchyrollManager.Instance.CrunOptions.BackgroundImageOpacity,
|
||||||
|
|
@ -186,33 +232,41 @@ public partial class ProgramManager : ObservableObject{
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Init(){
|
private async Task Init(){
|
||||||
CrunchyrollManager.Instance.InitOptions();
|
try{
|
||||||
|
CrunchyrollManager.Instance.InitOptions();
|
||||||
|
|
||||||
UpdateAvailable = await Updater.Instance.CheckForUpdatesAsync();
|
UpdateAvailable = await Updater.Instance.CheckForUpdatesAsync();
|
||||||
|
|
||||||
OpacityButton = UpdateAvailable ? 1.0 : 0.4;
|
OpacityButton = UpdateAvailable ? 1.0 : 0.4;
|
||||||
|
|
||||||
if (CrunchyrollManager.Instance.CrunOptions.AccentColor != null && !string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.AccentColor)){
|
if (CrunchyrollManager.Instance.CrunOptions.AccentColor != null && !string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.AccentColor)){
|
||||||
if (_faTheme != null) _faTheme.CustomAccentColor = Color.Parse(CrunchyrollManager.Instance.CrunOptions.AccentColor);
|
if (_faTheme != null) _faTheme.CustomAccentColor = Color.Parse(CrunchyrollManager.Instance.CrunOptions.AccentColor);
|
||||||
}
|
|
||||||
|
|
||||||
if (_faTheme != null && Application.Current != null){
|
|
||||||
if (CrunchyrollManager.Instance.CrunOptions.Theme == "System"){
|
|
||||||
_faTheme.PreferSystemTheme = true;
|
|
||||||
} else if (CrunchyrollManager.Instance.CrunOptions.Theme == "Dark"){
|
|
||||||
_faTheme.PreferSystemTheme = false;
|
|
||||||
Application.Current.RequestedThemeVariant = ThemeVariant.Dark;
|
|
||||||
} else{
|
|
||||||
_faTheme.PreferSystemTheme = false;
|
|
||||||
Application.Current.RequestedThemeVariant = ThemeVariant.Light;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_faTheme != null && Application.Current != null){
|
||||||
|
if (CrunchyrollManager.Instance.CrunOptions.Theme == "System"){
|
||||||
|
_faTheme.PreferSystemTheme = true;
|
||||||
|
} else if (CrunchyrollManager.Instance.CrunOptions.Theme == "Dark"){
|
||||||
|
_faTheme.PreferSystemTheme = false;
|
||||||
|
Application.Current.RequestedThemeVariant = ThemeVariant.Dark;
|
||||||
|
} else{
|
||||||
|
_faTheme.PreferSystemTheme = false;
|
||||||
|
Application.Current.RequestedThemeVariant = ThemeVariant.Light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await CrunchyrollManager.Instance.Init();
|
||||||
|
|
||||||
|
FinishedLoading = true;
|
||||||
|
|
||||||
|
await WorkOffArgsTasks();
|
||||||
|
|
||||||
|
StartRunners(true);
|
||||||
|
} catch (Exception e){
|
||||||
|
Console.Error.WriteLine(e);
|
||||||
|
} finally{
|
||||||
|
NavigationLock = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
await CrunchyrollManager.Instance.Init();
|
|
||||||
|
|
||||||
FinishedLoading = true;
|
|
||||||
|
|
||||||
await WorkOffArgsTasks();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -238,8 +292,7 @@ public partial class ProgramManager : ObservableObject{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void CleanUpOldUpdater(){
|
private void CleanUpOldUpdater(){
|
||||||
var executableExtension = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : string.Empty;
|
var executableExtension = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : string.Empty;
|
||||||
|
|
||||||
|
|
@ -256,4 +309,17 @@ public partial class ProgramManager : ObservableObject{
|
||||||
Console.WriteLine("No old updater file found to delete.");
|
Console.WriteLine("No old updater file found to delete.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DateTime GetLastRefreshTime(){
|
||||||
|
return checkForNewEpisodesRunner.LastRunTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartRunners(bool runImmediately = false){
|
||||||
|
checkForNewEpisodesRunner.StartOrRestartMinutes(CrunchyrollManager.Instance.CrunOptions.HistoryAutoRefreshIntervalMinutes,runImmediately);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopBackgroundTasks(){
|
||||||
|
checkForNewEpisodesRunner.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -139,8 +139,8 @@ public partial class QueueManager : ObservableObject{
|
||||||
(HistoryEpisode? historyEpisode, List<string> dublist, List<string> sublist, string downloadDirPath, string videoQuality) historyEpisode = (null, [], [], "", "");
|
(HistoryEpisode? historyEpisode, List<string> dublist, List<string> sublist, string downloadDirPath, string videoQuality) historyEpisode = (null, [], [], "", "");
|
||||||
|
|
||||||
if (CrunchyrollManager.Instance.CrunOptions.History){
|
if (CrunchyrollManager.Instance.CrunOptions.History){
|
||||||
var episode = sList.EpisodeAndLanguages.Items.First();
|
var variant = sList.EpisodeAndLanguages.Variants.First();
|
||||||
historyEpisode = CrunchyrollManager.Instance.History.GetHistoryEpisodeWithDubListAndDownloadDir(episode.SeriesId, episode.SeasonId, episode.Id);
|
historyEpisode = CrunchyrollManager.Instance.History.GetHistoryEpisodeWithDubListAndDownloadDir(variant.Item.SeriesId, variant.Item.SeasonId, variant.Item.Id);
|
||||||
if (historyEpisode.dublist.Count > 0){
|
if (historyEpisode.dublist.Count > 0){
|
||||||
dubLang = historyEpisode.dublist;
|
dubLang = historyEpisode.dublist;
|
||||||
}
|
}
|
||||||
|
|
@ -238,8 +238,9 @@ public partial class QueueManager : ObservableObject{
|
||||||
Console.WriteLine("Added Episode to Queue but couldn't find all selected dubs");
|
Console.WriteLine("Added Episode to Queue but couldn't find all selected dubs");
|
||||||
Console.Error.WriteLine("Added Episode to Queue but couldn't find all selected dubs - Available dubs/subs: ");
|
Console.Error.WriteLine("Added Episode to Queue but couldn't find all selected dubs - Available dubs/subs: ");
|
||||||
|
|
||||||
var languages = sList.EpisodeAndLanguages.Items.Select((a, index) =>
|
var languages = sList.EpisodeAndLanguages.Variants
|
||||||
$"{(a.IsPremiumOnly ? "+ " : "")}{sList.EpisodeAndLanguages.Langs.ElementAtOrDefault(index)?.CrLocale ?? "Unknown"}").ToArray();
|
.Select(v => $"{(v.Item.IsPremiumOnly ? "+ " : "")}{v.Lang?.CrLocale ?? "Unknown"}")
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"{selected.SeasonTitle} - Season {selected.Season} - {selected.EpisodeTitle} dubs - [{string.Join(", ", languages)}] subs - [{string.Join(", ", selected.AvailableSubs ??[])}]");
|
$"{selected.SeasonTitle} - Season {selected.Season} - {selected.EpisodeTitle} dubs - [{string.Join(", ", languages)}] subs - [{string.Join(", ", selected.AvailableSubs ??[])}]");
|
||||||
|
|
@ -252,8 +253,9 @@ public partial class QueueManager : ObservableObject{
|
||||||
Console.WriteLine("Episode couldn't be added to Queue");
|
Console.WriteLine("Episode couldn't be added to Queue");
|
||||||
Console.Error.WriteLine("Episode couldn't be added to Queue - Available dubs/subs: ");
|
Console.Error.WriteLine("Episode couldn't be added to Queue - Available dubs/subs: ");
|
||||||
|
|
||||||
var languages = sList.EpisodeAndLanguages.Items.Select((a, index) =>
|
var languages = sList.EpisodeAndLanguages.Variants
|
||||||
$"{(a.IsPremiumOnly ? "+ " : "")}{sList.EpisodeAndLanguages.Langs.ElementAtOrDefault(index)?.CrLocale ?? "Unknown"}").ToArray();
|
.Select(v => $"{(v.Item.IsPremiumOnly ? "+ " : "")}{v.Lang?.CrLocale ?? "Unknown"}")
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
Console.Error.WriteLine($"{selected.SeasonTitle} - Season {selected.Season} - {selected.EpisodeTitle} dubs - [{string.Join(", ", languages)}] subs - [{string.Join(", ", selected.AvailableSubs ??[])}]");
|
Console.Error.WriteLine($"{selected.SeasonTitle} - Season {selected.Season} - {selected.EpisodeTitle} dubs - [{string.Join(", ", languages)}] subs - [{string.Join(", ", selected.AvailableSubs ??[])}]");
|
||||||
MessageBus.Current.SendMessage(new ToastMessage($"Couldn't add episode to the queue with current dub settings", ToastType.Error, 2));
|
MessageBus.Current.SendMessage(new ToastMessage($"Couldn't add episode to the queue with current dub settings", ToastType.Error, 2));
|
||||||
|
|
@ -374,7 +376,7 @@ public partial class QueueManager : ObservableObject{
|
||||||
|
|
||||||
|
|
||||||
public async Task CrAddSeriesToQueue(CrunchySeriesList list, CrunchyMultiDownload data){
|
public async Task CrAddSeriesToQueue(CrunchySeriesList list, CrunchyMultiDownload data){
|
||||||
var selected = CrunchyrollManager.Instance.CrSeries.ItemSelectMultiDub(list.Data, data.DubLang, data.But, data.AllEpisodes, data.E);
|
var selected = CrunchyrollManager.Instance.CrSeries.ItemSelectMultiDub(list.Data, data.DubLang, data.AllEpisodes, data.E);
|
||||||
|
|
||||||
var failed = false;
|
var failed = false;
|
||||||
var partialAdd = false;
|
var partialAdd = false;
|
||||||
|
|
|
||||||
|
|
@ -6,29 +6,19 @@ using ReactiveUI.Avalonia;
|
||||||
namespace CRD;
|
namespace CRD;
|
||||||
|
|
||||||
sealed class Program{
|
sealed class Program{
|
||||||
// Initialization code. Don't use any Avalonia, third-party APIs or any
|
|
||||||
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
|
|
||||||
// yet and stuff might break.
|
|
||||||
[STAThread]
|
[STAThread]
|
||||||
public static void Main(string[] args){
|
public static void Main(string[] args){
|
||||||
var isHeadless = args.Contains("--headless");
|
var isHeadless = args.Contains("--headless");
|
||||||
|
|
||||||
BuildAvaloniaApp(isHeadless).StartWithClassicDesktopLifetime(args);
|
BuildAvaloniaApp(isHeadless).StartWithClassicDesktopLifetime(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Avalonia configuration, don't remove; also used by visual designer.
|
|
||||||
// public static AppBuilder BuildAvaloniaApp()
|
|
||||||
// => AppBuilder.Configure<App>()
|
|
||||||
// .UsePlatformDetect()
|
|
||||||
// .WithInterFont()
|
|
||||||
// .LogToTrace();
|
|
||||||
|
|
||||||
public static AppBuilder BuildAvaloniaApp(bool isHeadless){
|
public static AppBuilder BuildAvaloniaApp(bool isHeadless){
|
||||||
var builder = AppBuilder.Configure<App>()
|
var builder = AppBuilder.Configure<App>()
|
||||||
.UsePlatformDetect()
|
.UsePlatformDetect()
|
||||||
.WithInterFont()
|
.WithInterFont()
|
||||||
.LogToTrace()
|
.LogToTrace()
|
||||||
.UseReactiveUI() ;
|
.UseReactiveUI(_ => { });
|
||||||
|
|
||||||
if (isHeadless){
|
if (isHeadless){
|
||||||
Console.WriteLine("Running in headless mode...");
|
Console.WriteLine("Running in headless mode...");
|
||||||
|
|
|
||||||
|
|
@ -276,6 +276,12 @@ public enum EpisodeDownloadMode{
|
||||||
OnlySubs,
|
OnlySubs,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum HistoryRefreshMode{
|
||||||
|
DefaultAll = 0,
|
||||||
|
DefaultActive = 1,
|
||||||
|
FastNewReleases = 50
|
||||||
|
}
|
||||||
|
|
||||||
public enum SonarrCoverType{
|
public enum SonarrCoverType{
|
||||||
Banner,
|
Banner,
|
||||||
FanArt,
|
FanArt,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
@ -360,158 +361,201 @@ public class Helpers{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetQualityOption(VideoPreset preset){
|
private static IEnumerable<string> GetQualityOption(VideoPreset preset){
|
||||||
return preset.Codec switch{
|
return preset.Codec switch{
|
||||||
"h264_nvenc" or "hevc_nvenc" => $"-cq {preset.Crf}", // For NVENC
|
"h264_nvenc" or "hevc_nvenc" =>["-cq", preset.Crf.ToString()],
|
||||||
"h264_qsv" or "hevc_qsv" => $"-global_quality {preset.Crf}", // For Intel QSV
|
"h264_qsv" or "hevc_qsv" =>["-global_quality", preset.Crf.ToString()],
|
||||||
"h264_amf" or "hevc_amf" => $"-qp {preset.Crf}", // For AMD VCE
|
"h264_amf" or "hevc_amf" =>["-qp", preset.Crf.ToString()],
|
||||||
_ => $"-crf {preset.Crf}", // For software codecs like libx264/libx265
|
_ =>["-crf", preset.Crf.ToString()]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<(bool IsOk, int ErrorCode)> RunFFmpegWithPresetAsync(string inputFilePath, VideoPreset preset, CrunchyEpMeta? data = null){
|
public static async Task<(bool IsOk, int ErrorCode)> RunFFmpegWithPresetAsync(
|
||||||
|
string inputFilePath,
|
||||||
|
VideoPreset preset,
|
||||||
|
CrunchyEpMeta? data = null){
|
||||||
try{
|
try{
|
||||||
string outputExtension = Path.GetExtension(inputFilePath);
|
string ext = Path.GetExtension(inputFilePath);
|
||||||
string directory = Path.GetDirectoryName(inputFilePath);
|
string dir = Path.GetDirectoryName(inputFilePath)!;
|
||||||
string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(inputFilePath);
|
string name = Path.GetFileNameWithoutExtension(inputFilePath);
|
||||||
string tempOutputFilePath = Path.Combine(directory, $"{fileNameWithoutExtension}_output{outputExtension}");
|
|
||||||
|
|
||||||
string additionalParams = string.Join(" ", preset.AdditionalParameters.Select(param => {
|
string tempOutput = Path.Combine(dir, $"{name}_output{ext}");
|
||||||
var splitIndex = param.IndexOf(' ');
|
|
||||||
if (splitIndex > 0){
|
|
||||||
var prefix = param[..splitIndex];
|
|
||||||
var value = param[(splitIndex + 1)..];
|
|
||||||
|
|
||||||
if (value.Contains(' ') && !(value.StartsWith("\"") && value.EndsWith("\""))){
|
|
||||||
value = $"\"{value}\"";
|
|
||||||
}
|
|
||||||
|
|
||||||
return $"{prefix} {value}";
|
|
||||||
}
|
|
||||||
|
|
||||||
return param;
|
|
||||||
}));
|
|
||||||
|
|
||||||
string qualityOption = GetQualityOption(preset);
|
|
||||||
|
|
||||||
TimeSpan? totalDuration = await GetMediaDurationAsync(CfgManager.PathFFMPEG, inputFilePath);
|
TimeSpan? totalDuration = await GetMediaDurationAsync(CfgManager.PathFFMPEG, inputFilePath);
|
||||||
if (totalDuration == null){
|
|
||||||
Console.Error.WriteLine("Unable to retrieve input file duration.");
|
var args = new List<string>{
|
||||||
} else{
|
"-nostdin",
|
||||||
Console.WriteLine($"Total Duration: {totalDuration}");
|
"-hide_banner",
|
||||||
|
"-loglevel", "error",
|
||||||
|
"-i", inputFilePath,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(preset.Codec)){
|
||||||
|
args.Add("-c:v");
|
||||||
|
args.Add(preset.Codec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
args.AddRange(GetQualityOption(preset));
|
||||||
|
|
||||||
string ffmpegCommand = $"-loglevel info -i \"{inputFilePath}\" -c:v {preset.Codec} {qualityOption} -vf \"scale={preset.Resolution},fps={preset.FrameRate}\" {additionalParams} \"{tempOutputFilePath}\"";
|
args.Add("-vf");
|
||||||
using (var process = new Process()){
|
args.Add($"scale={preset.Resolution},fps={preset.FrameRate}");
|
||||||
process.StartInfo.FileName = CfgManager.PathFFMPEG;
|
|
||||||
process.StartInfo.Arguments = ffmpegCommand;
|
|
||||||
process.StartInfo.RedirectStandardOutput = true;
|
|
||||||
process.StartInfo.RedirectStandardError = true;
|
|
||||||
process.StartInfo.UseShellExecute = false;
|
|
||||||
process.StartInfo.CreateNoWindow = true;
|
|
||||||
process.EnableRaisingEvents = true;
|
|
||||||
|
|
||||||
process.OutputDataReceived += (sender, e) => {
|
foreach (var param in preset.AdditionalParameters){
|
||||||
if (!string.IsNullOrEmpty(e.Data)){
|
args.AddRange(SplitArguments(param));
|
||||||
Console.WriteLine(e.Data);
|
}
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
process.ErrorDataReceived += (sender, e) => {
|
args.Add(tempOutput);
|
||||||
if (!string.IsNullOrEmpty(e.Data)){
|
|
||||||
Console.Error.WriteLine($"{e.Data}");
|
|
||||||
if (data != null && totalDuration != null){
|
|
||||||
ParseProgress(e.Data, totalDuration.Value, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
process.Start();
|
string commandString = BuildCommandString(CfgManager.PathFFMPEG, args);
|
||||||
process.BeginOutputReadLine();
|
int exitCode;
|
||||||
process.BeginErrorReadLine();
|
try{
|
||||||
|
exitCode = await RunFFmpegAsync(
|
||||||
using var reg = data?.Cts.Token.Register(() => {
|
CfgManager.PathFFMPEG,
|
||||||
|
args,
|
||||||
|
data?.Cts.Token ?? CancellationToken.None,
|
||||||
|
onStdErr: line => { Console.Error.WriteLine(line); },
|
||||||
|
onStdOut: Console.WriteLine
|
||||||
|
);
|
||||||
|
} catch (OperationCanceledException){
|
||||||
|
if (File.Exists(tempOutput)){
|
||||||
try{
|
try{
|
||||||
if (!process.HasExited)
|
File.Delete(tempOutput);
|
||||||
process.Kill(true);
|
|
||||||
} catch{
|
} catch{
|
||||||
// ignored
|
// ignored
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
try{
|
|
||||||
await process.WaitForExitAsync(data.Cts.Token);
|
|
||||||
} catch (OperationCanceledException){
|
|
||||||
if (File.Exists(tempOutputFilePath)){
|
|
||||||
try{
|
|
||||||
File.Delete(tempOutputFilePath);
|
|
||||||
} catch{
|
|
||||||
// ignored
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (IsOk: false, ErrorCode: -2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isSuccess = process.ExitCode == 0;
|
Console.Error.WriteLine("FFMPEG task was canceled");
|
||||||
|
return (false, -2);
|
||||||
if (isSuccess){
|
|
||||||
// Delete the original input file
|
|
||||||
File.Delete(inputFilePath);
|
|
||||||
|
|
||||||
// Rename the output file to the original name
|
|
||||||
File.Move(tempOutputFilePath, inputFilePath);
|
|
||||||
} else{
|
|
||||||
// If something went wrong, delete the temporary output file
|
|
||||||
if (File.Exists(tempOutputFilePath)){
|
|
||||||
try{
|
|
||||||
File.Delete(tempOutputFilePath);
|
|
||||||
} catch{
|
|
||||||
/* ignore */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.Error.WriteLine("FFmpeg processing failed.");
|
|
||||||
Console.Error.WriteLine($"Command: {ffmpegCommand}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (IsOk: isSuccess, ErrorCode: process.ExitCode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool success = exitCode == 0;
|
||||||
|
|
||||||
|
if (success){
|
||||||
|
File.Delete(inputFilePath);
|
||||||
|
File.Move(tempOutput, inputFilePath);
|
||||||
|
} else{
|
||||||
|
if (File.Exists(tempOutput)){
|
||||||
|
File.Delete(tempOutput);
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine("FFmpeg processing failed.");
|
||||||
|
Console.Error.WriteLine("Command:");
|
||||||
|
Console.Error.WriteLine(commandString);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (success, exitCode);
|
||||||
} catch (Exception ex){
|
} catch (Exception ex){
|
||||||
Console.Error.WriteLine($"An error occurred: {ex.Message}");
|
Console.Error.WriteLine(ex);
|
||||||
return (IsOk: false, ErrorCode: -1);
|
|
||||||
|
return (false, -1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ParseProgress(string progressString, TimeSpan totalDuration, CrunchyEpMeta data){
|
private static IEnumerable<string> SplitArguments(string commandLine){
|
||||||
try{
|
var args = new List<string>();
|
||||||
if (progressString.Contains("time=")){
|
var current = new StringBuilder();
|
||||||
var timeIndex = progressString.IndexOf("time=") + 5;
|
bool inQuotes = false;
|
||||||
var timeString = progressString.Substring(timeIndex, 11);
|
|
||||||
|
|
||||||
|
foreach (char c in commandLine){
|
||||||
if (TimeSpan.TryParse(timeString, out var currentTime)){
|
if (c == '"'){
|
||||||
int progress = (int)(currentTime.TotalSeconds / totalDuration.TotalSeconds * 100);
|
inQuotes = !inQuotes;
|
||||||
Console.WriteLine($"Progress: {progress:F2}%");
|
continue;
|
||||||
|
}
|
||||||
data.DownloadProgress = new DownloadProgress(){
|
|
||||||
IsDownloading = true,
|
if (char.IsWhiteSpace(c) && !inQuotes){
|
||||||
Percent = progress,
|
if (current.Length > 0){
|
||||||
Time = 0,
|
args.Add(current.ToString());
|
||||||
DownloadSpeedBytes = 0,
|
current.Clear();
|
||||||
Doing = "Encoding"
|
}
|
||||||
};
|
} else{
|
||||||
|
current.Append(c);
|
||||||
QueueManager.Instance.Queue.Refresh();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (Exception e){
|
|
||||||
Console.Error.WriteLine("Failed to calculate encoding progess");
|
|
||||||
Console.Error.WriteLine(e.Message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (current.Length > 0)
|
||||||
|
args.Add(current.ToString());
|
||||||
|
|
||||||
|
return args;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string BuildCommandString(string exe, IEnumerable<string> args){
|
||||||
|
static string Quote(string s){
|
||||||
|
if (string.IsNullOrWhiteSpace(s))
|
||||||
|
return "\"\"";
|
||||||
|
|
||||||
|
return s.Contains(' ') || s.Contains('"')
|
||||||
|
? $"\"{s.Replace("\"", "\\\"")}\""
|
||||||
|
: s;
|
||||||
|
}
|
||||||
|
|
||||||
|
return exe + " " + string.Join(" ", args.Select(Quote));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<int> RunFFmpegAsync(
|
||||||
|
string ffmpegPath,
|
||||||
|
IEnumerable<string> args,
|
||||||
|
CancellationToken token,
|
||||||
|
Action<string>? onStdErr = null,
|
||||||
|
Action<string>? onStdOut = null){
|
||||||
|
using var process = new Process();
|
||||||
|
|
||||||
|
process.StartInfo = new ProcessStartInfo{
|
||||||
|
FileName = ffmpegPath,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
RedirectStandardInput = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var arg in args)
|
||||||
|
process.StartInfo.ArgumentList.Add(arg);
|
||||||
|
|
||||||
|
process.Start();
|
||||||
|
|
||||||
|
// capture streams instead of process
|
||||||
|
var stdout = process.StandardOutput;
|
||||||
|
var stderr = process.StandardError;
|
||||||
|
|
||||||
|
async Task ReadStreamAsync(StreamReader reader, Action<string>? callback){
|
||||||
|
while (await reader.ReadLineAsync(token) is{ } line)
|
||||||
|
callback?.Invoke(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
var stdoutTask = ReadStreamAsync(stdout, onStdOut);
|
||||||
|
var stderrTask = ReadStreamAsync(stderr, onStdErr);
|
||||||
|
|
||||||
|
var proc = process;
|
||||||
|
|
||||||
|
await using var reg = token.Register(() => {
|
||||||
|
try{
|
||||||
|
proc.Kill(true);
|
||||||
|
} catch{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try{
|
||||||
|
await process.WaitForExitAsync(token);
|
||||||
|
} catch (OperationCanceledException){
|
||||||
|
try{
|
||||||
|
if (!process.HasExited)
|
||||||
|
process.Kill(true);
|
||||||
|
} catch{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.WhenAll(stdoutTask, stderrTask);
|
||||||
|
|
||||||
|
return process.ExitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public static async Task<TimeSpan?> GetMediaDurationAsync(string ffmpegPath, string inputFilePath){
|
public static async Task<TimeSpan?> GetMediaDurationAsync(string ffmpegPath, string inputFilePath){
|
||||||
try{
|
try{
|
||||||
using (var process = new Process()){
|
using (var process = new Process()){
|
||||||
|
|
@ -855,7 +899,7 @@ public class Helpers{
|
||||||
bool mergeVideo){
|
bool mergeVideo){
|
||||||
if (target == null) throw new ArgumentNullException(nameof(target));
|
if (target == null) throw new ArgumentNullException(nameof(target));
|
||||||
if (source == null) throw new ArgumentNullException(nameof(source));
|
if (source == null) throw new ArgumentNullException(nameof(source));
|
||||||
|
|
||||||
var serverSet = new HashSet<string>(target.servers);
|
var serverSet = new HashSet<string>(target.servers);
|
||||||
|
|
||||||
void AddServer(string s){
|
void AddServer(string s){
|
||||||
|
|
@ -866,14 +910,14 @@ public class Helpers{
|
||||||
foreach (var kvp in source){
|
foreach (var kvp in source){
|
||||||
var key = kvp.Key;
|
var key = kvp.Key;
|
||||||
var src = kvp.Value;
|
var src = kvp.Value;
|
||||||
|
|
||||||
if (!src.servers.Contains(key))
|
if (!src.servers.Contains(key))
|
||||||
src.servers.Add(key);
|
src.servers.Add(key);
|
||||||
|
|
||||||
AddServer(key);
|
AddServer(key);
|
||||||
foreach (var s in src.servers)
|
foreach (var s in src.servers)
|
||||||
AddServer(s);
|
AddServer(s);
|
||||||
|
|
||||||
if (mergeAudio && src.audio != null){
|
if (mergeAudio && src.audio != null){
|
||||||
target.audio ??= [];
|
target.audio ??= [];
|
||||||
target.audio.AddRange(src.audio);
|
target.audio.AddRange(src.audio);
|
||||||
|
|
@ -911,6 +955,8 @@ public class Helpers{
|
||||||
if (result == ContentDialogResult.Primary){
|
if (result == ContentDialogResult.Primary){
|
||||||
timer.Stop();
|
timer.Stop();
|
||||||
}
|
}
|
||||||
|
} catch (Exception e){
|
||||||
|
Console.Error.WriteLine(e);
|
||||||
} finally{
|
} finally{
|
||||||
ShutdownLock.Release();
|
ShutdownLock.Release();
|
||||||
}
|
}
|
||||||
|
|
@ -967,4 +1013,18 @@ public class Helpers{
|
||||||
Console.Error.WriteLine($"Failed to start shutdown process: {ex.Message}");
|
Console.Error.WriteLine($"Failed to start shutdown process: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool ExecuteFile(string filePath){
|
||||||
|
try{
|
||||||
|
Process.Start(new ProcessStartInfo{
|
||||||
|
FileName = filePath,
|
||||||
|
UseShellExecute = true
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (Exception ex){
|
||||||
|
Console.Error.WriteLine($"Execution failed: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -296,7 +296,7 @@ public static class ApiUrls{
|
||||||
|
|
||||||
public static string Auth => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/auth/v1/token";
|
public static string Auth => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/auth/v1/token";
|
||||||
public static string Profile => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/accounts/v1/me/profile";
|
public static string Profile => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/accounts/v1/me/profile";
|
||||||
public static string Profiles => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/accounts/v1/me/multiprofile";
|
public static string MultiProfile => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/accounts/v1/me/multiprofile";
|
||||||
public static string CmsToken => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/index/v2";
|
public static string CmsToken => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/index/v2";
|
||||||
public static string Search => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/content/v2/discover/search";
|
public static string Search => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/content/v2/discover/search";
|
||||||
public static string Browse => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/content/v2/discover/browse";
|
public static string Browse => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/content/v2/discover/browse";
|
||||||
|
|
|
||||||
|
|
@ -2,37 +2,40 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CRD.Utils.Files;
|
using CRD.Utils.Files;
|
||||||
using CRD.Utils.Structs;
|
using CRD.Utils.Structs;
|
||||||
using CRD.Views;
|
using CRD.Views;
|
||||||
|
using SixLabors.Fonts;
|
||||||
|
|
||||||
namespace CRD.Utils.Muxing;
|
namespace CRD.Utils.Muxing;
|
||||||
|
|
||||||
public class FontsManager{
|
public class FontsManager{
|
||||||
#region Singelton
|
#region Singelton
|
||||||
|
|
||||||
private static FontsManager? instance;
|
private static readonly Lock Padlock = new Lock();
|
||||||
private static readonly object padlock = new object();
|
|
||||||
|
|
||||||
public static FontsManager Instance{
|
public static FontsManager Instance{
|
||||||
get{
|
get{
|
||||||
if (instance == null){
|
if (field == null){
|
||||||
lock (padlock){
|
lock (Padlock){
|
||||||
if (instance == null){
|
if (field == null){
|
||||||
instance = new FontsManager();
|
field = new FontsManager();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return instance;
|
return field;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public Dictionary<string, string> Fonts{ get; private set; } = new(StringComparer.OrdinalIgnoreCase){
|
public Dictionary<string, string> Fonts{ get; private set; } = new(StringComparer.OrdinalIgnoreCase){
|
||||||
{ "Adobe Arabic", "AdobeArabic-Bold.otf" },
|
{ "Adobe Arabic", "AdobeArabic-Bold.otf" },
|
||||||
{ "Andale Mono", "andalemo.ttf" },
|
{ "Andale Mono", "andalemo.ttf" },
|
||||||
{ "Arial", "arial.ttf" },
|
{ "Arial", "arial.ttf" },
|
||||||
|
|
@ -103,9 +106,14 @@ public class FontsManager{
|
||||||
{ "Webdings", "webdings.ttf" }
|
{ "Webdings", "webdings.ttf" }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private string root = "https://static.crunchyroll.com/vilos-v2/web/vilos/assets/libass-fonts/";
|
||||||
|
|
||||||
public string root = "https://static.crunchyroll.com/vilos-v2/web/vilos/assets/libass-fonts/";
|
|
||||||
|
|
||||||
|
private readonly FontIndex index = new();
|
||||||
|
|
||||||
|
private void EnsureIndex(string fontsDir){
|
||||||
|
index.Rebuild(fontsDir);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task GetFontsAsync(){
|
public async Task GetFontsAsync(){
|
||||||
Console.WriteLine("Downloading fonts...");
|
Console.WriteLine("Downloading fonts...");
|
||||||
|
|
@ -115,134 +123,334 @@ public class FontsManager{
|
||||||
var fontLoc = Path.Combine(CfgManager.PathFONTS_DIR, font);
|
var fontLoc = Path.Combine(CfgManager.PathFONTS_DIR, font);
|
||||||
|
|
||||||
if (File.Exists(fontLoc) && new FileInfo(fontLoc).Length != 0){
|
if (File.Exists(fontLoc) && new FileInfo(fontLoc).Length != 0){
|
||||||
// Console.WriteLine($"{font} already downloaded!");
|
continue;
|
||||||
} else{
|
}
|
||||||
var fontFolder = Path.GetDirectoryName(fontLoc);
|
|
||||||
if (File.Exists(fontLoc) && new FileInfo(fontLoc).Length == 0){
|
|
||||||
File.Delete(fontLoc);
|
|
||||||
}
|
|
||||||
|
|
||||||
try{
|
var fontFolder = Path.GetDirectoryName(fontLoc);
|
||||||
if (!Directory.Exists(fontFolder)){
|
if (File.Exists(fontLoc) && new FileInfo(fontLoc).Length == 0)
|
||||||
Directory.CreateDirectory(fontFolder);
|
File.Delete(fontLoc);
|
||||||
}
|
|
||||||
} catch (Exception e){
|
|
||||||
Console.WriteLine($"Failed to create directory: {e.Message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var fontUrl = root + font;
|
try{
|
||||||
|
if (!Directory.Exists(fontFolder))
|
||||||
|
Directory.CreateDirectory(fontFolder!);
|
||||||
|
} catch (Exception e){
|
||||||
|
Console.WriteLine($"Failed to create directory: {e.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
var httpClient = HttpClientReq.Instance.GetHttpClient();
|
var fontUrl = root + font;
|
||||||
try{
|
|
||||||
var response = await httpClient.GetAsync(fontUrl);
|
var httpClient = HttpClientReq.Instance.GetHttpClient();
|
||||||
if (response.IsSuccessStatusCode){
|
try{
|
||||||
var fontData = await response.Content.ReadAsByteArrayAsync();
|
var response = await httpClient.GetAsync(fontUrl);
|
||||||
await File.WriteAllBytesAsync(fontLoc, fontData);
|
if (response.IsSuccessStatusCode){
|
||||||
Console.WriteLine($"Downloaded: {font}");
|
var fontData = await response.Content.ReadAsByteArrayAsync();
|
||||||
} else{
|
await File.WriteAllBytesAsync(fontLoc, fontData);
|
||||||
Console.Error.WriteLine($"Failed to download: {font}");
|
Console.WriteLine($"Downloaded: {font}");
|
||||||
}
|
} else{
|
||||||
} catch (Exception e){
|
Console.Error.WriteLine($"Failed to download: {font}");
|
||||||
Console.Error.WriteLine($"Error downloading {font}: {e.Message}");
|
|
||||||
}
|
}
|
||||||
|
} catch (Exception e){
|
||||||
|
Console.Error.WriteLine($"Error downloading {font}: {e.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine("All required fonts downloaded!");
|
Console.WriteLine("All required fonts downloaded!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static List<string> ExtractFontsFromAss(string ass){
|
public static List<string> ExtractFontsFromAss(string ass){
|
||||||
var lines = ass.Replace("\r", "").Split('\n');
|
if (string.IsNullOrWhiteSpace(ass))
|
||||||
var styles = new List<string>();
|
return new List<string>();
|
||||||
|
|
||||||
|
ass = ass.Replace("\r", "");
|
||||||
|
var lines = ass.Split('\n');
|
||||||
|
|
||||||
|
var fonts = new List<string>();
|
||||||
|
|
||||||
foreach (var line in lines){
|
foreach (var line in lines){
|
||||||
if (line.StartsWith("Style: ")){
|
if (line.StartsWith("Style: ", StringComparison.OrdinalIgnoreCase)){
|
||||||
var parts = line.Split(',');
|
var parts = line.Substring(7).Split(',');
|
||||||
if (parts.Length > 1)
|
if (parts.Length > 1){
|
||||||
styles.Add(parts[1].Trim());
|
var fontName = parts[1].Trim();
|
||||||
|
fonts.Add(NormalizeFontKey(fontName));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var fontMatches = Regex.Matches(ass, @"\\fn([^\\}]+)");
|
var fontMatches = Regex.Matches(ass, @"\\fn([^\\}]+)");
|
||||||
foreach (Match match in fontMatches){
|
foreach (Match match in fontMatches){
|
||||||
if (match.Groups.Count > 1)
|
if (match.Groups.Count > 1){
|
||||||
styles.Add(match.Groups[1].Value);
|
var fontName = match.Groups[1].Value.Trim();
|
||||||
|
fonts.Add(NormalizeFontKey(fontName));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return styles.Distinct().ToList(); // Using Linq to remove duplicates
|
return fonts
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Dictionary<string, string> GetDictFromKeyList(List<string> keysList){
|
public Dictionary<string, string> GetDictFromKeyList(List<string> keysList, bool keepUnknown = true){
|
||||||
Dictionary<string, string> filteredDictionary = new Dictionary<string, string>();
|
Dictionary<string, string> filteredDictionary = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
foreach (string key in keysList){
|
foreach (string key in keysList){
|
||||||
if (Fonts.TryGetValue(key, out var font)){
|
var k = NormalizeFontKey(key);
|
||||||
filteredDictionary.Add(key, font);
|
|
||||||
|
if (Fonts.TryGetValue(k, out var fontFile)){
|
||||||
|
filteredDictionary[k] = fontFile;
|
||||||
|
} else if (keepUnknown){
|
||||||
|
filteredDictionary[k] = k;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return filteredDictionary;
|
return filteredDictionary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string GetFontMimeType(string fontFileOrPath){
|
||||||
public static string GetFontMimeType(string fontFile){
|
var ext = Path.GetExtension(fontFileOrPath);
|
||||||
if (Regex.IsMatch(fontFile, @"\.otf$"))
|
if (ext.Equals(".otf", StringComparison.OrdinalIgnoreCase))
|
||||||
return "application/vnd.ms-opentype";
|
return "application/vnd.ms-opentype";
|
||||||
else if (Regex.IsMatch(fontFile, @"\.ttf$"))
|
if (ext.Equals(".ttf", StringComparison.OrdinalIgnoreCase))
|
||||||
return "application/x-truetype-font";
|
return "application/x-truetype-font";
|
||||||
else
|
if (ext.Equals(".ttc", StringComparison.OrdinalIgnoreCase) || ext.Equals(".otc", StringComparison.OrdinalIgnoreCase))
|
||||||
return "application/octet-stream";
|
return "application/x-truetype-font";
|
||||||
|
if (ext.Equals(".woff", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return "font/woff";
|
||||||
|
if (ext.Equals(".woff2", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return "font/woff2";
|
||||||
|
return "application/octet-stream";
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ParsedFont> MakeFontsList(string fontsDir, List<SubtitleFonts> subs){
|
public List<ParsedFont> MakeFontsList(string fontsDir, List<SubtitleFonts> subs){
|
||||||
Dictionary<string, string> fontsNameList = new Dictionary<string, string>();
|
EnsureIndex(fontsDir);
|
||||||
List<string> subsList = new List<string>();
|
|
||||||
List<ParsedFont> fontsList = new List<ParsedFont>();
|
var required = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
bool isNstr = true;
|
var subsLocales = new List<string>();
|
||||||
|
var fontsList = new List<ParsedFont>();
|
||||||
|
var missing = new List<string>();
|
||||||
|
|
||||||
foreach (var s in subs){
|
foreach (var s in subs){
|
||||||
foreach (var keyValuePair in s.Fonts){
|
subsLocales.Add(s.Language.Locale);
|
||||||
if (!fontsNameList.ContainsKey(keyValuePair.Key)){
|
|
||||||
fontsNameList.Add(keyValuePair.Key, keyValuePair.Value);
|
foreach (var kv in s.Fonts)
|
||||||
|
required.Add(NormalizeFontKey(kv.Key));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subsLocales.Count > 0)
|
||||||
|
Console.WriteLine("\nSubtitles: {0} (Total: {1})", string.Join(", ", subsLocales), subsLocales.Count);
|
||||||
|
|
||||||
|
if (required.Count > 0)
|
||||||
|
Console.WriteLine("Required fonts: {0} (Total: {1})", string.Join(", ", required), required.Count);
|
||||||
|
|
||||||
|
foreach (var requested in required){
|
||||||
|
if (TryResolveFontPath(requested, fontsDir, out var resolvedPath, out var exact)){
|
||||||
|
if (!File.Exists(resolvedPath) || new FileInfo(resolvedPath).Length == 0){
|
||||||
|
missing.Add(requested);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
subsList.Add(s.Language.Locale);
|
var attachName = MakeUniqueAttachmentName(resolvedPath, fontsList);
|
||||||
}
|
|
||||||
|
|
||||||
if (subsList.Count > 0){
|
fontsList.Add(new ParsedFont{
|
||||||
Console.WriteLine("\nSubtitles: {0} (Total: {1})", string.Join(", ", subsList), subsList.Count);
|
Name = attachName,
|
||||||
isNstr = false;
|
Path = resolvedPath,
|
||||||
}
|
Mime = GetFontMimeType(resolvedPath)
|
||||||
|
});
|
||||||
|
|
||||||
if (fontsNameList.Count > 0){
|
if (!exact) Console.WriteLine($"Soft-resolved '{requested}' -> '{Path.GetFileName(resolvedPath)}'");
|
||||||
Console.WriteLine((isNstr ? "\n" : "") + "Required fonts: {0} (Total: {1})", string.Join(", ", fontsNameList), fontsNameList.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<string> missingFonts = new List<string>();
|
|
||||||
|
|
||||||
foreach (var f in fontsNameList){
|
|
||||||
if (Fonts.TryGetValue(f.Key, out var fontFile)){
|
|
||||||
string fontPath = Path.Combine(fontsDir, fontFile);
|
|
||||||
string mime = GetFontMimeType(fontFile);
|
|
||||||
if (File.Exists(fontPath) && new FileInfo(fontPath).Length != 0){
|
|
||||||
fontsList.Add(new ParsedFont{ Name = fontFile, Path = fontPath, Mime = mime });
|
|
||||||
}
|
|
||||||
} else{
|
} else{
|
||||||
missingFonts.Add(f.Key);
|
missing.Add(requested);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (missingFonts.Count > 0){
|
if (missing.Count > 0)
|
||||||
MainWindow.Instance.ShowError($"Missing Fonts: \n{string.Join(", ", fontsNameList)}");
|
MainWindow.Instance.ShowError($"Missing Fonts:\n{string.Join(", ", missing)}");
|
||||||
}
|
|
||||||
|
|
||||||
return fontsList;
|
return fontsList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool TryResolveFontPath(string requestedName, string fontsDir, out string resolvedPath, out bool isExactMatch){
|
||||||
|
resolvedPath = string.Empty;
|
||||||
|
isExactMatch = true;
|
||||||
|
|
||||||
|
var req = NormalizeFontKey(requestedName);
|
||||||
|
|
||||||
|
if (index.TryResolve(req, out resolvedPath))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (Fonts.TryGetValue(req, out var crFile)){
|
||||||
|
var p = Path.Combine(fontsDir, crFile);
|
||||||
|
if (File.Exists(p)){
|
||||||
|
resolvedPath = p;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var family = StripStyleSuffix(req);
|
||||||
|
if (!family.Equals(req, StringComparison.OrdinalIgnoreCase)){
|
||||||
|
isExactMatch = false;
|
||||||
|
|
||||||
|
if (index.TryResolve(family, out resolvedPath))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (Fonts.TryGetValue(family, out var crFamilyFile)){
|
||||||
|
var p = Path.Combine(fontsDir, crFamilyFile);
|
||||||
|
if (File.Exists(p)){
|
||||||
|
resolvedPath = p;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string StripStyleSuffix(string name){
|
||||||
|
var n = name;
|
||||||
|
|
||||||
|
n = Regex.Replace(n, @"\s+(Bold\s+Italic|Bold\s+Oblique|Black\s+Italic|Black|Bold|Italic|Oblique|Regular)$",
|
||||||
|
"", RegexOptions.IgnoreCase).Trim();
|
||||||
|
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string NormalizeFontKey(string s){
|
||||||
|
if (string.IsNullOrWhiteSpace(s))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
s = s.Trim().Trim('"');
|
||||||
|
|
||||||
|
if (s.StartsWith("@"))
|
||||||
|
s = s.Substring(1);
|
||||||
|
|
||||||
|
s = Regex.Replace(s, @"(?<=[a-z])([A-Z])", " $1");
|
||||||
|
|
||||||
|
s = s.Replace('_', ' ').Replace('-', ' ');
|
||||||
|
|
||||||
|
s = Regex.Replace(s, @"\s+", " ").Trim();
|
||||||
|
|
||||||
|
s = Regex.Replace(s, @"\s+Regular$", "", RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string MakeUniqueAttachmentName(string path, List<ParsedFont> existing){
|
||||||
|
var baseName = Path.GetFileName(path);
|
||||||
|
|
||||||
|
if (existing.All(e => !baseName.Equals(e.Name, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
return baseName;
|
||||||
|
|
||||||
|
var hash = Convert.ToHexString(SHA1.HashData(Encoding.UTF8.GetBytes(path)))
|
||||||
|
.Substring(0, 8)
|
||||||
|
.ToLowerInvariant();
|
||||||
|
|
||||||
|
return $"{hash}-{baseName}";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private sealed class FontIndex{
|
||||||
|
private readonly Dictionary<string, Candidate> map = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public void Rebuild(string fontsDir){
|
||||||
|
map.Clear();
|
||||||
|
if (!Directory.Exists(fontsDir)) return;
|
||||||
|
|
||||||
|
foreach (var path in Directory.EnumerateFiles(fontsDir, "*.*", SearchOption.AllDirectories)){
|
||||||
|
var ext = Path.GetExtension(path).ToLowerInvariant();
|
||||||
|
if (ext is not (".ttf" or ".otf" or ".ttc" or ".otc" or ".woff" or ".woff2"))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foreach (var desc in LoadDescriptions(path)){
|
||||||
|
foreach (var alias in BuildAliases(desc)){
|
||||||
|
Add(alias, path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryResolve(string fontName, out string path){
|
||||||
|
path = string.Empty;
|
||||||
|
if (string.IsNullOrWhiteSpace(fontName)) return false;
|
||||||
|
|
||||||
|
var key = NormalizeFontKey(fontName);
|
||||||
|
|
||||||
|
if (map.TryGetValue(fontName, out var c1)){
|
||||||
|
path = c1.Path;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (map.TryGetValue(key, out var c2)){
|
||||||
|
path = c2.Path;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Add(string alias, string path){
|
||||||
|
if (string.IsNullOrWhiteSpace(alias)) return;
|
||||||
|
|
||||||
|
var a1 = alias.Trim();
|
||||||
|
var a2 = NormalizeFontKey(a1);
|
||||||
|
|
||||||
|
Upsert(a1, path);
|
||||||
|
Upsert(a2, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Upsert(string key, string path){
|
||||||
|
if (string.IsNullOrWhiteSpace(key)) return;
|
||||||
|
|
||||||
|
var cand = new Candidate(path, GetScore(path));
|
||||||
|
if (map.TryGetValue(key, out var existing)){
|
||||||
|
if (cand.Score > existing.Score)
|
||||||
|
map[key] = cand;
|
||||||
|
} else{
|
||||||
|
map[key] = cand;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetScore(string path){
|
||||||
|
var ext = Path.GetExtension(path).ToLowerInvariant();
|
||||||
|
return ext switch{
|
||||||
|
".ttf" => 100,
|
||||||
|
".otf" => 95,
|
||||||
|
".ttc" => 90,
|
||||||
|
".otc" => 85,
|
||||||
|
".woff" => 40,
|
||||||
|
".woff2" => 35,
|
||||||
|
_ => 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<FontDescription> LoadDescriptions(string fontPath){
|
||||||
|
var ext = Path.GetExtension(fontPath).ToLowerInvariant();
|
||||||
|
if (ext is ".ttc" or ".otc")
|
||||||
|
return FontDescription.LoadFontCollectionDescriptions(fontPath);
|
||||||
|
|
||||||
|
return new[]{ FontDescription.LoadDescription(fontPath) };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<string> BuildAliases(FontDescription d){
|
||||||
|
var family = d.FontFamilyInvariantCulture.Trim();
|
||||||
|
var sub = d.FontSubFamilyNameInvariantCulture.Trim(); // Regular/Bold/Italic
|
||||||
|
var full = d.FontNameInvariantCulture.Trim(); // "Family Subfamily"
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(family)) yield return family;
|
||||||
|
if (!string.IsNullOrWhiteSpace(full)) yield return full;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(family) &&
|
||||||
|
!string.IsNullOrWhiteSpace(sub) &&
|
||||||
|
!sub.Equals("Regular", StringComparison.OrdinalIgnoreCase)){
|
||||||
|
yield return $"{family} {sub}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly record struct Candidate(string Path, int Score);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SubtitleFonts{
|
public class SubtitleFonts{
|
||||||
public LanguageItem Language{ get; set; }
|
public LanguageItem Language{ get; set; } = new();
|
||||||
public Dictionary<string, string> Fonts{ get; set; }
|
public Dictionary<string, string> Fonts{ get; set; } = new();
|
||||||
}
|
}
|
||||||
83
CRD/Utils/PeriodicWorkRunner.cs
Normal file
83
CRD/Utils/PeriodicWorkRunner.cs
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CRD.Utils;
|
||||||
|
|
||||||
|
public class PeriodicWorkRunner(Func<CancellationToken, Task> work) : IDisposable{
|
||||||
|
private CancellationTokenSource? cts;
|
||||||
|
private Task? loopTask;
|
||||||
|
|
||||||
|
private TimeSpan currentInterval;
|
||||||
|
|
||||||
|
public DateTime LastRunTime = DateTime.MinValue;
|
||||||
|
|
||||||
|
public void StartOrRestart(TimeSpan interval, bool runImmediately = false, bool force = false){
|
||||||
|
if (interval <= TimeSpan.Zero){
|
||||||
|
Stop();
|
||||||
|
currentInterval = Timeout.InfiniteTimeSpan;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!force && interval == currentInterval){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentInterval = interval;
|
||||||
|
|
||||||
|
Stop();
|
||||||
|
|
||||||
|
cts = new CancellationTokenSource();
|
||||||
|
loopTask = RunLoopAsync(interval, runImmediately, cts.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartOrRestartMinutes(int minutes, bool runImmediately = false, bool force = false)
|
||||||
|
=> StartOrRestart(TimeSpan.FromMinutes(minutes), runImmediately);
|
||||||
|
|
||||||
|
public void Stop(){
|
||||||
|
if (cts is null) return;
|
||||||
|
|
||||||
|
try{
|
||||||
|
cts.Cancel();
|
||||||
|
} finally{
|
||||||
|
cts.Dispose();
|
||||||
|
cts = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RunLoopAsync(TimeSpan interval, bool runImmediately, CancellationToken token){
|
||||||
|
if (runImmediately){
|
||||||
|
await SafeRunWork(token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
using var timer = new PeriodicTimer(interval);
|
||||||
|
|
||||||
|
try{
|
||||||
|
while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false)){
|
||||||
|
await SafeRunWork(token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
} catch (OperationCanceledException){
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int running = 0;
|
||||||
|
|
||||||
|
private async Task SafeRunWork(CancellationToken token){
|
||||||
|
if (Interlocked.Exchange(ref running, 1) == 1){
|
||||||
|
Console.Error.WriteLine("Task is already running!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try{
|
||||||
|
await work(token).ConfigureAwait(false);
|
||||||
|
LastRunTime = DateTime.Now;
|
||||||
|
} catch (OperationCanceledException) when (token.IsCancellationRequested){
|
||||||
|
} catch (Exception ex){
|
||||||
|
Console.Error.WriteLine(ex);
|
||||||
|
} finally{
|
||||||
|
Interlocked.Exchange(ref running, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() => Stop();
|
||||||
|
}
|
||||||
|
|
@ -56,6 +56,11 @@ public class CrDownloadOptions{
|
||||||
[JsonProperty("download_finished_sound_path")]
|
[JsonProperty("download_finished_sound_path")]
|
||||||
public string? DownloadFinishedSoundPath{ get; set; }
|
public string? DownloadFinishedSoundPath{ get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("download_finished_execute")]
|
||||||
|
public bool DownloadFinishedExecute{ get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("download_finished_execute_path")]
|
||||||
|
public string? DownloadFinishedExecutePath{ get; set; }
|
||||||
|
|
||||||
[JsonProperty("background_image_opacity")]
|
[JsonProperty("background_image_opacity")]
|
||||||
public double BackgroundImageOpacity{ get; set; }
|
public double BackgroundImageOpacity{ get; set; }
|
||||||
|
|
@ -92,6 +97,13 @@ public class CrDownloadOptions{
|
||||||
|
|
||||||
[JsonProperty("history_count_sonarr")]
|
[JsonProperty("history_count_sonarr")]
|
||||||
public bool HistoryCountSonarr{ get; set; }
|
public bool HistoryCountSonarr{ get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("history_auto_refresh_interval_minutes")]
|
||||||
|
public int HistoryAutoRefreshIntervalMinutes{ get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("history_auto_refresh_mode")]
|
||||||
|
public HistoryRefreshMode HistoryAutoRefreshMode{ get; set; }
|
||||||
|
|
||||||
|
|
||||||
[JsonProperty("sonarr_properties")]
|
[JsonProperty("sonarr_properties")]
|
||||||
public SonarrProperties? SonarrProperties{ get; set; }
|
public SonarrProperties? SonarrProperties{ get; set; }
|
||||||
|
|
@ -141,6 +153,17 @@ public class CrDownloadOptions{
|
||||||
[JsonProperty("flare_solverr_properties")]
|
[JsonProperty("flare_solverr_properties")]
|
||||||
public FlareSolverrProperties? FlareSolverrProperties{ get; set; }
|
public FlareSolverrProperties? FlareSolverrProperties{ get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("tray_icon_enabled")]
|
||||||
|
public bool TrayIconEnabled{ get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("tray_start_minimized")]
|
||||||
|
public bool StartMinimizedToTray{ get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("tray_on_minimize")]
|
||||||
|
public bool MinimizeToTray{ get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("tray_on_close")]
|
||||||
|
public bool MinimizeToTrayOnClose{ get; set; }
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
@ -310,6 +333,9 @@ public class CrDownloadOptions{
|
||||||
|
|
||||||
[JsonProperty("calendar_show_upcoming_episodes")]
|
[JsonProperty("calendar_show_upcoming_episodes")]
|
||||||
public bool CalendarShowUpcomingEpisodes{ get; set; }
|
public bool CalendarShowUpcomingEpisodes{ get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("calendar_update_history")]
|
||||||
|
public bool UpdateHistoryFromCalendar{ get; set; }
|
||||||
|
|
||||||
[JsonProperty("stream_endpoint_settings")]
|
[JsonProperty("stream_endpoint_settings")]
|
||||||
public CrAuthSettings? StreamEndpoint{ get; set; }
|
public CrAuthSettings? StreamEndpoint{ get; set; }
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,20 @@ using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace CRD.Utils.Structs.Crunchyroll;
|
namespace CRD.Utils.Structs.Crunchyroll;
|
||||||
|
|
||||||
|
public class CrMultiProfile{
|
||||||
|
|
||||||
|
[JsonProperty("tier_max_profiles")]
|
||||||
|
public int? TierMaxProfiles{ get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("max_profiles")]
|
||||||
|
public int? MaxProfiles{ get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("profiles")]
|
||||||
|
public List<CrProfile> Profiles{ get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
public class CrProfile{
|
public class CrProfile{
|
||||||
|
|
||||||
public string? Avatar{ get; set; }
|
public string? Avatar{ get; set; }
|
||||||
public string? Email{ get; set; }
|
public string? Email{ get; set; }
|
||||||
public string? Username{ get; set; }
|
public string? Username{ get; set; }
|
||||||
|
|
@ -20,8 +33,14 @@ public class CrProfile{
|
||||||
[JsonProperty("preferred_content_subtitle_language")]
|
[JsonProperty("preferred_content_subtitle_language")]
|
||||||
public string? PreferredContentSubtitleLanguage{ get; set; }
|
public string? PreferredContentSubtitleLanguage{ get; set; }
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonProperty("can_switch")]
|
||||||
public Subscription? Subscription{ get; set; }
|
public bool CanSwitch{ get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("is_selected")]
|
||||||
|
public bool IsSelected{ get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("is_pin_protected")]
|
||||||
|
public bool IsPinProtected{ get; set; }
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public bool HasPremium{ get; set; }
|
public bool HasPremium{ get; set; }
|
||||||
|
|
|
||||||
|
|
@ -190,7 +190,7 @@ public class CrBrowseEpisodeVersion{
|
||||||
public Locale? AudioLocale{ get; set; }
|
public Locale? AudioLocale{ get; set; }
|
||||||
|
|
||||||
public string? Guid{ get; set; }
|
public string? Guid{ get; set; }
|
||||||
public bool? Original{ get; set; }
|
public bool Original{ get; set; }
|
||||||
public string? Variant{ get; set; }
|
public string? Variant{ get; set; }
|
||||||
|
|
||||||
[JsonProperty("season_guid")]
|
[JsonProperty("season_guid")]
|
||||||
|
|
@ -200,6 +200,6 @@ public class CrBrowseEpisodeVersion{
|
||||||
public string? MediaGuid{ get; set; }
|
public string? MediaGuid{ get; set; }
|
||||||
|
|
||||||
[JsonProperty("is_premium_only")]
|
[JsonProperty("is_premium_only")]
|
||||||
public bool? IsPremiumOnly{ get; set; }
|
public bool IsPremiumOnly{ get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Avalonia.Media.Imaging;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CRD.Utils.Structs.History;
|
using CRD.Utils.Structs.History;
|
||||||
using CRD.Views;
|
using CRD.Views;
|
||||||
|
|
@ -12,6 +14,23 @@ public class AuthData{
|
||||||
public string Password{ get; set; }
|
public string Password{ get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public partial class AccountProfile : ObservableObject{
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _profileName = "";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _avatarUrl = "";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private Bitmap? _profileImage;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _canBeSelected;
|
||||||
|
|
||||||
|
public string? ProfileId{ get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public class CrAuthSettings{
|
public class CrAuthSettings{
|
||||||
public string Endpoint{ get; set; }
|
public string Endpoint{ get; set; }
|
||||||
public string Authorization{ get; set; }
|
public string Authorization{ get; set; }
|
||||||
|
|
@ -51,9 +70,18 @@ public class LanguageItem{
|
||||||
public string Language{ get; set; }
|
public string Language{ get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public readonly record struct EpisodeVariant(CrunchyEpisode Item, LanguageItem Lang);
|
||||||
|
|
||||||
public class EpisodeAndLanguage{
|
public class EpisodeAndLanguage{
|
||||||
public List<CrunchyEpisode> Items{ get; set; }
|
public List<EpisodeVariant> Variants{ get; set; } = new();
|
||||||
public List<LanguageItem> Langs{ get; set; }
|
|
||||||
|
public bool AddUnique(CrunchyEpisode item, LanguageItem lang){
|
||||||
|
if (Variants.Any(v => v.Lang.CrLocale == lang.CrLocale))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Variants.Add(new EpisodeVariant(item, lang));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CrunchyMultiDownload(List<string> dubLang, bool? all = null, bool? but = null, List<string>? e = null, string? s = null){
|
public class CrunchyMultiDownload(List<string> dubLang, bool? all = null, bool? but = null, List<string>? e = null, string? s = null){
|
||||||
|
|
@ -131,6 +159,11 @@ public class StringItemWithDisplayName{
|
||||||
public string value{ get; set; }
|
public string value{ get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class RefreshModeOption{
|
||||||
|
public string DisplayName{ get; set; }
|
||||||
|
public HistoryRefreshMode value{ get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public class WindowSettings{
|
public class WindowSettings{
|
||||||
public double Width{ get; set; }
|
public double Width{ get; set; }
|
||||||
public double Height{ get; set; }
|
public double Height{ get; set; }
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia.Media.Imaging;
|
using Avalonia.Media.Imaging;
|
||||||
using CRD.Downloader;
|
using CRD.Downloader;
|
||||||
|
|
@ -140,10 +141,16 @@ public class HistoryEpisode : INotifyPropertyChanged{
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DownloadEpisodeDefault(){
|
public async Task DownloadEpisodeDefault(){
|
||||||
await DownloadEpisode();
|
await DownloadEpisode(EpisodeDownloadMode.Default,"",false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DownloadEpisode(EpisodeDownloadMode episodeDownloadMode = EpisodeDownloadMode.Default, string overrideDownloadPath = ""){
|
public async Task DownloadEpisode(EpisodeDownloadMode episodeDownloadMode, string overrideDownloadPath,bool chekQueueForId){
|
||||||
|
|
||||||
|
if (chekQueueForId && QueueManager.Instance.Queue.Any(item => item.Data.Any(epmeta => epmeta.MediaId == EpisodeId))){
|
||||||
|
Console.Error.WriteLine($"Episode already in queue! E{EpisodeSeasonNum}-{EpisodeTitle}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (EpisodeType){
|
switch (EpisodeType){
|
||||||
case EpisodeType.MusicVideo:
|
case EpisodeType.MusicVideo:
|
||||||
await QueueManager.Instance.CrAddMusicVideoToQueue(EpisodeId ?? string.Empty, overrideDownloadPath);
|
await QueueManager.Instance.CrAddMusicVideoToQueue(EpisodeId ?? string.Empty, overrideDownloadPath);
|
||||||
|
|
|
||||||
|
|
@ -64,16 +64,16 @@ public class HistorySeries : INotifyPropertyChanged{
|
||||||
public string HistorySeriesVideoQualityOverride{ get; set; } = "";
|
public string HistorySeriesVideoQualityOverride{ get; set; } = "";
|
||||||
|
|
||||||
[JsonProperty("history_series_available_soft_subs")]
|
[JsonProperty("history_series_available_soft_subs")]
|
||||||
public List<string> HistorySeriesAvailableSoftSubs{ get; set; } =[];
|
public List<string> HistorySeriesAvailableSoftSubs{ get; set; } = [];
|
||||||
|
|
||||||
[JsonProperty("history_series_available_dub_lang")]
|
[JsonProperty("history_series_available_dub_lang")]
|
||||||
public List<string> HistorySeriesAvailableDubLang{ get; set; } =[];
|
public List<string> HistorySeriesAvailableDubLang{ get; set; } = [];
|
||||||
|
|
||||||
[JsonProperty("history_series_soft_subs_override")]
|
[JsonProperty("history_series_soft_subs_override")]
|
||||||
public ObservableCollection<string> HistorySeriesSoftSubsOverride{ get; set; } =[];
|
public ObservableCollection<string> HistorySeriesSoftSubsOverride{ get; set; } = [];
|
||||||
|
|
||||||
[JsonProperty("history_series_dub_lang_override")]
|
[JsonProperty("history_series_dub_lang_override")]
|
||||||
public ObservableCollection<string> HistorySeriesDubLangOverride{ get; set; } =[];
|
public ObservableCollection<string> HistorySeriesDubLangOverride{ get; set; } = [];
|
||||||
|
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
|
|
@ -329,7 +329,7 @@ public class HistorySeries : INotifyPropertyChanged{
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData)));
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AddNewMissingToDownloads(){
|
public async Task AddNewMissingToDownloads(bool chekQueueForId = false){
|
||||||
bool foundWatched = false;
|
bool foundWatched = false;
|
||||||
var options = CrunchyrollManager.Instance.CrunOptions;
|
var options = CrunchyrollManager.Instance.CrunOptions;
|
||||||
|
|
||||||
|
|
@ -355,7 +355,7 @@ public class HistorySeries : INotifyPropertyChanged{
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ShouldCountEpisode(ep, sonarrEnabled && useSonarrCounting, countMissing, false)){
|
if (ShouldCountEpisode(ep, sonarrEnabled && useSonarrCounting, countMissing, false)){
|
||||||
await ep.DownloadEpisode();
|
await ep.DownloadEpisode(EpisodeDownloadMode.Default, "", chekQueueForId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -372,14 +372,14 @@ public class HistorySeries : INotifyPropertyChanged{
|
||||||
|
|
||||||
if (ep.SpecialEpisode){
|
if (ep.SpecialEpisode){
|
||||||
if (historyAddSpecials && ShouldCountEpisode(ep, sonarrEnabled && useSonarrCounting, countMissing, false)){
|
if (historyAddSpecials && ShouldCountEpisode(ep, sonarrEnabled && useSonarrCounting, countMissing, false)){
|
||||||
await ep.DownloadEpisode();
|
await ep.DownloadEpisode(EpisodeDownloadMode.Default, "", chekQueueForId);
|
||||||
}
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ShouldCountEpisode(ep, sonarrEnabled && useSonarrCounting, countMissing, foundWatched)){
|
if (ShouldCountEpisode(ep, sonarrEnabled && useSonarrCounting, countMissing, foundWatched)){
|
||||||
await ep.DownloadEpisode();
|
await ep.DownloadEpisode(EpisodeDownloadMode.Default, "", chekQueueForId);
|
||||||
} else{
|
} else{
|
||||||
foundWatched = true;
|
foundWatched = true;
|
||||||
if (!historyAddSpecials && !countMissing){
|
if (!historyAddSpecials && !countMissing){
|
||||||
|
|
@ -467,54 +467,61 @@ public class HistorySeries : INotifyPropertyChanged{
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateSeriesFolderPath(){
|
public void UpdateSeriesFolderPath(){
|
||||||
var season = Seasons.FirstOrDefault(season => !string.IsNullOrEmpty(season.SeasonDownloadPath));
|
// Reset state first
|
||||||
|
SeriesFolderPath = string.Empty;
|
||||||
|
SeriesFolderPathExists = false;
|
||||||
|
|
||||||
|
var season = Seasons.FirstOrDefault(s => !string.IsNullOrEmpty(s.SeasonDownloadPath));
|
||||||
|
|
||||||
|
// Series path
|
||||||
if (!string.IsNullOrEmpty(SeriesDownloadPath) && Directory.Exists(SeriesDownloadPath)){
|
if (!string.IsNullOrEmpty(SeriesDownloadPath) && Directory.Exists(SeriesDownloadPath)){
|
||||||
SeriesFolderPath = SeriesDownloadPath;
|
SeriesFolderPath = SeriesDownloadPath;
|
||||||
SeriesFolderPathExists = true;
|
SeriesFolderPathExists = true;
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SeriesFolderPathExists)));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (season is{ SeasonDownloadPath: not null }){
|
// Season path
|
||||||
|
if (!string.IsNullOrEmpty(season?.SeasonDownloadPath)){
|
||||||
try{
|
try{
|
||||||
var seasonPath = season.SeasonDownloadPath;
|
var directoryInfo = new DirectoryInfo(season.SeasonDownloadPath);
|
||||||
var directoryInfo = new DirectoryInfo(seasonPath);
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(directoryInfo.Parent?.FullName)){
|
var parentFolder = directoryInfo.Parent?.FullName;
|
||||||
string parentFolderPath = directoryInfo.Parent?.FullName ?? string.Empty;
|
|
||||||
|
|
||||||
if (Directory.Exists(parentFolderPath)){
|
if (!string.IsNullOrEmpty(parentFolder) && Directory.Exists(parentFolder)){
|
||||||
SeriesFolderPath = parentFolderPath;
|
SeriesFolderPath = parentFolder;
|
||||||
SeriesFolderPathExists = true;
|
SeriesFolderPathExists = true;
|
||||||
}
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SeriesFolderPathExists)));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} catch (Exception e){
|
} catch (Exception e){
|
||||||
Console.Error.WriteLine($"An error occurred while opening the folder: {e.Message}");
|
Console.Error.WriteLine($"Error resolving season folder: {e.Message}");
|
||||||
}
|
}
|
||||||
} else{
|
}
|
||||||
string customPath;
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(SeriesTitle))
|
// Auto generated path
|
||||||
return;
|
if (string.IsNullOrEmpty(SeriesTitle)){
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SeriesFolderPathExists)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var seriesTitle = FileNameManager.CleanupFilename(SeriesTitle);
|
var seriesTitle = FileNameManager.CleanupFilename(SeriesTitle);
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(seriesTitle))
|
if (string.IsNullOrEmpty(seriesTitle)){
|
||||||
return;
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SeriesFolderPathExists)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check Crunchyroll download directory
|
string basePath =
|
||||||
var downloadDirPath = CrunchyrollManager.Instance.CrunOptions.DownloadDirPath;
|
!string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.DownloadDirPath)
|
||||||
if (!string.IsNullOrEmpty(downloadDirPath)){
|
? CrunchyrollManager.Instance.CrunOptions.DownloadDirPath
|
||||||
customPath = Path.Combine(downloadDirPath, seriesTitle);
|
: CfgManager.PathVIDEOS_DIR;
|
||||||
} else{
|
|
||||||
// Fallback to configured VIDEOS_DIR path
|
|
||||||
customPath = Path.Combine(CfgManager.PathVIDEOS_DIR, seriesTitle);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if custom path exists
|
var customPath = Path.Combine(basePath, seriesTitle);
|
||||||
if (Directory.Exists(customPath)){
|
|
||||||
SeriesFolderPath = customPath;
|
if (Directory.Exists(customPath)){
|
||||||
SeriesFolderPathExists = true;
|
SeriesFolderPath = customPath;
|
||||||
}
|
SeriesFolderPathExists = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SeriesFolderPathExists)));
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SeriesFolderPathExists)));
|
||||||
|
|
|
||||||
|
|
@ -127,8 +127,7 @@ public class EpisodeHighlightTextBlock : TextBlock{
|
||||||
streamingService == StreamingService.Crunchyroll ? new HashSet<string>(CrunchyrollManager.Instance.CrunOptions.DlSubs) :
|
streamingService == StreamingService.Crunchyroll ? new HashSet<string>(CrunchyrollManager.Instance.CrunOptions.DlSubs) :
|
||||||
new HashSet<string>();
|
new HashSet<string>();
|
||||||
|
|
||||||
var higlight = dubSet.IsSubsetOf(Episode?.HistoryEpisodeAvailableDubLang ?? []) &&
|
var higlight = dubSet.IsSubsetOf(Episode?.HistoryEpisodeAvailableDubLang ?? []) && (subSet.IsSubsetOf(Episode?.HistoryEpisodeAvailableSoftSubs ?? []) || subSet.Contains("all"));
|
||||||
subSet.IsSubsetOf(Episode?.HistoryEpisodeAvailableSoftSubs ?? []);
|
|
||||||
|
|
||||||
if (higlight){
|
if (higlight){
|
||||||
Foreground = Brushes.Orange;
|
Foreground = Brushes.Orange;
|
||||||
|
|
|
||||||
|
|
@ -155,7 +155,7 @@ public class HighlightingTextBlock : TextBlock{
|
||||||
foreach (var item in Items){
|
foreach (var item in Items){
|
||||||
var run = new Run(item);
|
var run = new Run(item);
|
||||||
|
|
||||||
if (highlightSet.Contains(item)){
|
if (highlightSet.Contains(item) || highlightSet.Contains("all")){
|
||||||
run.Foreground = Brushes.Orange;
|
run.Foreground = Brushes.Orange;
|
||||||
// run.FontWeight = FontWeight.Bold;
|
// run.FontWeight = FontWeight.Bold;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using CRD.Downloader.Crunchyroll;
|
using CRD.Downloader.Crunchyroll;
|
||||||
using CRD.Utils;
|
using CRD.Utils;
|
||||||
|
using CRD.Utils.Structs;
|
||||||
|
using CRD.Utils.UI;
|
||||||
|
using CRD.ViewModels.Utils;
|
||||||
using CRD.Views.Utils;
|
using CRD.Views.Utils;
|
||||||
using FluentAvalonia.UI.Controls;
|
using FluentAvalonia.UI.Controls;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
@ -22,6 +25,9 @@ public partial class AccountPageViewModel : ViewModelBase{
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _loginLogoutText = "";
|
private string _loginLogoutText = "";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _hasMultiProfile;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _remainingTime = "";
|
private string _remainingTime = "";
|
||||||
|
|
@ -50,8 +56,8 @@ public partial class AccountPageViewModel : ViewModelBase{
|
||||||
RemainingTime = "Subscription maybe ended";
|
RemainingTime = "Subscription maybe ended";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CrunchyrollManager.Instance.CrAuthEndpoint1.Profile.Subscription != null){
|
if (CrunchyrollManager.Instance.CrAuthEndpoint1.Subscription != null){
|
||||||
Console.Error.WriteLine(JsonConvert.SerializeObject(CrunchyrollManager.Instance.CrAuthEndpoint1.Profile.Subscription, Formatting.Indented));
|
Console.Error.WriteLine(JsonConvert.SerializeObject(CrunchyrollManager.Instance.CrAuthEndpoint1.Subscription, Formatting.Indented));
|
||||||
}
|
}
|
||||||
} else{
|
} else{
|
||||||
RemainingTime = $"{(IsCancelled ? "Subscription ending in: " : "Subscription refreshing in: ")}{remaining:dd\\:hh\\:mm\\:ss}";
|
RemainingTime = $"{(IsCancelled ? "Subscription ending in: " : "Subscription refreshing in: ")}{remaining:dd\\:hh\\:mm\\:ss}";
|
||||||
|
|
@ -59,13 +65,18 @@ public partial class AccountPageViewModel : ViewModelBase{
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdatetProfile(){
|
public void UpdatetProfile(){
|
||||||
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
|
var firstEndpoint = CrunchyrollManager.Instance.CrAuthEndpoint1;
|
||||||
|
var firstEndpointProfile = firstEndpoint.Profile;
|
||||||
|
|
||||||
|
HasMultiProfile = firstEndpoint.MultiProfile.Profiles.Count > 1;
|
||||||
|
ProfileName = firstEndpointProfile.ProfileName ?? firstEndpointProfile.Username ?? "???"; // Default or fetched user name
|
||||||
|
LoginLogoutText = firstEndpointProfile.Username == "???" ? "Login" : "Logout"; // Default state
|
||||||
LoadProfileImage("https://static.crunchyroll.com/assets/avatar/170x170/" +
|
LoadProfileImage("https://static.crunchyroll.com/assets/avatar/170x170/" +
|
||||||
(string.IsNullOrEmpty(CrunchyrollManager.Instance.CrAuthEndpoint1.Profile.Avatar) ? "crbrand_avatars_logo_marks_mangagirl_taupe.png" : CrunchyrollManager.Instance.CrAuthEndpoint1.Profile.Avatar));
|
(string.IsNullOrEmpty(firstEndpointProfile.Avatar) ? "crbrand_avatars_logo_marks_mangagirl_taupe.png" : firstEndpointProfile.Avatar));
|
||||||
|
|
||||||
|
|
||||||
var subscriptions = CrunchyrollManager.Instance.CrAuthEndpoint1.Profile.Subscription;
|
var subscriptions = CrunchyrollManager.Instance.CrAuthEndpoint1.Subscription;
|
||||||
|
|
||||||
if (subscriptions != null){
|
if (subscriptions != null){
|
||||||
if (subscriptions.SubscriptionProducts is{ Count: >= 1 }){
|
if (subscriptions.SubscriptionProducts is{ Count: >= 1 }){
|
||||||
|
|
@ -84,8 +95,8 @@ public partial class AccountPageViewModel : ViewModelBase{
|
||||||
UnknownEndDate = true;
|
UnknownEndDate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CrunchyrollManager.Instance.CrAuthEndpoint1.Profile.Subscription?.NextRenewalDate != null && !UnknownEndDate){
|
if (!UnknownEndDate){
|
||||||
_targetTime = CrunchyrollManager.Instance.CrAuthEndpoint1.Profile.Subscription.NextRenewalDate;
|
_targetTime = subscriptions.NextRenewalDate;
|
||||||
_timer = new DispatcherTimer{
|
_timer = new DispatcherTimer{
|
||||||
Interval = TimeSpan.FromSeconds(1)
|
Interval = TimeSpan.FromSeconds(1)
|
||||||
};
|
};
|
||||||
|
|
@ -101,8 +112,8 @@ public partial class AccountPageViewModel : ViewModelBase{
|
||||||
|
|
||||||
RaisePropertyChanged(nameof(RemainingTime));
|
RaisePropertyChanged(nameof(RemainingTime));
|
||||||
|
|
||||||
if (CrunchyrollManager.Instance.CrAuthEndpoint1.Profile.Subscription != null){
|
if (subscriptions != null){
|
||||||
Console.Error.WriteLine(JsonConvert.SerializeObject(CrunchyrollManager.Instance.CrAuthEndpoint1.Profile.Subscription, Formatting.Indented));
|
Console.Error.WriteLine(JsonConvert.SerializeObject(subscriptions, Formatting.Indented));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -137,6 +148,41 @@ public partial class AccountPageViewModel : ViewModelBase{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
public async Task OpenMultiProfileDialog(){
|
||||||
|
var multiProfile = CrunchyrollManager.Instance.CrAuthEndpoint1.MultiProfile;
|
||||||
|
|
||||||
|
var profiels = multiProfile.Profiles.Select(multiProfileProfile => new AccountProfile{
|
||||||
|
AvatarUrl = string.IsNullOrEmpty(multiProfileProfile.Avatar) ? "" : ("https://static.crunchyroll.com/assets/avatar/170x170/" + multiProfileProfile.Avatar),
|
||||||
|
ProfileName = multiProfileProfile.Username ?? multiProfileProfile.ProfileName ?? "???", CanBeSelected = multiProfileProfile is{ IsSelected: false, CanSwitch: true, IsPinProtected: false },
|
||||||
|
ProfileId = multiProfileProfile.ProfileId,
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
var dialog = new CustomContentDialog(){
|
||||||
|
Name = "CRD Select Profile",
|
||||||
|
Title = "Select Profile",
|
||||||
|
IsPrimaryButtonEnabled = false,
|
||||||
|
CloseButtonText = "Close",
|
||||||
|
FullSizeDesired = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
var viewModel = new ContentDialogMultiProfileSelectViewModel(dialog, profiels);
|
||||||
|
dialog.Content = new ContentDialogMultiProfileSelectView(){
|
||||||
|
DataContext = viewModel
|
||||||
|
};
|
||||||
|
|
||||||
|
var dialogResult = await dialog.ShowAsync();
|
||||||
|
|
||||||
|
if (dialogResult == ContentDialogResult.Primary){
|
||||||
|
var selectedProfile = viewModel.SelectedItem;
|
||||||
|
|
||||||
|
await CrunchyrollManager.Instance.CrAuthEndpoint1.ChangeProfile(selectedProfile.ProfileId ?? string.Empty);
|
||||||
|
await CrunchyrollManager.Instance.CrAuthEndpoint2.ChangeProfile(selectedProfile.ProfileId ?? string.Empty);
|
||||||
|
|
||||||
|
UpdatetProfile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async void LoadProfileImage(string imageUrl){
|
public async void LoadProfileImage(string imageUrl){
|
||||||
try{
|
try{
|
||||||
ProfileImage = await Helpers.LoadImage(imageUrl);
|
ProfileImage = await Helpers.LoadImage(imageUrl);
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,9 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _showUpcomingEpisodes;
|
private bool _showUpcomingEpisodes;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _updateHistoryFromCalendar;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _hideDubs;
|
private bool _hideDubs;
|
||||||
|
|
@ -74,6 +77,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
||||||
CustomCalendar = CrunchyrollManager.Instance.CrunOptions.CustomCalendar;
|
CustomCalendar = CrunchyrollManager.Instance.CrunOptions.CustomCalendar;
|
||||||
HideDubs = CrunchyrollManager.Instance.CrunOptions.CalendarHideDubs;
|
HideDubs = CrunchyrollManager.Instance.CrunOptions.CalendarHideDubs;
|
||||||
ShowUpcomingEpisodes = CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes;
|
ShowUpcomingEpisodes = CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes;
|
||||||
|
UpdateHistoryFromCalendar = CrunchyrollManager.Instance.CrunOptions.UpdateHistoryFromCalendar;
|
||||||
|
|
||||||
ComboBoxItem? dubfilter = CalendarDubFilter.FirstOrDefault(a => a.Content != null && (string)a.Content == CrunchyrollManager.Instance.CrunOptions.CalendarDubFilter) ?? null;
|
ComboBoxItem? dubfilter = CalendarDubFilter.FirstOrDefault(a => a.Content != null && (string)a.Content == CrunchyrollManager.Instance.CrunOptions.CalendarDubFilter) ?? null;
|
||||||
CurrentCalendarDubFilter = dubfilter ?? CalendarDubFilter[0];
|
CurrentCalendarDubFilter = dubfilter ?? CalendarDubFilter[0];
|
||||||
|
|
@ -289,4 +293,14 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
||||||
CfgManager.WriteCrSettings();
|
CfgManager.WriteCrSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
partial void OnUpdateHistoryFromCalendarChanged(bool value){
|
||||||
|
if (loading){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CrunchyrollManager.Instance.CrunOptions.UpdateHistoryFromCalendar = value;
|
||||||
|
CfgManager.WriteCrSettings();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -44,17 +44,17 @@ public partial class HistoryPageViewModel : ViewModelBase{
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private ComboBoxItem? _selectedView;
|
private ComboBoxItem? _selectedView;
|
||||||
|
|
||||||
public ObservableCollection<ComboBoxItem> ViewsList{ get; } =[];
|
public ObservableCollection<ComboBoxItem> ViewsList{ get; } = [];
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private SortingListElement? _selectedSorting;
|
private SortingListElement? _selectedSorting;
|
||||||
|
|
||||||
public ObservableCollection<SortingListElement> SortingList{ get; } =[];
|
public ObservableCollection<SortingListElement> SortingList{ get; } = [];
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private FilterListElement? _selectedFilter;
|
private FilterListElement? _selectedFilter;
|
||||||
|
|
||||||
public ObservableCollection<FilterListElement> FilterList{ get; } =[];
|
public ObservableCollection<FilterListElement> FilterList{ get; } = [];
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private double _posterWidth;
|
private double _posterWidth;
|
||||||
|
|
@ -115,7 +115,16 @@ public partial class HistoryPageViewModel : ViewModelBase{
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private static string _progressText;
|
private static string _progressText;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _searchInput;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _isSearchOpen;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public bool _isSearchActiveClosed;
|
||||||
|
|
||||||
#region Table Mode
|
#region Table Mode
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
|
|
@ -125,8 +134,8 @@ public partial class HistoryPageViewModel : ViewModelBase{
|
||||||
public Symbol _selectedDownloadIcon = Symbol.ClosedCaption;
|
public Symbol _selectedDownloadIcon = Symbol.ClosedCaption;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public Vector LastScrollOffset { get; set; } = Vector.Zero;
|
public Vector LastScrollOffset{ get; set; } = Vector.Zero;
|
||||||
|
|
||||||
public HistoryPageViewModel(){
|
public HistoryPageViewModel(){
|
||||||
ProgramManager = ProgramManager.Instance;
|
ProgramManager = ProgramManager.Instance;
|
||||||
|
|
@ -324,11 +333,32 @@ public partial class HistoryPageViewModel : ViewModelBase{
|
||||||
filteredItems.RemoveAll(item => item.SeriesType == SeriesType.Series);
|
filteredItems.RemoveAll(item => item.SeriesType == SeriesType.Series);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(SearchInput)){
|
||||||
|
var tokens = SearchInput
|
||||||
|
.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
|
||||||
|
filteredItems.RemoveAll(item => {
|
||||||
|
var title = item.SeriesTitle ?? string.Empty;
|
||||||
|
|
||||||
|
return tokens.Any(t => title.IndexOf(t, StringComparison.OrdinalIgnoreCase) < 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
FilteredItems.Clear();
|
FilteredItems.Clear();
|
||||||
FilteredItems.AddRange(filteredItems);
|
FilteredItems.AddRange(filteredItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
partial void OnSearchInputChanged(string value){
|
||||||
|
ApplyFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnIsSearchOpenChanged(bool value){
|
||||||
|
IsSearchActiveClosed = !string.IsNullOrEmpty(SearchInput) && !IsSearchOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
partial void OnScaleValueChanged(double value){
|
partial void OnScaleValueChanged(double value){
|
||||||
double t = (ScaleValue - 0.5) / (1 - 0.5);
|
double t = (ScaleValue - 0.5) / (1 - 0.5);
|
||||||
|
|
||||||
|
|
@ -374,6 +404,10 @@ public partial class HistoryPageViewModel : ViewModelBase{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
public void ClearSearchCommand(){
|
||||||
|
SearchInput = "";
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
public void NavToSeries(){
|
public void NavToSeries(){
|
||||||
|
|
@ -419,7 +453,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
|
||||||
.SelectMany(item => item.Seasons)
|
.SelectMany(item => item.Seasons)
|
||||||
.SelectMany(season => season.EpisodesList)
|
.SelectMany(season => season.EpisodesList)
|
||||||
.Where(historyEpisode => !string.IsNullOrEmpty(historyEpisode.SonarrEpisodeId) && !historyEpisode.SonarrHasFile)
|
.Where(historyEpisode => !string.IsNullOrEmpty(historyEpisode.SonarrEpisodeId) && !historyEpisode.SonarrHasFile)
|
||||||
.Select(historyEpisode => historyEpisode.DownloadEpisode())
|
.Select(historyEpisode => historyEpisode.DownloadEpisodeDefault())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -515,7 +549,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
public async Task DownloadSeasonAll(HistorySeason season){
|
public async Task DownloadSeasonAll(HistorySeason season){
|
||||||
foreach (var episode in season.EpisodesList){
|
foreach (var episode in season.EpisodesList){
|
||||||
await episode.DownloadEpisode();
|
await episode.DownloadEpisodeDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -528,7 +562,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
|
||||||
MessageBus.Current.SendMessage(new ToastMessage($"There are no missing episodes", ToastType.Error, 3));
|
MessageBus.Current.SendMessage(new ToastMessage($"There are no missing episodes", ToastType.Error, 3));
|
||||||
} else{
|
} else{
|
||||||
foreach (var episode in missingEpisodes){
|
foreach (var episode in missingEpisodes){
|
||||||
await episode.DownloadEpisode();
|
await episode.DownloadEpisodeDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -536,16 +570,16 @@ public partial class HistoryPageViewModel : ViewModelBase{
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
public async Task DownloadSeasonMissingSonarr(HistorySeason season){
|
public async Task DownloadSeasonMissingSonarr(HistorySeason season){
|
||||||
foreach (var episode in season.EpisodesList.Where(episode => !episode.SonarrHasFile)){
|
foreach (var episode in season.EpisodesList.Where(episode => !episode.SonarrHasFile)){
|
||||||
await episode.DownloadEpisode();
|
await episode.DownloadEpisodeDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
public async Task DownloadEpisodeOnlyOptions(HistoryEpisode episode){
|
public async Task DownloadEpisodeOnlyOptions(HistoryEpisode episode){
|
||||||
var downloadMode = SelectedDownloadMode;
|
var downloadMode = SelectedDownloadMode;
|
||||||
|
|
||||||
if (downloadMode != EpisodeDownloadMode.Default){
|
if (downloadMode != EpisodeDownloadMode.Default){
|
||||||
await episode.DownloadEpisode(downloadMode);
|
await episode.DownloadEpisode(downloadMode,"",false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -555,7 +589,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
|
||||||
|
|
||||||
if (downloadMode != EpisodeDownloadMode.Default){
|
if (downloadMode != EpisodeDownloadMode.Default){
|
||||||
foreach (var episode in season.EpisodesList){
|
foreach (var episode in season.EpisodesList){
|
||||||
await episode.DownloadEpisode(downloadMode);
|
await episode.DownloadEpisode(downloadMode,"",false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -567,9 +601,10 @@ public partial class HistoryPageViewModel : ViewModelBase{
|
||||||
|
|
||||||
foreach (var historyEpisode in seriesArgs.Season.EpisodesList){
|
foreach (var historyEpisode in seriesArgs.Season.EpisodesList){
|
||||||
if (historyEpisode.WasDownloaded == allDownloaded){
|
if (historyEpisode.WasDownloaded == allDownloaded){
|
||||||
seriesArgs.Season.UpdateDownloaded(historyEpisode.EpisodeId);
|
historyEpisode.ToggleWasDownloaded();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
seriesArgs.Season.UpdateDownloaded();
|
||||||
}
|
}
|
||||||
|
|
||||||
seriesArgs.Series?.UpdateNewEpisodes();
|
seriesArgs.Series?.UpdateNewEpisodes();
|
||||||
|
|
@ -606,7 +641,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
|
||||||
public void ToggleInactive(){
|
public void ToggleInactive(){
|
||||||
CfgManager.UpdateHistoryFile();
|
CfgManager.UpdateHistoryFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnSelectedDownloadModeChanged(EpisodeDownloadMode value){
|
partial void OnSelectedDownloadModeChanged(EpisodeDownloadMode value){
|
||||||
SelectedDownloadIcon = SelectedDownloadMode switch{
|
SelectedDownloadIcon = SelectedDownloadMode switch{
|
||||||
EpisodeDownloadMode.OnlyVideo => Symbol.Video,
|
EpisodeDownloadMode.OnlyVideo => Symbol.Video,
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,20 @@ public partial class SeriesPageViewModel : ViewModelBase{
|
||||||
|
|
||||||
SelectedSeries.UpdateSeriesFolderPath();
|
SelectedSeries.UpdateSeriesFolderPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
public void ClearFolderPathCommand(HistorySeason? season){
|
||||||
|
|
||||||
|
if (season != null){
|
||||||
|
season.SeasonDownloadPath = string.Empty;
|
||||||
|
} else{
|
||||||
|
SelectedSeries.SeriesDownloadPath = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
CfgManager.UpdateHistoryFile();
|
||||||
|
SelectedSeries.UpdateSeriesFolderPath();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
public async Task OpenFeaturedMusicDialog(){
|
public async Task OpenFeaturedMusicDialog(){
|
||||||
|
|
@ -213,7 +227,7 @@ public partial class SeriesPageViewModel : ViewModelBase{
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
public async Task DownloadSeasonAll(HistorySeason season){
|
public async Task DownloadSeasonAll(HistorySeason season){
|
||||||
foreach (var episode in season.EpisodesList){
|
foreach (var episode in season.EpisodesList){
|
||||||
await episode.DownloadEpisode();
|
await episode.DownloadEpisodeDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -226,7 +240,7 @@ public partial class SeriesPageViewModel : ViewModelBase{
|
||||||
MessageBus.Current.SendMessage(new ToastMessage($"There are no missing episodes", ToastType.Error, 3));
|
MessageBus.Current.SendMessage(new ToastMessage($"There are no missing episodes", ToastType.Error, 3));
|
||||||
} else{
|
} else{
|
||||||
foreach (var episode in missingEpisodes){
|
foreach (var episode in missingEpisodes){
|
||||||
await episode.DownloadEpisode();
|
await episode.DownloadEpisodeDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -236,7 +250,7 @@ public partial class SeriesPageViewModel : ViewModelBase{
|
||||||
var downloadMode = SelectedDownloadMode;
|
var downloadMode = SelectedDownloadMode;
|
||||||
|
|
||||||
if (downloadMode != EpisodeDownloadMode.Default){
|
if (downloadMode != EpisodeDownloadMode.Default){
|
||||||
await episode.DownloadEpisode(downloadMode);
|
await episode.DownloadEpisode(downloadMode,"",false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -246,7 +260,7 @@ public partial class SeriesPageViewModel : ViewModelBase{
|
||||||
|
|
||||||
if (downloadMode != EpisodeDownloadMode.Default){
|
if (downloadMode != EpisodeDownloadMode.Default){
|
||||||
foreach (var episode in season.EpisodesList){
|
foreach (var episode in season.EpisodesList){
|
||||||
await episode.DownloadEpisode(downloadMode);
|
await episode.DownloadEpisode(downloadMode,"",false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -254,7 +268,7 @@ public partial class SeriesPageViewModel : ViewModelBase{
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
public async Task DownloadSeasonMissingSonarr(HistorySeason season){
|
public async Task DownloadSeasonMissingSonarr(HistorySeason season){
|
||||||
foreach (var episode in season.EpisodesList.Where(episode => !episode.SonarrHasFile)){
|
foreach (var episode in season.EpisodesList.Where(episode => !episode.SonarrHasFile)){
|
||||||
await episode.DownloadEpisode();
|
await episode.DownloadEpisodeDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -262,11 +276,11 @@ public partial class SeriesPageViewModel : ViewModelBase{
|
||||||
public void ToggleDownloadedMark(HistorySeason season){
|
public void ToggleDownloadedMark(HistorySeason season){
|
||||||
bool allDownloaded = season.EpisodesList.All(ep => ep.WasDownloaded);
|
bool allDownloaded = season.EpisodesList.All(ep => ep.WasDownloaded);
|
||||||
|
|
||||||
foreach (var historyEpisode in season.EpisodesList){
|
foreach (var historyEpisode in season.EpisodesList.Where(historyEpisode => historyEpisode.WasDownloaded == allDownloaded)){
|
||||||
if (historyEpisode.WasDownloaded == allDownloaded){
|
historyEpisode.ToggleWasDownloaded();
|
||||||
season.UpdateDownloaded(historyEpisode.EpisodeId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
season.UpdateDownloaded();
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ public partial class ContentDialogFeaturedMusicViewModel : ViewModelBase{
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
public void DownloadEpisode(HistoryEpisode episode){
|
public void DownloadEpisode(HistoryEpisode episode){
|
||||||
episode.DownloadEpisode(EpisodeDownloadMode.Default, FolderPath);
|
episode.DownloadEpisode(EpisodeDownloadMode.Default, FolderPath,false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SaveButton(ContentDialog sender, ContentDialogButtonClickEventArgs args){
|
private void SaveButton(ContentDialog sender, ContentDialogButtonClickEventArgs args){
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CRD.Utils;
|
||||||
|
using CRD.Utils.Structs;
|
||||||
|
using CRD.Utils.UI;
|
||||||
|
using FluentAvalonia.UI.Controls;
|
||||||
|
|
||||||
|
namespace CRD.ViewModels.Utils;
|
||||||
|
|
||||||
|
public partial class ContentDialogMultiProfileSelectViewModel: ViewModelBase{
|
||||||
|
|
||||||
|
private readonly CustomContentDialog dialog;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private AccountProfile _selectedItem;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private ObservableCollection<AccountProfile> _profileList = [];
|
||||||
|
|
||||||
|
public ContentDialogMultiProfileSelectViewModel(CustomContentDialog contentDialog, List<AccountProfile> profiles){
|
||||||
|
ArgumentNullException.ThrowIfNull(contentDialog);
|
||||||
|
|
||||||
|
dialog = contentDialog;
|
||||||
|
dialog.Closed += DialogOnClosed;
|
||||||
|
dialog.PrimaryButtonClick += SaveButton;
|
||||||
|
|
||||||
|
try{
|
||||||
|
_ = LoadProfiles(profiles);
|
||||||
|
} catch (Exception e){
|
||||||
|
Console.WriteLine(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadProfiles(List<AccountProfile> profiles){
|
||||||
|
foreach (var accountProfile in profiles){
|
||||||
|
accountProfile.ProfileImage = await Helpers.LoadImage(accountProfile.AvatarUrl);
|
||||||
|
ProfileList.Add(accountProfile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnSelectedItemChanged(AccountProfile value){
|
||||||
|
dialog.Hide(ContentDialogResult.Primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveButton(ContentDialog sender, ContentDialogButtonClickEventArgs args){
|
||||||
|
dialog.PrimaryButtonClick -= SaveButton;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DialogOnClosed(ContentDialog sender, ContentDialogClosedEventArgs args){
|
||||||
|
dialog.Closed -= DialogOnClosed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
|
@ -19,6 +20,7 @@ using CRD.Downloader.Crunchyroll;
|
||||||
using CRD.Utils;
|
using CRD.Utils;
|
||||||
using CRD.Utils.Files;
|
using CRD.Utils.Files;
|
||||||
using CRD.Utils.Sonarr;
|
using CRD.Utils.Sonarr;
|
||||||
|
using CRD.Utils.Structs;
|
||||||
using CRD.Utils.Structs.Crunchyroll;
|
using CRD.Utils.Structs.Crunchyroll;
|
||||||
using CRD.Utils.Structs.History;
|
using CRD.Utils.Structs.History;
|
||||||
using FluentAvalonia.Styling;
|
using FluentAvalonia.Styling;
|
||||||
|
|
@ -50,6 +52,24 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _historyCountSonarr;
|
private bool _historyCountSonarr;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private double? _historyAutoRefreshIntervalMinutes;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private HistoryRefreshMode _historyAutoRefreshMode;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _historyAutoRefreshModeHint;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _historyAutoRefreshLastRunTime;
|
||||||
|
|
||||||
|
public ObservableCollection<RefreshModeOption> HistoryAutoRefreshModes{ get; } = new(){
|
||||||
|
new RefreshModeOption(){ DisplayName = "Default All", value = HistoryRefreshMode.DefaultAll },
|
||||||
|
new RefreshModeOption(){ DisplayName = "Default Active", value = HistoryRefreshMode.DefaultActive },
|
||||||
|
new RefreshModeOption(){ DisplayName = "Fast New Releases", value = HistoryRefreshMode.FastNewReleases },
|
||||||
|
};
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private double? _simultaneousDownloads;
|
private double? _simultaneousDownloads;
|
||||||
|
|
@ -74,6 +94,18 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private double? _retryDelay;
|
private double? _retryDelay;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _trayIconEnabled;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _startMinimizedToTray;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _minimizeToTray;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _minimizeToTrayOnClose;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private ComboBoxItem _selectedHistoryLang;
|
private ComboBoxItem _selectedHistoryLang;
|
||||||
|
|
@ -231,6 +263,12 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _downloadFinishedSoundPath;
|
private string _downloadFinishedSoundPath;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _downloadFinishedExecute;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _downloadFinishedExecutePath;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _currentIp = "";
|
private string _currentIp = "";
|
||||||
|
|
@ -263,6 +301,9 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
||||||
|
|
||||||
DownloadFinishedSoundPath = options.DownloadFinishedSoundPath ?? string.Empty;
|
DownloadFinishedSoundPath = options.DownloadFinishedSoundPath ?? string.Empty;
|
||||||
DownloadFinishedPlaySound = options.DownloadFinishedPlaySound;
|
DownloadFinishedPlaySound = options.DownloadFinishedPlaySound;
|
||||||
|
|
||||||
|
DownloadFinishedExecutePath = options.DownloadFinishedExecutePath ?? string.Empty;
|
||||||
|
DownloadFinishedExecute = options.DownloadFinishedExecute;
|
||||||
|
|
||||||
DownloadDirPath = string.IsNullOrEmpty(options.DownloadDirPath) ? CfgManager.PathVIDEOS_DIR : options.DownloadDirPath;
|
DownloadDirPath = string.IsNullOrEmpty(options.DownloadDirPath) ? CfgManager.PathVIDEOS_DIR : options.DownloadDirPath;
|
||||||
TempDownloadDirPath = string.IsNullOrEmpty(options.DownloadTempDirPath) ? CfgManager.PathTEMP_DIR : options.DownloadTempDirPath;
|
TempDownloadDirPath = string.IsNullOrEmpty(options.DownloadTempDirPath) ? CfgManager.PathTEMP_DIR : options.DownloadTempDirPath;
|
||||||
|
|
@ -300,6 +341,9 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
||||||
HistoryAddSpecials = options.HistoryAddSpecials;
|
HistoryAddSpecials = options.HistoryAddSpecials;
|
||||||
HistorySkipUnmonitored = options.HistorySkipUnmonitored;
|
HistorySkipUnmonitored = options.HistorySkipUnmonitored;
|
||||||
HistoryCountSonarr = options.HistoryCountSonarr;
|
HistoryCountSonarr = options.HistoryCountSonarr;
|
||||||
|
HistoryAutoRefreshIntervalMinutes = options.HistoryAutoRefreshIntervalMinutes;
|
||||||
|
HistoryAutoRefreshMode = options.HistoryAutoRefreshMode;
|
||||||
|
HistoryAutoRefreshLastRunTime = ProgramManager.Instance.GetLastRefreshTime() == DateTime.MinValue ? "Never" : ProgramManager.Instance.GetLastRefreshTime().ToString("g", CultureInfo.CurrentCulture);
|
||||||
DownloadSpeed = options.DownloadSpeedLimit;
|
DownloadSpeed = options.DownloadSpeedLimit;
|
||||||
DownloadSpeedInBits = options.DownloadSpeedInBits;
|
DownloadSpeedInBits = options.DownloadSpeedInBits;
|
||||||
DownloadMethodeNew = options.DownloadMethodeNew;
|
DownloadMethodeNew = options.DownloadMethodeNew;
|
||||||
|
|
@ -310,6 +354,11 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
||||||
SimultaneousDownloads = options.SimultaneousDownloads;
|
SimultaneousDownloads = options.SimultaneousDownloads;
|
||||||
SimultaneousProcessingJobs = options.SimultaneousProcessingJobs;
|
SimultaneousProcessingJobs = options.SimultaneousProcessingJobs;
|
||||||
LogMode = options.LogMode;
|
LogMode = options.LogMode;
|
||||||
|
|
||||||
|
TrayIconEnabled = options.TrayIconEnabled;
|
||||||
|
StartMinimizedToTray = options.StartMinimizedToTray;
|
||||||
|
MinimizeToTray = options.MinimizeToTray;
|
||||||
|
MinimizeToTrayOnClose = options.MinimizeToTrayOnClose;
|
||||||
|
|
||||||
ComboBoxItem? theme = AppThemes.FirstOrDefault(a => a.Content != null && (string)a.Content == options.Theme) ?? null;
|
ComboBoxItem? theme = AppThemes.FirstOrDefault(a => a.Content != null && (string)a.Content == options.Theme) ?? null;
|
||||||
CurrentAppTheme = theme ?? AppThemes[0];
|
CurrentAppTheme = theme ?? AppThemes[0];
|
||||||
|
|
@ -331,6 +380,8 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
||||||
var settings = CrunchyrollManager.Instance.CrunOptions;
|
var settings = CrunchyrollManager.Instance.CrunOptions;
|
||||||
|
|
||||||
settings.DownloadFinishedPlaySound = DownloadFinishedPlaySound;
|
settings.DownloadFinishedPlaySound = DownloadFinishedPlaySound;
|
||||||
|
|
||||||
|
settings.DownloadFinishedExecute = DownloadFinishedExecute;
|
||||||
|
|
||||||
settings.DownloadMethodeNew = DownloadMethodeNew;
|
settings.DownloadMethodeNew = DownloadMethodeNew;
|
||||||
settings.DownloadAllowEarlyStart = DownloadAllowEarlyStart;
|
settings.DownloadAllowEarlyStart = DownloadAllowEarlyStart;
|
||||||
|
|
@ -347,6 +398,8 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
||||||
settings.HistoryIncludeCrArtists = HistoryIncludeCrArtists;
|
settings.HistoryIncludeCrArtists = HistoryIncludeCrArtists;
|
||||||
settings.HistorySkipUnmonitored = HistorySkipUnmonitored;
|
settings.HistorySkipUnmonitored = HistorySkipUnmonitored;
|
||||||
settings.HistoryCountSonarr = HistoryCountSonarr;
|
settings.HistoryCountSonarr = HistoryCountSonarr;
|
||||||
|
settings.HistoryAutoRefreshIntervalMinutes =Math.Clamp((int)(HistoryAutoRefreshIntervalMinutes ?? 0), 0, 1000000000) ;
|
||||||
|
settings.HistoryAutoRefreshMode = HistoryAutoRefreshMode;
|
||||||
settings.DownloadSpeedLimit = Math.Clamp((int)(DownloadSpeed ?? 0), 0, 1000000000);
|
settings.DownloadSpeedLimit = Math.Clamp((int)(DownloadSpeed ?? 0), 0, 1000000000);
|
||||||
settings.DownloadSpeedInBits = DownloadSpeedInBits;
|
settings.DownloadSpeedInBits = DownloadSpeedInBits;
|
||||||
settings.SimultaneousDownloads = Math.Clamp((int)(SimultaneousDownloads ?? 0), 1, 10);
|
settings.SimultaneousDownloads = Math.Clamp((int)(SimultaneousDownloads ?? 0), 1, 10);
|
||||||
|
|
@ -404,6 +457,11 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
||||||
}
|
}
|
||||||
|
|
||||||
settings.FlareSolverrProperties = propsFlareSolverr;
|
settings.FlareSolverrProperties = propsFlareSolverr;
|
||||||
|
|
||||||
|
settings.TrayIconEnabled = TrayIconEnabled;
|
||||||
|
settings.StartMinimizedToTray = StartMinimizedToTray;
|
||||||
|
settings.MinimizeToTray = MinimizeToTray;
|
||||||
|
settings.MinimizeToTrayOnClose = MinimizeToTrayOnClose;
|
||||||
|
|
||||||
settings.LogMode = LogMode;
|
settings.LogMode = LogMode;
|
||||||
|
|
||||||
|
|
@ -429,7 +487,7 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
||||||
CrunchyrollManager.Instance.CrunOptions.DownloadDirPath = path;
|
CrunchyrollManager.Instance.CrunOptions.DownloadDirPath = path;
|
||||||
DownloadDirPath = string.IsNullOrEmpty(path) ? CfgManager.PathVIDEOS_DIR : path;
|
DownloadDirPath = string.IsNullOrEmpty(path) ? CfgManager.PathVIDEOS_DIR : path;
|
||||||
},
|
},
|
||||||
pathGetter: () => CrunchyrollManager.Instance.CrunOptions.DownloadDirPath,
|
pathGetter: () => CrunchyrollManager.Instance.CrunOptions.DownloadDirPath ?? string.Empty,
|
||||||
defaultPath: CfgManager.PathVIDEOS_DIR
|
defaultPath: CfgManager.PathVIDEOS_DIR
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -441,7 +499,7 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
||||||
CrunchyrollManager.Instance.CrunOptions.DownloadTempDirPath = path;
|
CrunchyrollManager.Instance.CrunOptions.DownloadTempDirPath = path;
|
||||||
TempDownloadDirPath = string.IsNullOrEmpty(path) ? CfgManager.PathTEMP_DIR : path;
|
TempDownloadDirPath = string.IsNullOrEmpty(path) ? CfgManager.PathTEMP_DIR : path;
|
||||||
},
|
},
|
||||||
pathGetter: () => CrunchyrollManager.Instance.CrunOptions.DownloadTempDirPath,
|
pathGetter: () => CrunchyrollManager.Instance.CrunOptions.DownloadTempDirPath ?? string.Empty,
|
||||||
defaultPath: CfgManager.PathTEMP_DIR
|
defaultPath: CfgManager.PathTEMP_DIR
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -490,7 +548,7 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
||||||
BackgroundImagePath = path;
|
BackgroundImagePath = path;
|
||||||
Helpers.SetBackgroundImage(path, BackgroundImageOpacity, BackgroundImageBlurRadius);
|
Helpers.SetBackgroundImage(path, BackgroundImageOpacity, BackgroundImageBlurRadius);
|
||||||
},
|
},
|
||||||
pathGetter: () => CrunchyrollManager.Instance.CrunOptions.BackgroundImagePath,
|
pathGetter: () => CrunchyrollManager.Instance.CrunOptions.BackgroundImagePath ?? string.Empty,
|
||||||
defaultPath: string.Empty
|
defaultPath: string.Empty
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -519,11 +577,39 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
||||||
CrunchyrollManager.Instance.CrunOptions.DownloadFinishedSoundPath = path;
|
CrunchyrollManager.Instance.CrunOptions.DownloadFinishedSoundPath = path;
|
||||||
DownloadFinishedSoundPath = path;
|
DownloadFinishedSoundPath = path;
|
||||||
},
|
},
|
||||||
pathGetter: () => CrunchyrollManager.Instance.CrunOptions.DownloadFinishedSoundPath,
|
pathGetter: () => CrunchyrollManager.Instance.CrunOptions.DownloadFinishedSoundPath ?? string.Empty,
|
||||||
defaultPath: string.Empty
|
defaultPath: string.Empty
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Download Finished Execute File
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
public void ClearFinishedExectuePath(){
|
||||||
|
CrunchyrollManager.Instance.CrunOptions.DownloadFinishedExecutePath = string.Empty;
|
||||||
|
DownloadFinishedExecutePath = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
public async Task OpenFileDialogAsyncInternalFinishedExecute(){
|
||||||
|
await OpenFileDialogAsyncInternal(
|
||||||
|
title: "Select File",
|
||||||
|
fileTypes: new List<FilePickerFileType>{
|
||||||
|
new("All Files"){
|
||||||
|
Patterns = new[]{ "*.*" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pathSetter: (path) => {
|
||||||
|
CrunchyrollManager.Instance.CrunOptions.DownloadFinishedExecutePath = path;
|
||||||
|
DownloadFinishedExecutePath = path;
|
||||||
|
},
|
||||||
|
pathGetter: () => CrunchyrollManager.Instance.CrunOptions.DownloadFinishedExecutePath ?? string.Empty,
|
||||||
|
defaultPath: string.Empty
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private async Task OpenFileDialogAsyncInternal(
|
private async Task OpenFileDialogAsyncInternal(
|
||||||
|
|
@ -559,10 +645,10 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
||||||
_faTheme.PreferSystemTheme = true;
|
_faTheme.PreferSystemTheme = true;
|
||||||
} else if (value?.Content?.ToString() == "Dark"){
|
} else if (value?.Content?.ToString() == "Dark"){
|
||||||
_faTheme.PreferSystemTheme = false;
|
_faTheme.PreferSystemTheme = false;
|
||||||
Application.Current.RequestedThemeVariant = ThemeVariant.Dark;
|
Application.Current?.RequestedThemeVariant = ThemeVariant.Dark;
|
||||||
} else{
|
} else{
|
||||||
_faTheme.PreferSystemTheme = false;
|
_faTheme.PreferSystemTheme = false;
|
||||||
Application.Current.RequestedThemeVariant = ThemeVariant.Light;
|
Application.Current?.RequestedThemeVariant = ThemeVariant.Light;
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateSettings();
|
UpdateSettings();
|
||||||
|
|
@ -604,6 +690,11 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
||||||
_faTheme.CustomAccentColor = color;
|
_faTheme.CustomAccentColor = color;
|
||||||
UpdateSettings();
|
UpdateSettings();
|
||||||
}
|
}
|
||||||
|
partial void OnTrayIconEnabledChanged(bool value){
|
||||||
|
((App)Application.Current!).SetTrayIconVisible(value);
|
||||||
|
UpdateSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
protected override void OnPropertyChanged(PropertyChangedEventArgs e){
|
protected override void OnPropertyChanged(PropertyChangedEventArgs e){
|
||||||
base.OnPropertyChanged(e);
|
base.OnPropertyChanged(e);
|
||||||
|
|
@ -613,11 +704,22 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
||||||
or nameof(ListBoxColor)
|
or nameof(ListBoxColor)
|
||||||
or nameof(CurrentAppTheme)
|
or nameof(CurrentAppTheme)
|
||||||
or nameof(UseCustomAccent)
|
or nameof(UseCustomAccent)
|
||||||
|
or nameof(TrayIconEnabled)
|
||||||
or nameof(LogMode)){
|
or nameof(LogMode)){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateSettings();
|
UpdateSettings();
|
||||||
|
|
||||||
|
HistoryAutoRefreshModeHint = HistoryAutoRefreshMode switch{
|
||||||
|
HistoryRefreshMode.DefaultAll =>
|
||||||
|
"Refreshes the full history using the default method and includes all entries",
|
||||||
|
HistoryRefreshMode.DefaultActive =>
|
||||||
|
"Refreshes the history using the default method and includes only active entries",
|
||||||
|
HistoryRefreshMode.FastNewReleases =>
|
||||||
|
"Uses the faster refresh method, similar to the custom calendar, focusing on newly released items",
|
||||||
|
_ => ""
|
||||||
|
};
|
||||||
|
|
||||||
if (e.PropertyName is nameof(History)){
|
if (e.PropertyName is nameof(History)){
|
||||||
if (CrunchyrollManager.Instance.CrunOptions.History){
|
if (CrunchyrollManager.Instance.CrunOptions.History){
|
||||||
|
|
@ -658,7 +760,7 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
public async void CheckIp(){
|
public async Task CheckIp(){
|
||||||
var result = await HttpClientReq.Instance.SendHttpRequest(HttpClientReq.CreateRequestMessage("https://icanhazip.com", HttpMethod.Get, false));
|
var result = await HttpClientReq.Instance.SendHttpRequest(HttpClientReq.CreateRequestMessage("https://icanhazip.com", HttpMethod.Get, false));
|
||||||
Console.Error.WriteLine("Your IP: " + result.ResponseContent);
|
Console.Error.WriteLine("Your IP: " + result.ResponseContent);
|
||||||
if (result.IsOk){
|
if (result.IsOk){
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:vm="clr-namespace:CRD.ViewModels"
|
xmlns:vm="clr-namespace:CRD.ViewModels"
|
||||||
|
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||||
x:DataType="vm:AccountPageViewModel"
|
x:DataType="vm:AccountPageViewModel"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="CRD.Views.AccountPageView">
|
x:Class="CRD.Views.AccountPageView">
|
||||||
|
|
@ -13,13 +14,33 @@
|
||||||
|
|
||||||
<StackPanel VerticalAlignment="Top" HorizontalAlignment="Center">
|
<StackPanel VerticalAlignment="Top" HorizontalAlignment="Center">
|
||||||
|
|
||||||
<!-- Profile Image -->
|
<Grid Width="170" Height="170" Margin="20"
|
||||||
<Image Width="170" Height="170" Margin="20"
|
HorizontalAlignment="Center"
|
||||||
Source="{Binding ProfileImage}">
|
VerticalAlignment="Top">
|
||||||
<Image.Clip>
|
|
||||||
<EllipseGeometry Rect="0,0,170,170" />
|
<!-- Profile Image -->
|
||||||
</Image.Clip>
|
<Image Source="{Binding ProfileImage}">
|
||||||
</Image>
|
<Image.Clip>
|
||||||
|
<EllipseGeometry Rect="0,0,170,170" />
|
||||||
|
</Image.Clip>
|
||||||
|
</Image>
|
||||||
|
|
||||||
|
<!-- Switch Button (Overlay Bottom Right) -->
|
||||||
|
<Button Width="42"
|
||||||
|
Height="42"
|
||||||
|
BorderThickness="0"
|
||||||
|
CornerRadius="21"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Bottom"
|
||||||
|
Margin="0,0,4,4"
|
||||||
|
Command="{Binding OpenMultiProfileDialogCommand}"
|
||||||
|
IsVisible="{Binding HasMultiProfile}"
|
||||||
|
ToolTip.Tip="Switch Profile">
|
||||||
|
|
||||||
|
<controls:SymbolIcon Symbol="Switch" FontSize="30"/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<!-- Profile Name -->
|
<!-- Profile Name -->
|
||||||
<TextBlock Text="{Binding ProfileName}" HorizontalAlignment="Center" TextAlignment="Center" FontSize="20" Margin="10" />
|
<TextBlock Text="{Binding ProfileName}" HorizontalAlignment="Center" TextAlignment="Center" FontSize="20" Margin="10" />
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,10 @@
|
||||||
<CheckBox IsChecked="{Binding ShowUpcomingEpisodes}"
|
<CheckBox IsChecked="{Binding ShowUpcomingEpisodes}"
|
||||||
Content="Show Upcoming episodes" Margin="5 5 0 0">
|
Content="Show Upcoming episodes" Margin="5 5 0 0">
|
||||||
</CheckBox>
|
</CheckBox>
|
||||||
|
|
||||||
|
<CheckBox IsChecked="{Binding UpdateHistoryFromCalendar}"
|
||||||
|
Content="Update History from Calendar" Margin="5 5 0 0">
|
||||||
|
</CheckBox>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
</controls:SettingsExpander.Footer>
|
</controls:SettingsExpander.Footer>
|
||||||
|
|
@ -185,7 +189,8 @@
|
||||||
<Grid HorizontalAlignment="Center">
|
<Grid HorizontalAlignment="Center">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Image HorizontalAlignment="Center" IsVisible="{Binding !AnilistEpisode}" Source="../Assets/coming_soon_ep.jpg" />
|
<Image HorizontalAlignment="Center" IsVisible="{Binding !AnilistEpisode}" Source="../Assets/coming_soon_ep.jpg" />
|
||||||
<Image HorizontalAlignment="Center" MaxHeight="150" Source="{Binding ImageBitmap}" />
|
<Image HorizontalAlignment="Center" IsVisible="{Binding !AnilistEpisode}" Source="{Binding ImageBitmap}" />
|
||||||
|
<Image HorizontalAlignment="Center" IsVisible="{Binding AnilistEpisode}" MaxHeight="150" Source="{Binding ImageBitmap}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
Unloaded="OnUnloaded"
|
Unloaded="OnUnloaded"
|
||||||
Loaded="Control_OnLoaded">
|
Loaded="Control_OnLoaded">
|
||||||
|
|
||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
<ui:UiIntToVisibilityConverter x:Key="UiIntToVisibilityConverter" />
|
<ui:UiIntToVisibilityConverter x:Key="UiIntToVisibilityConverter" />
|
||||||
<ui:UiSonarrIdToVisibilityConverter x:Key="UiSonarrIdToVisibilityConverter" />
|
<ui:UiSonarrIdToVisibilityConverter x:Key="UiSonarrIdToVisibilityConverter" />
|
||||||
<ui:UiListToStringConverter x:Key="UiListToStringConverter" />
|
<ui:UiListToStringConverter x:Key="UiListToStringConverter" />
|
||||||
|
|
@ -70,6 +70,48 @@
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
|
|
||||||
|
|
||||||
|
<StackPanel>
|
||||||
|
<ToggleButton x:Name="DropdownButtonSearch" Width="70" Height="70" Background="Transparent" BorderThickness="0" Margin="5 0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
IsChecked="{Binding IsSearchOpen, Mode=TwoWay}"
|
||||||
|
IsEnabled="{Binding !ProgramManager.FetchingData}">
|
||||||
|
<Grid>
|
||||||
|
<StackPanel Orientation="Vertical">
|
||||||
|
<controls:SymbolIcon Symbol="Zoom" FontSize="32" />
|
||||||
|
<TextBlock Text="Search" HorizontalAlignment="Center" FontSize="12"></TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Ellipse Width="10" Height="10"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Margin="0,0,0,0"
|
||||||
|
Fill="Orange"
|
||||||
|
IsHitTestVisible="False"
|
||||||
|
IsVisible="{Binding IsSearchActiveClosed}" />
|
||||||
|
</Grid>
|
||||||
|
</ToggleButton>
|
||||||
|
<Popup IsLightDismissEnabled="True"
|
||||||
|
IsOpen="{Binding IsSearchOpen, Mode=TwoWay}"
|
||||||
|
Placement="BottomEdgeAlignedRight"
|
||||||
|
PlacementTarget="{Binding ElementName=DropdownButtonSearch}">
|
||||||
|
|
||||||
|
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="10">
|
||||||
|
|
||||||
|
<TextBox x:Name="SearchBar" Width="160"
|
||||||
|
Watermark="Search"
|
||||||
|
Text="{Binding SearchInput, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
<Button Content="✕" Margin="6,0,0,0"
|
||||||
|
Command="{Binding ClearSearchCommand}" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
|
||||||
|
</Popup>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<Rectangle Width="1" Height="50" Fill="Gray" Margin="10,0" />
|
<Rectangle Width="1" Height="50" Fill="Gray" Margin="10,0" />
|
||||||
|
|
||||||
<StackPanel Margin="10,0">
|
<StackPanel Margin="10,0">
|
||||||
|
|
@ -115,6 +157,7 @@
|
||||||
<!-- <ToggleButton IsChecked="{Binding EditMode}" Margin="10 0" IsEnabled="{Binding !FetchingData}">Edit</ToggleButton> -->
|
<!-- <ToggleButton IsChecked="{Binding EditMode}" Margin="10 0" IsEnabled="{Binding !FetchingData}">Edit</ToggleButton> -->
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
|
||||||
<StackPanel Grid.Row="0" Grid.Column="2" Orientation="Horizontal">
|
<StackPanel Grid.Row="0" Grid.Column="2" Orientation="Horizontal">
|
||||||
|
|
||||||
<Slider VerticalAlignment="Center" Minimum="0.5" Maximum="1" Width="100"
|
<Slider VerticalAlignment="Center" Minimum="0.5" Maximum="1" Width="100"
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using System.Linq;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.Threading;
|
||||||
using CRD.Downloader;
|
using CRD.Downloader;
|
||||||
using CRD.Downloader.Crunchyroll;
|
using CRD.Downloader.Crunchyroll;
|
||||||
using CRD.Utils;
|
using CRD.Utils;
|
||||||
|
|
@ -46,6 +47,8 @@ public partial class MainWindow : AppWindow{
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private object selectedNavVieItem;
|
private object selectedNavVieItem;
|
||||||
|
|
||||||
|
private ToastNotification? toast;
|
||||||
|
|
||||||
private const int TitleBarHeightAdjustment = 31;
|
private const int TitleBarHeightAdjustment = 31;
|
||||||
|
|
||||||
|
|
@ -70,6 +73,7 @@ public partial class MainWindow : AppWindow{
|
||||||
PositionChanged += OnPositionChanged;
|
PositionChanged += OnPositionChanged;
|
||||||
SizeChanged += OnSizeChanged;
|
SizeChanged += OnSizeChanged;
|
||||||
|
|
||||||
|
toast = this.FindControl<ToastNotification>("Toast");
|
||||||
|
|
||||||
//select first element as default
|
//select first element as default
|
||||||
var nv = this.FindControl<NavigationView>("NavView");
|
var nv = this.FindControl<NavigationView>("NavView");
|
||||||
|
|
@ -150,55 +154,48 @@ public partial class MainWindow : AppWindow{
|
||||||
|
|
||||||
|
|
||||||
public void ShowToast(string message, ToastType type, int durationInSeconds = 5){
|
public void ShowToast(string message, ToastType type, int durationInSeconds = 5){
|
||||||
var toastControl = this.FindControl<ToastNotification>("Toast");
|
Dispatcher.UIThread.Post(() => toast?.Show(message, type, durationInSeconds));
|
||||||
toastControl?.Show(message, type, durationInSeconds);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void NavView_SelectionChanged(object? sender, NavigationViewSelectionChangedEventArgs e){
|
private void NavView_SelectionChanged(object? sender, NavigationViewSelectionChangedEventArgs e){
|
||||||
if (sender is NavigationView navView){
|
if (sender is NavigationView{ SelectedItem: NavigationViewItem selectedItem } navView){
|
||||||
var selectedItem = navView.SelectedItem as NavigationViewItem;
|
switch (selectedItem.Tag){
|
||||||
if (selectedItem != null){
|
case "DownloadQueue":
|
||||||
switch (selectedItem.Tag){
|
navView.Content = Activator.CreateInstance<DownloadsPageViewModel>();
|
||||||
case "DownloadQueue":
|
selectedNavVieItem = selectedItem;
|
||||||
navView.Content = Activator.CreateInstance(typeof(DownloadsPageViewModel));
|
break;
|
||||||
selectedNavVieItem = selectedItem;
|
case "AddDownload":
|
||||||
break;
|
navView.Content = Activator.CreateInstance<AddDownloadPageViewModel>();
|
||||||
case "AddDownload":
|
selectedNavVieItem = selectedItem;
|
||||||
navView.Content = Activator.CreateInstance(typeof(AddDownloadPageViewModel));
|
break;
|
||||||
selectedNavVieItem = selectedItem;
|
case "Calendar":
|
||||||
break;
|
navView.Content = Activator.CreateInstance<CalendarPageViewModel>();
|
||||||
case "Calendar":
|
selectedNavVieItem = selectedItem;
|
||||||
navView.Content = Activator.CreateInstance(typeof(CalendarPageViewModel));
|
break;
|
||||||
selectedNavVieItem = selectedItem;
|
case "History":
|
||||||
break;
|
navView.Content = Activator.CreateInstance<HistoryPageViewModel>();
|
||||||
case "History":
|
navigationStack.Clear();
|
||||||
navView.Content = Activator.CreateInstance(typeof(HistoryPageViewModel));
|
navigationStack.Push(navView.Content);
|
||||||
navigationStack.Clear();
|
selectedNavVieItem = selectedItem;
|
||||||
navigationStack.Push(navView.Content);
|
break;
|
||||||
selectedNavVieItem = selectedItem;
|
case "Seasons":
|
||||||
break;
|
navView.Content = Activator.CreateInstance<UpcomingPageViewModel>();
|
||||||
case "Seasons":
|
selectedNavVieItem = selectedItem;
|
||||||
navView.Content = Activator.CreateInstance(typeof(UpcomingPageViewModel));
|
break;
|
||||||
selectedNavVieItem = selectedItem;
|
case "Account":
|
||||||
break;
|
navView.Content = Activator.CreateInstance<AccountPageViewModel>();
|
||||||
case "Account":
|
selectedNavVieItem = selectedItem;
|
||||||
navView.Content = Activator.CreateInstance(typeof(AccountPageViewModel));
|
break;
|
||||||
selectedNavVieItem = selectedItem;
|
case "Settings":
|
||||||
break;
|
var viewModel = Activator.CreateInstance<SettingsPageViewModel>();
|
||||||
case "Settings":
|
navView.Content = viewModel;
|
||||||
var viewModel = (SettingsPageViewModel)Activator.CreateInstance(typeof(SettingsPageViewModel));
|
selectedNavVieItem = selectedItem;
|
||||||
navView.Content = viewModel;
|
break;
|
||||||
selectedNavVieItem = selectedItem;
|
case "Update":
|
||||||
break;
|
navView.Content = Activator.CreateInstance<UpdateViewModel>();
|
||||||
case "Update":
|
selectedNavVieItem = selectedItem;
|
||||||
navView.Content = Activator.CreateInstance(typeof(UpdateViewModel));
|
break;
|
||||||
selectedNavVieItem = selectedItem;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// (sender as NavigationView).Content = Activator.CreateInstance(typeof(DownloadsPageViewModel));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -209,7 +206,7 @@ public partial class MainWindow : AppWindow{
|
||||||
if (settings != null){
|
if (settings != null){
|
||||||
var screens = Screens.All;
|
var screens = Screens.All;
|
||||||
if (settings.ScreenIndex >= 0 && settings.ScreenIndex < screens.Count){
|
if (settings.ScreenIndex >= 0 && settings.ScreenIndex < screens.Count){
|
||||||
var screen = screens[settings.ScreenIndex];
|
// var screen = screens[settings.ScreenIndex];
|
||||||
|
|
||||||
// Restore the position first
|
// Restore the position first
|
||||||
Position = new PixelPoint(settings.PosX, settings.PosY);
|
Position = new PixelPoint(settings.PosX, settings.PosY);
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,13 @@
|
||||||
<Button Margin="0 0 5 10" FontStyle="Italic"
|
<Button Margin="0 0 5 10" FontStyle="Italic"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Command="{Binding OpenFolderDialogAsync}">
|
Command="{Binding OpenFolderDialogAsync}">
|
||||||
|
<Button.ContextFlyout>
|
||||||
|
<MenuFlyout>
|
||||||
|
<MenuItem Header="Clear Path"
|
||||||
|
Command="{Binding ClearFolderPathCommand}" />
|
||||||
|
</MenuFlyout>
|
||||||
|
</Button.ContextFlyout>
|
||||||
|
|
||||||
<ToolTip.Tip>
|
<ToolTip.Tip>
|
||||||
<TextBlock Text="{Binding SelectedSeries.SeriesDownloadPath,
|
<TextBlock Text="{Binding SelectedSeries.SeriesDownloadPath,
|
||||||
Converter={StaticResource EmptyToDefault},
|
Converter={StaticResource EmptyToDefault},
|
||||||
|
|
@ -646,6 +653,13 @@
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Command="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).OpenFolderDialogAsync}"
|
Command="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).OpenFolderDialogAsync}"
|
||||||
CommandParameter="{Binding .}">
|
CommandParameter="{Binding .}">
|
||||||
|
<Button.ContextFlyout>
|
||||||
|
<MenuFlyout>
|
||||||
|
<MenuItem Header="Clear Path"
|
||||||
|
Command="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).ClearFolderPathCommand}"
|
||||||
|
CommandParameter="{Binding .}"/>
|
||||||
|
</MenuFlyout>
|
||||||
|
</Button.ContextFlyout>
|
||||||
<ToolTip.Tip>
|
<ToolTip.Tip>
|
||||||
<TextBlock Text="{Binding SeasonDownloadPath,
|
<TextBlock Text="{Binding SeasonDownloadPath,
|
||||||
Converter={StaticResource EmptyToDefault},
|
Converter={StaticResource EmptyToDefault},
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
|
using CRD.Downloader;
|
||||||
using CRD.Utils.Sonarr;
|
using CRD.Utils.Sonarr;
|
||||||
using CRD.ViewModels;
|
using CRD.ViewModels;
|
||||||
|
|
||||||
|
|
@ -13,6 +14,7 @@ public partial class SettingsPageView : UserControl{
|
||||||
private void OnUnloaded(object? sender, RoutedEventArgs e){
|
private void OnUnloaded(object? sender, RoutedEventArgs e){
|
||||||
if (DataContext is SettingsPageViewModel viewModel){
|
if (DataContext is SettingsPageViewModel viewModel){
|
||||||
SonarrClient.Instance.RefreshSonarr();
|
SonarrClient.Instance.RefreshSonarr();
|
||||||
|
ProgramManager.Instance.StartRunners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
78
CRD/Views/Utils/ContentDialogMultiProfileSelectView.axaml
Normal file
78
CRD/Views/Utils/ContentDialogMultiProfileSelectView.axaml
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
<ui:CustomContentDialog xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="clr-namespace:CRD.ViewModels.Utils"
|
||||||
|
xmlns:structs="clr-namespace:CRD.Utils.Structs"
|
||||||
|
xmlns:ui="clr-namespace:CRD.Utils.UI"
|
||||||
|
x:DataType="vm:ContentDialogMultiProfileSelectViewModel"
|
||||||
|
x:Class="CRD.Views.Utils.ContentDialogMultiProfileSelectView">
|
||||||
|
|
||||||
|
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<ScrollViewer Grid.Row="3"
|
||||||
|
HorizontalScrollBarVisibility="Auto"
|
||||||
|
VerticalScrollBarVisibility="Disabled">
|
||||||
|
|
||||||
|
<ListBox ItemsSource="{Binding ProfileList}"
|
||||||
|
SelectedItem="{Binding SelectedItem}"
|
||||||
|
Background="Transparent"
|
||||||
|
BorderThickness="0">
|
||||||
|
|
||||||
|
<!-- Horizontal layout -->
|
||||||
|
<ListBox.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<StackPanel Orientation="Horizontal"/>
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ListBox.ItemsPanel>
|
||||||
|
|
||||||
|
<ListBox.Styles>
|
||||||
|
<Style Selector="ListBoxItem" x:DataType="structs:AccountProfile">
|
||||||
|
<Setter Property="IsEnabled">
|
||||||
|
<Setter.Value>
|
||||||
|
<Binding Path="CanBeSelected"/>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="ListBoxItem:disabled">
|
||||||
|
<Setter Property="Opacity" Value="0.4"/>
|
||||||
|
</Style>
|
||||||
|
</ListBox.Styles>
|
||||||
|
|
||||||
|
<ListBox.ItemTemplate >
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel Width="220"
|
||||||
|
Margin="0,0,12,0"
|
||||||
|
IsEnabled="{Binding CanBeSelected}">
|
||||||
|
|
||||||
|
<Grid Width="170"
|
||||||
|
Height="170"
|
||||||
|
Margin="20"
|
||||||
|
HorizontalAlignment="Center">
|
||||||
|
|
||||||
|
<Image Source="{Binding ProfileImage}">
|
||||||
|
<Image.Clip>
|
||||||
|
<EllipseGeometry Rect="0,0,170,170"/>
|
||||||
|
</Image.Clip>
|
||||||
|
</Image>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<TextBlock Text="{Binding ProfileName}"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
FontSize="20"
|
||||||
|
Margin="10"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
|
||||||
|
</ListBox>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
|
||||||
|
</ui:CustomContentDialog>
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace CRD.Views.Utils;
|
||||||
|
|
||||||
|
public partial class ContentDialogMultiProfileSelectView : UserControl{
|
||||||
|
public ContentDialogMultiProfileSelectView(){
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -62,6 +62,43 @@
|
||||||
<CheckBox IsChecked="{Binding HistorySkipUnmonitored}"> </CheckBox>
|
<CheckBox IsChecked="{Binding HistorySkipUnmonitored}"> </CheckBox>
|
||||||
</controls:SettingsExpanderItem.Footer>
|
</controls:SettingsExpanderItem.Footer>
|
||||||
</controls:SettingsExpanderItem>
|
</controls:SettingsExpanderItem>
|
||||||
|
|
||||||
|
<controls:SettingsExpanderItem Content="Auto History Refresh" Description="Automatically refresh your history at a set interval">
|
||||||
|
<controls:SettingsExpanderItem.Footer>
|
||||||
|
<StackPanel Spacing="8" Width="520">
|
||||||
|
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Text="Refresh interval (minutes)" />
|
||||||
|
<DockPanel LastChildFill="True">
|
||||||
|
<controls:NumberBox Minimum="0"
|
||||||
|
Maximum="1000000000"
|
||||||
|
Value="{Binding HistoryAutoRefreshIntervalMinutes}"
|
||||||
|
SpinButtonPlacementMode="Hidden"
|
||||||
|
HorizontalAlignment="Stretch" />
|
||||||
|
</DockPanel>
|
||||||
|
<TextBlock Opacity="0.7" FontSize="12" TextWrapping="Wrap"
|
||||||
|
Text="Set to 0 to disable automatic refresh." />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Text="Refresh mode" />
|
||||||
|
<ComboBox ItemsSource="{Binding HistoryAutoRefreshModes}"
|
||||||
|
DisplayMemberBinding="{Binding DisplayName}"
|
||||||
|
SelectedValueBinding="{Binding value}"
|
||||||
|
SelectedValue="{Binding HistoryAutoRefreshMode}"/>
|
||||||
|
<TextBlock Opacity="0.7" FontSize="12" TextWrapping="Wrap"
|
||||||
|
Text="{Binding HistoryAutoRefreshModeHint}" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Opacity="0.7" FontSize="12" TextWrapping="Wrap" Text="{Binding HistoryAutoRefreshLastRunTime,StringFormat='Last refresh: {0}'}" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</controls:SettingsExpanderItem.Footer>
|
||||||
|
</controls:SettingsExpanderItem>
|
||||||
|
|
||||||
</controls:SettingsExpander>
|
</controls:SettingsExpander>
|
||||||
|
|
||||||
|
|
@ -241,51 +278,50 @@
|
||||||
|
|
||||||
<CheckBox IsChecked="{Binding DownloadFinishedPlaySound}"> </CheckBox>
|
<CheckBox IsChecked="{Binding DownloadFinishedPlaySound}"> </CheckBox>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<!-- <Grid HorizontalAlignment="Right" Margin="0 5 0 0"> -->
|
</controls:SettingsExpanderItem.Footer>
|
||||||
<!-- <Grid.ColumnDefinitions> -->
|
</controls:SettingsExpanderItem>
|
||||||
<!-- <ColumnDefinition Width="Auto" /> -->
|
|
||||||
<!-- <ColumnDefinition Width="150" /> -->
|
<controls:SettingsExpanderItem Content="Execute on completion" Description="Enable to run a selected file after all downloads complete">
|
||||||
<!-- </Grid.ColumnDefinitions> -->
|
<controls:SettingsExpanderItem.Footer>
|
||||||
<!-- -->
|
<StackPanel Spacing="10">
|
||||||
<!-- <Grid.RowDefinitions> -->
|
<StackPanel Orientation="Horizontal" Spacing="10" HorizontalAlignment="Right">
|
||||||
<!-- <RowDefinition Height="Auto" /> -->
|
<TextBlock IsVisible="{Binding DownloadFinishedExecute}"
|
||||||
<!-- <RowDefinition Height="Auto" /> -->
|
Text="{Binding DownloadFinishedExecutePath, Mode=OneWay}"
|
||||||
<!-- </Grid.RowDefinitions> -->
|
FontSize="15"
|
||||||
<!-- -->
|
Opacity="0.8"
|
||||||
<!-- <TextBlock Text="Opacity" -->
|
TextWrapping="NoWrap"
|
||||||
<!-- FontSize="15" -->
|
TextAlignment="Center"
|
||||||
<!-- Opacity="0.8" -->
|
VerticalAlignment="Center" />
|
||||||
<!-- VerticalAlignment="Center" -->
|
|
||||||
<!-- HorizontalAlignment="Right" -->
|
<Button IsVisible="{Binding DownloadFinishedExecute}"
|
||||||
<!-- Margin="0 0 5 10" -->
|
Command="{Binding OpenFileDialogAsyncInternalFinishedExecute}"
|
||||||
<!-- Grid.Row="0" Grid.Column="0" /> -->
|
VerticalAlignment="Center"
|
||||||
<!-- <controls:NumberBox Minimum="0" Maximum="1" -->
|
FontStyle="Italic">
|
||||||
<!-- SmallChange="0.05" -->
|
<ToolTip.Tip>
|
||||||
<!-- LargeChange="0.1" -->
|
<TextBlock Text="Select file to execute when downloads finish" FontSize="15" />
|
||||||
<!-- SimpleNumberFormat="F2" -->
|
</ToolTip.Tip>
|
||||||
<!-- Value="{Binding BackgroundImageOpacity}" -->
|
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||||
<!-- SpinButtonPlacementMode="Inline" -->
|
<controls:SymbolIcon Symbol="Folder" FontSize="18" />
|
||||||
<!-- HorizontalAlignment="Stretch" -->
|
</StackPanel>
|
||||||
<!-- Margin="0 0 0 10" -->
|
</Button>
|
||||||
<!-- Grid.Row="0" Grid.Column="1" /> -->
|
|
||||||
<!-- -->
|
<Button IsVisible="{Binding DownloadFinishedExecute}"
|
||||||
<!-- <TextBlock Text="Blur Radius" -->
|
Command="{Binding ClearFinishedExectuePath}"
|
||||||
<!-- FontSize="15" -->
|
VerticalAlignment="Center"
|
||||||
<!-- Opacity="0.8" -->
|
FontStyle="Italic">
|
||||||
<!-- VerticalAlignment="Center" -->
|
<ToolTip.Tip>
|
||||||
<!-- HorizontalAlignment="Right" -->
|
<TextBlock Text="Clear selected file" FontSize="15" />
|
||||||
<!-- Margin="0 0 5 0" -->
|
</ToolTip.Tip>
|
||||||
<!-- Grid.Row="1" Grid.Column="0" /> -->
|
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||||
<!-- <controls:NumberBox Minimum="0" Maximum="40" -->
|
<controls:SymbolIcon Symbol="Clear" FontSize="18" />
|
||||||
<!-- SmallChange="1" -->
|
</StackPanel>
|
||||||
<!-- LargeChange="5" -->
|
</Button>
|
||||||
<!-- SimpleNumberFormat="F0" -->
|
|
||||||
<!-- Value="{Binding BackgroundImageBlurRadius}" -->
|
<CheckBox IsChecked="{Binding DownloadFinishedExecute}"> </CheckBox>
|
||||||
<!-- SpinButtonPlacementMode="Inline" -->
|
</StackPanel>
|
||||||
<!-- HorizontalAlignment="Stretch" -->
|
|
||||||
<!-- Grid.Row="1" Grid.Column="1" /> -->
|
|
||||||
<!-- </Grid> -->
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
</controls:SettingsExpanderItem.Footer>
|
</controls:SettingsExpanderItem.Footer>
|
||||||
|
|
@ -423,6 +459,38 @@
|
||||||
</controls:SettingsExpanderItem>
|
</controls:SettingsExpanderItem>
|
||||||
|
|
||||||
|
|
||||||
|
</controls:SettingsExpander>
|
||||||
|
|
||||||
|
<controls:SettingsExpander Header="General Settings"
|
||||||
|
IconSource="Bullets"
|
||||||
|
Description="Adjust General settings"
|
||||||
|
IsExpanded="False">
|
||||||
|
|
||||||
|
<controls:SettingsExpanderItem Content="Show tray icon">
|
||||||
|
<controls:SettingsExpanderItem.Footer>
|
||||||
|
<CheckBox IsChecked="{Binding TrayIconEnabled}"> </CheckBox>
|
||||||
|
</controls:SettingsExpanderItem.Footer>
|
||||||
|
</controls:SettingsExpanderItem>
|
||||||
|
|
||||||
|
<controls:SettingsExpanderItem IsVisible="{Binding TrayIconEnabled}" Content="Start minimized to tray" Description="Launches in the tray without opening a window">
|
||||||
|
<controls:SettingsExpanderItem.Footer>
|
||||||
|
<CheckBox IsChecked="{Binding StartMinimizedToTray}"> </CheckBox>
|
||||||
|
</controls:SettingsExpanderItem.Footer>
|
||||||
|
</controls:SettingsExpanderItem>
|
||||||
|
|
||||||
|
<controls:SettingsExpanderItem IsVisible="{Binding TrayIconEnabled}" Content="Minimize to tray" Description="Minimizing hides the window and removes it from the taskbar">
|
||||||
|
<controls:SettingsExpanderItem.Footer>
|
||||||
|
<CheckBox IsChecked="{Binding MinimizeToTray}"> </CheckBox>
|
||||||
|
</controls:SettingsExpanderItem.Footer>
|
||||||
|
</controls:SettingsExpanderItem>
|
||||||
|
|
||||||
|
<controls:SettingsExpanderItem IsVisible="{Binding TrayIconEnabled}" Content="Close to tray" Description="Clicking X hides the app in the tray instead of exiting">
|
||||||
|
<controls:SettingsExpanderItem.Footer>
|
||||||
|
<CheckBox IsChecked="{Binding MinimizeToTrayOnClose}"> </CheckBox>
|
||||||
|
</controls:SettingsExpanderItem.Footer>
|
||||||
|
</controls:SettingsExpanderItem>
|
||||||
|
|
||||||
|
|
||||||
</controls:SettingsExpander>
|
</controls:SettingsExpander>
|
||||||
|
|
||||||
<controls:SettingsExpander Header="App Appearance"
|
<controls:SettingsExpander Header="App Appearance"
|
||||||
|
|
|
||||||
BIN
images/Calendar_Custom_Settings.png
(Stored with Git LFS)
BIN
images/Calendar_Custom_Settings.png
(Stored with Git LFS)
Binary file not shown.
BIN
images/History_Overview.png
(Stored with Git LFS)
BIN
images/History_Overview.png
(Stored with Git LFS)
Binary file not shown.
BIN
images/History_Overview_Table.png
(Stored with Git LFS)
BIN
images/History_Overview_Table.png
(Stored with Git LFS)
Binary file not shown.
BIN
images/Settings.png
(Stored with Git LFS)
BIN
images/Settings.png
(Stored with Git LFS)
Binary file not shown.
BIN
images/Settings_CR_Download.png
(Stored with Git LFS)
Normal file
BIN
images/Settings_CR_Download.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
images/Settings_CR_Dub.png
(Stored with Git LFS)
Normal file
BIN
images/Settings_CR_Dub.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
images/Settings_CR_Hardsub.png
(Stored with Git LFS)
Normal file
BIN
images/Settings_CR_Hardsub.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
images/Settings_CR_Muxing.png
(Stored with Git LFS)
Normal file
BIN
images/Settings_CR_Muxing.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
images/Settings_CR_Muxing_New_Encoding_Preset.png
(Stored with Git LFS)
Normal file
BIN
images/Settings_CR_Muxing_New_Encoding_Preset.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
images/Settings_CR_Softsubs.png
(Stored with Git LFS)
Normal file
BIN
images/Settings_CR_Softsubs.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
images/Settings_Download.png
(Stored with Git LFS)
BIN
images/Settings_Download.png
(Stored with Git LFS)
Binary file not shown.
BIN
images/Settings_Download_CR.png
(Stored with Git LFS)
BIN
images/Settings_Download_CR.png
(Stored with Git LFS)
Binary file not shown.
BIN
images/Settings_G.png
(Stored with Git LFS)
Normal file
BIN
images/Settings_G.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
images/Settings_G_Download.png
(Stored with Git LFS)
Normal file
BIN
images/Settings_G_Download.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
images/Settings_G_FlareSolverr.png
(Stored with Git LFS)
Normal file
BIN
images/Settings_G_FlareSolverr.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
images/Settings_G_General.png
(Stored with Git LFS)
Normal file
BIN
images/Settings_G_General.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
images/Settings_G_History.png
(Stored with Git LFS)
Normal file
BIN
images/Settings_G_History.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
images/Settings_History.png
(Stored with Git LFS)
BIN
images/Settings_History.png
(Stored with Git LFS)
Binary file not shown.
BIN
images/Settings_Muxing.png
(Stored with Git LFS)
BIN
images/Settings_Muxing.png
(Stored with Git LFS)
Binary file not shown.
BIN
images/Settings_Muxing_New_Encoding_Preset.png
(Stored with Git LFS)
BIN
images/Settings_Muxing_New_Encoding_Preset.png
(Stored with Git LFS)
Binary file not shown.
BIN
images/Settings_Softsubs.png
(Stored with Git LFS)
BIN
images/Settings_Softsubs.png
(Stored with Git LFS)
Binary file not shown.
Loading…
Reference in a new issue