From acdbc7467bd4c11bf04be3b742eb39c176d6151f Mon Sep 17 00:00:00 2001 From: Elwador <75888166+Elwador@users.noreply.github.com> Date: Fri, 12 Jul 2024 04:35:33 +0200 Subject: [PATCH] Add - Add search functionality to the add tab Add - Add custom calendar option to display newly released episodes for the last 6 days and the current day Fix - Fix decryption issue with file paths containing special characters --- CRD/Downloader/CRAuth.cs | 2 +- CRD/Downloader/CrEpisode.cs | 51 +++-- CRD/Downloader/CrSeries.cs | 146 +++++++++----- CRD/Downloader/Crunchyroll.cs | 212 +++++++++++---------- CRD/Utils/Helpers.cs | 62 ++++++ CRD/Utils/Http/HttpClientReq.cs | 1 + CRD/Utils/Structs/CalendarStructs.cs | 2 +- CRD/Utils/Structs/CrBrowseEpisode.cs | 205 ++++++++++++++++++++ CRD/Utils/Structs/CrBrowseSeries.cs | 117 ++++++++++++ CRD/Utils/Structs/CrSearchSeries.cs | 15 ++ CRD/Utils/Structs/CrSeriesSearch.cs | 82 ++++++-- CRD/Utils/Structs/Playback.cs | 44 ++++- CRD/ViewModels/AddDownloadPageViewModel.cs | 133 +++++++++++-- CRD/ViewModels/CalendarPageViewModel.cs | 96 +++++++++- CRD/Views/AddDownloadPageView.axaml | 98 +++++++++- CRD/Views/CalendarPageView.axaml | 5 + 16 files changed, 1049 insertions(+), 222 deletions(-) create mode 100644 CRD/Utils/Structs/CrBrowseEpisode.cs create mode 100644 CRD/Utils/Structs/CrBrowseSeries.cs create mode 100644 CRD/Utils/Structs/CrSearchSeries.cs diff --git a/CRD/Downloader/CRAuth.cs b/CRD/Downloader/CRAuth.cs index e111eef..916a25a 100644 --- a/CRD/Downloader/CRAuth.cs +++ b/CRD/Downloader/CRAuth.cs @@ -138,7 +138,7 @@ public class CrAuth{ } } - public async void LoginWithToken(){ + public async Task LoginWithToken(){ if (crunInstance.Token?.refresh_token == null){ Console.Error.WriteLine("Missing Refresh Token"); return; diff --git a/CRD/Downloader/CrEpisode.cs b/CRD/Downloader/CrEpisode.cs index 5e6d19f..057a150 100644 --- a/CRD/Downloader/CrEpisode.cs +++ b/CRD/Downloader/CrEpisode.cs @@ -17,22 +17,22 @@ namespace CRD.Downloader; public class CrEpisode(){ private readonly Crunchyroll crunInstance = Crunchyroll.Instance; - public async Task ParseEpisodeById(string id, string crLocale,bool forcedLang = false){ + public async Task ParseEpisodeById(string id, string crLocale, bool forcedLang = false){ if (crunInstance.CmsToken?.Cms == null){ Console.Error.WriteLine("Missing CMS Access Token"); return null; } NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query); - + query["preferred_audio_language"] = "ja-JP"; if (!string.IsNullOrEmpty(crLocale)){ query["locale"] = crLocale; if (forcedLang){ - query["force_locale"] = crLocale; + query["force_locale"] = crLocale; } } - + var request = HttpClientReq.CreateRequestMessage($"{Api.Cms}/episodes/{id}", HttpMethod.Get, true, true, query); @@ -59,7 +59,7 @@ public class CrEpisode(){ } - public async Task EpisodeData(CrunchyEpisode dlEpisode,bool updateHistory = false){ + public async Task EpisodeData(CrunchyEpisode dlEpisode, bool updateHistory = false){ bool serieshasversions = true; // Dictionary episodes = new Dictionary(); @@ -158,8 +158,7 @@ public class CrEpisode(){ // var ret = new Dictionary(); var retMeta = new CrunchyEpMeta(); - - + for (int index = 0; index < episodeP.EpisodeAndLanguages.Items.Count; index++){ var item = episodeP.EpisodeAndLanguages.Items[index]; @@ -186,8 +185,10 @@ public class CrEpisode(){ var epMeta = new CrunchyEpMeta(); epMeta.Data = new List{ new(){ MediaId = item.Id, Versions = item.Versions, IsSubbed = item.IsSubbed, IsDubbed = item.IsDubbed } }; - epMeta.SeriesTitle = episodeP.EpisodeAndLanguages.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeriesTitle)).SeriesTitle ?? Regex.Replace(episodeP.EpisodeAndLanguages.Items[0].SeriesTitle, @"\(\w+ Dub\)", "").TrimEnd(); - epMeta.SeasonTitle = episodeP.EpisodeAndLanguages.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeasonTitle)).SeasonTitle ?? Regex.Replace(episodeP.EpisodeAndLanguages.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd(); + epMeta.SeriesTitle = episodeP.EpisodeAndLanguages.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeriesTitle)).SeriesTitle ?? + Regex.Replace(episodeP.EpisodeAndLanguages.Items[0].SeriesTitle, @"\(\w+ Dub\)", "").TrimEnd(); + epMeta.SeasonTitle = episodeP.EpisodeAndLanguages.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeasonTitle)).SeasonTitle ?? + Regex.Replace(episodeP.EpisodeAndLanguages.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd(); epMeta.EpisodeNumber = item.Episode; epMeta.EpisodeTitle = item.Title; epMeta.SeasonId = item.SeasonId; @@ -204,8 +205,8 @@ public class CrEpisode(){ DownloadSpeed = 0 }; epMeta.AvailableSubs = item.SubtitleLocales; - epMeta.Description = item.Description; - + epMeta.Description = item.Description; + if (episodeP.EpisodeAndLanguages.Langs.Count > 0){ epMeta.SelectedDubs = dubLang .Where(language => episodeP.EpisodeAndLanguages.Langs.Any(epLang => epLang.CrLocale == language)) @@ -223,7 +224,6 @@ public class CrEpisode(){ if (retMeta.Data != null){ epMetaData.Lang = episodeP.EpisodeAndLanguages.Langs[index]; retMeta.Data.Add(epMetaData); - } else{ epMetaData.Lang = episodeP.EpisodeAndLanguages.Langs[index]; epMeta.Data[0] = epMetaData; @@ -238,4 +238,31 @@ public class CrEpisode(){ return retMeta; } + + public async Task GetNewEpisodes(string? crLocale){ + + NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query); + + if (!string.IsNullOrEmpty(crLocale)){ + query["locale"] = crLocale; + } + + query["n"] = "200"; + query["sort_by"] = "newly_added"; + query["type"] = "episode"; + + var request = HttpClientReq.CreateRequestMessage($"{Api.Browse}", HttpMethod.Get, true, false, query); + + var response = await HttpClientReq.Instance.SendHttpRequest(request); + + if (!response.IsOk){ + Console.Error.WriteLine("Series Request Failed"); + return null; + } + + CrBrowseEpisodeBase? series = Helpers.Deserialize(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings); + + + return series; + } } \ No newline at end of file diff --git a/CRD/Downloader/CrSeries.cs b/CRD/Downloader/CrSeries.cs index 4c1965b..4530b4a 100644 --- a/CRD/Downloader/CrSeries.cs +++ b/CRD/Downloader/CrSeries.cs @@ -11,17 +11,17 @@ using System.Web; using CRD.Utils; using CRD.Utils.Structs; using CRD.Views; +using DynamicData; using Newtonsoft.Json; using ReactiveUI; namespace CRD.Downloader; public class CrSeries(){ - private readonly Crunchyroll crunInstance = Crunchyroll.Instance; - + public async Task> DownloadFromSeriesId(string id, CrunchyMultiDownload data){ - var series = await ListSeriesId(id, "" ,data); + var series = await ListSeriesId(id, "", data); if (series != null){ var selected = ItemSelectMultiDub(series.Value.Data, data.DubLang, data.But, data.AllEpisodes, data.E); @@ -47,7 +47,7 @@ public class CrSeries(){ foreach (var kvp in eps){ var key = kvp.Key; var episode = kvp.Value; - + for (int index = 0; index < episode.Items.Count; index++){ var item = episode.Items[index]; @@ -55,7 +55,7 @@ public class CrSeries(){ MessageBus.Current.SendMessage(new ToastMessage($"Episode is a premium episode – make sure that you are signed in with an account that has an active premium subscription", ToastType.Error, 3)); continue; } - + if (!dubLang.Contains(episode.Langs[index].CrLocale)) continue; @@ -83,7 +83,7 @@ public class CrSeries(){ epMeta.EpisodeNumber = item.Episode; epMeta.EpisodeTitle = item.Title; epMeta.SeasonId = item.SeasonId; - epMeta.Season = Helpers.ExtractNumberAfterS(item.Identifier) ?? item.SeasonNumber + ""; + epMeta.Season = Helpers.ExtractNumberAfterS(item.Identifier) ?? item.SeasonNumber + ""; epMeta.ShowId = item.SeriesId; epMeta.AbsolutEpisodeNumberE = epNum; epMeta.Image = images[images.Count / 2].FirstOrDefault().Source; @@ -101,7 +101,7 @@ public class CrSeries(){ .Where(language => episode.Langs.Any(epLang => epLang.CrLocale == language)) .ToList(); } - + var epMetaData = epMeta.Data[0]; if (!string.IsNullOrEmpty(item.StreamsLink)){ @@ -133,12 +133,12 @@ public class CrSeries(){ } - public async Task ListSeriesId(string id,string crLocale, CrunchyMultiDownload? data){ + public async Task ListSeriesId(string id, string crLocale, CrunchyMultiDownload? data){ await crunInstance.CrAuth.RefreshToken(true); bool serieshasversions = true; - CrSeriesSearch? parsedSeries = await ParseSeriesById(id,crLocale); // one piece - GRMG8ZQZR + CrSeriesSearch? parsedSeries = await ParseSeriesById(id, crLocale); // one piece - GRMG8ZQZR if (parsedSeries == null){ Console.Error.WriteLine("Parse Data Invalid"); @@ -154,13 +154,12 @@ public class CrSeries(){ var s = result[season][key]; if (data?.S != null && s.Id != data.Value.S) continue; int fallbackIndex = 0; - var seasonData = await GetSeasonDataById(s.Id,""); + var seasonData = await GetSeasonDataById(s.Id, ""); if (seasonData.Data != null){ - if (crunInstance.CrunOptions.History){ crunInstance.CrHistory.UpdateWithSeasonData(seasonData); } - + foreach (var episode in seasonData.Data){ // Prepare the episode array EpisodeAndLanguage item; @@ -285,27 +284,21 @@ public class CrSeries(){ return crunchySeriesList; } - public async Task GetSeasonDataById(string seasonID,string? crLocale,bool forcedLang = false, bool log = false){ + public async Task GetSeasonDataById(string seasonID, string? crLocale, bool forcedLang = false, bool log = false){ CrunchyEpisodeList episodeList = new CrunchyEpisodeList(){ Data = new List(), Total = 0, Meta = new Meta() }; - if (crunInstance.CmsToken?.Cms == null){ - Console.Error.WriteLine("Missing CMS Token"); - return episodeList; - } - NameValueCollection query; if (log){ - query = HttpUtility.ParseQueryString(new UriBuilder().Query); - + query["preferred_audio_language"] = "ja-JP"; if (!string.IsNullOrEmpty(crLocale)){ query["locale"] = crLocale; if (forcedLang){ - query["force_locale"] = crLocale; + query["force_locale"] = crLocale; } } - + var showRequest = HttpClientReq.CreateRequestMessage($"{Api.Cms}/seasons/{seasonID}", HttpMethod.Get, true, true, query); var response = await HttpClientReq.Instance.SendHttpRequest(showRequest); @@ -318,17 +311,17 @@ public class CrSeries(){ } query = HttpUtility.ParseQueryString(new UriBuilder().Query); - + query["preferred_audio_language"] = "ja-JP"; if (!string.IsNullOrEmpty(crLocale)){ query["locale"] = crLocale; if (forcedLang){ - query["force_locale"] = crLocale; + query["force_locale"] = crLocale; } } - - var episodeRequest = HttpClientReq.CreateRequestMessage( $"{Api.Cms}/seasons/{seasonID}/episodes",HttpMethod.Get, true,true,query); - + + var episodeRequest = HttpClientReq.CreateRequestMessage($"{Api.Cms}/seasons/{seasonID}/episodes", HttpMethod.Get, true, true, query); + var episodeRequestResponse = await HttpClientReq.Instance.SendHttpRequest(episodeRequest); if (!episodeRequestResponse.IsOk){ @@ -375,27 +368,21 @@ public class CrSeries(){ return ret; } - - public async Task ParseSeriesById(string id,string? crLocale,bool forced = false){ - if (crunInstance.CmsToken?.Cms == null){ - Console.Error.WriteLine("Missing CMS Access Token"); - return null; - } + public async Task ParseSeriesById(string id, string? crLocale, bool forced = false){ NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query); - + query["preferred_audio_language"] = "ja-JP"; if (!string.IsNullOrEmpty(crLocale)){ query["locale"] = crLocale; if (forced){ - query["force_locale"] = crLocale; + query["force_locale"] = crLocale; } - } - + var request = HttpClientReq.CreateRequestMessage($"{Api.Cms}/series/{id}/seasons", HttpMethod.Get, true, true, query); - + var response = await HttpClientReq.Instance.SendHttpRequest(request); if (!response.IsOk){ @@ -412,26 +399,20 @@ public class CrSeries(){ return seasonsList; } - - public async Task SeriesById(string id,string? crLocale,bool forced = false){ - if (crunInstance.CmsToken?.Cms == null){ - Console.Error.WriteLine("Missing CMS Access Token"); - return null; - } + public async Task SeriesById(string id, string? crLocale, bool forced = false){ NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query); - + query["preferred_audio_language"] = "ja-JP"; if (!string.IsNullOrEmpty(crLocale)){ query["locale"] = crLocale; if (forced){ - query["force_locale"] = crLocale; + query["force_locale"] = crLocale; } - } var request = HttpClientReq.CreateRequestMessage($"{Api.Cms}/series/{id}", HttpMethod.Get, true, true, query); - + var response = await HttpClientReq.Instance.SendHttpRequest(request); if (!response.IsOk){ @@ -448,5 +429,72 @@ public class CrSeries(){ return series; } - + + + public async Task Search(string searchString,string? crLocale){ + + NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query); + + if (!string.IsNullOrEmpty(crLocale)){ + query["locale"] = crLocale; + } + query["q"] = searchString; + query["n"] = "6"; + query["type"] = "top_results"; + + var request = HttpClientReq.CreateRequestMessage($"{Api.Search}", HttpMethod.Get, true, false, query); + + var response = await HttpClientReq.Instance.SendHttpRequest(request); + + if (!response.IsOk){ + Console.Error.WriteLine("Series Request Failed"); + return null; + } + + CrSearchSeriesBase? series = Helpers.Deserialize(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings); + + return series; + } + + public async Task GetAllSeries(string? crLocale){ + CrBrowseSeriesBase? complete = new CrBrowseSeriesBase(); + complete.Data =[]; + + var i = 0; + + do{ + NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query); + + if (!string.IsNullOrEmpty(crLocale)){ + query["locale"] = crLocale; + } + + query["start"] = i + ""; + query["n"] = "50"; + query["sort_by"] = "alphabetical"; + + var request = HttpClientReq.CreateRequestMessage($"{Api.Browse}", HttpMethod.Get, true, false, query); + + var response = await HttpClientReq.Instance.SendHttpRequest(request); + + if (!response.IsOk){ + Console.Error.WriteLine("Series Request Failed"); + return null; + } + + CrBrowseSeriesBase? series = Helpers.Deserialize(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings); + + if (series != null){ + complete.Total = series.Total; + if (series.Data != null) complete.Data.AddRange(series.Data); + } else{ + break; + } + + i += 50; + } while (i < complete.Total); + + + return complete; + } } \ No newline at end of file diff --git a/CRD/Downloader/Crunchyroll.cs b/CRD/Downloader/Crunchyroll.cs index 4fa9434..1546aa5 100644 --- a/CRD/Downloader/Crunchyroll.cs +++ b/CRD/Downloader/Crunchyroll.cs @@ -51,7 +51,19 @@ public class Crunchyroll{ #region Calendar Variables private Dictionary calendar = new(); - private Dictionary calendarLanguage = new(); + private Dictionary calendarLanguage = new(){ + { "en-us", "https://www.crunchyroll.com/simulcastcalendar" }, + { "es", "https://www.crunchyroll.com/es/simulcastcalendar" }, + { "es-es", "https://www.crunchyroll.com/es-es/simulcastcalendar" }, + { "pt-br", "https://www.crunchyroll.com/pt-br/simulcastcalendar" }, + { "pt-pt", "https://www.crunchyroll.com/pt-pt/simulcastcalendar" }, + { "fr", "https://www.crunchyroll.com/fr/simulcastcalendar" }, + { "de", "https://www.crunchyroll.com/de/simulcastcalendar" }, + { "ar", "https://www.crunchyroll.com/ar/simulcastcalendar" }, + { "it", "https://www.crunchyroll.com/it/simulcastcalendar" }, + { "ru", "https://www.crunchyroll.com/ru/simulcastcalendar" }, + { "hi", "https://www.crunchyroll.com/hi/simulcastcalendar" }, + }; #endregion @@ -122,15 +134,7 @@ public class Crunchyroll{ PreferredContentSubtitleLanguage = "de-DE", HasPremium = false, }; - - - if (CfgManager.CheckIfFileExists(CfgManager.PathCrToken)){ - Token = CfgManager.DeserializeFromFile(CfgManager.PathCrToken); - CrAuth.LoginWithToken(); - } else{ - await CrAuth.AuthAnonymous(); - } - + Console.WriteLine($"Can Decrypt: {_widevine.canDecrypt}"); CrunOptions.AutoDownload = false; @@ -163,7 +167,20 @@ public class Crunchyroll{ CrunOptions.History = true; CfgManager.UpdateSettingsFromFile(); + + if (CrunOptions.LogMode){ + CfgManager.EnableLogMode(); + } else{ + CfgManager.DisableLogMode(); + } + if (CfgManager.CheckIfFileExists(CfgManager.PathCrToken)){ + Token = CfgManager.DeserializeFromFile(CfgManager.PathCrToken); + CrAuth.LoginWithToken(); + } else{ + await CrAuth.AuthAnonymous(); + } + if (CrunOptions.History){ if (File.Exists(CfgManager.PathCrHistory)){ HistoryList = JsonConvert.DeserializeObject>(File.ReadAllText(CfgManager.PathCrHistory)) ??[]; @@ -172,25 +189,8 @@ public class Crunchyroll{ RefreshSonarr(); } - if (CrunOptions.LogMode){ - CfgManager.EnableLogMode(); - } else{ - CfgManager.DisableLogMode(); - } - calendarLanguage = new(){ - { "en-us", "https://www.crunchyroll.com/simulcastcalendar" }, - { "es", "https://www.crunchyroll.com/es/simulcastcalendar" }, - { "es-es", "https://www.crunchyroll.com/es-es/simulcastcalendar" }, - { "pt-br", "https://www.crunchyroll.com/pt-br/simulcastcalendar" }, - { "pt-pt", "https://www.crunchyroll.com/pt-pt/simulcastcalendar" }, - { "fr", "https://www.crunchyroll.com/fr/simulcastcalendar" }, - { "de", "https://www.crunchyroll.com/de/simulcastcalendar" }, - { "ar", "https://www.crunchyroll.com/ar/simulcastcalendar" }, - { "it", "https://www.crunchyroll.com/it/simulcastcalendar" }, - { "ru", "https://www.crunchyroll.com/ru/simulcastcalendar" }, - { "hi", "https://www.crunchyroll.com/hi/simulcastcalendar" }, - }; + } public async void RefreshSonarr(){ @@ -300,7 +300,7 @@ public class Crunchyroll{ calEpisode.DateTime = episodeTime; calEpisode.HasPassed = hasPassed; calEpisode.EpisodeName = episodeName; - calEpisode.SeasonUrl = seasonLink; + calEpisode.SeriesUrl = seasonLink; calEpisode.EpisodeUrl = episodeLink; calEpisode.ThumbnailUrl = thumbnailUrl; calEpisode.IsPremiumOnly = isPremiumOnly; @@ -1194,8 +1194,10 @@ public class Crunchyroll{ var keyId = BitConverter.ToString(encryptionKeys[0].KeyID).Replace("-", "").ToLower(); var key = BitConverter.ToString(encryptionKeys[0].Bytes).Replace("-", "").ToLower(); var commandBase = $"--show-progress --key {keyId}:{key}"; - var commandVideo = commandBase + $" \"{tempTsFile}.video.enc.m4s\" \"{tempTsFile}.video.m4s\""; - var commandAudio = commandBase + $" \"{tempTsFile}.audio.enc.m4s\" \"{tempTsFile}.audio.m4s\""; + var tempTsFileName = Path.GetFileName(tempTsFile); + var tempTsFileWorkDir = Path.GetDirectoryName(tempTsFile) ?? CfgManager.PathVIDEOS_DIR; + var commandVideo = commandBase + $" \"{tempTsFileName}.video.enc.m4s\" \"{tempTsFileName}.video.m4s\""; + var commandAudio = commandBase + $" \"{tempTsFileName}.audio.enc.m4s\" \"{tempTsFileName}.audio.m4s\""; if (videoDownloaded){ Console.WriteLine("Started decrypting video"); data.DownloadProgress = new DownloadProgress(){ @@ -1206,7 +1208,7 @@ public class Crunchyroll{ Doing = "Decrypting video" }; Queue.Refresh(); - var decryptVideo = await Helpers.ExecuteCommandAsync("mp4decrypt", CfgManager.PathMP4Decrypt, commandVideo); + var decryptVideo = await Helpers.ExecuteCommandAsyncWorkDir("mp4decrypt", CfgManager.PathMP4Decrypt, commandVideo,tempTsFileWorkDir); if (!decryptVideo.IsOk){ Console.Error.WriteLine($"Decryption failed with exit code {decryptVideo.ErrorCode}"); @@ -1216,40 +1218,46 @@ public class Crunchyroll{ } catch (IOException ex){ Console.WriteLine($"An error occurred: {ex.Message}"); } - } else{ - Console.WriteLine("Decryption done for video"); - if (!options.Nocleanup){ - try{ - if (File.Exists($"{tempTsFile}.video.enc.m4s")){ - File.Delete($"{tempTsFile}.video.enc.m4s"); - } - - if (File.Exists($"{tempTsFile}.video.enc.m4s.resume")){ - File.Delete($"{tempTsFile}.video.enc.m4s.resume"); - } - } catch (Exception ex){ - Console.WriteLine($"Failed to delete file {tempTsFile}.video.enc.m4s. Error: {ex.Message}"); - // Handle exceptions if you need to log them or throw - } - } - - try{ - if (File.Exists($"{tsFile}.video.m4s")){ - File.Delete($"{tsFile}.video.m4s"); - } - - File.Move($"{tempTsFile}.video.m4s", $"{tsFile}.video.m4s"); - } catch (IOException ex){ - Console.WriteLine($"An error occurred: {ex.Message}"); - } - - files.Add(new DownloadedMedia{ - Type = DownloadMediaType.Video, - Path = $"{tsFile}.video.m4s", - Lang = lang.Value, - IsPrimary = isPrimary - }); + return new DownloadResponse{ + Data = files, + Error = dlFailed, + FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(fileDir, fileName)) : "./unknown", + ErrorText = "Decryption failed" + }; } + + Console.WriteLine("Decryption done for video"); + if (!options.Nocleanup){ + try{ + if (File.Exists($"{tempTsFile}.video.enc.m4s")){ + File.Delete($"{tempTsFile}.video.enc.m4s"); + } + + if (File.Exists($"{tempTsFile}.video.enc.m4s.resume")){ + File.Delete($"{tempTsFile}.video.enc.m4s.resume"); + } + } catch (Exception ex){ + Console.WriteLine($"Failed to delete file {tempTsFile}.video.enc.m4s. Error: {ex.Message}"); + // Handle exceptions if you need to log them or throw + } + } + + try{ + if (File.Exists($"{tsFile}.video.m4s")){ + File.Delete($"{tsFile}.video.m4s"); + } + + File.Move($"{tempTsFile}.video.m4s", $"{tsFile}.video.m4s"); + } catch (IOException ex){ + Console.WriteLine($"An error occurred: {ex.Message}"); + } + + files.Add(new DownloadedMedia{ + Type = DownloadMediaType.Video, + Path = $"{tsFile}.video.m4s", + Lang = lang.Value, + IsPrimary = isPrimary + }); } else{ Console.WriteLine("No Video downloaded"); } @@ -1264,7 +1272,7 @@ public class Crunchyroll{ Doing = "Decrypting audio" }; Queue.Refresh(); - var decryptAudio = await Helpers.ExecuteCommandAsync("mp4decrypt", CfgManager.PathMP4Decrypt, commandAudio); + var decryptAudio = await Helpers.ExecuteCommandAsyncWorkDir("mp4decrypt", CfgManager.PathMP4Decrypt, commandAudio,tempTsFileWorkDir); if (!decryptAudio.IsOk){ Console.Error.WriteLine($"Decryption failed with exit code {decryptAudio.ErrorCode}"); @@ -1273,40 +1281,46 @@ public class Crunchyroll{ } catch (IOException ex){ Console.WriteLine($"An error occurred: {ex.Message}"); } - } else{ - Console.WriteLine("Decryption done for audio"); - if (!options.Nocleanup){ - try{ - if (File.Exists($"{tempTsFile}.audio.enc.m4s")){ - File.Delete($"{tempTsFile}.audio.enc.m4s"); - } - - if (File.Exists($"{tempTsFile}.audio.enc.m4s.resume")){ - File.Delete($"{tempTsFile}.audio.enc.m4s.resume"); - } - } catch (Exception ex){ - Console.WriteLine($"Failed to delete file {tempTsFile}.audio.enc.m4s. Error: {ex.Message}"); - // Handle exceptions if you need to log them or throw - } - } - - try{ - if (File.Exists($"{tsFile}.audio.m4s")){ - File.Delete($"{tsFile}.audio.m4s"); - } - - File.Move($"{tempTsFile}.audio.m4s", $"{tsFile}.audio.m4s"); - } catch (IOException ex){ - Console.WriteLine($"An error occurred: {ex.Message}"); - } - - files.Add(new DownloadedMedia{ - Type = DownloadMediaType.Audio, - Path = $"{tsFile}.audio.m4s", - Lang = lang.Value, - IsPrimary = isPrimary - }); + return new DownloadResponse{ + Data = files, + Error = dlFailed, + FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(fileDir, fileName)) : "./unknown", + ErrorText = "Decryption failed" + }; } + + Console.WriteLine("Decryption done for audio"); + if (!options.Nocleanup){ + try{ + if (File.Exists($"{tempTsFile}.audio.enc.m4s")){ + File.Delete($"{tempTsFile}.audio.enc.m4s"); + } + + if (File.Exists($"{tempTsFile}.audio.enc.m4s.resume")){ + File.Delete($"{tempTsFile}.audio.enc.m4s.resume"); + } + } catch (Exception ex){ + Console.WriteLine($"Failed to delete file {tempTsFile}.audio.enc.m4s. Error: {ex.Message}"); + // Handle exceptions if you need to log them or throw + } + } + + try{ + if (File.Exists($"{tsFile}.audio.m4s")){ + File.Delete($"{tsFile}.audio.m4s"); + } + + File.Move($"{tempTsFile}.audio.m4s", $"{tsFile}.audio.m4s"); + } catch (IOException ex){ + Console.WriteLine($"An error occurred: {ex.Message}"); + } + + files.Add(new DownloadedMedia{ + Type = DownloadMediaType.Audio, + Path = $"{tsFile}.audio.m4s", + Lang = lang.Value, + IsPrimary = isPrimary + }); } else{ Console.WriteLine("No Audio downloaded"); } diff --git a/CRD/Utils/Helpers.cs b/CRD/Utils/Helpers.cs index cd722ec..3979a10 100644 --- a/CRD/Utils/Helpers.cs +++ b/CRD/Utils/Helpers.cs @@ -3,9 +3,11 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; +using System.Net.Http; using System.Runtime.Serialization; using System.Text.RegularExpressions; using System.Threading.Tasks; +using Avalonia.Media.Imaging; using Newtonsoft.Json; namespace CRD.Utils; @@ -193,6 +195,47 @@ public class Helpers{ return (IsOk: false, ErrorCode: -1); } } + + public static async Task<(bool IsOk, int ErrorCode)> ExecuteCommandAsyncWorkDir(string type, string bin, string command,string workingDir){ + try{ + using (var process = new Process()){ + process.StartInfo.WorkingDirectory = workingDir; + process.StartInfo.FileName = bin; + process.StartInfo.Arguments = command; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + process.StartInfo.UseShellExecute = false; + process.StartInfo.CreateNoWindow = true; + + process.OutputDataReceived += (sender, e) => { + if (!string.IsNullOrEmpty(e.Data)){ + Console.WriteLine(e.Data); + } + }; + + process.ErrorDataReceived += (sender, e) => { + if (!string.IsNullOrEmpty(e.Data)){ + Console.WriteLine($"{e.Data}"); + } + }; + + process.Start(); + + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + await process.WaitForExitAsync(); + + // Define success condition more appropriately based on the application + bool isSuccess = process.ExitCode == 0; + + return (IsOk: isSuccess, ErrorCode: process.ExitCode); + } + } catch (Exception ex){ + Console.Error.WriteLine($"An error occurred: {ex.Message}"); + return (IsOk: false, ErrorCode: -1); + } + } public static double CalculateCosineSimilarity(string text1, string text2){ var vector1 = ComputeWordFrequency(text1); @@ -272,4 +315,23 @@ public class Helpers{ return null; } } + + + public static async Task LoadImage(string imageUrl){ + try{ + using (var client = new HttpClient()){ + var response = await client.GetAsync(imageUrl); + response.EnsureSuccessStatusCode(); + using (var stream = await response.Content.ReadAsStreamAsync()){ + return new Bitmap(stream); + } + } + } catch (Exception ex){ + // Handle exceptions + Console.Error.WriteLine("Failed to load image: " + ex.Message); + } + + return null; + } + } \ No newline at end of file diff --git a/CRD/Utils/Http/HttpClientReq.cs b/CRD/Utils/Http/HttpClientReq.cs index 3cb7455..a77266b 100644 --- a/CRD/Utils/Http/HttpClientReq.cs +++ b/CRD/Utils/Http/HttpClientReq.cs @@ -123,6 +123,7 @@ public static class Api{ public static readonly string BetaProfile = ApiBeta + "/accounts/v1/me/profile"; public static readonly string BetaCmsToken = ApiBeta + "/index/v2"; public static readonly string Search = ApiBeta + "/content/v2/discover/search"; + public static readonly string Browse = ApiBeta + "/content/v2/discover/browse"; public static readonly string Cms = ApiBeta + "/content/v2/cms"; public static readonly string BetaBrowse = ApiBeta + "/content/v1/browse"; public static readonly string BetaCms = ApiBeta + "/cms/v2"; diff --git a/CRD/Utils/Structs/CalendarStructs.cs b/CRD/Utils/Structs/CalendarStructs.cs index 7444790..1a93b01 100644 --- a/CRD/Utils/Structs/CalendarStructs.cs +++ b/CRD/Utils/Structs/CalendarStructs.cs @@ -26,7 +26,7 @@ public partial class CalendarEpisode : INotifyPropertyChanged{ public DateTime? DateTime{ get; set; } public bool? HasPassed{ get; set; } public string? EpisodeName{ get; set; } - public string? SeasonUrl{ get; set; } + public string? SeriesUrl{ get; set; } public string? EpisodeUrl{ get; set; } public string? ThumbnailUrl{ get; set; } public Bitmap? ImageBitmap{ get; set; } diff --git a/CRD/Utils/Structs/CrBrowseEpisode.cs b/CRD/Utils/Structs/CrBrowseEpisode.cs new file mode 100644 index 0000000..b0c4505 --- /dev/null +++ b/CRD/Utils/Structs/CrBrowseEpisode.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using Avalonia.Media.Imaging; +using Newtonsoft.Json; + +namespace CRD.Utils.Structs; + +public class CrBrowseEpisodeBase{ + public int Total{ get; set; } + public List? Data{ get; set; } + public Meta Meta{ get; set; } +} + +public class CrBrowseEpisode : INotifyPropertyChanged{ + [JsonProperty("external_id")] + public string? ExternalId{ get; set; } + + [JsonProperty("last_public")] + public DateTime LastPublic{ get; set; } + + public string? Description{ get; set; } + + public bool New{ get; set; } + + [JsonProperty("linked_resource_key")] + public string? LinkedResourceKey{ get; set; } + + [JsonProperty("slug_title")] + public string? SlugTitle{ get; set; } + + public string? Title{ get; set; } + + [JsonProperty("promo_title")] + public string? PromoTitle{ get; set; } + + [JsonProperty("episode_metadata")] + public CrBrowseEpisodeMetaData EpisodeMetadata{ get; set; } + + public string? Id{ get; set; } + + public Images Images{ get; set; } + + [JsonProperty("promo_description")] + public string? PromoDescription{ get; set; } + + public string? Slug{ get; set; } + + public string? Type{ get; set; } + + [JsonProperty("channel_id")] + public string? ChannelId{ get; set; } + + [JsonProperty("streams_link")] + public string? StreamsLink{ get; set; } + + [JsonIgnore] + public Bitmap? ImageBitmap{ get; set; } + + public event PropertyChangedEventHandler? PropertyChanged; + public async void LoadImage(string url){ + ImageBitmap = await Helpers.LoadImage(url); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ImageBitmap))); + } + +} + +public class CrBrowseEpisodeMetaData{ + [JsonProperty("audio_locale")] + public Locale? AudioLocale{ get; set; } + + [JsonProperty("content_descriptors")] + public List? ContentDescriptors{ get; set; } + + [JsonProperty("availability_notes")] + public string? AvailabilityNotes{ get; set; } + + public string? Episode{ get; set; } + + [JsonProperty("episode_air_date")] + public DateTime EpisodeAirDate{ get; set; } + + [JsonProperty("episode_number")] + public int EpisodeCount{ get; set; } + + [JsonProperty("duration_ms")] + public int DurationMs{ get; set; } + + [JsonProperty("extended_maturity_rating")] + public Dictionary? + ExtendedMaturityRating{ get; set; } + + [JsonProperty("is_dubbed")] + public bool IsDubbed{ get; set; } + + [JsonProperty("is_mature")] + public bool IsMature{ get; set; } + + [JsonProperty("is_subbed")] + public bool IsSubbed{ get; set; } + + [JsonProperty("mature_blocked")] + public bool MatureBlocked{ get; set; } + + [JsonProperty("is_premium_only")] + public bool IsPremiumOnly{ get; set; } + + [JsonProperty("is_clip")] + public bool IsClip{ get; set; } + + [JsonProperty("maturity_ratings")] + public List? MaturityRatings{ get; set; } + + [JsonProperty("season_number")] + public double SeasonNumber{ get; set; } + + [JsonProperty("season_sequence_number")] + public double SeasonSequenceNumber{ get; set; } + + [JsonProperty("sequence_number")] + public double SequenceNumber{ get; set; } + + [JsonProperty("upload_date")] + public DateTime? UploadDate{ get; set; } + + [JsonProperty("subtitle_locales")] + public List? SubtitleLocales{ get; set; } + + [JsonProperty("premium_available_date")] + public DateTime? PremiumAvailableDate{ get; set; } + + + [JsonProperty("availability_ends")] + public DateTime? AvailabilityEnds{ get; set; } + + + [JsonProperty("availability_starts")] + public DateTime? AvailabilityStarts{ get; set; } + + + [JsonProperty("free_available_date")] + public DateTime? FreeAvailableDate{ get; set; } + + [JsonProperty("identifier")] + public string? Identifier{ get; set; } + + [JsonProperty("season_id")] + public string? SeasonId{ get; set; } + + [JsonProperty("series_id")] + public string? SeriesId{ get; set; } + + [JsonProperty("season_display_number")] + public string? SeasonDisplayNumber{ get; set; } + + [JsonProperty("eligible_region")] + public string? EligibleRegion{ get; set; } + + [JsonProperty("available_date")] + public DateTime? AvailableDate{ get; set; } + + [JsonProperty("premium_date")] + public DateTime? PremiumDate{ get; set; } + + [JsonProperty("available_offline")] + public bool AvailableOffline{ get; set; } + + [JsonProperty("closed_captions_available")] + public bool ClosedCaptionsAvailable{ get; set; } + + [JsonProperty("season_slug_title")] + public string? SeasonSlugTitle{ get; set; } + + [JsonProperty("season_title")] + public string? SeasonTitle{ get; set; } + + [JsonProperty("series_slug_title")] + public string? SeriesSlugTitle{ get; set; } + + [JsonProperty("series_title")] + public string? SeriesTitle{ get; set; } + + [JsonProperty("versions")] + public List? versions{ get; set; } + +} + +public struct CrBrowseEpisodeVersion{ + [JsonProperty("audio_locale")] + public Locale? AudioLocale{ get; set; } + + public string? Guid{ get; set; } + public bool? Original{ get; set; } + public string? Variant{ get; set; } + + [JsonProperty("season_guid")] + public string? SeasonGuid{ get; set; } + + [JsonProperty("media_guid")] + public string? MediaGuid{ get; set; } + + [JsonProperty("is_premium_only")] + public bool? IsPremiumOnly{ get; set; } + +} \ No newline at end of file diff --git a/CRD/Utils/Structs/CrBrowseSeries.cs b/CRD/Utils/Structs/CrBrowseSeries.cs new file mode 100644 index 0000000..e8a2447 --- /dev/null +++ b/CRD/Utils/Structs/CrBrowseSeries.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using Avalonia.Media.Imaging; +using Newtonsoft.Json; + +namespace CRD.Utils.Structs; + +public class CrBrowseSeriesBase{ + public int Total{ get; set; } + public List? Data{ get; set; } + public Meta Meta{ get; set; } +} + +public class CrBrowseSeries : INotifyPropertyChanged{ + [JsonProperty("external_id")] + public string? ExternalId{ get; set; } + + [JsonProperty("last_public")] + public DateTime LastPublic{ get; set; } + + public string? Description{ get; set; } + + public bool New{ get; set; } + + [JsonProperty("linked_resource_key")] + public string? LinkedResourceKey{ get; set; } + + [JsonProperty("slug_title")] + public string? SlugTitle{ get; set; } + + public string? Title{ get; set; } + + [JsonProperty("promo_title")] + public string? PromoTitle{ get; set; } + + [JsonProperty("series_metadata")] + public CrBrowseSeriesMetaData SeriesMetadata{ get; set; } + + public string? Id{ get; set; } + + public Images Images{ get; set; } + + [JsonProperty("promo_description")] + public string? PromoDescription{ get; set; } + + public string? Slug{ get; set; } + + public string? Type{ get; set; } + + [JsonProperty("channel_id")] + public string? ChannelId{ get; set; } + + [JsonIgnore] + public Bitmap? ImageBitmap{ get; set; } + + public event PropertyChangedEventHandler? PropertyChanged; + public async void LoadImage(string url){ + ImageBitmap = await Helpers.LoadImage(url); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ImageBitmap))); + } + +} + +public class CrBrowseSeriesMetaData{ + [JsonProperty("audio_locales")] + public List? AudioLocales{ get; set; } + + [JsonProperty("awards")] + public List awards{ get; set; } + + [JsonProperty("availability_notes")] + public string? AvailabilityNotes{ get; set; } + + [JsonProperty("content_descriptors")] + public List? ContentDescriptors{ get; set; } + + [JsonProperty("episode_count")] + public int EpisodeCount{ get; set; } + + [JsonProperty("extended_description")] + public string? ExtendedDescription{ get; set; } + + [JsonProperty("extended_maturity_rating")] + public Dictionary? + ExtendedMaturityRating{ get; set; } + + [JsonProperty("is_dubbed")] + public bool IsDubbed{ get; set; } + + [JsonProperty("is_mature")] + public bool IsMature{ get; set; } + + [JsonProperty("is_simulcast")] + public bool IsSimulcast{ get; set; } + + [JsonProperty("is_subbed")] + public bool IsSubbed{ get; set; } + + [JsonProperty("mature_blocked")] + public bool MatureBlocked{ get; set; } + + [JsonProperty("maturity_ratings")] + public List? MaturityRatings{ get; set; } + + [JsonProperty("season_count")] + public int SeasonCount{ get; set; } + + [JsonProperty("series_launch_year")] + public int SeriesLaunchYear{ get; set; } + + [JsonProperty("subtitle_locales")] + public List? SubtitleLocales{ get; set; } + + [JsonProperty("tenant_categories")] + public List? TenantCategories{ get; set; } +} \ No newline at end of file diff --git a/CRD/Utils/Structs/CrSearchSeries.cs b/CRD/Utils/Structs/CrSearchSeries.cs new file mode 100644 index 0000000..8f19b12 --- /dev/null +++ b/CRD/Utils/Structs/CrSearchSeries.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace CRD.Utils.Structs; + +public class CrSearchSeries{ + public int count{ get; set; } + public List? Items{ get; set; } + public string? type{ get; set; } +} + +public class CrSearchSeriesBase{ + public int Total{ get; set; } + public List? Data{ get; set; } + public Meta Meta{ get; set; } +} \ No newline at end of file diff --git a/CRD/Utils/Structs/CrSeriesSearch.cs b/CRD/Utils/Structs/CrSeriesSearch.cs index 60f455a..0f707b0 100644 --- a/CRD/Utils/Structs/CrSeriesSearch.cs +++ b/CRD/Utils/Structs/CrSeriesSearch.cs @@ -11,48 +11,90 @@ public class CrSeriesSearch{ public struct SeriesSearchItem{ public string Description{ get; set; } - [JsonProperty("seo_description")] public string SeoDescription{ get; set; } - [JsonProperty("number_of_episodes")] public int NumberOfEpisodes{ get; set; } - [JsonProperty("is_dubbed")] public bool IsDubbed{ get; set; } + + [JsonProperty("seo_description")] + public string SeoDescription{ get; set; } + + [JsonProperty("number_of_episodes")] + public int NumberOfEpisodes{ get; set; } + + [JsonProperty("is_dubbed")] + public bool IsDubbed{ get; set; } + public string Identifier{ get; set; } - [JsonProperty("channel_id")] public string ChannelId{ get; set; } - [JsonProperty("slug_title")] public string SlugTitle{ get; set; } + + [JsonProperty("channel_id")] + public string ChannelId{ get; set; } + + [JsonProperty("slug_title")] + public string SlugTitle{ get; set; } [JsonProperty("season_sequence_number")] public int SeasonSequenceNumber{ get; set; } - [JsonProperty("season_tags")] public List SeasonTags{ get; set; } + [JsonProperty("season_tags")] + public List SeasonTags{ get; set; } [JsonProperty("extended_maturity_rating")] public Dictionary ExtendedMaturityRating{ get; set; } - [JsonProperty("is_mature")] public bool IsMature{ get; set; } - [JsonProperty("audio_locale")] public string AudioLocale{ get; set; } - [JsonProperty("season_number")] public int SeasonNumber{ get; set; } + [JsonProperty("is_mature")] + public bool IsMature{ get; set; } + + [JsonProperty("audio_locale")] + public string AudioLocale{ get; set; } + + [JsonProperty("season_number")] + public int SeasonNumber{ get; set; } + public Dictionary Images{ get; set; } - [JsonProperty("mature_blocked")] public bool MatureBlocked{ get; set; } + + [JsonProperty("mature_blocked")] + public bool MatureBlocked{ get; set; } + public List? Versions{ get; set; } public string Title{ get; set; } - [JsonProperty("is_subbed")] public bool IsSubbed{ get; set; } + + [JsonProperty("is_subbed")] + public bool IsSubbed{ get; set; } + public string Id{ get; set; } - [JsonProperty("audio_locales")] public List AudioLocales{ get; set; } - [JsonProperty("subtitle_locales")] public List SubtitleLocales{ get; set; } - [JsonProperty("availability_notes")] public string AvailabilityNotes{ get; set; } - [JsonProperty("series_id")] public string SeriesId{ get; set; } + + [JsonProperty("audio_locales")] + public List AudioLocales{ get; set; } + + [JsonProperty("subtitle_locales")] + public List SubtitleLocales{ get; set; } + + [JsonProperty("availability_notes")] + public string AvailabilityNotes{ get; set; } + + [JsonProperty("series_id")] + public string SeriesId{ get; set; } [JsonProperty("season_display_number")] public string SeasonDisplayNumber{ get; set; } - [JsonProperty("is_complete")] public bool IsComplete{ get; set; } + [JsonProperty("is_complete")] + public bool IsComplete{ get; set; } + public List Keywords{ get; set; } - [JsonProperty("maturity_ratings")] public List MaturityRatings{ get; set; } - [JsonProperty("is_simulcast")] public bool IsSimulcast{ get; set; } - [JsonProperty("seo_title")] public string SeoTitle{ get; set; } + + [JsonProperty("maturity_ratings")] + public List MaturityRatings{ get; set; } + + [JsonProperty("is_simulcast")] + public bool IsSimulcast{ get; set; } + + [JsonProperty("seo_title")] + public string SeoTitle{ get; set; } } public struct Version{ - [JsonProperty("audio_locale")] public string? AudioLocale{ get; set; } + [JsonProperty("audio_locale")] + public string? AudioLocale{ get; set; } + public string? Guid{ get; set; } public bool? Original{ get; set; } public string? Variant{ get; set; } diff --git a/CRD/Utils/Structs/Playback.cs b/CRD/Utils/Structs/Playback.cs index 2944b34..02e5254 100644 --- a/CRD/Utils/Structs/Playback.cs +++ b/CRD/Utils/Structs/Playback.cs @@ -10,20 +10,34 @@ public class PlaybackData{ } public class StreamDetails{ - [JsonProperty("hardsub_locale")] public Locale? HardsubLocale{ get; set; } + [JsonProperty("hardsub_locale")] + public Locale? HardsubLocale{ get; set; } + public string? Url{ get; set; } - [JsonProperty("hardsub_lang")] public string? HardsubLang{ get; set; } - [JsonProperty("audio_lang")] public string? AudioLang{ get; set; } + + [JsonProperty("hardsub_lang")] + public string? HardsubLang{ get; set; } + + [JsonProperty("audio_lang")] + public string? AudioLang{ get; set; } + public string? Type{ get; set; } } public class PlaybackMeta{ - [JsonProperty("media_id")] public string? MediaId{ get; set; } + [JsonProperty("media_id")] + public string? MediaId{ get; set; } + public Subtitles? Subtitles{ get; set; } public List? Bifs{ get; set; } public List? Versions{ get; set; } - [JsonProperty("audio_locale")] public Locale? AudioLocale{ get; set; } - [JsonProperty("closed_captions")] public Subtitles? ClosedCaptions{ get; set; } + + [JsonProperty("audio_locale")] + public Locale? AudioLocale{ get; set; } + + [JsonProperty("closed_captions")] + public Subtitles? ClosedCaptions{ get; set; } + public Dictionary? Captions{ get; set; } } @@ -38,12 +52,22 @@ public class CrunchyStreams : Dictionary; public class Subtitles : Dictionary; public class PlaybackVersion{ - [JsonProperty("audio_locale")] public Locale AudioLocale{ get; set; } // Assuming Locale is defined elsewhere + [JsonProperty("audio_locale")] + public Locale AudioLocale{ get; set; } // Assuming Locale is defined elsewhere + public string? Guid{ get; set; } - [JsonProperty("is_premium_only")] public bool IsPremiumOnly{ get; set; } - [JsonProperty("media_guid")] public string? MediaGuid{ get; set; } + + [JsonProperty("is_premium_only")] + public bool IsPremiumOnly{ get; set; } + + [JsonProperty("media_guid")] + public string? MediaGuid{ get; set; } + public bool Original{ get; set; } - [JsonProperty("season_guid")] public string? SeasonGuid{ get; set; } + + [JsonProperty("season_guid")] + public string? SeasonGuid{ get; set; } + public string? Variant{ get; set; } } diff --git a/CRD/ViewModels/AddDownloadPageViewModel.cs b/CRD/ViewModels/AddDownloadPageViewModel.cs index d60c47a..d8c5aba 100644 --- a/CRD/ViewModels/AddDownloadPageViewModel.cs +++ b/CRD/ViewModels/AddDownloadPageViewModel.cs @@ -2,8 +2,11 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; using System.Net.Http; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Media.Imaging; @@ -13,6 +16,7 @@ using CRD.Downloader; using CRD.Utils; using CRD.Utils.Structs; using CRD.Views; +using DynamicData; using ReactiveUI; @@ -37,9 +41,22 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ [ObservableProperty] public bool _showLoading = false; + [ObservableProperty] + public bool _searchEnabled = false; + + [ObservableProperty] + public bool _searchVisible = true; + + [ObservableProperty] + public bool _searchPopupVisible = false; + public ObservableCollection Items{ get; } = new(); + public ObservableCollection SearchItems{ get; set; } = new(); public ObservableCollection SelectedItems{ get; } = new(); + [ObservableProperty] + public CrBrowseSeries _selectedSearchItem; + [ObservableProperty] public ComboBoxItem _currentSelectedSeason; @@ -51,26 +68,75 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ private CrunchySeriesList? currentSeriesList; + private readonly SemaphoreSlim _updateSearchSemaphore = new SemaphoreSlim(1, 1); + public AddDownloadPageViewModel(){ SelectedItems.CollectionChanged += OnSelectedItemsChanged; } + private async Task UpdateSearch(string value){ + var searchResults = await Crunchyroll.Instance.CrSeries.Search(value, ""); + var searchItems = searchResults?.Data?.First().Items; + SearchItems.Clear(); + if (searchItems is{ Count: > 0 }){ + foreach (var episode in searchItems){ + if (episode.ImageBitmap == null){ + if (episode.Images.PosterTall != null){ + var posterTall = episode.Images.PosterTall.First(); + var imageUrl = posterTall.Find(ele => ele.Height == 180).Source + ?? (posterTall.Count >= 2 ? posterTall[1].Source : posterTall.FirstOrDefault().Source); + episode.LoadImage(imageUrl); + } + } + + SearchItems.Add(episode); + } + + SearchPopupVisible = true; + RaisePropertyChanged(nameof(SearchItems)); + RaisePropertyChanged(nameof(SearchVisible)); + return; + } + + SearchPopupVisible = false; + RaisePropertyChanged(nameof(SearchVisible)); + SearchItems.Clear(); + } + partial void OnUrlInputChanged(string value){ - if (UrlInput.Length > 9){ + if (SearchEnabled){ + UpdateSearch(value); + ButtonText = "Select Searched Series"; + ButtonEnabled = false; + } else if (UrlInput.Length > 9){ if (UrlInput.Contains("/watch/concert/") || UrlInput.Contains("/artist/")){ MessageBus.Current.SendMessage(new ToastMessage("Concerts / Artists not implemented yet", ToastType.Error, 1)); } else if (UrlInput.Contains("/watch/")){ //Episode ButtonText = "Add Episode to Queue"; ButtonEnabled = true; + SearchVisible = false; } else if (UrlInput.Contains("/series/")){ //Series ButtonText = "List Episodes"; ButtonEnabled = true; + SearchVisible = false; } else{ ButtonEnabled = false; + SearchVisible = true; } + } else{ + ButtonText = "Enter Url"; + ButtonEnabled = false; + SearchVisible = true; + } + } + + partial void OnSearchEnabledChanged(bool value){ + if (SearchEnabled){ + ButtonText = "Select Searched Series"; + ButtonEnabled = false; } else{ ButtonText = "Enter Url"; ButtonEnabled = false; @@ -106,6 +172,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ AddAllEpisodes = false; ButtonText = "Enter Url"; ButtonEnabled = false; + SearchVisible = true; } else if (UrlInput.Length > 9){ episodesBySeason.Clear(); SeasonList.Clear(); @@ -119,7 +186,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ if (match.Success){ var locale = match.Groups[1].Value; // Capture the locale part var id = match.Groups[2].Value; // Capture the ID part - Crunchyroll.Instance.AddEpisodeToQue(id, Languages.Locale2language(locale).CrLocale, Crunchyroll.Instance.CrunOptions.DubLang,true); + Crunchyroll.Instance.AddEpisodeToQue(id, Languages.Locale2language(locale).CrLocale, Crunchyroll.Instance.CrunOptions.DubLang, true); UrlInput = ""; selectedEpisodes.Clear(); SelectedItems.Clear(); @@ -208,7 +275,43 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ } } - async partial void OnCurrentSelectedSeasonChanged(ComboBoxItem? value){ + async partial void OnSelectedSearchItemChanged(CrBrowseSeries value){ + if (value == null){ + return; + } + + SearchPopupVisible = false; + RaisePropertyChanged(nameof(SearchVisible)); + SearchItems.Clear(); + SearchVisible = false; + ButtonEnabled = false; + ShowLoading = true; + var list = await Crunchyroll.Instance.CrSeries.ListSeriesId(value.Id, "", new CrunchyMultiDownload(Crunchyroll.Instance.CrunOptions.DubLang, true)); + ShowLoading = false; + if (list != null){ + currentSeriesList = list; + foreach (var episode in currentSeriesList.Value.List){ + if (episodesBySeason.ContainsKey("S" + episode.Season)){ + episodesBySeason["S" + episode.Season].Add(new ItemModel(episode.Img, episode.Description, episode.Time, episode.Name, "S" + episode.Season, "E" + episode.EpisodeNum, episode.E, + episode.Lang)); + } else{ + episodesBySeason.Add("S" + episode.Season, new List{ + new ItemModel(episode.Img, episode.Description, episode.Time, episode.Name, "S" + episode.Season, "E" + episode.EpisodeNum, episode.E, episode.Lang) + }); + SeasonList.Add(new ComboBoxItem{ Content = "S" + episode.Season }); + } + } + + CurrentSelectedSeason = SeasonList[0]; + ButtonEnabled = false; + AllButtonEnabled = true; + ButtonText = "Select Episodes"; + } else{ + ButtonEnabled = true; + } + } + + partial void OnCurrentSelectedSeasonChanged(ComboBoxItem? value){ if (value == null){ return; } @@ -218,7 +321,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ if (episodesBySeason.TryGetValue(key, out var season)){ foreach (var episode in season){ if (episode.ImageBitmap == null){ - await episode.LoadImage(); + episode.LoadImage(episode.ImageUrl); Items.Add(episode); if (selectedEpisodes.Contains(episode.AbsolutNum)){ SelectedItems.Add(episode); @@ -234,7 +337,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ } } -public class ItemModel(string imageUrl, string description, string time, string title, string season, string episode, string absolutNum, List availableAudios){ +public class ItemModel(string imageUrl, string description, string time, string title, string season, string episode, string absolutNum, List availableAudios) : INotifyPropertyChanged{ public string ImageUrl{ get; set; } = imageUrl; public Bitmap? ImageBitmap{ get; set; } public string Title{ get; set; } = title; @@ -248,19 +351,11 @@ public class ItemModel(string imageUrl, string description, string time, string public string TitleFull{ get; set; } = season + episode + " - " + title; public List AvailableAudios{ get; set; } = availableAudios; - - public async Task LoadImage(){ - try{ - using (var client = new HttpClient()){ - var response = await client.GetAsync(ImageUrl); - response.EnsureSuccessStatusCode(); - using (var stream = await response.Content.ReadAsStreamAsync()){ - ImageBitmap = new Bitmap(stream); - } - } - } catch (Exception ex){ - // Handle exceptions - Console.Error.WriteLine("Failed to load image: " + ex.Message); - } + + public event PropertyChangedEventHandler? PropertyChanged; + public async void LoadImage(string url){ + ImageBitmap = await Helpers.LoadImage(url); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ImageBitmap))); } + } \ No newline at end of file diff --git a/CRD/ViewModels/CalendarPageViewModel.cs b/CRD/ViewModels/CalendarPageViewModel.cs index 081ed36..5079f97 100644 --- a/CRD/ViewModels/CalendarPageViewModel.cs +++ b/CRD/ViewModels/CalendarPageViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using Avalonia.Controls; @@ -14,8 +15,14 @@ namespace CRD.ViewModels; public partial class CalendarPageViewModel : ViewModelBase{ public ObservableCollection CalendarDays{ get; set; } - [ObservableProperty] private ComboBoxItem? _currentCalendarLanguage; - [ObservableProperty] private bool? _showLoading = false; + [ObservableProperty] + private ComboBoxItem? _currentCalendarLanguage; + + [ObservableProperty] + private bool _showLoading = false; + + [ObservableProperty] + private bool _customCalendar = false; public ObservableCollection CalendarLanguage{ get; } = new(){ new ComboBoxItem(){ Content = "en-us" }, @@ -68,6 +75,7 @@ public partial class CalendarPageViewModel : ViewModelBase{ ShowLoading = false; return; } + currentWeek = week; CalendarDays.Clear(); CalendarDays.AddRange(week.CalendarDays); @@ -95,6 +103,12 @@ public partial class CalendarPageViewModel : ViewModelBase{ [RelayCommand] public void Refresh(){ + + if (CustomCalendar){ + BuildCustomCalendar(); + return; + } + string mondayDate; if (currentWeek is{ FirstDayOfWeekString: not null }){ @@ -140,4 +154,82 @@ public partial class CalendarPageViewModel : ViewModelBase{ CfgManager.WriteSettingsToFile(); } } + + partial void OnCustomCalendarChanged(bool value){ + if (CustomCalendar){ + BuildCustomCalendar(); + } + } + + private async void BuildCustomCalendar(){ + ShowLoading = true; + + var newEpisodesBase = await Crunchyroll.Instance.CrEpisode.GetNewEpisodes(Crunchyroll.Instance.CrunOptions.HistoryLang); + + CalendarWeek week = new CalendarWeek(); + week.CalendarDays = new List(); + + DateTime today = DateTime.Now; + + 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(); + + week.CalendarDays.Add(calDay); + } + + week.CalendarDays.Reverse(); + + if (newEpisodesBase is{ Data.Count: > 0 }){ + var newEpisodes = newEpisodesBase.Data; + + foreach (var crBrowseEpisode in newEpisodes){ + var episodeAirDate = crBrowseEpisode.EpisodeMetadata.EpisodeAirDate; + + if (crBrowseEpisode.EpisodeMetadata.SeasonTitle != null && crBrowseEpisode.EpisodeMetadata.SeasonTitle.EndsWith("Dub)")){ + continue; + } + + var calendarDay = week.CalendarDays.FirstOrDefault(day => day.DateTime != null && day.DateTime.Value.Date == episodeAirDate.Date); + + if (calendarDay != null){ + CalendarEpisode calEpisode = new CalendarEpisode(); + + calEpisode.DateTime = episodeAirDate; + calEpisode.HasPassed = DateTime.Now > episodeAirDate; + calEpisode.EpisodeName = crBrowseEpisode.Title; + calEpisode.SeriesUrl = "https://www.crunchyroll.com/series/" + crBrowseEpisode.EpisodeMetadata.SeriesId; + calEpisode.EpisodeUrl = $"https://www.crunchyroll.com/de/watch/{crBrowseEpisode.Id}/"; + calEpisode.ThumbnailUrl = crBrowseEpisode.Images.Thumbnail.First().First().Source; + calEpisode.IsPremiumOnly = crBrowseEpisode.EpisodeMetadata.IsPremiumOnly; + calEpisode.IsPremiere = crBrowseEpisode.EpisodeMetadata.Episode == "1"; + calEpisode.SeasonName = crBrowseEpisode.EpisodeMetadata.SeasonTitle; + calEpisode.EpisodeNumber = crBrowseEpisode.EpisodeMetadata.Episode; + + calendarDay.CalendarEpisodes?.Add(calEpisode); + } + } + } + + + foreach (var day in week.CalendarDays){ + if (day.CalendarEpisodes != null) day.CalendarEpisodes = day.CalendarEpisodes.OrderBy(e => e.DateTime).ToList(); + } + + currentWeek = week; + CalendarDays.Clear(); + CalendarDays.AddRange(week.CalendarDays); + RaisePropertyChanged(nameof(CalendarDays)); + ShowLoading = false; + foreach (var calendarDay in CalendarDays){ + foreach (var calendarDayCalendarEpisode in calendarDay.CalendarEpisodes){ + if (calendarDayCalendarEpisode.ImageBitmap == null){ + calendarDayCalendarEpisode.LoadImage(); + } + } + } + } } \ No newline at end of file diff --git a/CRD/Views/AddDownloadPageView.axaml b/CRD/Views/AddDownloadPageView.axaml index 9514c37..f719e9d 100644 --- a/CRD/Views/AddDownloadPageView.axaml +++ b/CRD/Views/AddDownloadPageView.axaml @@ -4,6 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" xmlns:vm="clr-namespace:CRD.ViewModels" + xmlns:structs="clr-namespace:CRD.Utils.Structs" x:DataType="vm:AddDownloadPageViewModel" x:Class="CRD.Views.AddDownloadPageView"> @@ -19,9 +20,82 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -36,9 +110,16 @@ Content="{Binding ButtonText}"> - - + + + + + + + + + IsVisible="{Binding ShowLoading}"> + SelectedItems="{Binding SelectedItems}" ItemsSource="{Binding Items}" x:Name="Grid"> diff --git a/CRD/Views/CalendarPageView.axaml b/CRD/Views/CalendarPageView.axaml index fe091fc..50df0a9 100644 --- a/CRD/Views/CalendarPageView.axaml +++ b/CRD/Views/CalendarPageView.axaml @@ -27,6 +27,7 @@