diff --git a/CRD/App.axaml.cs b/CRD/App.axaml.cs index 99ea3d9..82489bf 100644 --- a/CRD/App.axaml.cs +++ b/CRD/App.axaml.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; @@ -7,8 +6,6 @@ using CRD.ViewModels; using MainWindow = CRD.Views.MainWindow; using System.Linq; using CRD.Downloader; -using CRD.Downloader.Crunchyroll; -using CRD.Utils.Updater; namespace CRD; diff --git a/CRD/Downloader/CalendarManager.cs b/CRD/Downloader/CalendarManager.cs index f07ce4c..64affd5 100644 --- a/CRD/Downloader/CalendarManager.cs +++ b/CRD/Downloader/CalendarManager.cs @@ -4,11 +4,16 @@ using System.Globalization; using System.Linq; using System.Net; using System.Net.Http; +using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; using CRD.Downloader.Crunchyroll; using CRD.Utils; using CRD.Utils.Structs; +using CRD.Utils.Structs.History; +using DynamicData; using HtmlAgilityPack; +using Newtonsoft.Json; namespace CRD.Downloader; @@ -87,7 +92,7 @@ public class CalendarManager{ var date = day.SelectSingleNode(".//time[@datetime]")?.GetAttributeValue("datetime", "No date"); DateTime dayDateTime = DateTime.Parse(date, null, DateTimeStyles.RoundtripKind); - if (week.FirstDayOfWeek == null){ + if (week.FirstDayOfWeek == DateTime.MinValue){ week.FirstDayOfWeek = dayDateTime; week.FirstDayOfWeekString = dayDateTime.ToString("yyyy-MM-dd"); } @@ -147,8 +152,10 @@ public class CalendarManager{ } - public async Task BuildCustomCalendar(bool forceUpdate){ - if (!forceUpdate && calendar.TryGetValue("C" + DateTime.Now.ToString("yyyy-MM-dd"), out var forDate)){ + public async Task BuildCustomCalendar(DateTime calTargetDate, bool forceUpdate){ + await LoadAnilistUpcoming(); + + if (!forceUpdate && calendar.TryGetValue("C" + calTargetDate.ToString("yyyy-MM-dd"), out var forDate)){ return forDate; } @@ -156,14 +163,14 @@ public class CalendarManager{ CalendarWeek week = new CalendarWeek(); week.CalendarDays = new List(); - DateTime today = DateTime.Now; + DateTime targetDay = calTargetDate; for (int i = 0; i < 7; i++){ CalendarDay calDay = new CalendarDay(); calDay.CalendarEpisodes = new List(); - calDay.DateTime = today.AddDays(-i); - calDay.DayName = calDay.DateTime.Value.DayOfWeek.ToString(); + calDay.DateTime = targetDay.AddDays(-i); + calDay.DayName = calDay.DateTime.DayOfWeek.ToString(); week.CalendarDays.Add(calDay); } @@ -171,21 +178,68 @@ public class CalendarManager{ week.CalendarDays.Reverse(); var firstDayOfWeek = week.CalendarDays.First().DateTime; + week.FirstDayOfWeek = firstDayOfWeek; - var newEpisodesBase = await CrunchyrollManager.Instance.CrEpisode.GetNewEpisodes(CrunchyrollManager.Instance.CrunOptions.HistoryLang, 200, firstDayOfWeek, true); + var newEpisodesBase = await CrunchyrollManager.Instance.CrEpisode.GetNewEpisodes("", 200, firstDayOfWeek, true); if (newEpisodesBase is{ Data.Count: > 0 }){ var newEpisodes = newEpisodesBase.Data; + //EpisodeAirDate foreach (var crBrowseEpisode in newEpisodes){ - var targetDate = CrunchyrollManager.Instance.CrunOptions.CalendarFilterByAirDate ? crBrowseEpisode.EpisodeMetadata.EpisodeAirDate : crBrowseEpisode.LastPublic; + DateTime episodeAirDate = crBrowseEpisode.EpisodeMetadata.EpisodeAirDate.Kind == DateTimeKind.Utc + ? crBrowseEpisode.EpisodeMetadata.EpisodeAirDate.ToLocalTime() + : crBrowseEpisode.EpisodeMetadata.EpisodeAirDate; - if (CrunchyrollManager.Instance.CrunOptions.CalendarHideDubs && crBrowseEpisode.EpisodeMetadata.SeasonTitle != null && - (crBrowseEpisode.EpisodeMetadata.SeasonTitle.EndsWith("Dub)") || crBrowseEpisode.EpisodeMetadata.AudioLocale != Locale.JaJp)){ - continue; + DateTime premiumAvailableStart = crBrowseEpisode.EpisodeMetadata.PremiumAvailableDate.Kind == DateTimeKind.Utc + ? crBrowseEpisode.EpisodeMetadata.PremiumAvailableDate.ToLocalTime() + : crBrowseEpisode.EpisodeMetadata.PremiumAvailableDate; + + DateTime now = DateTime.Now; + DateTime oneYearFromNow = now.AddYears(1); + + DateTime targetDate; + + if (CrunchyrollManager.Instance.CrunOptions.CalendarFilterByAirDate){ + targetDate = episodeAirDate; + + if (targetDate >= oneYearFromNow){ + DateTime freeAvailableStart = crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.Kind == DateTimeKind.Utc + ? crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.ToLocalTime() + : crBrowseEpisode.EpisodeMetadata.FreeAvailableDate; + + if (freeAvailableStart <= oneYearFromNow){ + targetDate = freeAvailableStart; + } else{ + targetDate = premiumAvailableStart; + } + } + } else{ + targetDate = premiumAvailableStart; + + if (targetDate >= oneYearFromNow){ + DateTime freeAvailableStart = crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.Kind == DateTimeKind.Utc + ? crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.ToLocalTime() + : crBrowseEpisode.EpisodeMetadata.FreeAvailableDate; + + if (freeAvailableStart <= oneYearFromNow){ + targetDate = freeAvailableStart; + } else{ + targetDate = episodeAirDate; + } + } } var dubFilter = CrunchyrollManager.Instance.CrunOptions.CalendarDubFilter; + + if (CrunchyrollManager.Instance.CrunOptions.CalendarHideDubs && crBrowseEpisode.EpisodeMetadata.SeasonTitle != null && + (crBrowseEpisode.EpisodeMetadata.SeasonTitle.EndsWith("Dub)") || crBrowseEpisode.EpisodeMetadata.SeasonTitle.EndsWith("Audio)")) && + (string.IsNullOrEmpty(dubFilter) || dubFilter == "none" || (crBrowseEpisode.EpisodeMetadata.AudioLocale != null && crBrowseEpisode.EpisodeMetadata.AudioLocale.GetEnumMemberValue() != dubFilter))){ + //|| crBrowseEpisode.EpisodeMetadata.AudioLocale != Locale.JaJp + continue; + } + + if (!string.IsNullOrEmpty(dubFilter) && dubFilter != "none"){ if (crBrowseEpisode.EpisodeMetadata.AudioLocale != null && crBrowseEpisode.EpisodeMetadata.AudioLocale.GetEnumMemberValue() != dubFilter){ continue; @@ -193,7 +247,7 @@ public class CalendarManager{ } var calendarDay = (from day in week.CalendarDays - where day.DateTime.HasValue && day.DateTime.Value.Date == targetDate.Date + where day.DateTime != DateTime.MinValue && day.DateTime.Date == targetDate.Date select day).FirstOrDefault(); if (calendarDay != null){ @@ -209,13 +263,62 @@ public class CalendarManager{ calEpisode.IsPremiere = crBrowseEpisode.EpisodeMetadata.Episode == "1"; calEpisode.SeasonName = crBrowseEpisode.EpisodeMetadata.SeasonTitle; calEpisode.EpisodeNumber = crBrowseEpisode.EpisodeMetadata.Episode; + calEpisode.CrSeriesID = crBrowseEpisode.EpisodeMetadata.SeriesId; - calendarDay.CalendarEpisodes?.Add(calEpisode); + var existingEpisode = calendarDay.CalendarEpisodes + ?.FirstOrDefault(e => e.SeasonName == calEpisode.SeasonName); + + if (existingEpisode != null){ + if (!int.TryParse(existingEpisode.EpisodeNumber, out var num)){ + existingEpisode.EpisodeNumber = "..."; + } else{ + var existingNumbers = existingEpisode.EpisodeNumber + .Split('-') + .Select(n => int.TryParse(n, out var num) ? num : 0) + .Where(n => n > 0) + .ToList(); + + if (int.TryParse(calEpisode.EpisodeNumber, out var newEpisodeNumber)){ + existingNumbers.Add(newEpisodeNumber); + } + + existingNumbers.Sort(); + var lowest = existingNumbers.First(); + var highest = existingNumbers.Last(); + + // Update the existing episode's number to the new range + existingEpisode.EpisodeNumber = lowest == highest + ? lowest.ToString() + : $"{lowest}-{highest}"; + + if (lowest == 1){ + existingEpisode.IsPremiere = true; + } + } + + existingEpisode.CalendarEpisodes.Add(calEpisode); + } else{ + calendarDay.CalendarEpisodes?.Add(calEpisode); + } + } + } + + + foreach (var calendarDay in week.CalendarDays){ + if (calendarDay.DateTime.Date >= DateTime.Now.Date){ + if (ProgramManager.Instance.AnilistUpcoming.ContainsKey(calendarDay.DateTime.ToString("yyyy-MM-dd"))){ + var list = ProgramManager.Instance.AnilistUpcoming[calendarDay.DateTime.ToString("yyyy-MM-dd")]; + + foreach (var calendarEpisode in list.Where(calendarEpisode => calendarDay.DateTime.Date == calendarEpisode.DateTime.Date) + .Where(calendarEpisode => calendarDay.CalendarEpisodes.All(ele => ele.CrSeriesID != calendarEpisode.CrSeriesID))){ + calendarDay.CalendarEpisodes.Add(calendarEpisode); + } + } } } foreach (var weekCalendarDay in week.CalendarDays){ - if (weekCalendarDay.CalendarEpisodes != null) + if (weekCalendarDay.CalendarEpisodes.Count > 0) weekCalendarDay.CalendarEpisodes = weekCalendarDay.CalendarEpisodes .OrderBy(e => e.DateTime) .ThenBy(e => e.SeasonName) @@ -232,9 +335,232 @@ public class CalendarManager{ if (day.CalendarEpisodes != null) day.CalendarEpisodes = day.CalendarEpisodes.OrderBy(e => e.DateTime).ToList(); } - calendar["C" + DateTime.Now.ToString("yyyy-MM-dd")] = week; + calendar["C" + calTargetDate.ToString("yyyy-MM-dd")] = week; return week; } + + + private async Task LoadAnilistUpcoming(){ + DateTime today = DateTime.Today; + + string formattedDate = today.ToString("yyyy-MM-dd"); + + if (ProgramManager.Instance.AnilistUpcoming.ContainsKey(formattedDate)){ + return; + } + + DateTimeOffset todayMidnight = DateTimeOffset.Now.Date; + + long todayMidnightUnix = todayMidnight.ToUnixTimeSeconds(); + long sevenDaysLaterUnix = todayMidnight.AddDays(8).ToUnixTimeSeconds(); + + AniListResponseCalendar? aniListResponse = null; + + int currentPage = 1; // Start from page 1 + bool hasNextPage; + + do{ + var variables = new{ + weekStart = todayMidnightUnix, + weekEnd = sevenDaysLaterUnix, + page = currentPage + }; + + var payload = new{ + query, + variables + }; + + string jsonPayload = JsonConvert.SerializeObject(payload, Formatting.Indented); + + var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Anilist){ + Content = new StringContent(jsonPayload, Encoding.UTF8, "application/json") + }; + + var response = await HttpClientReq.Instance.SendHttpRequest(request); + + if (!response.IsOk){ + Console.Error.WriteLine("Anilist Request Failed for upcoming calendar episodes"); + return; + } + + AniListResponseCalendar currentResponse = Helpers.Deserialize( + response.ResponseContent, CrunchyrollManager.Instance.SettingsJsonSerializerSettings + ) ?? new AniListResponseCalendar(); + + + aniListResponse ??= currentResponse; + + if (aniListResponse != currentResponse){ + aniListResponse.Data?.Page?.AiringSchedules?.AddRange(currentResponse.Data?.Page?.AiringSchedules ??[]); + } + + hasNextPage = currentResponse.Data?.Page?.PageInfo?.HasNextPage ?? false; + + currentPage++; + } while (hasNextPage && currentPage < 20); + + + var list = aniListResponse.Data?.Page?.AiringSchedules ??[]; + + list = list.Where(ele => ele.Media?.ExternalLinks != null && ele.Media.ExternalLinks.Any(external => + string.Equals(external.Site, "Crunchyroll", StringComparison.OrdinalIgnoreCase))).ToList(); + + List calendarEpisodes =[]; + + foreach (var anilistEle in list){ + var calEp = new CalendarEpisode(); + + calEp.DateTime = DateTimeOffset.FromUnixTimeSeconds(anilistEle.AiringAt).UtcDateTime.ToLocalTime(); + calEp.HasPassed = false; + calEp.EpisodeName = anilistEle.Media?.Title.English; + calEp.SeriesUrl = $"https://www.crunchyroll.com/{CrunchyrollManager.Instance.CrunOptions.HistoryLang}/series/"; + calEp.EpisodeUrl = $"https://www.crunchyroll.com/{CrunchyrollManager.Instance.CrunOptions.HistoryLang}/watch/"; + calEp.ThumbnailUrl = anilistEle.Media?.CoverImage.ExtraLarge ?? ""; //https://www.crunchyroll.com/i/coming_soon_beta_thumb.jpg + calEp.IsPremiumOnly = true; + calEp.IsPremiere = anilistEle.Episode == 1; + calEp.SeasonName = anilistEle.Media?.Title.English; + calEp.EpisodeNumber = anilistEle.Episode.ToString(); + calEp.AnilistEpisode = true; + + var crunchyrollID = ""; + + if (anilistEle.Media?.ExternalLinks != null){ + var url = anilistEle.Media.ExternalLinks.First(external => + string.Equals(external.Site, "Crunchyroll", StringComparison.OrdinalIgnoreCase)).Url; + + string pattern = @"series\/([^\/]+)"; + + Match match = Regex.Match(url, pattern); + if (match.Success){ + crunchyrollID = match.Groups[1].Value; + + calEp.CrSeriesID = crunchyrollID; + + if (CrunchyrollManager.Instance.CrunOptions.History){ + var historySeries = CrunchyrollManager.Instance.HistoryList.FirstOrDefault(item => item.SeriesId == crunchyrollID); + + if (historySeries != null){ + var oldestRelease = DateTime.MinValue; + foreach (var historySeriesSeason in historySeries.Seasons){ + if (historySeriesSeason.EpisodesList.Any()){ + var releaseDate = historySeriesSeason.EpisodesList.Last().EpisodeCrPremiumAirDate; + + if (releaseDate.HasValue && oldestRelease < releaseDate.Value){ + oldestRelease = releaseDate.Value; + } + } + } + + if (oldestRelease != DateTime.MinValue){ + calEp.DateTime = new DateTime( + calEp.DateTime.Year, + calEp.DateTime.Month, + calEp.DateTime.Day, + oldestRelease.Hour, + oldestRelease.Minute, + oldestRelease.Second, + calEp.DateTime.Kind + ); + } + } + } + } else{ + crunchyrollID = ""; + } + } + + calendarEpisodes.Add(calEp); + } + + foreach (var calendarEpisode in calendarEpisodes){ + var airDate = calendarEpisode.DateTime.ToString("yyyy-MM-dd"); + + if (!ProgramManager.Instance.AnilistUpcoming.TryGetValue(airDate, out var value)){ + value = new List(); + ProgramManager.Instance.AnilistUpcoming[airDate] = value; + } + + value.Add(calendarEpisode); + } + } + + #region Query + + private string query = @"query ($weekStart: Int, $weekEnd: Int, $page: Int) { + Page(page: $page) { + pageInfo { + hasNextPage + total + } + airingSchedules( + airingAt_greater: $weekStart + airingAt_lesser: $weekEnd + ) { + id + episode + airingAt + media { + id + idMal + title { + romaji + native + english + } + startDate { + year + month + day + } + endDate { + year + month + day + } + status + season + format + synonyms + episodes + description + bannerImage + isAdult + coverImage { + extraLarge + color + } + trailer { + id + site + thumbnail + } + externalLinks { + site + icon + color + url + } + relations { + edges { + relationType(version: 2) + node { + id + title { + romaji + native + english + } + siteUrl + } + } + } + } + } + } +}"; + + #endregion } \ No newline at end of file diff --git a/CRD/Downloader/Crunchyroll/CRAuth.cs b/CRD/Downloader/Crunchyroll/CRAuth.cs index 64c3c72..1b7db37 100644 --- a/CRD/Downloader/Crunchyroll/CRAuth.cs +++ b/CRD/Downloader/Crunchyroll/CRAuth.cs @@ -1,8 +1,7 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; -using System.Net.Http.Headers; using System.Threading.Tasks; using CRD.Utils; using CRD.Utils.Structs; @@ -17,40 +16,51 @@ public class CrAuth{ private readonly CrunchyrollManager crunInstance = CrunchyrollManager.Instance; public async Task AuthAnonymous(){ + string uuid = Guid.NewGuid().ToString(); + var formData = new Dictionary{ { "grant_type", "client_id" }, - { "scope", "offline_access" } + { "scope", "offline_access" }, + { "device_id", uuid }, + { "device_type", "Chrome on Windows" } }; - var requestContent = new FormUrlEncodedContent(formData); - requestContent.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); - var request = new HttpRequestMessage(HttpMethod.Post, Api.BetaAuth){ + var requestContent = new FormUrlEncodedContent(formData); + + var crunchyAuthHeaders = new Dictionary{ + { "Authorization", ApiUrls.authBasicSwitch }, + { "User-Agent", ApiUrls.ChromeUserAgent } + }; + + var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.BetaAuth){ Content = requestContent }; - request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Api.authBasicSwitch); + foreach (var header in crunchyAuthHeaders){ + request.Headers.Add(header.Key, header.Value); + } var response = await HttpClientReq.Instance.SendHttpRequest(request); if (response.IsOk){ - JsonTokenToFileAndVariable(response.ResponseContent); + JsonTokenToFileAndVariable(response.ResponseContent, uuid); } else{ Console.Error.WriteLine("Anonymous login failed"); } crunInstance.Profile = new CrProfile{ Username = "???", - Avatar = "003-cr-hime-excited.png", + Avatar = "crbrand_avatars_logo_marks_mangagirl_taupe.png", PreferredContentAudioLanguage = "ja-JP", PreferredContentSubtitleLanguage = "de-DE" }; } - private void JsonTokenToFileAndVariable(string content){ + private void JsonTokenToFileAndVariable(string content, string deviceId){ crunInstance.Token = Helpers.Deserialize(content, crunInstance.SettingsJsonSerializerSettings); - - if (crunInstance.Token != null && crunInstance.Token.expires_in != null){ + if (crunInstance.Token is{ expires_in: not null }){ + crunInstance.Token.device_id = deviceId; crunInstance.Token.expires = DateTime.Now.AddSeconds((double)crunInstance.Token.expires_in); CfgManager.WriteTokenToYamlFile(crunInstance.Token, CfgManager.PathCrToken); @@ -58,25 +68,36 @@ public class CrAuth{ } public async Task Auth(AuthData data){ - var formData = new Dictionary{ - { "username", data.Username }, - { "password", data.Password }, - { "grant_type", "password" }, - { "scope", "offline_access" } - }; - var requestContent = new FormUrlEncodedContent(formData); - requestContent.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); + string uuid = Guid.NewGuid().ToString(); - var request = new HttpRequestMessage(HttpMethod.Post, Api.BetaAuth){ + var formData = new Dictionary{ + { "username", data.Username }, // Replace with actual data + { "password", data.Password }, // Replace with actual data + { "grant_type", "password" }, + { "scope", "offline_access" }, + { "device_id", uuid }, + { "device_type", "Chrome on Windows" } + }; + + var requestContent = new FormUrlEncodedContent(formData); + + var crunchyAuthHeaders = new Dictionary{ + { "Authorization", ApiUrls.authBasicSwitch }, + { "User-Agent", ApiUrls.ChromeUserAgent } + }; + + var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.BetaAuth){ Content = requestContent }; - request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Api.authBasicSwitch); + foreach (var header in crunchyAuthHeaders){ + request.Headers.Add(header.Key, header.Value); + } var response = await HttpClientReq.Instance.SendHttpRequest(request); if (response.IsOk){ - JsonTokenToFileAndVariable(response.ResponseContent); + JsonTokenToFileAndVariable(response.ResponseContent, uuid); } else{ if (response.ResponseContent.Contains("invalid_credentials")){ MessageBus.Current.SendMessage(new ToastMessage($"Failed to login - because of invalid login credentials", ToastType.Error, 10)); @@ -99,7 +120,7 @@ public class CrAuth{ return; } - var request = HttpClientReq.CreateRequestMessage(Api.BetaProfile, HttpMethod.Get, true, true, null); + var request = HttpClientReq.CreateRequestMessage(ApiUrls.BetaProfile, HttpMethod.Get, true, true, null); var response = await HttpClientReq.Instance.SendHttpRequest(request); @@ -109,7 +130,7 @@ 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 requestSubs = HttpClientReq.CreateRequestMessage(ApiUrls.Subscription + crunInstance.Token.account_id, HttpMethod.Get, true, false, null); var responseSubs = await HttpClientReq.Instance.SendHttpRequest(requestSubs); @@ -152,35 +173,50 @@ public class CrAuth{ public async Task LoginWithToken(){ if (crunInstance.Token?.refresh_token == null){ Console.Error.WriteLine("Missing Refresh Token"); + await AuthAnonymous(); return; } + string uuid = Guid.NewGuid().ToString(); + var formData = new Dictionary{ { "refresh_token", crunInstance.Token.refresh_token }, - { "grant_type", "refresh_token" }, - { "scope", "offline_access" } + { "scope", "offline_access" }, + { "device_id", uuid }, + { "device_type", "Chrome on Windows" }, + { "grant_type", "refresh_token" } }; - var requestContent = new FormUrlEncodedContent(formData); - requestContent.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); - var request = new HttpRequestMessage(HttpMethod.Post, Api.BetaAuth){ + var requestContent = new FormUrlEncodedContent(formData); + + var crunchyAuthHeaders = new Dictionary{ + { "Authorization", ApiUrls.authBasicSwitch }, + { "User-Agent", ApiUrls.ChromeUserAgent } + }; + + var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.BetaAuth){ Content = requestContent }; - request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Api.authBasicSwitch); + foreach (var header in crunchyAuthHeaders){ + request.Headers.Add(header.Key, header.Value); + } + + HttpClientReq.Instance.SetETPCookie(crunInstance.Token.refresh_token); var response = await HttpClientReq.Instance.SendHttpRequest(request); if (response.IsOk){ - JsonTokenToFileAndVariable(response.ResponseContent); + JsonTokenToFileAndVariable(response.ResponseContent, uuid); + + if (crunInstance.Token?.refresh_token != null){ + HttpClientReq.Instance.SetETPCookie(crunInstance.Token.refresh_token); + + await GetProfile(); + } } else{ Console.Error.WriteLine("Token Auth Failed"); - } - - if (crunInstance.Token?.refresh_token != null){ - HttpClientReq.Instance.SetETPCookie(crunInstance.Token.refresh_token); - - await GetProfile(); + await AuthAnonymous(); } } @@ -198,24 +234,37 @@ public class CrAuth{ return; } - var formData = new Dictionary{ - { "refresh_token", crunInstance.Token?.refresh_token ?? string.Empty }, - { "grant_type", "refresh_token" }, - { "scope", "offline_access" } - }; - var requestContent = new FormUrlEncodedContent(formData); - requestContent.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); + string uuid = Guid.NewGuid().ToString(); - var request = new HttpRequestMessage(HttpMethod.Post, Api.BetaAuth){ + var formData = new Dictionary{ + { "refresh_token", crunInstance.Token.refresh_token }, + { "grant_type", "refresh_token" }, + { "scope", "offline_access" }, + { "device_id", uuid }, + { "device_type", "Chrome on Windows" } + }; + + var requestContent = new FormUrlEncodedContent(formData); + + var crunchyAuthHeaders = new Dictionary{ + { "Authorization", ApiUrls.authBasicSwitch }, + { "User-Agent", ApiUrls.ChromeUserAgent } + }; + + var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.BetaAuth){ Content = requestContent }; - request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Api.authBasicSwitch); + foreach (var header in crunchyAuthHeaders){ + request.Headers.Add(header.Key, header.Value); + } + + HttpClientReq.Instance.SetETPCookie(crunInstance.Token.refresh_token); var response = await HttpClientReq.Instance.SendHttpRequest(request); if (response.IsOk){ - JsonTokenToFileAndVariable(response.ResponseContent); + JsonTokenToFileAndVariable(response.ResponseContent, uuid); } else{ Console.Error.WriteLine("Refresh Token Auth Failed"); } diff --git a/CRD/Downloader/Crunchyroll/CrEpisode.cs b/CRD/Downloader/Crunchyroll/CrEpisode.cs index d7775bc..fb067ad 100644 --- a/CRD/Downloader/Crunchyroll/CrEpisode.cs +++ b/CRD/Downloader/Crunchyroll/CrEpisode.cs @@ -26,7 +26,7 @@ public class CrEpisode(){ } - var request = HttpClientReq.CreateRequestMessage($"{Api.Cms}/episodes/{id}", HttpMethod.Get, true, true, query); + var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/episodes/{id}", HttpMethod.Get, true, true, query); var response = await HttpClientReq.Instance.SendHttpRequest(request); @@ -181,6 +181,7 @@ public class CrEpisode(){ }; epMeta.AvailableSubs = item.SubtitleLocales; epMeta.Description = item.Description; + epMeta.Hslang = CrunchyrollManager.Instance.CrunOptions.Hslang; if (episodeP.EpisodeAndLanguages.Langs.Count > 0){ epMeta.SelectedDubs = dubLang @@ -236,7 +237,7 @@ public class CrEpisode(){ query["sort_by"] = "newly_added"; query["type"] = "episode"; - var request = HttpClientReq.CreateRequestMessage($"{Api.Browse}", HttpMethod.Get, true, false, query); + var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Browse}", HttpMethod.Get, true, false, query); var response = await HttpClientReq.Instance.SendHttpRequest(request); diff --git a/CRD/Downloader/Crunchyroll/CrMovies.cs b/CRD/Downloader/Crunchyroll/CrMovies.cs index 2aba63e..3789d5e 100644 --- a/CRD/Downloader/Crunchyroll/CrMovies.cs +++ b/CRD/Downloader/Crunchyroll/CrMovies.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using System.Net.Http; -using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Web; using CRD.Utils; @@ -26,7 +25,7 @@ public class CrMovies{ } - var request = HttpClientReq.CreateRequestMessage($"{Api.Cms}/movies/{id}", HttpMethod.Get, true, true, query); + var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/movies/{id}", HttpMethod.Get, true, true, query); var response = await HttpClientReq.Instance.SendHttpRequest(request); @@ -81,6 +80,7 @@ public class CrMovies{ }; epMeta.AvailableSubs = new List(); epMeta.Description = episodeP.Description; + epMeta.Hslang = CrunchyrollManager.Instance.CrunOptions.Hslang; return epMeta; } diff --git a/CRD/Downloader/Crunchyroll/CrMusic.cs b/CRD/Downloader/Crunchyroll/CrMusic.cs index 4261df2..0076fc8 100644 --- a/CRD/Downloader/Crunchyroll/CrMusic.cs +++ b/CRD/Downloader/Crunchyroll/CrMusic.cs @@ -23,8 +23,8 @@ public class CrMusic{ } public async Task ParseArtistMusicVideosByIdAsync(string id, string crLocale, bool forcedLang = false){ - var musicVideosTask = FetchMediaListAsync($"{Api.Content}/music/artists/{id}/music_videos", crLocale, forcedLang); - var concertsTask = FetchMediaListAsync($"{Api.Content}/music/artists/{id}/concerts", crLocale, forcedLang); + var musicVideosTask = FetchMediaListAsync($"{ApiUrls.Content}/music/artists/{id}/music_videos", crLocale, forcedLang); + var concertsTask = FetchMediaListAsync($"{ApiUrls.Content}/music/artists/{id}/concerts", crLocale, forcedLang); await Task.WhenAll(musicVideosTask, concertsTask); @@ -42,7 +42,7 @@ public class CrMusic{ } private async Task ParseMediaByIdAsync(string id, string crLocale, bool forcedLang, string endpoint){ - var mediaList = await FetchMediaListAsync($"{Api.Content}/{endpoint}/{id}", crLocale, forcedLang); + var mediaList = await FetchMediaListAsync($"{ApiUrls.Content}/{endpoint}/{id}", crLocale, forcedLang); switch (mediaList.Total){ case < 1: @@ -110,6 +110,7 @@ public class CrMusic{ epMeta.AvailableSubs = new List(); epMeta.Description = episodeP.Description; epMeta.Music = true; + epMeta.Hslang = CrunchyrollManager.Instance.CrunOptions.Hslang; return epMeta; } diff --git a/CRD/Downloader/Crunchyroll/CrSeries.cs b/CRD/Downloader/Crunchyroll/CrSeries.cs index 448fe2c..da9edbf 100644 --- a/CRD/Downloader/Crunchyroll/CrSeries.cs +++ b/CRD/Downloader/Crunchyroll/CrSeries.cs @@ -77,6 +77,7 @@ public class CrSeries(){ Time = 0, DownloadSpeed = 0 }; + epMeta.Hslang = CrunchyrollManager.Instance.CrunOptions.Hslang; epMeta.Description = item.Description; epMeta.AvailableSubs = item.SubtitleLocales; if (episode.Langs.Count > 0){ @@ -308,7 +309,7 @@ public class CrSeries(){ } } - var showRequest = HttpClientReq.CreateRequestMessage($"{Api.Cms}/seasons/{seasonID}", HttpMethod.Get, true, true, query); + var showRequest = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/seasons/{seasonID}", HttpMethod.Get, true, true, query); var response = await HttpClientReq.Instance.SendHttpRequest(showRequest); @@ -329,7 +330,7 @@ public class CrSeries(){ } } - var episodeRequest = HttpClientReq.CreateRequestMessage($"{Api.Cms}/seasons/{seasonID}/episodes", HttpMethod.Get, true, true, query); + var episodeRequest = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/seasons/{seasonID}/episodes", HttpMethod.Get, true, true, query); var episodeRequestResponse = await HttpClientReq.Instance.SendHttpRequest(episodeRequest); @@ -391,7 +392,7 @@ public class CrSeries(){ } - var request = HttpClientReq.CreateRequestMessage($"{Api.Cms}/series/{id}/seasons", HttpMethod.Get, true, true, query); + var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/series/{id}/seasons", HttpMethod.Get, true, true, query); var response = await HttpClientReq.Instance.SendHttpRequest(request); @@ -422,7 +423,7 @@ public class CrSeries(){ } } - var request = HttpClientReq.CreateRequestMessage($"{Api.Cms}/series/{id}", HttpMethod.Get, true, true, query); + var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/series/{id}", HttpMethod.Get, true, true, query); var response = await HttpClientReq.Instance.SendHttpRequest(request); @@ -457,7 +458,7 @@ public class CrSeries(){ query["n"] = "6"; query["type"] = "top_results"; - var request = HttpClientReq.CreateRequestMessage($"{Api.Search}", HttpMethod.Get, true, false, query); + var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Search}", HttpMethod.Get, true, false, query); var response = await HttpClientReq.Instance.SendHttpRequest(request); @@ -468,6 +469,20 @@ public class CrSeries(){ CrSearchSeriesBase? series = Helpers.Deserialize(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings); + if (crunInstance.CrunOptions.History){ + var historyIDs = new HashSet(crunInstance.HistoryList.Select(item => item.SeriesId ?? "")); + + if (series?.Data != null){ + foreach (var crSearchSeries in series.Data){ + if (crSearchSeries.Items != null){ + foreach (var crBrowseSeries in crSearchSeries.Items.Where(crBrowseSeries => historyIDs.Contains(crBrowseSeries.Id ?? "unknownID"))){ + crBrowseSeries.IsInHistory = true; + } + } + } + } + } + return series; } @@ -488,7 +503,7 @@ public class CrSeries(){ query["n"] = "50"; query["sort_by"] = "alphabetical"; - var request = HttpClientReq.CreateRequestMessage($"{Api.Browse}", HttpMethod.Get, true, false, query); + var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Browse}", HttpMethod.Get, true, false, query); var response = await HttpClientReq.Instance.SendHttpRequest(request); diff --git a/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs b/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs index 4a85f52..1f70c7e 100644 --- a/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs +++ b/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs @@ -5,12 +5,12 @@ using System.Globalization; using System.IO; using System.Linq; using System.Net.Http; +using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Xml; -using Avalonia.Media; using CRD.Utils; using CRD.Utils.DRM; using CRD.Utils.Ffmpeg_Encoding; @@ -21,7 +21,10 @@ using CRD.Utils.Sonarr; using CRD.Utils.Structs; using CRD.Utils.Structs.Crunchyroll; using CRD.Utils.Structs.History; +using CRD.ViewModels.Utils; using CRD.Views; +using CRD.Views.Utils; +using FluentAvalonia.UI.Controls; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using LanguageItem = CRD.Utils.Structs.LanguageItem; @@ -116,11 +119,15 @@ public class CrunchyrollManager{ options.Theme = "System"; options.SelectedCalendarLanguage = "en-us"; options.CalendarDubFilter = "none"; + options.CustomCalendar = true; options.DlVideoOnce = true; options.StreamEndpoint = "web/firefox"; options.SubsAddScaledBorder = ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes; options.HistoryLang = DefaultLocale; + options.BackgroundImageOpacity = 0.5; + options.BackgroundImageBlurRadius = 10; + options.History = true; CfgManager.UpdateSettingsFromFile(options); @@ -141,7 +148,7 @@ public class CrunchyrollManager{ Profile = new CrProfile{ Username = "???", - Avatar = "003-cr-hime-excited.png", + Avatar = "crbrand_avatars_logo_marks_mangagirl_taupe.png", PreferredContentAudioLanguage = "ja-JP", PreferredContentSubtitleLanguage = "de-DE", HasPremium = false, @@ -235,6 +242,8 @@ public class CrunchyrollManager{ } if (options.SkipMuxing == false){ + bool syncError = false; + data.DownloadProgress = new DownloadProgress(){ IsDownloading = true, Percent = 100, @@ -275,6 +284,10 @@ public class CrunchyrollManager{ if (result is{ merger: not null, isMuxed: true }){ mergers.Add(result.merger); } + + if (result.syncError){ + syncError = true; + } } foreach (var merger in mergers){ @@ -291,7 +304,7 @@ public class CrunchyrollManager{ QueueManager.Instance.Queue.Refresh(); - await Helpers.RunFFmpegWithPresetAsync(merger?.options.Output, FfmpegEncoding.GetPreset(CrunOptions.EncodingPresetName)); + await Helpers.RunFFmpegWithPresetAsync(merger?.options.Output, FfmpegEncoding.GetPreset(CrunOptions.EncodingPresetName), data); } if (CrunOptions.DownloadToTempFolder){ @@ -319,6 +332,8 @@ public class CrunchyrollManager{ }, fileNameAndPath); + syncError = result.syncError; + if (result is{ merger: not null, isMuxed: true }){ result.merger.CleanUp(); } @@ -334,7 +349,7 @@ public class CrunchyrollManager{ QueueManager.Instance.Queue.Refresh(); - await Helpers.RunFFmpegWithPresetAsync(result.merger?.options.Output, FfmpegEncoding.GetPreset(CrunOptions.EncodingPresetName)); + await Helpers.RunFFmpegWithPresetAsync(result.merger?.options.Output, FfmpegEncoding.GetPreset(CrunOptions.EncodingPresetName), data); } if (CrunOptions.DownloadToTempFolder){ @@ -349,10 +364,10 @@ public class CrunchyrollManager{ Percent = 100, Time = 0, DownloadSpeed = 0, - Doing = "Done" + Doing = "Done" + (syncError ? " - Couldn't sync dubs" : "") }; - if (CrunOptions.RemoveFinishedDownload){ + if (CrunOptions.RemoveFinishedDownload && !syncError){ QueueManager.Instance.Queue.Remove(data); } } else{ @@ -402,7 +417,6 @@ public class CrunchyrollManager{ data.DownloadProgress = new DownloadProgress{ IsDownloading = true, - Done = true, Percent = 100, Time = 0, DownloadSpeed = 0, @@ -471,7 +485,7 @@ public class CrunchyrollManager{ #endregion - private async Task<(Merger? merger, bool isMuxed)> MuxStreams(List data, CrunchyMuxOptions options, string filename){ + private async Task<(Merger? merger, bool isMuxed, bool syncError)> MuxStreams(List data, CrunchyMuxOptions options, string filename){ var muxToMp3 = false; if (options.Novids == true || data.FindAll(a => a.Type == DownloadMediaType.Video).Count == 0){ @@ -480,7 +494,7 @@ public class CrunchyrollManager{ muxToMp3 = true; } else{ Console.WriteLine("Skip muxing since no videos are downloaded"); - return (null, false); + return (null, false, false); } } @@ -523,9 +537,9 @@ public class CrunchyrollManager{ OnlyAudio = data.Where(a => a.Type == DownloadMediaType.Audio).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(), Output = $"{filename}.{(muxToMp3 ? "mp3" : options.Mp4 ? "mp4" : "mkv")}", Subtitles = data.Where(a => a.Type == DownloadMediaType.Subtitle).Select(a => new SubtitleInput - { File = a.Path ?? string.Empty, Language = a.Language, ClosedCaption = a.Cc, Signs = a.Signs, RelatedVideoDownloadMedia = a.RelatedVideoDownloadMedia }).ToList(), + { File = a.Path ?? string.Empty, Language = a.Language, ClosedCaption = a.Cc ?? false, Signs = a.Signs ?? false, RelatedVideoDownloadMedia = a.RelatedVideoDownloadMedia }).ToList(), KeepAllVideos = options.KeepAllVideos, - Fonts = FontsManager.Instance.MakeFontsList(CfgManager.PathFONTS_DIR, subsList), // Assuming MakeFontsList is properly defined + Fonts = FontsManager.Instance.MakeFontsList(CfgManager.PathFONTS_DIR, subsList), Chapters = data.Where(a => a.Type == DownloadMediaType.Chapters).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(), VideoTitle = options.VideoTitle, Options = new MuxOptions(){ @@ -549,7 +563,7 @@ public class CrunchyrollManager{ Console.Error.WriteLine("MKVmerge not found"); } - bool isMuxed; + bool isMuxed, syncError = false; if (options.SyncTiming && CrunOptions.DlVideoOnce){ var basePath = merger.options.OnlyVid.First().Path; @@ -559,6 +573,12 @@ public class CrunchyrollManager{ foreach (var syncVideo in syncVideosList){ if (!string.IsNullOrEmpty(syncVideo.Path)){ var delay = await merger.ProcessVideo(basePath, syncVideo.Path); + + if (delay <= -100){ + syncError = true; + continue; + } + var audio = merger.options.OnlyAudio.FirstOrDefault(audio => audio.Language.CrLocale == syncVideo.Lang.CrLocale); if (audio != null){ audio.Delay = (int)(delay * 1000); @@ -585,21 +605,10 @@ public class CrunchyrollManager{ isMuxed = true; } - return (merger, isMuxed); + return (merger, isMuxed, syncError); } private async Task DownloadMediaList(CrunchyEpMeta data, CrDownloadOptions options){ - // if (CmsToken?.Cms == null){ - // Console.WriteLine("Missing CMS Token"); - // MainWindow.Instance.ShowError("Missing CMS Token - are you signed in?"); - // return new DownloadResponse{ - // Data = new List(), - // Error = true, - // FileName = "./unknown", - // ErrorText = "Login problem" - // }; - // } - if (Profile.Username == "???"){ MainWindow.Instance.ShowError("User Account not recognized - are you signed in?"); return new DownloadResponse{ @@ -610,31 +619,55 @@ public class CrunchyrollManager{ }; } - if (!File.Exists(CfgManager.PathFFMPEG)){ - Console.Error.WriteLine("Missing ffmpeg"); - MainWindow.Instance.ShowError("FFmpeg not found"); - return new DownloadResponse{ - Data = new List(), - Error = true, - FileName = "./unknown", - ErrorText = "Missing ffmpeg" - }; - } + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)){ + if (!File.Exists(CfgManager.PathFFMPEG)){ + Console.Error.WriteLine("Missing ffmpeg"); + MainWindow.Instance.ShowError($"FFmpeg not found at: {CfgManager.PathFFMPEG}"); + return new DownloadResponse{ + Data = new List(), + Error = true, + FileName = "./unknown", + ErrorText = "Missing ffmpeg" + }; + } - if (!File.Exists(CfgManager.PathMKVMERGE)){ - Console.Error.WriteLine("Missing Mkvmerge"); - MainWindow.Instance.ShowError("Mkvmerge not found"); - return new DownloadResponse{ - Data = new List(), - Error = true, - FileName = "./unknown", - ErrorText = "Missing Mkvmerge" - }; + if (!File.Exists(CfgManager.PathMKVMERGE)){ + Console.Error.WriteLine("Missing Mkvmerge"); + MainWindow.Instance.ShowError($"Mkvmerge not found at: {CfgManager.PathMKVMERGE}"); + return new DownloadResponse{ + Data = new List(), + Error = true, + FileName = "./unknown", + ErrorText = "Missing Mkvmerge" + }; + } + } else{ + if (!Helpers.IsInstalled("ffmpeg", "-version") && !File.Exists(Path.Combine(AppContext.BaseDirectory, "lib", "ffmpeg"))){ + Console.Error.WriteLine("Ffmpeg is not installed or not in the system PATH."); + MainWindow.Instance.ShowError("Ffmpeg is not installed on the system or not found in the PATH."); + return new DownloadResponse{ + Data = new List(), + Error = true, + FileName = "./unknown", + ErrorText = "Ffmpeg is not installed" + }; + } + + if (!Helpers.IsInstalled("mkvmerge", "--version") && !File.Exists(Path.Combine(AppContext.BaseDirectory, "lib", "mkvmerge"))){ + Console.Error.WriteLine("Mkvmerge is not installed or not in the system PATH."); + MainWindow.Instance.ShowError("Mkvmerge is not installed on the system or not found in the PATH."); + return new DownloadResponse{ + Data = new List(), + Error = true, + FileName = "./unknown", + ErrorText = "Mkvmerge is not installed" + }; + } } if (!_widevine.canDecrypt){ Console.Error.WriteLine("L3 key files missing"); - MainWindow.Instance.ShowError("Can't find CDM files in widevine folder "); + MainWindow.Instance.ShowError("Can't find CDM files in the Widevine folder.\nFor more information, please check the FAQ section in the Wiki on the GitHub page."); return new DownloadResponse{ Data = new List(), Error = true, @@ -645,7 +678,7 @@ public class CrunchyrollManager{ if (!File.Exists(CfgManager.PathMP4Decrypt)){ Console.Error.WriteLine("mp4decrypt not found"); - MainWindow.Instance.ShowError("Can't find mp4decrypt in lib folder "); + MainWindow.Instance.ShowError($"Can't find mp4decrypt in lib folder at: {CfgManager.PathMP4Decrypt}"); return new DownloadResponse{ Data = new List(), Error = true, @@ -864,14 +897,52 @@ public class CrunchyrollManager{ return true; }).ToList(); } else{ - dlFailed = true; + if (hsLangs.Count > 0){ + var dialog = new ContentDialog(){ + Title = "Hardsub Select", + PrimaryButtonText = "Select", + CloseButtonText = "Close" + }; - return new DownloadResponse{ - Data = new List(), - Error = dlFailed, - FileName = "./unknown", - ErrorText = "Hardsubs not available" - }; + var viewModel = new ContentDialogDropdownSelectViewModel(dialog, + data.SeriesTitle + (!string.IsNullOrEmpty(data.Season) + ? " - S" + data.Season + "E" + (data.EpisodeNumber != string.Empty ? data.EpisodeNumber : data.AbsolutEpisodeNumberE) + : "") + " - " + + data.EpisodeTitle, hsLangs); + dialog.Content = new ContentDialogDropdownSelectView(){ + DataContext = viewModel + }; + + var result = await dialog.ShowAsync(); + + if (result == ContentDialogResult.Primary){ + string selectedValue = viewModel.SelectedDropdownItem.stringValue; + + if (hsLangs.IndexOf(selectedValue) > -1){ + Console.WriteLine($"Selecting stream with {Languages.Locale2language(selectedValue).Language} hardsubs"); + streams = streams.Where((s) => s.HardsubLang != "-" && s.HardsubLang == selectedValue).ToList(); + data.Hslang = selectedValue; + } + } else{ + dlFailed = true; + + return new DownloadResponse{ + Data = new List(), + Error = dlFailed, + FileName = "./unknown", + ErrorText = "Hardsub not available" + }; + } + } else{ + dlFailed = true; + + return new DownloadResponse{ + Data = new List(), + Error = dlFailed, + FileName = "./unknown", + ErrorText = "No Hardsubs available" + }; + } } } } else{ @@ -986,12 +1057,12 @@ public class CrunchyrollManager{ int chosenVideoQuality; if (options.DlVideoOnce && dlVideoOnce && options.SyncTiming){ chosenVideoQuality = 1; - } else if (options.QualityVideo == "best"){ + } else if (data.VideoQuality == "best"){ chosenVideoQuality = videos.Count; - } else if (options.QualityVideo == "worst"){ + } else if (data.VideoQuality == "worst"){ chosenVideoQuality = 1; } else{ - var tempIndex = videos.FindIndex(a => a.quality.height + "" == options.QualityVideo); + var tempIndex = videos.FindIndex(a => a.quality.height + "" == data.VideoQuality.Replace("p", "")); if (tempIndex < 0){ chosenVideoQuality = videos.Count; } else{ @@ -1049,6 +1120,7 @@ public class CrunchyrollManager{ variables.Add(new Variable("height", chosenVideoSegments.quality.height, false)); variables.Add(new Variable("width", chosenVideoSegments.quality.width, false)); + if (string.IsNullOrEmpty(data.Resolution)) data.Resolution = chosenVideoSegments.quality.height + "p"; LanguageItem? lang = Languages.languages.FirstOrDefault(a => a.Code == curStream.AudioLang); if (lang == null){ @@ -1070,7 +1142,37 @@ public class CrunchyrollManager{ fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray()); - string outFile = Path.Combine(FileNameManager.ParseFileName(options.FileName + "." + (epMeta.Lang?.CrLocale ?? lang.Value.Name), variables, options.Numbers, options.Override).ToArray()); + + string onlyFileName = Path.GetFileNameWithoutExtension(fileName); + int maxLength = 220; + + if (onlyFileName.Length > maxLength){ + Console.Error.WriteLine($"Filename too long {onlyFileName}"); + if (options.FileName.Split("\\").Last().Contains("${title}") && onlyFileName.Length - (data.EpisodeTitle ?? string.Empty).Length < maxLength){ + var titleVariable = variables.Find(e => e.Name == "title"); + + if (titleVariable != null){ + int excessLength = (onlyFileName.Length - maxLength); + + if (excessLength > 0 && ((string)titleVariable.ReplaceWith).Length > excessLength){ + titleVariable.ReplaceWith = ((string)titleVariable.ReplaceWith).Substring(0, ((string)titleVariable.ReplaceWith).Length - excessLength); + fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray()); + onlyFileName = Path.GetFileNameWithoutExtension(fileName); + + if (onlyFileName.Length > maxLength){ + fileName = Helpers.LimitFileNameLength(fileName, maxLength); + } + } + } + } else{ + fileName = Helpers.LimitFileNameLength(fileName, maxLength); + } + + Console.Error.WriteLine($"Filename changed to {Path.GetFileNameWithoutExtension(fileName)}"); + } + + //string outFile = Path.Combine(FileNameManager.ParseFileName(options.FileName + "." + (epMeta.Lang?.CrLocale ?? lang.Value.Name), variables, options.Numbers, options.Override).ToArray()); + string outFile = fileName + "." + (epMeta.Lang?.CrLocale ?? lang.Value.CrLocale); string tempFile = Path.Combine(FileNameManager.ParseFileName($"temp-{(currentVersion.Guid != null ? currentVersion.Guid : currentMediaId)}", variables, options.Numbers, options.Override) .ToArray()); @@ -1163,7 +1265,7 @@ public class CrunchyrollManager{ var json = JsonConvert.SerializeObject(reqBodyData); var reqBody = new StringContent(json, Encoding.UTF8, "application/json"); - var decRequest = HttpClientReq.CreateRequestMessage($"{Api.DRM}", HttpMethod.Post, false, false, null); + var decRequest = HttpClientReq.CreateRequestMessage($"{ApiUrls.DRM}", HttpMethod.Post, false, false, null); decRequest.Content = reqBody; var decRequestResponse = await HttpClientReq.Instance.SendHttpRequest(decRequest); @@ -1270,6 +1372,7 @@ public class CrunchyrollManager{ IsPrimary = isPrimary }; files.Add(videoDownloadMedia); + data.downloadedFiles.Add($"{tsFile}.video.m4s"); } else{ Console.WriteLine("No Video downloaded"); } @@ -1335,6 +1438,7 @@ public class CrunchyrollManager{ Lang = lang.Value, IsPrimary = isPrimary }); + data.downloadedFiles.Add($"{tsFile}.audio.m4s"); } else{ Console.WriteLine("No Audio downloaded"); } @@ -1351,6 +1455,7 @@ public class CrunchyrollManager{ IsPrimary = isPrimary }; files.Add(videoDownloadMedia); + data.downloadedFiles.Add($"{tsFile}.video.m4s"); } if (audioDownloaded){ @@ -1360,6 +1465,7 @@ public class CrunchyrollManager{ Lang = lang.Value, IsPrimary = isPrimary }); + data.downloadedFiles.Add($"{tsFile}.audio.m4s"); } } } else if (options.Novids){ @@ -1410,6 +1516,7 @@ public class CrunchyrollManager{ File.WriteAllText($"{tsFile}.txt", string.Join("\r\n", compiledChapters)); files.Add(new DownloadedMedia{ Path = $"{tsFile}.txt", Lang = lang, Type = DownloadMediaType.Chapters }); + data.downloadedFiles.Add($"{tsFile}.txt"); } catch{ Console.Error.WriteLine("Failed to write chapter file"); } @@ -1429,9 +1536,9 @@ public class CrunchyrollManager{ } else{ Console.WriteLine("Subtitles downloading skipped!"); } - } - - await Task.Delay(options.Waittime); + } + + // await Task.Delay(options.Waittime); } } @@ -1468,12 +1575,14 @@ public class CrunchyrollManager{ Type = DownloadMediaType.Description, Path = fullPath, }); + data.downloadedFiles.Add(fullPath); } else{ if (files.All(e => e.Type != DownloadMediaType.Description)){ files.Add(new DownloadedMedia{ Type = DownloadMediaType.Description, Path = fullPath, }); + data.downloadedFiles.Add(fullPath); } } @@ -1649,6 +1758,7 @@ public class CrunchyrollManager{ Lang = sxData.Language, RelatedVideoDownloadMedia = videoDownloadMedia }); + data.downloadedFiles.Add(sxData.Path); } else{ Console.WriteLine($"Failed to download subtitle: ${sxData.File}"); } @@ -1678,7 +1788,10 @@ public class CrunchyrollManager{ M3U8Json videoJson = new M3U8Json{ Segments = chosenVideoSegments.segments.Cast().ToList() }; - + + data.downloadedFiles.Add(chosenVideoSegments.pssh != null ? $"{tempTsFile}.video.enc.m4s" : $"{tsFile}.video.m4s"); + data.downloadedFiles.Add(chosenVideoSegments.pssh != null ? $"{tempTsFile}.video.enc.m4s.resume" : $"{tsFile}.video.m4s.resume"); + var videoDownloader = new HlsDownloader(new HlsOptions{ Output = chosenVideoSegments.pssh != null ? $"{tempTsFile}.video.enc.m4s" : $"{tsFile}.video.m4s", Timeout = options.Timeout, @@ -1689,6 +1802,7 @@ public class CrunchyrollManager{ Override = options.Force, }, data, true, false); + var videoDownloadResult = await videoDownloader.Download(); return (videoDownloadResult.Ok, videoDownloadResult.Parts, tsFile); @@ -1730,6 +1844,9 @@ public class CrunchyrollManager{ M3U8Json audioJson = new M3U8Json{ Segments = chosenAudioSegments.segments.Cast().ToList() }; + + data.downloadedFiles.Add(chosenAudioSegments.pssh != null ? $"{tempTsFile}.audio.enc.m4s" : $"{tsFile}.audio.m4s"); + data.downloadedFiles.Add(chosenAudioSegments.pssh != null ? $"{tempTsFile}.audio.enc.m4s.resume" : $"{tsFile}.audio.m4s.resume"); var audioDownloader = new HlsDownloader(new HlsOptions{ Output = chosenAudioSegments.pssh != null ? $"{tempTsFile}.audio.enc.m4s" : $"{tsFile}.audio.m4s", @@ -1863,7 +1980,7 @@ public class CrunchyrollManager{ private async Task ParseChapters(string currentMediaId, List compiledChapters){ var showRequest = HttpClientReq.CreateRequestMessage($"https://static.crunchyroll.com/skip-events/production/{currentMediaId}.json", HttpMethod.Get, true, true, null); - var showRequestResponse = await HttpClientReq.Instance.SendHttpRequest(showRequest); + var showRequestResponse = await HttpClientReq.Instance.SendHttpRequest(showRequest, true); if (showRequestResponse.IsOk){ CrunchyChapters chapterData = new CrunchyChapters(); @@ -1873,7 +1990,7 @@ public class CrunchyrollManager{ JObject jObject = JObject.Parse(showRequestResponse.ResponseContent); if (jObject.TryGetValue("lastUpdate", out JToken lastUpdateToken)){ - chapterData.lastUpdate = lastUpdateToken.ToObject(); + chapterData.lastUpdate = lastUpdateToken.ToObject(); } if (jObject.TryGetValue("mediaId", out JToken mediaIdToken)){ @@ -1954,7 +2071,7 @@ public class CrunchyrollManager{ showRequest = HttpClientReq.CreateRequestMessage($"https://static.crunchyroll.com/datalab-intro-v2/{currentMediaId}.json", HttpMethod.Get, true, true, null); - showRequestResponse = await HttpClientReq.Instance.SendHttpRequest(showRequest); + showRequestResponse = await HttpClientReq.Instance.SendHttpRequest(showRequest, true); if (showRequestResponse.IsOk){ CrunchyOldChapter chapterData = Helpers.Deserialize(showRequestResponse.ResponseContent, SettingsJsonSerializerSettings); @@ -1988,7 +2105,7 @@ public class CrunchyrollManager{ return true; } - Console.Error.WriteLine("Old Chapter API request failed"); + Console.Error.WriteLine("Chapter request failed"); return false; } diff --git a/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs b/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs new file mode 100644 index 0000000..ab2fde3 --- /dev/null +++ b/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs @@ -0,0 +1,550 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Media; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CRD.Utils; +using CRD.Utils.Ffmpeg_Encoding; +using CRD.Utils.Sonarr; +using CRD.Utils.Structs; +using CRD.Utils.Structs.History; +using CRD.ViewModels; +using CRD.ViewModels.Utils; +using CRD.Views.Utils; +using FluentAvalonia.UI.Controls; + +// ReSharper disable InconsistentNaming + +namespace CRD.Downloader.Crunchyroll.ViewModels; + +public partial class CrunchyrollSettingsViewModel : ViewModelBase{ + [ObservableProperty] + private bool _downloadVideo = true; + + [ObservableProperty] + private bool _downloadAudio = true; + + [ObservableProperty] + private bool _downloadChapters = true; + + [ObservableProperty] + private bool _addScaledBorderAndShadow; + + [ObservableProperty] + private bool _includeSignSubs; + + [ObservableProperty] + private bool _includeCcSubs; + + [ObservableProperty] + private ComboBoxItem _selectedScaledBorderAndShadow; + + public ObservableCollection ScaledBorderAndShadow{ get; } = new(){ + new ComboBoxItem(){ Content = "ScaledBorderAndShadow: yes" }, + new ComboBoxItem(){ Content = "ScaledBorderAndShadow: no" }, + }; + + [ObservableProperty] + private bool _skipMuxing; + + [ObservableProperty] + private bool _muxToMp4; + + [ObservableProperty] + private bool _syncTimings; + + [ObservableProperty] + private bool _defaultSubSigns; + + [ObservableProperty] + private bool _defaultSubForcedDisplay; + + [ObservableProperty] + private bool _includeEpisodeDescription; + + [ObservableProperty] + private bool _downloadVideoForEveryDub; + + [ObservableProperty] + private bool _keepDubsSeparate; + + [ObservableProperty] + private bool _skipSubMux; + + [ObservableProperty] + private double? _leadingNumbers; + + [ObservableProperty] + private double? _partSize; + + [ObservableProperty] + private string _fileName = ""; + + [ObservableProperty] + private string _fileTitle = ""; + + [ObservableProperty] + private ObservableCollection _mkvMergeOptions = new(); + + [ObservableProperty] + private string _mkvMergeOption = ""; + + [ObservableProperty] + private string _ffmpegOption = ""; + + [ObservableProperty] + private ObservableCollection _ffmpegOptions = new(); + + [ObservableProperty] + private string _selectedSubs = "all"; + + [ObservableProperty] + private ComboBoxItem _selectedHSLang; + + [ObservableProperty] + private ComboBoxItem _selectedDescriptionLang; + + [ObservableProperty] + private string _selectedDubs = "ja-JP"; + + [ObservableProperty] + private ObservableCollection _selectedDubLang = new(); + + [ObservableProperty] + private ComboBoxItem _selectedStreamEndpoint; + + [ObservableProperty] + private ComboBoxItem _selectedDefaultDubLang; + + [ObservableProperty] + private ComboBoxItem _selectedDefaultSubLang; + + [ObservableProperty] + private ComboBoxItem? _selectedVideoQuality; + + [ObservableProperty] + private ComboBoxItem? _selectedAudioQuality; + + [ObservableProperty] + private ObservableCollection _selectedSubLang = new(); + + [ObservableProperty] + private Color _listBoxColor; + + public ObservableCollection VideoQualityList{ get; } = new(){ + new ComboBoxItem(){ Content = "best" }, + new ComboBoxItem(){ Content = "1080" }, + new ComboBoxItem(){ Content = "720" }, + new ComboBoxItem(){ Content = "480" }, + new ComboBoxItem(){ Content = "360" }, + new ComboBoxItem(){ Content = "240" }, + new ComboBoxItem(){ Content = "worst" }, + }; + + public ObservableCollection AudioQualityList{ get; } = new(){ + new ComboBoxItem(){ Content = "best" }, + new ComboBoxItem(){ Content = "128kB/s" }, + new ComboBoxItem(){ Content = "96kB/s" }, + new ComboBoxItem(){ Content = "64kB/s" }, + new ComboBoxItem(){ Content = "worst" }, + }; + + public ObservableCollection HardSubLangList{ get; } = new(){ + new ComboBoxItem(){ Content = "none" }, + }; + + public ObservableCollection DescriptionLangList{ get; } = new(){ + new ComboBoxItem(){ Content = "default" }, + new ComboBoxItem(){ Content = "de-DE" }, + new ComboBoxItem(){ Content = "en-US" }, + new ComboBoxItem(){ Content = "es-419" }, + new ComboBoxItem(){ Content = "es-ES" }, + new ComboBoxItem(){ Content = "fr-FR" }, + new ComboBoxItem(){ Content = "it-IT" }, + new ComboBoxItem(){ Content = "pt-BR" }, + new ComboBoxItem(){ Content = "pt-PT" }, + new ComboBoxItem(){ Content = "ru-RU" }, + new ComboBoxItem(){ Content = "hi-IN" }, + new ComboBoxItem(){ Content = "ar-SA" }, + }; + + public ObservableCollection DubLangList{ get; } = new(){ + }; + + + public ObservableCollection DefaultDubLangList{ get; } = new(){ + }; + + public ObservableCollection DefaultSubLangList{ get; } = new(){ + }; + + + public ObservableCollection SubLangList{ get; } = new(){ + new ListBoxItem(){ Content = "all" }, + new ListBoxItem(){ Content = "none" }, + }; + + public ObservableCollection StreamEndpoints{ get; } = new(){ + new ComboBoxItem(){ Content = "web/firefox" }, + new ComboBoxItem(){ Content = "console/switch" }, + new ComboBoxItem(){ Content = "console/ps4" }, + new ComboBoxItem(){ Content = "console/ps5" }, + new ComboBoxItem(){ Content = "console/xbox_one" }, + new ComboBoxItem(){ Content = "web/edge" }, + // new ComboBoxItem(){ Content = "web/safari" }, + new ComboBoxItem(){ Content = "web/chrome" }, + new ComboBoxItem(){ Content = "web/fallback" }, + // new ComboBoxItem(){ Content = "ios/iphone" }, + // new ComboBoxItem(){ Content = "ios/ipad" }, + new ComboBoxItem(){ Content = "android/phone" }, + new ComboBoxItem(){ Content = "tv/samsung" }, + }; + + [ObservableProperty] + private bool _isEncodeEnabled; + + [ObservableProperty] + private StringItem _selectedEncodingPreset; + + public ObservableCollection EncodingPresetsList{ get; } = new(); + + + [ObservableProperty] + private bool _cCSubsMuxingFlag; + + [ObservableProperty] + private string _cCSubsFont; + + [ObservableProperty] + private bool _signsSubsAsForced; + + private bool settingsLoaded; + + public CrunchyrollSettingsViewModel(){ + + foreach (var languageItem in Languages.languages){ + HardSubLangList.Add(new ComboBoxItem{ Content = languageItem.CrLocale }); + SubLangList.Add(new ListBoxItem{ Content = languageItem.CrLocale }); + DubLangList.Add(new ListBoxItem{ Content = languageItem.CrLocale }); + DefaultDubLangList.Add(new ComboBoxItem{ Content = languageItem.CrLocale }); + DefaultSubLangList.Add(new ComboBoxItem{ Content = languageItem.CrLocale }); + } + + foreach (var encodingPreset in FfmpegEncoding.presets){ + EncodingPresetsList.Add(new StringItem{ stringValue = encodingPreset.PresetName ?? "Unknown Preset Name" }); + } + + + CrDownloadOptions options = CrunchyrollManager.Instance.CrunOptions; + + StringItem? encodingPresetSelected = EncodingPresetsList.FirstOrDefault(a => a.stringValue != null && a.stringValue == options.EncodingPresetName) ?? null; + SelectedEncodingPreset = encodingPresetSelected ?? EncodingPresetsList[0]; + + ComboBoxItem? descriptionLang = DescriptionLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.DescriptionLang) ?? null; + SelectedDescriptionLang = descriptionLang ?? DescriptionLangList[0]; + + ComboBoxItem? hsLang = HardSubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == Languages.Locale2language(options.Hslang).CrLocale) ?? null; + SelectedHSLang = hsLang ?? HardSubLangList[0]; + + ComboBoxItem? defaultDubLang = DefaultDubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.DefaultAudio ?? "")) ?? null; + SelectedDefaultDubLang = defaultDubLang ?? DefaultDubLangList[0]; + + ComboBoxItem? defaultSubLang = DefaultSubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.DefaultSub ?? "")) ?? null; + SelectedDefaultSubLang = defaultSubLang ?? DefaultSubLangList[0]; + + ComboBoxItem? streamEndpoint = StreamEndpoints.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpoint ?? "")) ?? null; + SelectedStreamEndpoint = streamEndpoint ?? StreamEndpoints[0]; + + var softSubLang = SubLangList.Where(a => options.DlSubs.Contains(a.Content)).ToList(); + + SelectedSubLang.Clear(); + foreach (var listBoxItem in softSubLang){ + SelectedSubLang.Add(listBoxItem); + } + + var dubLang = DubLangList.Where(a => options.DubLang.Contains(a.Content)).ToList(); + + SelectedDubLang.Clear(); + foreach (var listBoxItem in dubLang){ + SelectedDubLang.Add(listBoxItem); + } + + AddScaledBorderAndShadow = options.SubsAddScaledBorder is ScaledBorderAndShadowSelection.ScaledBorderAndShadowNo or ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes; + SelectedScaledBorderAndShadow = GetScaledBorderAndShadowFromOptions(options); + + CCSubsFont = options.CcSubsFont ?? ""; + CCSubsMuxingFlag = options.CcSubsMuxingFlag; + SignsSubsAsForced = options.SignsSubsAsForced; + SkipMuxing = options.SkipMuxing; + IsEncodeEnabled = options.IsEncodeEnabled; + DefaultSubForcedDisplay = options.DefaultSubForcedDisplay; + DefaultSubSigns = options.DefaultSubSigns; + PartSize = options.Partsize; + IncludeEpisodeDescription = options.IncludeVideoDescription; + FileTitle = options.VideoTitle ?? ""; + IncludeSignSubs = options.IncludeSignsSubs; + IncludeCcSubs = options.IncludeCcSubs; + DownloadVideo = !options.Novids; + DownloadAudio = !options.Noaudio; + DownloadVideoForEveryDub = !options.DlVideoOnce; + KeepDubsSeparate = options.KeepDubsSeperate; + DownloadChapters = options.Chapters; + MuxToMp4 = options.Mp4; + SyncTimings = options.SyncTiming; + SkipSubMux = options.SkipSubsMux; + LeadingNumbers = options.Numbers; + FileName = options.FileName; + + ComboBoxItem? qualityAudio = AudioQualityList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.QualityAudio) ?? null; + SelectedAudioQuality = qualityAudio ?? AudioQualityList[0]; + + ComboBoxItem? qualityVideo = VideoQualityList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.QualityVideo) ?? null; + SelectedVideoQuality = qualityVideo ?? VideoQualityList[0]; + + MkvMergeOptions.Clear(); + if (options.MkvmergeOptions != null){ + foreach (var mkvmergeParam in options.MkvmergeOptions){ + MkvMergeOptions.Add(new StringItem(){ stringValue = mkvmergeParam }); + } + } + + FfmpegOptions.Clear(); + if (options.FfmpegOptions != null){ + foreach (var ffmpegParam in options.FfmpegOptions){ + FfmpegOptions.Add(new StringItem(){ stringValue = ffmpegParam }); + } + } + + var dubs = SelectedDubLang.Select(item => item.Content?.ToString()); + SelectedDubs = string.Join(", ", dubs) ?? ""; + + var subs = SelectedSubLang.Select(item => item.Content?.ToString()); + SelectedSubs = string.Join(", ", subs) ?? ""; + + SelectedSubLang.CollectionChanged += Changes; + SelectedDubLang.CollectionChanged += Changes; + + MkvMergeOptions.CollectionChanged += Changes; + FfmpegOptions.CollectionChanged += Changes; + + settingsLoaded = true; + } + + private void UpdateSettings(){ + if (!settingsLoaded){ + return; + } + + CrunchyrollManager.Instance.CrunOptions.SignsSubsAsForced = SignsSubsAsForced; + CrunchyrollManager.Instance.CrunOptions.CcSubsMuxingFlag = CCSubsMuxingFlag; + CrunchyrollManager.Instance.CrunOptions.CcSubsFont = CCSubsFont; + CrunchyrollManager.Instance.CrunOptions.EncodingPresetName = SelectedEncodingPreset.stringValue; + CrunchyrollManager.Instance.CrunOptions.IsEncodeEnabled = IsEncodeEnabled; + CrunchyrollManager.Instance.CrunOptions.DefaultSubSigns = DefaultSubSigns; + CrunchyrollManager.Instance.CrunOptions.DefaultSubForcedDisplay = DefaultSubForcedDisplay; + CrunchyrollManager.Instance.CrunOptions.IncludeVideoDescription = IncludeEpisodeDescription; + 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.SkipMuxing = SkipMuxing; + CrunchyrollManager.Instance.CrunOptions.Mp4 = MuxToMp4; + CrunchyrollManager.Instance.CrunOptions.SyncTiming = SyncTimings; + CrunchyrollManager.Instance.CrunOptions.SkipSubsMux = SkipSubMux; + CrunchyrollManager.Instance.CrunOptions.Numbers = Math.Clamp((int)(LeadingNumbers ?? 0), 0, 10); + CrunchyrollManager.Instance.CrunOptions.FileName = FileName; + CrunchyrollManager.Instance.CrunOptions.IncludeSignsSubs = IncludeSignSubs; + CrunchyrollManager.Instance.CrunOptions.IncludeCcSubs = IncludeCcSubs; + CrunchyrollManager.Instance.CrunOptions.Partsize = Math.Clamp((int)(PartSize ?? 0), 0, 10000); + + CrunchyrollManager.Instance.CrunOptions.SubsAddScaledBorder = GetScaledBorderAndShadowSelection(); + + List softSubs = new List(); + foreach (var listBoxItem in SelectedSubLang){ + softSubs.Add(listBoxItem.Content + ""); + } + + CrunchyrollManager.Instance.CrunOptions.DlSubs = softSubs; + + string descLang = SelectedDescriptionLang.Content + ""; + + CrunchyrollManager.Instance.CrunOptions.DescriptionLang = descLang != "default" ? descLang : CrunchyrollManager.Instance.DefaultLocale; + + string hslang = SelectedHSLang.Content + ""; + + CrunchyrollManager.Instance.CrunOptions.Hslang = hslang != "none" ? Languages.FindLang(hslang).Locale : hslang; + + CrunchyrollManager.Instance.CrunOptions.DefaultAudio = SelectedDefaultDubLang.Content + ""; + CrunchyrollManager.Instance.CrunOptions.DefaultSub = SelectedDefaultSubLang.Content + ""; + + + CrunchyrollManager.Instance.CrunOptions.StreamEndpoint = SelectedStreamEndpoint.Content + ""; + + List dubLangs = new List(); + foreach (var listBoxItem in SelectedDubLang){ + dubLangs.Add(listBoxItem.Content + ""); + } + + CrunchyrollManager.Instance.CrunOptions.DubLang = dubLangs; + + CrunchyrollManager.Instance.CrunOptions.QualityAudio = SelectedAudioQuality?.Content + ""; + CrunchyrollManager.Instance.CrunOptions.QualityVideo = SelectedVideoQuality?.Content + ""; + + List mkvmergeParams = new List(); + foreach (var mkvmergeParam in MkvMergeOptions){ + mkvmergeParams.Add(mkvmergeParam.stringValue); + } + + CrunchyrollManager.Instance.CrunOptions.MkvmergeOptions = mkvmergeParams; + + List ffmpegParams = new List(); + foreach (var ffmpegParam in FfmpegOptions){ + ffmpegParams.Add(ffmpegParam.stringValue); + } + + CrunchyrollManager.Instance.CrunOptions.FfmpegOptions = ffmpegParams; + + CfgManager.WriteSettingsToFile(); + } + + + private ScaledBorderAndShadowSelection GetScaledBorderAndShadowSelection(){ + if (!AddScaledBorderAndShadow){ + return ScaledBorderAndShadowSelection.DontAdd; + } + + if (SelectedScaledBorderAndShadow.Content + "" == "ScaledBorderAndShadow: yes"){ + return ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes; + } + + if (SelectedScaledBorderAndShadow.Content + "" == "ScaledBorderAndShadow: no"){ + return ScaledBorderAndShadowSelection.ScaledBorderAndShadowNo; + } + + return ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes; + } + + private ComboBoxItem GetScaledBorderAndShadowFromOptions(CrDownloadOptions options){ + switch (options.SubsAddScaledBorder){ + case (ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes): + return ScaledBorderAndShadow.FirstOrDefault(a => a.Content != null && (string)a.Content == "ScaledBorderAndShadow: yes") ?? ScaledBorderAndShadow[0]; + case ScaledBorderAndShadowSelection.ScaledBorderAndShadowNo: + return ScaledBorderAndShadow.FirstOrDefault(a => a.Content != null && (string)a.Content == "ScaledBorderAndShadow: no") ?? ScaledBorderAndShadow[0]; + default: + return ScaledBorderAndShadow[0]; + } + } + + [RelayCommand] + public void AddMkvMergeParam(){ + MkvMergeOptions.Add(new StringItem(){ stringValue = MkvMergeOption }); + MkvMergeOption = ""; + RaisePropertyChanged(nameof(MkvMergeOptions)); + } + + [RelayCommand] + public void RemoveMkvMergeParam(StringItem param){ + MkvMergeOptions.Remove(param); + RaisePropertyChanged(nameof(MkvMergeOptions)); + } + + [RelayCommand] + public void AddFfmpegParam(){ + FfmpegOptions.Add(new StringItem(){ stringValue = FfmpegOption }); + FfmpegOption = ""; + RaisePropertyChanged(nameof(FfmpegOptions)); + } + + [RelayCommand] + public void RemoveFfmpegParam(StringItem param){ + FfmpegOptions.Remove(param); + RaisePropertyChanged(nameof(FfmpegOptions)); + } + + private void Changes(object? sender, NotifyCollectionChangedEventArgs e){ + UpdateSettings(); + + var dubs = SelectedDubLang.Select(item => item.Content?.ToString()); + SelectedDubs = string.Join(", ", dubs) ?? ""; + + var subs = SelectedSubLang.Select(item => item.Content?.ToString()); + SelectedSubs = string.Join(", ", subs) ?? ""; + } + + protected override void OnPropertyChanged(PropertyChangedEventArgs e){ + base.OnPropertyChanged(e); + + if (e.PropertyName is nameof(SelectedDubs) + or nameof(SelectedSubs) + or nameof(ListBoxColor)){ + return; + } + + UpdateSettings(); + + if (e.PropertyName is nameof(History)){ + if (CrunchyrollManager.Instance.CrunOptions.History){ + if (File.Exists(CfgManager.PathCrHistory)){ + var decompressedJson = CfgManager.DecompressJsonFile(CfgManager.PathCrHistory); + if (!string.IsNullOrEmpty(decompressedJson)){ + CrunchyrollManager.Instance.HistoryList = Helpers.Deserialize>(decompressedJson, CrunchyrollManager.Instance.SettingsJsonSerializerSettings) ?? + new ObservableCollection(); + + foreach (var historySeries in CrunchyrollManager.Instance.HistoryList){ + historySeries.Init(); + foreach (var historySeriesSeason in historySeries.Seasons){ + historySeriesSeason.Init(); + } + } + } else{ + CrunchyrollManager.Instance.HistoryList =[]; + } + } + + _ = SonarrClient.Instance.RefreshSonarrLite(); + } else{ + CrunchyrollManager.Instance.HistoryList =[]; + } + } + + } + + [RelayCommand] + public async Task CreateEncodingPresetButtonPress(bool editMode){ + var dialog = new ContentDialog(){ + Title = "New Encoding Preset", + PrimaryButtonText = "Save", + CloseButtonText = "Close", + FullSizeDesired = true + }; + + var viewModel = new ContentDialogEncodingPresetViewModel(dialog, editMode); + dialog.Content = new ContentDialogEncodingPresetView(){ + DataContext = viewModel + }; + + var dialogResult = await dialog.ShowAsync(); + + if (dialogResult == ContentDialogResult.Primary){ + settingsLoaded = false; + EncodingPresetsList.Clear(); + foreach (var encodingPreset in FfmpegEncoding.presets){ + EncodingPresetsList.Add(new StringItem{ stringValue = encodingPreset.PresetName ?? "Unknown Preset Name" }); + } + + settingsLoaded = true; + StringItem? encodingPresetSelected = EncodingPresetsList.FirstOrDefault(a => a.stringValue != null && a.stringValue == CrunchyrollManager.Instance.CrunOptions.EncodingPresetName) ?? null; + SelectedEncodingPreset = encodingPresetSelected ?? EncodingPresetsList[0]; + } + } + +} \ No newline at end of file diff --git a/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml b/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml new file mode 100644 index 0000000..49a79eb --- /dev/null +++ b/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml @@ -0,0 +1,533 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml.cs b/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml.cs new file mode 100644 index 0000000..3ebaf82 --- /dev/null +++ b/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml.cs @@ -0,0 +1,26 @@ +using System.Linq; +using Avalonia.Controls; +using Avalonia.VisualTree; + +namespace CRD.Downloader.Crunchyroll.Views; + +public partial class CrunchyrollSettingsView : UserControl{ + public CrunchyrollSettingsView(){ + InitializeComponent(); + } + + private void ListBox_PointerWheelChanged(object sender, Avalonia.Input.PointerWheelEventArgs e){ + var listBox = sender as ListBox; + var scrollViewer = listBox?.GetVisualDescendants().OfType().FirstOrDefault(); + + if (scrollViewer != null){ + // Determine if the ListBox is at its bounds (top or bottom) + bool atTop = scrollViewer.Offset.Y <= 0 && e.Delta.Y > 0; + bool atBottom = scrollViewer.Offset.Y >= scrollViewer.Extent.Height - scrollViewer.Viewport.Height && e.Delta.Y < 0; + + if (atTop || atBottom){ + e.Handled = true; // Stop the event from propagating to the parent + } + } + } +} \ No newline at end of file diff --git a/CRD/Downloader/History.cs b/CRD/Downloader/History.cs index 46bc8c2..3ee691c 100644 --- a/CRD/Downloader/History.cs +++ b/CRD/Downloader/History.cs @@ -19,20 +19,18 @@ namespace CRD.Downloader; public class History(){ private readonly CrunchyrollManager crunInstance = CrunchyrollManager.Instance; - public async Task CRUpdateSeries(string seriesId, string? seasonId){ + public async Task CRUpdateSeries(string seriesId, string? seasonId){ await crunInstance.CrAuth.RefreshToken(true); CrSeriesSearch? parsedSeries = await crunInstance.CrSeries.ParseSeriesById(seriesId, "ja-JP", true); if (parsedSeries == null){ Console.Error.WriteLine("Parse Data Invalid - series is maybe only available with VPN or got deleted"); - return; + return false; } if (parsedSeries.Data != null){ foreach (var s in parsedSeries.Data){ - if (!string.IsNullOrEmpty(seasonId) && s.Id != seasonId) continue; - var sId = s.Id; if (s.Versions is{ Count: > 0 }){ foreach (var sVersion in s.Versions.Where(sVersion => sVersion.Original == true)){ @@ -44,8 +42,11 @@ public class History(){ } } + if (!string.IsNullOrEmpty(seasonId) && sId != seasonId) continue; + + var seasonData = await crunInstance.CrSeries.GetSeasonDataById(sId, string.IsNullOrEmpty(crunInstance.CrunOptions.HistoryLang) ? crunInstance.DefaultLocale : crunInstance.CrunOptions.HistoryLang, true); - if (seasonData.Data != null) await UpdateWithSeasonData(seasonData.Data); + if (seasonData.Data is{ Count: > 0 }) await UpdateWithSeasonData(seasonData.Data); } @@ -55,8 +56,11 @@ public class History(){ MatchHistorySeriesWithSonarr(false); await MatchHistoryEpisodesWithSonarr(false, historySeries); CfgManager.UpdateHistoryFile(); + return true; } } + + return false; } @@ -124,10 +128,12 @@ public class History(){ return (null, downloadDirPath); } - public (HistoryEpisode? historyEpisode, List dublist, List sublist, string downloadDirPath) GetHistoryEpisodeWithDubListAndDownloadDir(string? seriesId, string? seasonId, string episodeId){ + public (HistoryEpisode? historyEpisode, List dublist, List sublist, string downloadDirPath, string videoQuality) GetHistoryEpisodeWithDubListAndDownloadDir(string? seriesId, string? seasonId, + string episodeId){ var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId); var downloadDirPath = ""; + var videoQuality = ""; List dublist =[]; List sublist =[]; @@ -145,6 +151,10 @@ public class History(){ downloadDirPath = historySeries.SeriesDownloadPath; } + if (!string.IsNullOrEmpty(historySeries.HistorySeriesVideoQualityOverride)){ + videoQuality = historySeries.HistorySeriesVideoQualityOverride; + } + if (historySeason != null){ var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == episodeId); if (historySeason.HistorySeasonDubLangOverride.Count > 0){ @@ -159,13 +169,17 @@ public class History(){ downloadDirPath = historySeason.SeasonDownloadPath; } + if (!string.IsNullOrEmpty(historySeason.HistorySeasonVideoQualityOverride)){ + videoQuality = historySeason.HistorySeasonVideoQualityOverride; + } + if (historyEpisode != null){ - return (historyEpisode, dublist, sublist, downloadDirPath); + return (historyEpisode, dublist, sublist, downloadDirPath, videoQuality); } } } - return (null, dublist, sublist, downloadDirPath); + return (null, dublist, sublist, downloadDirPath, videoQuality); } public List GetDubList(string? seriesId, string? seasonId){ @@ -187,10 +201,11 @@ public class History(){ return dublist; } - public List GetSubList(string? seriesId, string? seasonId){ + public (List sublist, string videoQuality) GetSubList(string? seriesId, string? seasonId){ var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId); List sublist =[]; + var videoQuality = ""; if (historySeries != null){ var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId); @@ -198,12 +213,20 @@ public class History(){ sublist = historySeries.HistorySeriesSoftSubsOverride; } + if (!string.IsNullOrEmpty(historySeries.HistorySeriesVideoQualityOverride)){ + videoQuality = historySeries.HistorySeriesVideoQualityOverride; + } + if (historySeason is{ HistorySeasonSoftSubsOverride.Count: > 0 }){ sublist = historySeason.HistorySeasonSoftSubsOverride; } + + if (historySeason != null && !string.IsNullOrEmpty(historySeason.HistorySeasonVideoQualityOverride)){ + videoQuality = historySeason.HistorySeasonVideoQualityOverride; + } } - return sublist; + return (sublist, videoQuality); } @@ -242,15 +265,14 @@ public class History(){ var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == crunchyEpisode.Id); if (historyEpisode == null){ - var langList = new List(); - + if (crunchyEpisode.Versions != null){ langList.AddRange(crunchyEpisode.Versions.Select(version => version.AudioLocale)); } else{ langList.Add(crunchyEpisode.AudioLocale); } - + var newHistoryEpisode = new HistoryEpisode{ EpisodeTitle = GetEpisodeTitle(crunchyEpisode), EpisodeDescription = crunchyEpisode.Description, @@ -260,19 +282,19 @@ public class History(){ SpecialEpisode = !int.TryParse(crunchyEpisode.Episode, out _), HistoryEpisodeAvailableDubLang = Languages.SortListByLangList(langList), HistoryEpisodeAvailableSoftSubs = Languages.SortListByLangList(crunchyEpisode.SubtitleLocales), + EpisodeCrPremiumAirDate = crunchyEpisode.PremiumAvailableDate }; historySeason.EpisodesList.Add(newHistoryEpisode); } else{ - var langList = new List(); - + if (crunchyEpisode.Versions != null){ langList.AddRange(crunchyEpisode.Versions.Select(version => version.AudioLocale)); } else{ langList.Add(crunchyEpisode.AudioLocale); } - + //Update existing episode historyEpisode.EpisodeTitle = GetEpisodeTitle(crunchyEpisode); historyEpisode.SpecialEpisode = !int.TryParse(crunchyEpisode.Episode, out _); @@ -366,6 +388,7 @@ public class History(){ historySeries.HistorySeriesAvailableDubLang = Languages.SortListByLangList(series.AudioLocales); historySeries.HistorySeriesAvailableSoftSubs = Languages.SortListByLangList(series.SubtitleLocales); } + return; } @@ -505,16 +528,16 @@ public class History(){ }; foreach (var crunchyEpisode in seasonData){ - var langList = new List(); - + if (crunchyEpisode.Versions != null){ langList.AddRange(crunchyEpisode.Versions.Select(version => version.AudioLocale)); } else{ langList.Add(crunchyEpisode.AudioLocale); } + Languages.SortListByLangList(langList); - + var newHistoryEpisode = new HistoryEpisode{ EpisodeTitle = GetEpisodeTitle(crunchyEpisode), EpisodeDescription = crunchyEpisode.Description, @@ -524,6 +547,7 @@ public class History(){ SpecialEpisode = !int.TryParse(crunchyEpisode.Episode, out _), HistoryEpisodeAvailableDubLang = langList, HistoryEpisodeAvailableSoftSubs = crunchyEpisode.SubtitleLocales, + EpisodeCrPremiumAirDate = crunchyEpisode.PremiumAvailableDate }; newSeason.EpisodesList.Add(newHistoryEpisode); @@ -531,7 +555,7 @@ public class History(){ return newSeason; } - + public void MatchHistorySeriesWithSonarr(bool updateAll){ if (crunInstance.CrunOptions.SonarrProperties is{ SonarrEnabled: false }){ return; diff --git a/CRD/Downloader/ProgramManager.cs b/CRD/Downloader/ProgramManager.cs index d536d67..56149ed 100644 --- a/CRD/Downloader/ProgramManager.cs +++ b/CRD/Downloader/ProgramManager.cs @@ -1,14 +1,19 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Threading.Tasks; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Media; +using Avalonia.Platform.Storage; using Avalonia.Styling; using CommunityToolkit.Mvvm.ComponentModel; using CRD.Downloader.Crunchyroll; +using CRD.Utils; +using CRD.Utils.Structs; +using CRD.Utils.Structs.History; using CRD.Utils.Updater; using FluentAvalonia.Styling; @@ -50,12 +55,18 @@ public partial class ProgramManager : ObservableObject{ #endregion + + public Dictionary> AnilistSeasons = new(); + public Dictionary> AnilistUpcoming = new(); + private readonly FluentAvaloniaTheme? _faTheme; private Queue> taskQueue = new Queue>(); private bool exitOnTaskFinish = false; + public IStorageProvider StorageProvider; + public ProgramManager(){ _faTheme = Application.Current?.Styles[0] as FluentAvaloniaTheme; @@ -106,7 +117,7 @@ public partial class ProgramManager : ObservableObject{ private async void Init(){ CrunchyrollManager.Instance.InitOptions(); - + UpdateAvailable = await Updater.Instance.CheckForUpdatesAsync(); if (CrunchyrollManager.Instance.CrunOptions.AccentColor != null && !string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.AccentColor)){ @@ -125,6 +136,10 @@ public partial class ProgramManager : ObservableObject{ } } + if (!string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.BackgroundImagePath)){ + Helpers.SetBackgroundImage(CrunchyrollManager.Instance.CrunOptions.BackgroundImagePath, CrunchyrollManager.Instance.CrunOptions.BackgroundImageOpacity, + CrunchyrollManager.Instance.CrunOptions.BackgroundImageBlurRadius); + } await CrunchyrollManager.Instance.Init(); @@ -133,6 +148,7 @@ public partial class ProgramManager : ObservableObject{ await WorkOffArgsTasks(); } + private async Task WorkOffArgsTasks(){ if (taskQueue.Count == 0){ return; @@ -149,13 +165,11 @@ public partial class ProgramManager : ObservableObject{ Console.WriteLine("Exiting..."); IClassicDesktopStyleApplicationLifetime? lifetime = (IClassicDesktopStyleApplicationLifetime)Application.Current?.ApplicationLifetime; if (lifetime != null){ - lifetime.Shutdown(); + lifetime.Shutdown(); } else{ Environment.Exit(0); } - } - } diff --git a/CRD/Downloader/QueueManager.cs b/CRD/Downloader/QueueManager.cs index a6f13e8..725d391 100644 --- a/CRD/Downloader/QueueManager.cs +++ b/CRD/Downloader/QueueManager.cs @@ -3,10 +3,8 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Linq; -using System.Text.RegularExpressions; using System.Threading.Tasks; using CRD.Downloader.Crunchyroll; -using CRD.Utils; using CRD.Utils.CustomList; using CRD.Utils.Structs; using CRD.Utils.Structs.History; @@ -102,7 +100,7 @@ public class QueueManager{ var sList = await CrunchyrollManager.Instance.CrEpisode.EpisodeData((CrunchyEpisode)episodeL, updateHistory); - (HistoryEpisode? historyEpisode, List dublist, List sublist, string downloadDirPath) historyEpisode = (null, [], [], ""); + (HistoryEpisode? historyEpisode, List dublist, List sublist, string downloadDirPath,string videoQuality) historyEpisode = (null, [], [], "",""); if (CrunchyrollManager.Instance.CrunOptions.History){ var episode = sList.EpisodeAndLanguages.Items.First(); @@ -143,6 +141,8 @@ public class QueueManager{ } } + selected.VideoQuality = !string.IsNullOrEmpty(historyEpisode.videoQuality) ? historyEpisode.videoQuality : CrunchyrollManager.Instance.CrunOptions.QualityVideo; + selected.DownloadSubs = historyEpisode.sublist.Count > 0 ? historyEpisode.sublist : CrunchyrollManager.Instance.CrunOptions.DlSubs; Queue.Add(selected); @@ -162,6 +162,12 @@ public class QueueManager{ } } else{ Console.WriteLine("Episode couldn't be added to Queue"); + Console.Error.WriteLine("Episode couldn't be added to Queue - Available dubs/subs: "); + + var languages = sList.EpisodeAndLanguages.Items.Select((a, index) => + $"{(a.IsPremiumOnly ? "+ " : "")}{sList.EpisodeAndLanguages.Langs.ElementAtOrDefault(index).CrLocale ?? "Unknown"}").ToArray(); + + 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)); } } else{ @@ -184,7 +190,7 @@ public class QueueManager{ } - public void CrAddEpMetaToQueue(CrunchyEpMeta epMeta){ + public void CrAddMusicMetaToQueue(CrunchyEpMeta epMeta){ Queue.Add(epMeta); MessageBus.Current.SendMessage(new ToastMessage($"Added episode to the queue", ToastType.Information, 1)); } @@ -249,7 +255,9 @@ public class QueueManager{ } var subLangList = CrunchyrollManager.Instance.History.GetSubList(crunchyEpMeta.ShowId, crunchyEpMeta.SeasonId); - crunchyEpMeta.DownloadSubs = subLangList.Count > 0 ? subLangList : CrunchyrollManager.Instance.CrunOptions.DlSubs; + + crunchyEpMeta.VideoQuality = !string.IsNullOrEmpty(subLangList.videoQuality) ? subLangList.videoQuality : CrunchyrollManager.Instance.CrunOptions.QualityVideo; + crunchyEpMeta.DownloadSubs = subLangList.sublist.Count > 0 ? subLangList.sublist : CrunchyrollManager.Instance.CrunOptions.DlSubs; Queue.Add(crunchyEpMeta); diff --git a/CRD/Styling/ControlsGalleryStyles.axaml b/CRD/Styling/ControlsGalleryStyles.axaml index 50cb62c..d587586 100644 --- a/CRD/Styling/ControlsGalleryStyles.axaml +++ b/CRD/Styling/ControlsGalleryStyles.axaml @@ -2,9 +2,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ui="using:FluentAvalonia.UI.Controls" xmlns:uip="using:FluentAvalonia.UI.Controls.Primitives"> - - - + + + + @@ -217,8 +227,9 @@ @@ -50,7 +50,7 @@ IsEnabled="{Binding !ProgramManager.FetchingData}"> - + @@ -60,7 +60,7 @@ IsEnabled="{Binding !ProgramManager.FetchingData}"> - + @@ -74,7 +74,7 @@ IsEnabled="{Binding !ProgramManager.FetchingData}"> - + - + - + - + + + + + + + - - - - - + + + - - - + + + + + + + + + + + + + + + + + + - - - - - + + + - - - - - - - - - - - + - - - - - @@ -422,7 +424,7 @@ VerticalAlignment="Center" IsVisible="{Binding EditModeEnabled}"> - + @@ -435,11 +437,51 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -476,7 +518,7 @@ - + @@ -724,7 +766,7 @@ VerticalAlignment="Center" IsVisible="{Binding $parent[ScrollViewer].((history:HistorySeries)DataContext).EditModeEnabled}"> - + @@ -737,11 +779,52 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -778,7 +861,7 @@ - + diff --git a/CRD/Views/MainWindow.axaml b/CRD/Views/MainWindow.axaml index 626ddfa..2a75ebb 100644 --- a/CRD/Views/MainWindow.axaml +++ b/CRD/Views/MainWindow.axaml @@ -10,10 +10,11 @@ x:DataType="vm:MainWindowViewModel" Icon="/Assets/app_icon.ico" Title="Crunchy-Downloader"> - + + @@ -60,6 +61,8 @@ IconSource="Add" /> + diff --git a/CRD/Views/MainWindow.axaml.cs b/CRD/Views/MainWindow.axaml.cs index 0055164..1da74dc 100644 --- a/CRD/Views/MainWindow.axaml.cs +++ b/CRD/Views/MainWindow.axaml.cs @@ -5,7 +5,7 @@ using System.Linq; using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; -using Avalonia.Platform; +using CRD.Downloader; using CRD.Downloader.Crunchyroll; using CRD.Utils; using CRD.Utils.Files; @@ -13,7 +13,6 @@ using CRD.Utils.Structs; using CRD.Utils.Updater; using CRD.ViewModels; using CRD.Views.Utils; -using FluentAvalonia.Core; using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Windowing; using Newtonsoft.Json; @@ -56,14 +55,15 @@ public partial class MainWindow : AppWindow{ private Size _restoreSize; public MainWindow(){ + ProgramManager.Instance.StorageProvider = StorageProvider; AvaloniaXamlLoader.Load(this); InitializeComponent(); - + ExtendClientAreaTitleBarHeightHint = TitleBarHeightAdjustment; TitleBar.Height = TitleBarHeightAdjustment; TitleBar.ExtendsContentIntoTitleBar = true; TitleBar.TitleBarHitTestType = TitleBarHitTestType.Complex; - + Opened += OnOpened; Closing += OnClosing; @@ -83,17 +83,13 @@ public partial class MainWindow : AppWindow{ if (message.Refresh){ navigationStack.Pop(); var viewModel = Activator.CreateInstance(message.ViewModelType); - if (viewModel is SeriesPageViewModel){ - ((SeriesPageViewModel)viewModel).SetStorageProvider(StorageProvider); - } + navigationStack.Push(viewModel); nv.Content = viewModel; } else if (!message.Back && message.ViewModelType != null){ var viewModel = Activator.CreateInstance(message.ViewModelType); - if (viewModel is SeriesPageViewModel){ - ((SeriesPageViewModel)viewModel).SetStorageProvider(StorageProvider); - } + navigationStack.Push(viewModel); nv.Content = viewModel; @@ -143,21 +139,20 @@ public partial class MainWindow : AppWindow{ break; case "History": navView.Content = Activator.CreateInstance(typeof(HistoryPageViewModel)); - if (navView.Content is HistoryPageViewModel){ - ((HistoryPageViewModel)navView.Content).SetStorageProvider(StorageProvider); - } - navigationStack.Clear(); navigationStack.Push(navView.Content); selectedNavVieItem = selectedItem; break; + case "Seasons": + navView.Content = Activator.CreateInstance(typeof(UpcomingPageViewModel)); + selectedNavVieItem = selectedItem; + break; case "Account": navView.Content = Activator.CreateInstance(typeof(AccountPageViewModel)); selectedNavVieItem = selectedItem; break; case "Settings": var viewModel = (SettingsPageViewModel)Activator.CreateInstance(typeof(SettingsPageViewModel)); - viewModel.SetStorageProvider(StorageProvider); navView.Content = viewModel; selectedNavVieItem = selectedItem; break; diff --git a/CRD/Views/SeriesPageView.axaml b/CRD/Views/SeriesPageView.axaml index 6296fbc..bcb1de9 100644 --- a/CRD/Views/SeriesPageView.axaml +++ b/CRD/Views/SeriesPageView.axaml @@ -55,7 +55,7 @@ - + @@ -121,7 +121,7 @@ VerticalAlignment="Center" IsVisible="{Binding EditMode}"> - + @@ -134,11 +134,56 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -175,7 +220,7 @@ - + @@ -426,7 +471,7 @@ VerticalAlignment="Center" IsVisible="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).EditMode}"> - + @@ -439,11 +484,52 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -480,7 +566,7 @@ - + diff --git a/CRD/Views/SeriesPageView.axaml.cs b/CRD/Views/SeriesPageView.axaml.cs index 8863ad2..f26df67 100644 --- a/CRD/Views/SeriesPageView.axaml.cs +++ b/CRD/Views/SeriesPageView.axaml.cs @@ -1,6 +1,4 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Markup.Xaml; +using Avalonia.Controls; namespace CRD.Views; diff --git a/CRD/Views/SettingsPageView.axaml b/CRD/Views/SettingsPageView.axaml index 9c0b5d3..e508918 100644 --- a/CRD/Views/SettingsPageView.axaml +++ b/CRD/Views/SettingsPageView.axaml @@ -4,7 +4,6 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="clr-namespace:CRD.ViewModels" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" - xmlns:structs="clr-namespace:CRD.Utils.Structs" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:DataType="vm:SettingsPageViewModel" x:Class="CRD.Views.SettingsPageView" @@ -14,894 +13,10 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CRD/Views/UpcomingSeasonsPageView.axaml.cs b/CRD/Views/UpcomingSeasonsPageView.axaml.cs new file mode 100644 index 0000000..306381a --- /dev/null +++ b/CRD/Views/UpcomingSeasonsPageView.axaml.cs @@ -0,0 +1,34 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using CRD.Utils.Sonarr; +using CRD.Utils.Structs; +using CRD.ViewModels; + +namespace CRD.Views; + +public partial class UpcomingPageView : UserControl{ + public UpcomingPageView(){ + InitializeComponent(); + } + + private void SelectionChanged(object? sender, SelectionChangedEventArgs e){ + if (DataContext is UpcomingPageViewModel viewModel && sender is ListBox listBox){ + viewModel.SelectionChangedOfSeries((AnilistSeries?)listBox.SelectedItem); + } + } + + private void ScrollViewer_PointerWheelChanged(object sender, Avalonia.Input.PointerWheelEventArgs e){ + if (sender is ScrollViewer scrollViewer){ + // Determine if the ListBox is at its bounds (top or bottom) + bool atTop = scrollViewer.Offset.Y <= 0 && e.Delta.Y > 0; + bool atBottom = scrollViewer.Offset.Y >= scrollViewer.Extent.Height - scrollViewer.Viewport.Height && e.Delta.Y < 0; + + if (atTop || atBottom){ + e.Handled = true; // Stop the event from propagating to the parent + } + } + } + +} \ No newline at end of file diff --git a/CRD/Views/Utils/ContentDialogDropdownSelectView.axaml b/CRD/Views/Utils/ContentDialogDropdownSelectView.axaml new file mode 100644 index 0000000..cf56a08 --- /dev/null +++ b/CRD/Views/Utils/ContentDialogDropdownSelectView.axaml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CRD/Views/Utils/ContentDialogDropdownSelectView.axaml.cs b/CRD/Views/Utils/ContentDialogDropdownSelectView.axaml.cs new file mode 100644 index 0000000..611e8e1 --- /dev/null +++ b/CRD/Views/Utils/ContentDialogDropdownSelectView.axaml.cs @@ -0,0 +1,9 @@ +using Avalonia.Controls; + +namespace CRD.Views.Utils; + +public partial class ContentDialogDropdownSelectView : UserControl{ + public ContentDialogDropdownSelectView(){ + InitializeComponent(); + } +} \ No newline at end of file diff --git a/CRD/Views/Utils/ContentDialogEncodingPresetView.axaml.cs b/CRD/Views/Utils/ContentDialogEncodingPresetView.axaml.cs index c49d6ef..66fbfcf 100644 --- a/CRD/Views/Utils/ContentDialogEncodingPresetView.axaml.cs +++ b/CRD/Views/Utils/ContentDialogEncodingPresetView.axaml.cs @@ -1,6 +1,4 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Markup.Xaml; +using Avalonia.Controls; namespace CRD.Views.Utils; diff --git a/CRD/Views/Utils/ContentDialogInputLoginView.axaml.cs b/CRD/Views/Utils/ContentDialogInputLoginView.axaml.cs index 5e2d439..7bea508 100644 --- a/CRD/Views/Utils/ContentDialogInputLoginView.axaml.cs +++ b/CRD/Views/Utils/ContentDialogInputLoginView.axaml.cs @@ -1,6 +1,4 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Markup.Xaml; +using Avalonia.Controls; namespace CRD.Views.Utils; diff --git a/CRD/Views/Utils/ContentDialogSonarrMatchView.axaml.cs b/CRD/Views/Utils/ContentDialogSonarrMatchView.axaml.cs index 3ac06fc..338dda5 100644 --- a/CRD/Views/Utils/ContentDialogSonarrMatchView.axaml.cs +++ b/CRD/Views/Utils/ContentDialogSonarrMatchView.axaml.cs @@ -1,6 +1,4 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Markup.Xaml; +using Avalonia.Controls; namespace CRD.Views.Utils; diff --git a/CRD/Views/Utils/ContentDialogUpdateView.axaml.cs b/CRD/Views/Utils/ContentDialogUpdateView.axaml.cs index 966b116..8688188 100644 --- a/CRD/Views/Utils/ContentDialogUpdateView.axaml.cs +++ b/CRD/Views/Utils/ContentDialogUpdateView.axaml.cs @@ -1,6 +1,4 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Markup.Xaml; +using Avalonia.Controls; namespace CRD.Views.Utils; diff --git a/CRD/Views/Utils/GeneralSettingsView.axaml b/CRD/Views/Utils/GeneralSettingsView.axaml new file mode 100644 index 0000000..8fec208 --- /dev/null +++ b/CRD/Views/Utils/GeneralSettingsView.axaml @@ -0,0 +1,516 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +