Add - Option to create video file for each dub downloaded https://github.com/Crunchy-DL/Crunchy-Downloader/issues/60

Add - Filter for history (All, Missing Episodes, Missing Sonarr Episodes, Continuing Only) https://github.com/Crunchy-DL/Crunchy-Downloader/discussions/58
Add - Sonarr button to history to add missing series from sonarr to the history and add all episodes that are missing from sonarr https://github.com/Crunchy-DL/Crunchy-Downloader/discussions/58
Fix - RAM bug with history table view
Fix - Custom calendar UTC times https://github.com/Crunchy-DL/Crunchy-Downloader/discussions/59
Fix - Funimation subscription detection https://github.com/Crunchy-DL/Crunchy-Downloader/issues/56
This commit is contained in:
Elwador 2024-08-05 05:06:40 +02:00
parent 43823a69e8
commit 3f79e45131
17 changed files with 545 additions and 222 deletions

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:60deeb982f931b150ed8512e9ae284061722fd3719acc717ca812098ce4547f4
size 116598
oid sha256:eb8049bb754d9e938ac7adf0b7884274c68d265b7ce7b0c4fa2085f84677296b
size 5780

View File

@ -181,6 +181,10 @@ public class CalendarManager{
foreach (var crBrowseEpisode in newEpisodes){
var targetDate = CrunchyrollManager.Instance.CrunOptions.CalendarFilterByAirDate ? crBrowseEpisode.EpisodeMetadata.EpisodeAirDate : crBrowseEpisode.LastPublic;
if (targetDate.Kind == DateTimeKind.Utc){
targetDate = targetDate.ToLocalTime();
}
if (CrunchyrollManager.Instance.CrunOptions.CalendarHideDubs && crBrowseEpisode.EpisodeMetadata.SeasonTitle != null &&
(crBrowseEpisode.EpisodeMetadata.SeasonTitle.EndsWith("Dub)") || crBrowseEpisode.EpisodeMetadata.AudioLocale != Locale.JaJp)){
continue;

View File

@ -1,11 +1,9 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using System.Web;
using CRD.Utils;
using CRD.Utils.Structs;
using Newtonsoft.Json;
@ -13,7 +11,6 @@ using Newtonsoft.Json;
namespace CRD.Downloader.Crunchyroll;
public class CrAuth{
private readonly CrunchyrollManager crunInstance = CrunchyrollManager.Instance;
public async Task AuthAnonymous(){
@ -44,9 +41,7 @@ public class CrAuth{
PreferredContentAudioLanguage = "ja-JP",
PreferredContentSubtitleLanguage = "de-DE"
};
// CrunchyrollManager.Instance.CmsToken = new CrCmsToken();
}
private void JsonTokenToFileAndVariable(string content){
@ -104,35 +99,36 @@ public class CrAuth{
if (profileTemp != null){
crunInstance.Profile = profileTemp;
var requestSubs = HttpClientReq.CreateRequestMessage(Api.Subscription + crunInstance.Token.account_id, HttpMethod.Get, true, false, null);
var responseSubs = await HttpClientReq.Instance.SendHttpRequest(requestSubs);
if (responseSubs.IsOk){
var subsc = Helpers.Deserialize<Subscription>(responseSubs.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
crunInstance.Profile.Subscription = subsc;
if ( subsc is{ SubscriptionProducts:{ Count: 0 }, ThirdPartySubscriptionProducts.Count: > 0 }){
if (subsc is{ SubscriptionProducts:{ Count: 0 }, ThirdPartySubscriptionProducts.Count: > 0 }){
var thirdPartySub = subsc.ThirdPartySubscriptionProducts.First();
var expiration = thirdPartySub.InGrace ? thirdPartySub.InGraceExpirationDate : thirdPartySub.ExpirationDate;
var remaining = expiration - DateTime.UtcNow;
crunInstance.Profile.HasPremium = remaining > TimeSpan.Zero;
crunInstance.Profile.Subscription.IsActive = remaining > TimeSpan.Zero;
crunInstance.Profile.Subscription.NextRenewalDate = expiration;
} else if(subsc is{ SubscriptionProducts:{ Count: 0 }, NonrecurringSubscriptionProducts.Count: > 0 }){
} else if (subsc is{ SubscriptionProducts:{ Count: 0 }, NonrecurringSubscriptionProducts.Count: > 0 }){
var nonRecurringSub = subsc.NonrecurringSubscriptionProducts.First();
var remaining = nonRecurringSub.EndDate - DateTime.UtcNow;
crunInstance.Profile.HasPremium = remaining > TimeSpan.Zero;
crunInstance.Profile.Subscription.IsActive = remaining > TimeSpan.Zero;
crunInstance.Profile.Subscription.NextRenewalDate = nonRecurringSub.EndDate;
} else if (subsc is{ SubscriptionProducts:{ Count: 0 }, FunimationSubscriptions.Count: > 0 }){
crunInstance.Profile.HasPremium = true;
} else{
crunInstance.Profile.HasPremium = subsc.IsActive;
crunInstance.Profile.HasPremium = subsc.IsActive;
}
} else{
crunInstance.Profile.HasPremium = false;
Console.Error.WriteLine("Failed to check premium subscription status");
}
}
}
}
@ -167,11 +163,10 @@ public class CrAuth{
if (crunInstance.Token?.refresh_token != null){
HttpClientReq.Instance.SetETPCookie(crunInstance.Token.refresh_token);
await GetProfile();
}
// await GetCmsToken();
}
public async Task RefreshToken(bool needsToken){
@ -209,53 +204,7 @@ public class CrAuth{
} else{
Console.Error.WriteLine("Refresh Token Auth Failed");
}
// await GetCmsToken();
}
// public async Task GetCmsToken(){
// if (crunInstance.Token?.access_token == null){
// Console.Error.WriteLine($"Missing Access Token: {crunInstance.Token?.access_token}");
// return;
// }
//
// var request = HttpClientReq.CreateRequestMessage(Api.BetaCmsToken, HttpMethod.Get, true, true, null);
//
// var response = await HttpClientReq.Instance.SendHttpRequest(request);
//
// if (response.IsOk){
// crunInstance.CmsToken = JsonConvert.DeserializeObject<CrCmsToken>(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
// } else{
// Console.Error.WriteLine("CMS Token Auth Failed");
// }
// }
//
// public async Task GetCmsData(){
// if (crunInstance.CmsToken?.Cms == null){
// Console.Error.WriteLine("Missing CMS Token");
// return;
// }
//
// UriBuilder uriBuilder = new UriBuilder(Api.BetaCms + crunInstance.CmsToken.Cms.Bucket + "/index?");
//
// NameValueCollection query = HttpUtility.ParseQueryString(uriBuilder.Query);
//
// query["preferred_audio_language"] = "ja-JP";
// query["Policy"] = crunInstance.CmsToken.Cms.Policy;
// query["Signature"] = crunInstance.CmsToken.Cms.Signature;
// query["Key-Pair-Id"] = crunInstance.CmsToken.Cms.KeyPairId;
//
// uriBuilder.Query = query.ToString();
//
// var request = new HttpRequestMessage(HttpMethod.Get, uriBuilder.ToString());
//
// var response = await HttpClientReq.Instance.SendHttpRequest(request);
//
// if (response.IsOk){
// Console.WriteLine(response.ResponseContent);
// } else{
// Console.Error.WriteLine("Failed to Get CMS Index");
// }
// }
}

View File

@ -28,7 +28,6 @@ namespace CRD.Downloader.Crunchyroll;
public class CrunchyrollManager{
public CrToken? Token;
// public CrCmsToken? CmsToken;
public CrProfile Profile = new();
private readonly Lazy<CrDownloadOptions> _optionsLazy;
@ -44,6 +43,8 @@ public class CrunchyrollManager{
#endregion
public CrBrowseSeriesBase? AllCRSeries;
public string DefaultLocale = "en-US";
@ -170,9 +171,9 @@ public class CrunchyrollManager{
} else{
HistoryList =[];
}
SonarrClient.Instance.RefreshSonarr();
}
SonarrClient.Instance.RefreshSonarr();
}
}
@ -215,27 +216,55 @@ public class CrunchyrollManager{
};
QueueManager.Instance.Queue.Refresh();
if (CrunOptions is{ DlVideoOnce: false, KeepDubsSeperate: true }){
var groupByDub = Helpers.GroupByLanguageWithSubtitles(res.Data);
await MuxStreams(res.Data,
new CrunchyMuxOptions{
FfmpegOptions = options.FfmpegOptions,
SkipSubMux = options.SkipSubsMux,
Output = res.FileName,
Mp4 = options.Mp4,
VideoTitle = res.VideoTitle,
Novids = options.Novids,
NoCleanup = options.Nocleanup,
DefaultAudio = Languages.FindLang(options.DefaultAudio),
DefaultSub = Languages.FindLang(options.DefaultSub),
MkvmergeOptions = options.MkvmergeOptions,
ForceMuxer = options.Force,
SyncTiming = options.SyncTiming,
CcTag = options.CcTag,
KeepAllVideos = true,
MuxDescription = options.IncludeVideoDescription
},
res.FileName);
foreach (var keyValue in groupByDub){
await MuxStreams(keyValue.Value,
new CrunchyMuxOptions{
FfmpegOptions = options.FfmpegOptions,
SkipSubMux = options.SkipSubsMux,
Output = res.FileName,
Mp4 = options.Mp4,
VideoTitle = res.VideoTitle,
Novids = options.Novids,
NoCleanup = options.Nocleanup,
DefaultAudio = Languages.FindLang(options.DefaultAudio),
DefaultSub = Languages.FindLang(options.DefaultSub),
MkvmergeOptions = options.MkvmergeOptions,
ForceMuxer = options.Force,
SyncTiming = options.SyncTiming,
CcTag = options.CcTag,
KeepAllVideos = true,
MuxDescription = options.IncludeVideoDescription
},
res.FileName);
}
} else{
await MuxStreams(res.Data,
new CrunchyMuxOptions{
FfmpegOptions = options.FfmpegOptions,
SkipSubMux = options.SkipSubsMux,
Output = res.FileName,
Mp4 = options.Mp4,
VideoTitle = res.VideoTitle,
Novids = options.Novids,
NoCleanup = options.Nocleanup,
DefaultAudio = Languages.FindLang(options.DefaultAudio),
DefaultSub = Languages.FindLang(options.DefaultSub),
MkvmergeOptions = options.MkvmergeOptions,
ForceMuxer = options.Force,
SyncTiming = options.SyncTiming,
CcTag = options.CcTag,
KeepAllVideos = true,
MuxDescription = options.IncludeVideoDescription
},
res.FileName);
}
data.DownloadProgress = new DownloadProgress(){
IsDownloading = true,
Done = true,

View File

@ -544,6 +544,8 @@ public class History(){
}
}
private static readonly object _lock = new object();
public async Task MatchHistoryEpisodesWithSonarr(bool updateAll, HistorySeries historySeries){
if (crunInstance.CrunOptions.SonarrProperties is{ SonarrEnabled: false }){
return;
@ -574,9 +576,13 @@ public class History(){
historyEpisode.SonarrHasFile = episode.HasFile;
historyEpisode.SonarrAbsolutNumber = episode.AbsoluteEpisodeNumber + "";
historyEpisode.SonarrSeasonNumber = episode.SeasonNumber + "";
episodes.Remove(episode);
lock (_lock) {
episodes.Remove(episode);
}
} else{
failedEpisodes.Add(historyEpisode);
lock (_lock) {
failedEpisodes.Add(historyEpisode);
}
}
}
});
@ -598,7 +604,9 @@ public class History(){
historyEpisode.SonarrHasFile = episode.HasFile;
historyEpisode.SonarrAbsolutNumber = episode.AbsoluteEpisodeNumber + "";
historyEpisode.SonarrSeasonNumber = episode.SeasonNumber + "";
episodes.Remove(episode);
lock (_lock) {
episodes.Remove(episode);
}
} else{
var episode1 = episodes.Find(ele => {
if (ele == null){
@ -614,7 +622,9 @@ public class History(){
historyEpisode.SonarrHasFile = episode1.HasFile;
historyEpisode.SonarrAbsolutNumber = episode1.AbsoluteEpisodeNumber + "";
historyEpisode.SonarrSeasonNumber = episode1.SeasonNumber + "";
episodes.Remove(episode1);
lock (_lock) {
episodes.Remove(episode1);
}
} else{
var episode2 = episodes.Find(ele => {
if (ele == null){
@ -629,7 +639,9 @@ public class History(){
historyEpisode.SonarrHasFile = episode2.HasFile;
historyEpisode.SonarrAbsolutNumber = episode2.AbsoluteEpisodeNumber + "";
historyEpisode.SonarrSeasonNumber = episode2.SeasonNumber + "";
episodes.Remove(episode2);
lock (_lock) {
episodes.Remove(episode2);
}
} else{
Console.Error.WriteLine($"Could not match episode {historyEpisode.EpisodeTitle} to sonarr episode");
}
@ -700,6 +712,27 @@ public class History(){
return highestSimilarity < 0.8 ? null : closestMatch;
}
public CrBrowseSeries? FindClosestMatchCrSeries(List<CrBrowseSeries> episodeList, string title){
CrBrowseSeries? closestMatch = null;
double highestSimilarity = 0.0;
object lockObject = new object(); // To synchronize access to shared variables
Parallel.ForEach(episodeList, episode => {
if (episode != null){
double similarity = CalculateSimilarity(episode.Title, title);
lock (lockObject) // Ensure thread-safe access to shared variables
{
if (similarity > highestSimilarity){
highestSimilarity = similarity;
closestMatch = episode;
}
}
}
});
return highestSimilarity < 0.8 ? null : closestMatch;
}
private double CalculateSimilarity(string source, string target){
int distance = LevenshteinDistance(source, target);

View File

@ -191,6 +191,17 @@ public enum SortingType{
HistorySeriesAddDate,
}
public enum FilterType{
[EnumMember(Value = "All")]
All,
[EnumMember(Value = "Missing Episodes")]
MissingEpisodes,
[EnumMember(Value = "Missing Episodes Sonarr")]
MissingEpisodesSonarr,
[EnumMember(Value = "Continuing Only")]
ContinuingOnly,
}
public enum SonarrCoverType{
Banner,
FanArt,

View File

@ -8,6 +8,7 @@ using System.Runtime.Serialization;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Avalonia.Media.Imaging;
using CRD.Utils.Structs;
using Newtonsoft.Json;
namespace CRD.Utils;
@ -157,7 +158,7 @@ public class Helpers{
var title = chapters[i].Title;
var endTime = (i + 1 < chapters.Count) ? chapters[i + 1].StartTime : startTime + 10000; // Add 10 seconds to the last chapter end time
if (endTime < startTime) {
if (endTime < startTime){
endTime = startTime + 10000; // Correct end time if it is before start time
}
@ -210,12 +211,12 @@ public class Helpers{
return (IsOk: false, ErrorCode: -1);
}
}
public static void DeleteFile(string filePath){
if (string.IsNullOrEmpty(filePath)){
return;
}
try{
if (File.Exists(filePath)){
File.Delete(filePath);
@ -225,8 +226,8 @@ public class Helpers{
// Handle exceptions if you need to log them or throw
}
}
public static async Task<(bool IsOk, int ErrorCode)> ExecuteCommandAsyncWorkDir(string type, string bin, string command,string workingDir){
public static async Task<(bool IsOk, int ErrorCode)> ExecuteCommandAsyncWorkDir(string type, string bin, string command, string workingDir){
try{
using (var process = new Process()){
process.StartInfo.WorkingDirectory = workingDir;
@ -345,8 +346,8 @@ public class Helpers{
return null;
}
}
public static async Task<Bitmap?> LoadImage(string imageUrl){
try{
using (var client = new HttpClient()){
@ -363,5 +364,18 @@ public class Helpers{
return null;
}
public static Dictionary<string, List<DownloadedMedia>> GroupByLanguageWithSubtitles(List<DownloadedMedia> allMedia){
var languageGroups = allMedia
.GroupBy(media => {
if (media.Type == DownloadMediaType.Subtitle && media.RelatedVideoDownloadMedia != null){
return media.RelatedVideoDownloadMedia.Lang.CrLocale;
}
return media.Lang.CrLocale;
})
.ToDictionary(group => group.Key, group => group.ToList());
return languageGroups;
}
}

View File

@ -82,9 +82,9 @@ public class Merger{
args.Add($"-i \"{sub.value.File}\"");
metaData.Add($"-map {index}:s");
if (options.Defaults.Sub.Code == sub.value.Language.Code && CrunchyrollManager.Instance.CrunOptions.DefaultSubSigns == sub.value.Signs && sub.value.ClosedCaption == false){
args.Add($"-disposition:s:{sub.i} default");
metaData.Add($"-disposition:s:{sub.i} default");
} else{
args.Add($"-disposition:s:{sub.i} 0");
metaData.Add($"-disposition:s:{sub.i} 0");
}
index++;
}

View File

@ -110,6 +110,9 @@ public class CrDownloadOptions{
[YamlMember(Alias = "dl_video_once", ApplyNamingConventions = false)]
public bool DlVideoOnce{ get; set; }
[YamlMember(Alias = "keep_dubs_seperate", ApplyNamingConventions = false)]
public bool KeepDubsSeperate{ get; set; }
[YamlIgnore]
public bool? Skipmux{ get; set; }
@ -159,6 +162,9 @@ public class CrDownloadOptions{
[YamlMember(Alias = "history_add_specials", ApplyNamingConventions = false)]
public bool HistoryAddSpecials{ get; set; }
[YamlMember(Alias = "history_count_sonarr", ApplyNamingConventions = false)]
public bool HistoryCountSonarr{ get; set; }
[YamlMember(Alias = "sonarr_properties", ApplyNamingConventions = false)]
public SonarrProperties? SonarrProperties{ get; set; }

View File

@ -46,6 +46,9 @@ public class Subscription{
[JsonProperty("nonrecurring_subscription_products")]
public List<NonRecurringSubscriptionProduct>? NonrecurringSubscriptionProducts{ get; set; }
[JsonProperty("funimation_subscriptions")]
public List<object>? FunimationSubscriptions{ get; set; }
}
public class NonRecurringSubscriptionProduct{

View File

@ -63,6 +63,9 @@ public class HistorySeries : INotifyPropertyChanged{
[JsonIgnore]
public Bitmap? ThumbnailImage{ get; set; }
[JsonIgnore]
public bool IsImageLoaded{ get; private set; } = false;
[JsonIgnore]
public bool FetchingData{ get; set; }
@ -168,17 +171,19 @@ public class HistorySeries : INotifyPropertyChanged{
#endregion
public async Task LoadImage(){
if (IsImageLoaded || string.IsNullOrEmpty(ThumbnailImageUrl))
return;
try{
using (var client = new HttpClient()){
var response = await client.GetAsync(ThumbnailImageUrl);
response.EnsureSuccessStatusCode();
using (var stream = await response.Content.ReadAsStreamAsync()){
ThumbnailImage = new Bitmap(stream);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ThumbnailImage)));
}
}
using var client = new HttpClient();
var response = await client.GetAsync(ThumbnailImageUrl);
response.EnsureSuccessStatusCode();
using var stream = await response.Content.ReadAsStreamAsync();
ThumbnailImage = new Bitmap(stream);
IsImageLoaded = true;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ThumbnailImage)));
} catch (Exception ex){
// Handle exceptions
Console.Error.WriteLine("Failed to load image: " + ex.Message);
}
}
@ -187,50 +192,67 @@ public class HistorySeries : INotifyPropertyChanged{
int count = 0;
bool foundWatched = false;
var historyAddSpecials = CrunchyrollManager.Instance.CrunOptions.HistoryAddSpecials;
var sonarrEnabled = CrunchyrollManager.Instance.CrunOptions.SonarrProperties != null && CrunchyrollManager.Instance.CrunOptions.SonarrProperties.SonarrEnabled;
for (int i = Seasons.Count - 1; i >= 0; i--){
var season = Seasons[i];
if (sonarrEnabled && CrunchyrollManager.Instance.CrunOptions.HistoryCountSonarr && !string.IsNullOrEmpty(SonarrSeriesId)){
for (int i = Seasons.Count - 1; i >= 0; i--){
var season = Seasons[i];
if (season.SpecialSeason == true){
if (historyAddSpecials){
var episodes = season.EpisodesList;
for (int j = episodes.Count - 1; j >= 0; j--){
if (!episodes[j].WasDownloaded){
count++;
}
var episodesList = season.EpisodesList;
for (int j = episodesList.Count - 1; j >= 0; j--){
var episode = episodesList[j];
if (!string.IsNullOrEmpty(episode.SonarrEpisodeId) && !episode.SonarrHasFile){
count++;
}
}
continue;
}
} else{
for (int i = Seasons.Count - 1; i >= 0; i--){
var season = Seasons[i];
var episodesList = season.EpisodesList;
for (int j = episodesList.Count - 1; j >= 0; j--){
var episode = episodesList[j];
if (episode.SpecialEpisode){
if (historyAddSpecials && !episode.WasDownloaded){
count++;
if (season.SpecialSeason == true){
if (historyAddSpecials){
var episodes = season.EpisodesList;
for (int j = episodes.Count - 1; j >= 0; j--){
if (!episodes[j].WasDownloaded){
count++;
}
}
}
continue;
}
if (!episode.WasDownloaded && !foundWatched){
count++;
} else{
foundWatched = true;
if (!historyAddSpecials){
break;
var episodesList = season.EpisodesList;
for (int j = episodesList.Count - 1; j >= 0; j--){
var episode = episodesList[j];
if (episode.SpecialEpisode){
if (historyAddSpecials && !episode.WasDownloaded){
count++;
}
continue;
}
if (!episode.WasDownloaded && !foundWatched){
count++;
} else{
foundWatched = true;
if (!historyAddSpecials){
break;
}
}
}
}
if (foundWatched && !historyAddSpecials){
break;
if (foundWatched && !historyAddSpecials){
break;
}
}
}
NewEpisodes = count;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NewEpisodes)));
}

View File

@ -31,6 +31,7 @@ public partial class AccountPageViewModel : ViewModelBase{
private static DispatcherTimer? _timer;
private DateTime _targetTime;
private bool IsCancelled = false;
private bool UnknownEndDate = false;
public AccountPageViewModel(){
UpdatetProfile();
@ -68,6 +69,9 @@ public partial class AccountPageViewModel : ViewModelBase{
}
}else if(CrunchyrollManager.Instance.Profile.Subscription?.NonrecurringSubscriptionProducts.Count >= 1){
IsCancelled = true;
}else if(CrunchyrollManager.Instance.Profile.Subscription?.FunimationSubscriptions.Count >= 1){
IsCancelled = true;
UnknownEndDate = true;
}
if (CrunchyrollManager.Instance.Profile.Subscription?.NextRenewalDate != null){
@ -92,6 +96,10 @@ public partial class AccountPageViewModel : ViewModelBase{
}
}
if (UnknownEndDate){
RemainingTime = "Unknown Subscription end date";
}
}
[RelayCommand]

View File

@ -1,4 +1,6 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
@ -14,85 +16,116 @@ using CommunityToolkit.Mvvm.Input;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll;
using CRD.Utils;
using CRD.Utils.Sonarr;
using CRD.Utils.Structs;
using CRD.Utils.Structs.History;
using CRD.Views;
using DynamicData;
using HarfBuzzSharp;
using ReactiveUI;
namespace CRD.ViewModels;
public partial class HistoryPageViewModel : ViewModelBase{
public ObservableCollection<HistorySeries> Items{ get; }
public ObservableCollection<HistorySeries> FilteredItems{ get; }
[ObservableProperty]
private static bool _fetchingData;
[ObservableProperty]
public HistorySeries _selectedSeries;
private HistorySeries _selectedSeries;
[ObservableProperty]
public static bool _editMode;
private static bool _editMode;
[ObservableProperty]
public double _scaleValue;
private double _scaleValue;
[ObservableProperty]
public ComboBoxItem _selectedView;
private ComboBoxItem? _selectedView;
public ObservableCollection<ComboBoxItem> ViewsList{ get; } =[];
[ObservableProperty]
public SortingListElement _selectedSorting;
private SortingListElement? _selectedSorting;
public ObservableCollection<SortingListElement> SortingList{ get; } =[];
[ObservableProperty]
public double _posterWidth;
private FilterListElement? _selectedFilter;
public ObservableCollection<FilterListElement> FilterList{ get; } =[];
[ObservableProperty]
public double _posterHeight;
private double _posterWidth;
[ObservableProperty]
public double _posterImageWidth;
private double _posterHeight;
[ObservableProperty]
public double _posterImageHeight;
private double _posterImageWidth;
[ObservableProperty]
public double _posterTextSize;
private double _posterImageHeight;
[ObservableProperty]
public Thickness _cornerMargin;
private HistoryViewType currentViewType = HistoryViewType.Posters;
private double _posterTextSize;
[ObservableProperty]
public bool _isPosterViewSelected = false;
private Thickness _cornerMargin;
[ObservableProperty]
public bool _isTableViewSelected = false;
private bool _isPosterViewSelected = false;
[ObservableProperty]
public static bool _viewSelectionOpen;
private bool _isTableViewSelected = false;
[ObservableProperty]
public static bool _sortingSelectionOpen;
private static bool _viewSelectionOpen;
[ObservableProperty]
private static bool _sortingSelectionOpen;
[ObservableProperty]
private static bool _addingMissingSonarrSeries;
[ObservableProperty]
private static bool _sonarrOptionsOpen;
private IStorageProvider _storageProvider;
private SortingType currentSortingType = SortingType.NextAirDate;
private HistoryViewType currentViewType;
private SortingType currentSortingType;
private FilterType currentFilterType;
[ObservableProperty]
public static bool _sortDir = false;
private static bool _sortDir = false;
[ObservableProperty]
private static bool _sonarrAvailable;
[ObservableProperty]
private static string _progressText;
public HistoryPageViewModel(){
if (CrunchyrollManager.Instance.CrunOptions.SonarrProperties != null){
SonarrAvailable = CrunchyrollManager.Instance.CrunOptions.SonarrProperties.SonarrEnabled;
} else{
SonarrAvailable = false;
}
Items = CrunchyrollManager.Instance.HistoryList;
FilteredItems = new ObservableCollection<HistorySeries>();
HistoryPageProperties? properties = CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties;
currentViewType = properties?.SelectedView ?? HistoryViewType.Posters;
currentSortingType = properties?.SelectedSorting ?? SortingType.SeriesTitle;
currentFilterType = properties?.SelectedFilter ?? FilterType.All;
ScaleValue = properties?.ScaleValue ?? 0.73;
SortDir = properties?.Ascending ?? false;
@ -112,6 +145,19 @@ public partial class HistoryPageViewModel : ViewModelBase{
}
}
foreach (FilterType filterType in Enum.GetValues(typeof(FilterType))){
if (!SonarrAvailable && (filterType == FilterType.MissingEpisodesSonarr || filterType == FilterType.ContinuingOnly)){
continue;
}
var item = new FilterListElement(){ FilterTitle = filterType.GetEnumMemberValue(), SelectedType = filterType };
FilterList.Add(item);
if (filterType == currentFilterType){
SelectedFilter = item;
}
}
IsPosterViewSelected = currentViewType == HistoryViewType.Posters;
IsTableViewSelected = currentViewType == HistoryViewType.Table;
@ -172,6 +218,10 @@ public partial class HistoryPageViewModel : ViewModelBase{
currentSortingType = newValue.SelectedSorting;
if (CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties != null) CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.SelectedSorting = currentSortingType;
CrunchyrollManager.Instance.History.SortItems();
if (SelectedFilter != null){
OnSelectedFilterChanged(SelectedFilter);
}
} else{
Console.Error.WriteLine("Invalid viewtype selected");
}
@ -180,17 +230,46 @@ public partial class HistoryPageViewModel : ViewModelBase{
UpdateSettings();
}
private bool TryParseEnum<T>(string value, out T result) where T : struct, Enum{
foreach (var field in typeof(T).GetFields()){
var attribute = field.GetCustomAttribute<EnumMemberAttribute>();
if (attribute != null && attribute.Value == value){
result = (T)field.GetValue(null);
return true;
}
}
result = default;
return false;
partial void OnSelectedFilterChanged(FilterListElement? value){
if (value == null){
return;
}
currentFilterType = value.SelectedType;
if (CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties != null) CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.SelectedFilter = currentFilterType;
switch (currentFilterType){
case FilterType.All:
FilteredItems.Clear();
FilteredItems.AddRange(Items);
break;
case FilterType.MissingEpisodes:
List<HistorySeries> filteredItems = Items.Where(item => item.NewEpisodes > 0).ToList();
FilteredItems.Clear();
FilteredItems.AddRange(filteredItems);
break;
case FilterType.MissingEpisodesSonarr:
var missingSonarrFiltered = Items.Where(historySeries =>
!string.IsNullOrEmpty(historySeries.SonarrSeriesId) && // Check series ID
historySeries.Seasons.Any(season => // Check each season
season.EpisodesList.Any(historyEpisode => // Check each episode
!string.IsNullOrEmpty(historyEpisode.SonarrEpisodeId) && !historyEpisode.SonarrHasFile))) // Filter condition
.ToList();
FilteredItems.Clear();
FilteredItems.AddRange(missingSonarrFiltered);
break;
case FilterType.ContinuingOnly:
List<HistorySeries> continuingFiltered = Items.Where(item => !string.IsNullOrEmpty(item.SonarrNextAirDate)).ToList();
FilteredItems.Clear();
FilteredItems.AddRange(continuingFiltered);
break;
}
}
@ -230,6 +309,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
if (objectToRemove != null){
CrunchyrollManager.Instance.HistoryList.Remove(objectToRemove);
Items.Remove(objectToRemove);
FilteredItems.Remove(objectToRemove);
CfgManager.UpdateHistoryFile();
}
}
@ -245,18 +325,18 @@ public partial class HistoryPageViewModel : ViewModelBase{
}
[RelayCommand]
public async void RefreshAll(){
public async Task RefreshAll(){
FetchingData = true;
RaisePropertyChanged(nameof(FetchingData));
for (int i = 0; i < Items.Count; i++){
Items[i].SetFetchingData();
foreach (var item in FilteredItems){
item.SetFetchingData();
}
for (int i = 0; i < Items.Count; i++){
for (int i = 0; i < FilteredItems.Count; i++){
FetchingData = true;
RaisePropertyChanged(nameof(FetchingData));
await Items[i].FetchData("");
Items[i].UpdateNewEpisodes();
await FilteredItems[i].FetchData("");
FilteredItems[i].UpdateNewEpisodes();
}
FetchingData = false;
@ -266,8 +346,76 @@ public partial class HistoryPageViewModel : ViewModelBase{
[RelayCommand]
public async void AddMissingToQueue(){
for (int i = 0; i < Items.Count; i++){
await Items[i].AddNewMissingToDownloads();
var tasks = FilteredItems
.Select(item => item.AddNewMissingToDownloads());
await Task.WhenAll(tasks);
}
[RelayCommand]
public async Task DownloadMissingSonarr(){
await Task.WhenAll(
FilteredItems.Where(series => !string.IsNullOrEmpty(series.SonarrSeriesId))
.SelectMany(item => item.Seasons)
.SelectMany(season => season.EpisodesList)
.Where(historyEpisode => !string.IsNullOrEmpty(historyEpisode.SonarrEpisodeId) && !historyEpisode.SonarrHasFile)
.Select(historyEpisode => historyEpisode.DownloadEpisode())
);
}
[RelayCommand]
public async Task AddMissingSonarrSeriesToHistory(){
SonarrOptionsOpen = false;
AddingMissingSonarrSeries = true;
FetchingData = true;
var crInstance = CrunchyrollManager.Instance;
if (crInstance.AllCRSeries == null){
crInstance.AllCRSeries = await crInstance.CrSeries.GetAllSeries(string.IsNullOrEmpty(crInstance.CrunOptions.HistoryLang) ? crInstance.DefaultLocale : crInstance.CrunOptions.HistoryLang);
}
if (crInstance.AllCRSeries?.Data is{ Count: > 0 }){
var concurrentSeriesIds = new ConcurrentBag<string>();
Parallel.ForEach(SonarrClient.Instance.SonarrSeries, series => {
if (crInstance.HistoryList.All(historySeries => historySeries.SonarrSeriesId != series.Id.ToString())){
var match = crInstance.History.FindClosestMatchCrSeries(crInstance.AllCRSeries.Data, series.Title);
if (match != null){
Console.WriteLine($"[Sonarr Match] Found match with {series.Title} and CR - {match.Title}");
if (!string.IsNullOrEmpty(match.Id)){
concurrentSeriesIds.Add(match.Id);
} else{
Console.Error.WriteLine($"[Sonarr Match] Series ID empty for {series.Title}");
}
} else{
Console.Error.WriteLine($"[Sonarr Match] Could not match {series.Title}");
}
} else{
Console.Error.WriteLine($"[Sonarr Match] {series.Title} already matched");
}
});
var seriesIds = concurrentSeriesIds.ToList();
var totalSeries = seriesIds.Count;
for (int count = 0; count < totalSeries; count++){
ProgressText = $"{count + 1}/{totalSeries}";
// Await the CRUpdateSeries task for each seriesId
await crInstance.History.CRUpdateSeries(seriesIds[count], "");
}
// var updateTasks = seriesIds.Select(seriesId => crInstance.History.CRUpdateSeries(seriesId, ""));
// await Task.WhenAll(updateTasks);
}
ProgressText = "";
AddingMissingSonarrSeries = false;
FetchingData = false;
if (SelectedFilter != null){
OnSelectedFilterChanged(SelectedFilter);
}
}
@ -292,7 +440,6 @@ public partial class HistoryPageViewModel : ViewModelBase{
season.SeasonDownloadPath = selectedFolder.Path.LocalPath;
CfgManager.UpdateHistoryFile();
}
}
}
@ -319,26 +466,31 @@ public partial class HistoryPageViewModel : ViewModelBase{
}
}
}
[RelayCommand]
public async Task DownloadSeasonAll(HistorySeason season){
foreach (var historyEpisode in season.EpisodesList){
await historyEpisode.DownloadEpisode();
}
var downloadTasks = season.EpisodesList
.Select(episode => episode.DownloadEpisode());
await Task.WhenAll(downloadTasks);
}
[RelayCommand]
public async Task DownloadSeasonMissing(HistorySeason season){
foreach (var historyEpisode in season.EpisodesList.Where(historyEpisode => !historyEpisode.WasDownloaded)){
await historyEpisode.DownloadEpisode();
}
var downloadTasks = season.EpisodesList
.Where(episode => !episode.WasDownloaded)
.Select(episode => episode.DownloadEpisode());
await Task.WhenAll(downloadTasks);
}
[RelayCommand]
public async Task DownloadSeasonMissingSonarr(HistorySeason season){
foreach (var historyEpisode in season.EpisodesList.Where(historyEpisode => !historyEpisode.SonarrHasFile)){
await historyEpisode.DownloadEpisode();
}
var downloadTasks = season.EpisodesList
.Where(episode => !episode.SonarrHasFile)
.Select(episode => episode.DownloadEpisode());
await Task.WhenAll(downloadTasks);
}
@ -350,6 +502,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
public class HistoryPageProperties(){
public SortingType? SelectedSorting{ get; set; }
public HistoryViewType SelectedView{ get; set; }
public FilterType SelectedFilter{ get; set; }
public double? ScaleValue{ get; set; }
public bool Ascending{ get; set; }
@ -358,4 +511,9 @@ public class HistoryPageProperties(){
public class SortingListElement(){
public SortingType SelectedSorting{ get; set; }
public string? SortingTitle{ get; set; }
}
public class FilterListElement(){
public FilterType SelectedType{ get; set; }
public string? FilterTitle{ get; set; }
}

View File

@ -82,23 +82,28 @@ public partial class SeriesPageViewModel : ViewModelBase{
[RelayCommand]
public async Task DownloadSeasonAll(HistorySeason season){
foreach (var historyEpisode in season.EpisodesList){
await historyEpisode.DownloadEpisode();
}
var downloadTasks = season.EpisodesList
.Select(episode => episode.DownloadEpisode());
await Task.WhenAll(downloadTasks);
}
[RelayCommand]
public async Task DownloadSeasonMissing(HistorySeason season){
foreach (var historyEpisode in season.EpisodesList.Where(historyEpisode => !historyEpisode.WasDownloaded)){
await historyEpisode.DownloadEpisode();
}
var downloadTasks = season.EpisodesList
.Where(episode => !episode.WasDownloaded)
.Select(episode => episode.DownloadEpisode());
await Task.WhenAll(downloadTasks);
}
[RelayCommand]
public async Task DownloadSeasonMissingSonarr(HistorySeason season){
foreach (var historyEpisode in season.EpisodesList.Where(historyEpisode => !historyEpisode.SonarrHasFile)){
await historyEpisode.DownloadEpisode();
}
var downloadTasks = season.EpisodesList
.Where(episode => !episode.SonarrHasFile)
.Select(episode => episode.DownloadEpisode());
await Task.WhenAll(downloadTasks);
}
[RelayCommand]

View File

@ -68,6 +68,9 @@ public partial class SettingsPageViewModel : ViewModelBase{
[ObservableProperty]
private bool _downloadVideoForEveryDub;
[ObservableProperty]
private bool _keepDubsSeparate;
[ObservableProperty]
private bool _skipSubMux;
@ -78,6 +81,9 @@ public partial class SettingsPageViewModel : ViewModelBase{
[ObservableProperty]
private bool _historyAddSpecials;
[ObservableProperty]
private bool _historyCountSonarr;
[ObservableProperty]
private double? _leadingNumbers;
@ -388,6 +394,7 @@ public partial class SettingsPageViewModel : ViewModelBase{
DefaultSubForcedDisplay = options.DefaultSubForcedDisplay;
DefaultSubSigns = options.DefaultSubSigns;
HistoryAddSpecials = options.HistoryAddSpecials;
HistoryCountSonarr = options.HistoryCountSonarr;
DownloadSpeed = options.DownloadSpeedLimit;
IncludeEpisodeDescription = options.IncludeVideoDescription;
FileTitle = options.VideoTitle ?? "";
@ -395,6 +402,7 @@ public partial class SettingsPageViewModel : ViewModelBase{
DownloadVideo = !options.Novids;
DownloadAudio = !options.Noaudio;
DownloadVideoForEveryDub = !options.DlVideoOnce;
KeepDubsSeparate = options.KeepDubsSeperate;
DownloadChapters = options.Chapters;
MuxToMp4 = options.Mp4;
SyncTimings = options.SyncTiming;
@ -457,10 +465,12 @@ public partial class SettingsPageViewModel : ViewModelBase{
CrunchyrollManager.Instance.CrunOptions.DefaultSubForcedDisplay = DefaultSubForcedDisplay;
CrunchyrollManager.Instance.CrunOptions.IncludeVideoDescription = IncludeEpisodeDescription;
CrunchyrollManager.Instance.CrunOptions.HistoryAddSpecials = HistoryAddSpecials;
CrunchyrollManager.Instance.CrunOptions.HistoryCountSonarr = HistoryCountSonarr;
CrunchyrollManager.Instance.CrunOptions.VideoTitle = FileTitle;
CrunchyrollManager.Instance.CrunOptions.Novids = !DownloadVideo;
CrunchyrollManager.Instance.CrunOptions.Noaudio = !DownloadAudio;
CrunchyrollManager.Instance.CrunOptions.DlVideoOnce = !DownloadVideoForEveryDub;
CrunchyrollManager.Instance.CrunOptions.KeepDubsSeperate = KeepDubsSeparate;
CrunchyrollManager.Instance.CrunOptions.Chapters = DownloadChapters;
CrunchyrollManager.Instance.CrunOptions.Mp4 = MuxToMp4;
CrunchyrollManager.Instance.CrunOptions.SyncTiming = SyncTimings;

View File

@ -7,6 +7,7 @@
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:history="clr-namespace:CRD.Utils.Structs.History"
xmlns:structs="clr-namespace:CRD.Utils.Structs"
xmlns:local="clr-namespace:CRD.Utils"
x:DataType="vm:HistoryPageViewModel"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="CRD.Views.HistoryPageView">
@ -63,6 +64,46 @@
</StackPanel>
</ToggleButton>
<Rectangle Width="1" Height="50" Fill="Gray" Margin="10,0" />
<StackPanel Margin="10,0">
<ToggleButton x:Name="DropdownButtonSonarr" Width="70" Height="70" Background="Transparent"
BorderThickness="0" CornerRadius="5"
IsVisible="{Binding SonarrAvailable}"
IsChecked="{Binding SonarrOptionsOpen}"
IsEnabled="{Binding !FetchingData}">
<StackPanel Orientation="Vertical">
<controls:ImageIcon VerticalAlignment="Center" HorizontalAlignment="Center" Margin="0 1 0 0" Source="../Assets/sonarr.png" Width="30" Height="30" />
<TextBlock Text="Sonarr" TextWrapping="Wrap" FontSize="12"></TextBlock>
</StackPanel>
</ToggleButton>
<Popup IsLightDismissEnabled="True"
IsOpen="{Binding IsChecked, ElementName=DropdownButtonSonarr, Mode=TwoWay}"
Placement="BottomEdgeAlignedRight"
PlacementTarget="{Binding ElementName=DropdownButtonSonarr}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<StackPanel>
<Button Margin="10 5"
VerticalAlignment="Center"
HorizontalAlignment="Stretch"
IsEnabled="{Binding !AddingMissingSonarrSeries}"
Content="Add Sonarr Series"
Command="{Binding AddMissingSonarrSeriesToHistory}">
</Button>
<Button Margin="10 5"
VerticalAlignment="Center"
HorizontalAlignment="Stretch"
Content="Download Missing from Sonarr"
Command="{Binding DownloadMissingSonarr}">
</Button>
</StackPanel>
</Border>
</Popup>
</StackPanel>
<TextBlock VerticalAlignment="Center" Text="{Binding ProgressText}"></TextBlock>
<!-- <Button Command="{Binding RefreshAll}" Margin="10" IsEnabled="{Binding !FetchingData}">Refresh All</Button> -->
<!-- <Button Command="{Binding AddMissingToQueue}" Margin="10 0" IsEnabled="{Binding !FetchingData}">Add To Queue</Button> -->
<!-- <ToggleButton IsChecked="{Binding EditMode}" Margin="10 0" IsEnabled="{Binding !FetchingData}">Edit</ToggleButton> -->
@ -131,18 +172,38 @@
</Popup>
</StackPanel>
<Button Width="70" Height="70" Background="Transparent" BorderThickness="0" Margin="5 0"
VerticalAlignment="Center"
IsEnabled="False">
<StackPanel Orientation="Vertical">
<controls:SymbolIcon Symbol="Filter" FontSize="32" />
<TextBlock Text="Filter" FontSize="12"></TextBlock>
</StackPanel>
</Button>
<StackPanel>
<ToggleButton x:Name="DropdownButtonFilter" Width="70" Height="70" Background="Transparent" BorderThickness="0" Margin="5 0"
VerticalAlignment="Center"
IsEnabled="{Binding !FetchingData}">
<StackPanel Orientation="Vertical">
<controls:SymbolIcon Symbol="Filter" FontSize="32" />
<TextBlock Text="Filter" FontSize="12"></TextBlock>
</StackPanel>
</ToggleButton>
<Popup IsLightDismissEnabled="True"
IsOpen="{Binding IsChecked, ElementName=DropdownButtonFilter, Mode=TwoWay}"
Placement="BottomEdgeAlignedRight"
PlacementTarget="{Binding ElementName=DropdownButtonFilter}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<ListBox SelectionMode="Single" Width="210"
MaxHeight="400"
ItemsSource="{Binding FilterList}" SelectedItem="{Binding SelectedFilter}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding FilterTitle}"></TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
</Popup>
</StackPanel>
</StackPanel>
<ListBox Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" ItemsSource="{Binding Items}"
<ListBox Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" ItemsSource="{Binding FilteredItems}"
SelectedItem="{Binding SelectedSeries}"
Margin="5" IsVisible="{Binding IsPosterViewSelected}">
@ -235,12 +296,13 @@
<Grid Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" IsVisible="{Binding IsTableViewSelected}">
<ScrollViewer>
<ItemsControl ItemsSource="{Binding Items}"
IsEnabled="{Binding !FetchingData}"
Margin="5" IsVisible="{Binding IsTableViewSelected}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<controls:ItemsRepeater ItemsSource="{Binding FilteredItems}">
<controls:ItemsRepeater.Layout>
<controls:StackLayout Orientation="Vertical" Spacing="5" />
</controls:ItemsRepeater.Layout>
<controls:ItemsRepeater.ItemTemplate>
<DataTemplate DataType="{x:Type history:HistorySeries}">
<Grid>
<controls:SettingsExpander
x:Name="SettingsExpanderSeries"
@ -328,8 +390,10 @@
IsVisible="{Binding SonarrSeriesId, Converter={StaticResource UiSonarrIdToVisibilityConverter}}"
Command="{Binding OpenSonarrPage}">
<Grid>
<controls:ImageIcon Source="../Assets/sonarr.png"
Width="30" Height="30" />
<controls:ImageIcon
Source="../Assets/sonarr.png"
Width="30"
Height="30" />
</Grid>
</Button>
@ -529,7 +593,6 @@
Width="25"
Height="25" />
</StackPanel>
@ -819,11 +882,9 @@
</Button>
</Grid>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</controls:ItemsRepeater.ItemTemplate>
</controls:ItemsRepeater>
</ScrollViewer>
</Grid>

View File

@ -149,6 +149,12 @@
<CheckBox IsChecked="{Binding HistoryAddSpecials}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="History Missing/New Count from Sonarr" Description="The missing count (number in the orange corner) will count the episodes missing from sonarr">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding HistoryCountSonarr}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
</controls:SettingsExpander>
@ -213,7 +219,11 @@
<controls:SettingsExpanderItem Content="Download Video for every dub">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding DownloadVideoForEveryDub}"> </CheckBox>
<StackPanel>
<CheckBox IsChecked="{Binding DownloadVideoForEveryDub}"> </CheckBox>
<CheckBox IsVisible="{Binding DownloadVideoForEveryDub}" Content="Keep files separate" IsChecked="{Binding KeepDubsSeparate}"> </CheckBox>
</StackPanel>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>