diff --git a/CRD/CRD.csproj b/CRD/CRD.csproj deleted file mode 100644 index 7128825..0000000 --- a/CRD/CRD.csproj +++ /dev/null @@ -1,51 +0,0 @@ - - - WinExe - net8.0 - enable - true - app.manifest - true - en - Assets\app_icon.ico - 1.5.2.0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ContentDialogInputLoginView.axaml - Code - - - ContentDialogInputLoginView.axaml - Code - - - - - diff --git a/CRD/Downloader/CrEpisode.cs b/CRD/Downloader/CrEpisode.cs index 03e5263..5bb50df 100644 --- a/CRD/Downloader/CrEpisode.cs +++ b/CRD/Downloader/CrEpisode.cs @@ -68,6 +68,12 @@ public class CrEpisode(){ if (crunInstance.CrunOptions.History && updateHistory){ await crunInstance.CrHistory.UpdateWithEpisode(dlEpisode); + var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == dlEpisode.SeriesId); + if (historySeries != null){ + Crunchyroll.Instance.CrHistory.MatchHistorySeriesWithSonarr(false); + await Crunchyroll.Instance.CrHistory.MatchHistoryEpisodesWithSonarr(false, historySeries); + CfgManager.UpdateHistoryFile(); + } } var seasonIdentifier = !string.IsNullOrEmpty(dlEpisode.Identifier) ? dlEpisode.Identifier.Split('|')[1] : $"S{dlEpisode.SeasonNumber}"; @@ -102,7 +108,7 @@ public class CrEpisode(){ int epIndex = 1; - var isSpecial = !Regex.IsMatch(episode.EpisodeAndLanguages.Items[0].Episode ?? string.Empty, @"^\d+$"); // Checking if the episode is not a number (i.e., special). + var isSpecial = !Regex.IsMatch(episode.EpisodeAndLanguages.Items[0].Episode ?? string.Empty, @"^\d+(\.\d+)?$"); // Checking if the episode is not a number (i.e., special). string newKey; if (isSpecial && !string.IsNullOrEmpty(episode.EpisodeAndLanguages.Items[0].Episode)){ newKey = episode.EpisodeAndLanguages.Items[0].Episode ?? "SP" + (specialIndex + " " + episode.EpisodeAndLanguages.Items[0].Id); @@ -239,7 +245,7 @@ public class CrEpisode(){ return retMeta; } - public async Task GetNewEpisodes(string? crLocale, int requestAmount){ + public async Task GetNewEpisodes(string? crLocale, int requestAmount , bool forcedLang = false){ CrBrowseEpisodeBase? complete = new CrBrowseEpisodeBase(); complete.Data =[]; @@ -250,6 +256,9 @@ public class CrEpisode(){ if (!string.IsNullOrEmpty(crLocale)){ query["locale"] = crLocale; + if (forcedLang){ + query["force_locale"] = crLocale; + } } query["start"] = i + ""; diff --git a/CRD/Downloader/CrSeries.cs b/CRD/Downloader/CrSeries.cs index 4530b4a..c6fdf2c 100644 --- a/CRD/Downloader/CrSeries.cs +++ b/CRD/Downloader/CrSeries.cs @@ -56,6 +56,13 @@ public class CrSeries(){ continue; } + if (crunInstance.CrunOptions.History){ + var dubLangList = crunInstance.CrHistory.GetDubList(item.SeriesId, item.SeasonId); + if (dubLangList.Count > 0){ + dubLang = dubLangList; + } + } + if (!dubLang.Contains(episode.Langs[index].CrLocale)) continue; @@ -157,7 +164,7 @@ public class CrSeries(){ var seasonData = await GetSeasonDataById(s.Id, ""); if (seasonData.Data != null){ if (crunInstance.CrunOptions.History){ - crunInstance.CrHistory.UpdateWithSeasonData(seasonData); + crunInstance.CrHistory.UpdateWithSeasonData(seasonData,false); } foreach (var episode in seasonData.Data){ @@ -204,6 +211,15 @@ public class CrSeries(){ } } + if (crunInstance.CrunOptions.History){ + var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == id); + if (historySeries != null){ + crunInstance.CrHistory.MatchHistorySeriesWithSonarr(false); + await crunInstance.CrHistory.MatchHistoryEpisodesWithSonarr(false, historySeries); + CfgManager.UpdateHistoryFile(); + } + } + int specialIndex = 1; int epIndex = 1; @@ -212,7 +228,7 @@ public class CrSeries(){ foreach (var key in keys){ EpisodeAndLanguage item = episodes[key]; var episode = item.Items[0].Episode; - var isSpecial = episode != null && !Regex.IsMatch(episode, @"^\d+$"); // Checking if the episode is not a number (i.e., special). + var isSpecial = episode != null && !Regex.IsMatch(episode, @"^\d+(\.\d+)?$"); // Checking if the episode is not a number (i.e., special). // var newKey = $"{(isSpecial ? 'S' : 'E')}{(isSpecial ? specialIndex : epIndex).ToString()}"; string newKey; @@ -431,13 +447,13 @@ public class CrSeries(){ } - public async Task Search(string searchString,string? crLocale){ - + 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"; @@ -452,7 +468,7 @@ public class CrSeries(){ } CrSearchSeriesBase? series = Helpers.Deserialize(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings); - + return series; } diff --git a/CRD/Downloader/Crunchyroll.cs b/CRD/Downloader/Crunchyroll.cs index ee2c1a8..52cc000 100644 --- a/CRD/Downloader/Crunchyroll.cs +++ b/CRD/Downloader/Crunchyroll.cs @@ -81,6 +81,7 @@ public class Crunchyroll{ #endregion + public string DefaultLocale = "en-US"; public JsonSerializerSettings? SettingsJsonSerializerSettings = new(){ @@ -120,24 +121,7 @@ public class Crunchyroll{ Queue.CollectionChanged += UpdateItemListOnRemove; } - public async Task Init(){ - _widevine = Widevine.Instance; - - CrAuth = new CrAuth(); - CrEpisode = new CrEpisode(); - CrSeries = new CrSeries(); - CrHistory = new History(); - - Profile = new CrProfile{ - Username = "???", - Avatar = "003-cr-hime-excited.png", - PreferredContentAudioLanguage = "ja-JP", - PreferredContentSubtitleLanguage = "de-DE", - HasPremium = false, - }; - - Console.WriteLine($"Can Decrypt: {_widevine.canDecrypt}"); - + public void InitOptions(){ CrunOptions.AutoDownload = false; CrunOptions.RemoveFinishedDownload = false; CrunOptions.Chapters = true; @@ -164,12 +148,32 @@ public class Crunchyroll{ CrunOptions.DlVideoOnce = true; CrunOptions.StreamEndpoint = "web/firefox"; CrunOptions.SubsAddScaledBorder = ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes; - CrunOptions.HistoryLang = ""; - + CrunOptions.HistoryLang = DefaultLocale; CrunOptions.History = true; CfgManager.UpdateSettingsFromFile(); + + _widevine = Widevine.Instance; + + CrAuth = new CrAuth(); + CrEpisode = new CrEpisode(); + CrSeries = new CrSeries(); + CrHistory = new History(); + + Profile = new CrProfile{ + Username = "???", + Avatar = "003-cr-hime-excited.png", + PreferredContentAudioLanguage = "ja-JP", + PreferredContentSubtitleLanguage = "de-DE", + HasPremium = false, + }; + + Console.WriteLine($"Can Decrypt: {_widevine.canDecrypt}"); + } + + public async Task Init(){ + if (CrunOptions.LogMode){ CfgManager.EnableLogMode(); @@ -186,10 +190,22 @@ public class Crunchyroll{ if (CrunOptions.History){ if (File.Exists(CfgManager.PathCrHistory)){ - HistoryList = JsonConvert.DeserializeObject>(File.ReadAllText(CfgManager.PathCrHistory)) ??[]; - } + var decompressedJson = CfgManager.DecompressJsonFile(CfgManager.PathCrHistory); + if (!string.IsNullOrEmpty(decompressedJson)){ + HistoryList = JsonConvert.DeserializeObject>(decompressedJson) ?? new ObservableCollection(); - RefreshSonarr(); + foreach (var historySeries in HistoryList){ + historySeries.Init(); + foreach (var historySeriesSeason in historySeries.Seasons){ + historySeriesSeason.Init(); + } + } + } else{ + HistoryList =[]; + } + + RefreshSonarr(); + } } } @@ -340,6 +356,18 @@ public class Crunchyroll{ } var sList = await CrEpisode.EpisodeData((CrunchyEpisode)episodeL, updateHistory); + + (HistoryEpisode? historyEpisode, List dublist, string downloadDirPath) historyEpisode = (null, [], ""); + + if (CrunOptions.History){ + var episode = sList.EpisodeAndLanguages.Items.First(); + historyEpisode = CrHistory.GetHistoryEpisodeWithDubListAndDownloadDir(episode.SeriesId, episode.SeasonId, episode.Id); + if (historyEpisode.dublist.Count > 0){ + dubLang = historyEpisode.dublist; + } + } + + var selected = CrEpisode.EpisodeMeta(sList, dubLang); if (CrunOptions.IncludeVideoDescription){ @@ -351,7 +379,7 @@ public class Crunchyroll{ if (selected.Data is{ Count: > 0 }){ if (CrunOptions.History){ - var historyEpisode = CrHistory.GetHistoryEpisodeWithDownloadDir(selected.ShowId, selected.SeasonId, selected.Data.First().MediaId); + // var historyEpisode = CrHistory.GetHistoryEpisodeWithDownloadDir(selected.ShowId, selected.SeasonId, selected.Data.First().MediaId); if (CrunOptions.SonarrProperties is{ SonarrEnabled: true, UseSonarrNumbering: true }){ if (historyEpisode.historyEpisode != null){ if (!string.IsNullOrEmpty(historyEpisode.historyEpisode.SonarrEpisodeNumber)){ @@ -369,6 +397,7 @@ public class Crunchyroll{ } } + selected.DownloadSubs = CrunOptions.DlSubs; Queue.Add(selected); @@ -419,6 +448,7 @@ public class Crunchyroll{ } } + crunchyEpMeta.DownloadSubs = CrunOptions.DlSubs; Queue.Add(crunchyEpMeta); } else{ failed = true; @@ -569,7 +599,8 @@ public class Crunchyroll{ SkipSubMux = options.SkipSubMux, 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 }).ToList(), + 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(), KeepAllVideos = options.KeepAllVideos, Fonts = FontsManager.Instance.MakeFontsList(CfgManager.PathFONTS_DIR, subsList), // Assuming MakeFontsList is properly defined Chapters = data.Where(a => a.Type == DownloadMediaType.Chapters).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(), @@ -584,7 +615,7 @@ public class Crunchyroll{ }, CcTag = options.CcTag, mp3 = muxToMp3, - Description = data.Where(a => a.Type == DownloadMediaType.Description).Select(a => new MergerInput{ Path = a.Path ?? string.Empty }).ToList(), + Description = muxDesc ? data.Where(a => a.Type == DownloadMediaType.Description).Select(a => new MergerInput{ Path = a.Path ?? string.Empty }).ToList() :[], }); if (!File.Exists(CfgManager.PathFFMPEG)){ @@ -597,9 +628,31 @@ public class Crunchyroll{ bool isMuxed; - // if (options.SyncTiming){ - // await Merger.CreateDelays(); - // } + if (options.SyncTiming && CrunOptions.DlVideoOnce){ + var basePath = merger.options.OnlyVid.First().Path; + var syncVideosList = data.Where(a => a.Type == DownloadMediaType.SyncVideo).ToList(); + + if (!string.IsNullOrEmpty(basePath) && syncVideosList.Count > 0){ + foreach (var syncVideo in syncVideosList){ + if (!string.IsNullOrEmpty(syncVideo.Path)){ + var delay = await merger.ProcessVideo(basePath, syncVideo.Path); + var audio = merger.options.OnlyAudio.FirstOrDefault(audio => audio.Language.CrLocale == syncVideo.Lang.CrLocale); + if (audio != null){ + audio.Delay = (int)delay * 1000; + } + + var subtitles = merger.options.Subtitles.Where(a => a.RelatedVideoDownloadMedia == syncVideo).ToList(); + if (subtitles.Count > 0){ + foreach (var subMergerInput in subtitles){ + subMergerInput.Delay = (int)delay * 1000; + } + } + } + } + } + + syncVideosList.ForEach(syncVideo => Helpers.DeleteFile(syncVideo.Path)); + } if (!options.Mp4 && !muxToMp3){ await merger.Merge("mkvmerge", CfgManager.PathMKVMERGE); @@ -726,8 +779,8 @@ public class Crunchyroll{ if (epMeta.Versions != null){ if (epMeta.Lang != null){ currentVersion = epMeta.Versions.Find(a => a.AudioLocale == epMeta.Lang?.CrLocale); - } else if (options.DubLang.Count == 1){ - LanguageItem lang = Array.Find(Languages.languages, a => a.CrLocale == options.DubLang[0]); + } else if (data.SelectedDubs is{ Count: 1 }){ + LanguageItem lang = Array.Find(Languages.languages, a => a.CrLocale == data.SelectedDubs[0]); currentVersion = epMeta.Versions.Find(a => a.AudioLocale == lang.CrLocale); } else if (epMeta.Versions.Count == 1){ currentVersion = epMeta.Versions[0]; @@ -804,7 +857,8 @@ public class Crunchyroll{ var streams = new List(); variables.Add(new Variable("title", data.EpisodeTitle ?? string.Empty, true)); - variables.Add(new Variable("episode", (int.TryParse(data.EpisodeNumber, out int episodeNum) ? (object)episodeNum : data.AbsolutEpisodeNumberE) ?? string.Empty, false)); + variables.Add(new Variable("episode", + (double.TryParse(data.EpisodeNumber, NumberStyles.Any, CultureInfo.InvariantCulture, out double episodeNum) ? (object)Math.Round(episodeNum, 1) : data.AbsolutEpisodeNumberE) ?? string.Empty, false)); variables.Add(new Variable("seriesTitle", data.SeriesTitle ?? string.Empty, true)); variables.Add(new Variable("showTitle", data.SeasonTitle ?? string.Empty, true)); variables.Add(new Variable("season", data.Season != null ? Math.Round(double.Parse(data.Season, CultureInfo.InvariantCulture), 1) : 0, false)); @@ -915,6 +969,7 @@ public class Crunchyroll{ } string tsFile = ""; + var videoDownloadMedia = new DownloadedMedia(); if (!dlFailed && curStream != null && options is not{ Novids: true, Noaudio: true }){ var streamPlaylistsReq = HttpClientReq.CreateRequestMessage(curStream.Url ?? string.Empty, HttpMethod.Get, true, true, null); @@ -985,7 +1040,9 @@ public class Crunchyroll{ audios.Sort((a, b) => a.bandwidth.CompareTo(b.bandwidth)); int chosenVideoQuality; - if (options.QualityVideo == "best"){ + if (options.DlVideoOnce && dlVideoOnce && options.SyncTiming){ + chosenVideoQuality = 1; + } else if (options.QualityVideo == "best"){ chosenVideoQuality = videos.Count; } else if (options.QualityVideo == "worst"){ chosenVideoQuality = 1; @@ -1075,9 +1132,10 @@ public class Crunchyroll{ .ToArray()); string tempTsFile = Path.IsPathRooted(tempFile) ? tempFile : Path.Combine(fileDir, tempFile); - bool audioDownloaded = false, videoDownloaded = false; + bool audioDownloaded = false, videoDownloaded = false, syncTimingDownload = false; - if (options.DlVideoOnce && dlVideoOnce){ + + if (options.DlVideoOnce && dlVideoOnce && !options.SyncTiming){ Console.WriteLine("Already downloaded video, skipping video download..."); } else if (options.Novids){ Console.WriteLine("Skipping video download..."); @@ -1091,6 +1149,10 @@ public class Crunchyroll{ dlFailed = true; } + if (options.DlVideoOnce && dlVideoOnce && options.SyncTiming){ + syncTimingDownload = true; + } + dlVideoOnce = true; videoDownloaded = true; } @@ -1256,12 +1318,13 @@ public class Crunchyroll{ Console.WriteLine($"An error occurred: {ex.Message}"); } - files.Add(new DownloadedMedia{ - Type = DownloadMediaType.Video, + videoDownloadMedia = new DownloadedMedia{ + Type = syncTimingDownload ? DownloadMediaType.SyncVideo : DownloadMediaType.Video, Path = $"{tsFile}.video.m4s", Lang = lang.Value, IsPrimary = isPrimary - }); + }; + files.Add(videoDownloadMedia); } else{ Console.WriteLine("No Video downloaded"); } @@ -1335,12 +1398,13 @@ public class Crunchyroll{ } } else{ if (videoDownloaded){ - files.Add(new DownloadedMedia{ - Type = DownloadMediaType.Video, + videoDownloadMedia = new DownloadedMedia{ + Type = syncTimingDownload ? DownloadMediaType.SyncVideo : DownloadMediaType.Video, Path = $"{tsFile}.video.m4s", Lang = lang.Value, IsPrimary = isPrimary - }); + }; + files.Add(videoDownloadMedia); } if (audioDownloaded){ @@ -1405,8 +1469,8 @@ public class Crunchyroll{ } } - if (options.DlSubs.IndexOf("all") > -1){ - options.DlSubs = new List{ "all" }; + if (data.DownloadSubs.IndexOf("all") > -1){ + data.DownloadSubs = new List{ "all" }; } if (options.Hslang != "none"){ @@ -1414,8 +1478,8 @@ public class Crunchyroll{ options.SkipSubs = true; } - if (!options.SkipSubs && options.DlSubs.IndexOf("none") == -1){ - await DownloadSubtitles(options, pbData, audDub, fileName, files, fileDir); + if (!options.SkipSubs && data.DownloadSubs.IndexOf("none") == -1){ + await DownloadSubtitles(options, pbData, audDub, fileName, files, fileDir, data, (options.DlVideoOnce && dlVideoOnce && options.SyncTiming), videoDownloadMedia); } else{ Console.WriteLine("Subtitles downloading skipped!"); } @@ -1472,7 +1536,8 @@ public class Crunchyroll{ }; } - private static async Task DownloadSubtitles(CrDownloadOptions options, PlaybackData pbData, string audDub, string fileName, List files, string fileDir){ + private static async Task DownloadSubtitles(CrDownloadOptions options, PlaybackData pbData, string audDub, string fileName, List files, string fileDir, CrunchyEpMeta data, bool needsDelay, + DownloadedMedia videoDownloadMedia){ if (pbData.Meta != null && pbData.Meta.Subtitles != null && pbData.Meta.Subtitles.Count > 0){ List subsData = pbData.Meta.Subtitles.Values.ToList(); List capsData = pbData.Meta.ClosedCaptions?.Values.ToList() ?? new List(); @@ -1510,7 +1575,7 @@ public class Crunchyroll{ var isSigns = langItem.Code == audDub && !subsItem.isCC; var isCc = subsItem.isCC; - sxData.File = Languages.SubsFile(fileName, index + "", langItem, isCc, options.CcTag, isSigns, subsItem.format, !(options.DlSubs.Count == 1 && !options.DlSubs.Contains("all"))); + sxData.File = Languages.SubsFile(fileName, index + "", langItem, isCc, options.CcTag, isSigns, subsItem.format, !(data.DownloadSubs.Count == 1 && !data.DownloadSubs.Contains("all"))); sxData.Path = Path.Combine(fileDir, sxData.File); Helpers.EnsureDirectoriesExist(sxData.Path); @@ -1523,7 +1588,7 @@ public class Crunchyroll{ continue; } - if (options.DlSubs.Contains("all") || options.DlSubs.Contains(langItem.CrLocale)){ + if (data.DownloadSubs.Contains("all") || data.DownloadSubs.Contains(langItem.CrLocale)){ var subsAssReq = HttpClientReq.CreateRequestMessage(subsItem.url ?? string.Empty, HttpMethod.Get, false, false, null); var subsAssReqResponse = await HttpClientReq.Instance.SendHttpRequest(subsAssReq); @@ -1567,7 +1632,8 @@ public class Crunchyroll{ Title = sxData.Title, Fonts = sxData.Fonts, Language = sxData.Language, - Lang = sxData.Language + Lang = sxData.Language, + RelatedVideoDownloadMedia = videoDownloadMedia }); } else{ Console.WriteLine($"Failed to download subtitle: ${sxData.File}"); diff --git a/CRD/Downloader/History.cs b/CRD/Downloader/History.cs index e39ac88..8e99e70 100644 --- a/CRD/Downloader/History.cs +++ b/CRD/Downloader/History.cs @@ -1,15 +1,9 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.ComponentModel; using System.Globalization; using System.Linq; -using System.Net; -using System.Net.Http; using System.Text.RegularExpressions; using System.Threading.Tasks; -using Avalonia.Media.Imaging; -using CommunityToolkit.Mvvm.Input; using CRD.Utils; using CRD.Utils.Sonarr; using CRD.Utils.Sonarr.Models; @@ -17,7 +11,6 @@ using CRD.Utils.Structs; using CRD.Utils.Structs.History; using CRD.Views; using DynamicData; -using Newtonsoft.Json; using ReactiveUI; namespace CRD.Downloader; @@ -60,11 +53,17 @@ public class History(){ await UpdateWithSeasonData(seasonData); } } + + var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId); + + if (historySeries != null){ + MatchHistorySeriesWithSonarr(false); + await MatchHistoryEpisodesWithSonarr(false, historySeries); + CfgManager.UpdateHistoryFile(); + } } - private void UpdateHistoryFile(){ - CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, crunInstance.HistoryList); - } + public void SetAsDownloaded(string? seriesId, string? seasonId, string episodeId){ var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId); @@ -129,6 +128,59 @@ public class History(){ return (null, downloadDirPath); } + + public (HistoryEpisode? historyEpisode, List dublist, string downloadDirPath) GetHistoryEpisodeWithDubListAndDownloadDir(string? seriesId, string? seasonId, string episodeId){ + var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId); + + var downloadDirPath = ""; + List dublist = []; + + if (historySeries != null){ + var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId); + if (historySeries.HistorySeriesDubLangOverride.Count > 0){ + dublist = historySeries.HistorySeriesDubLangOverride; + } + if (!string.IsNullOrEmpty(historySeries.SeriesDownloadPath)){ + downloadDirPath = historySeries.SeriesDownloadPath; + } + + if (historySeason != null){ + var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == episodeId); + if (historySeason.HistorySeasonDubLangOverride.Count > 0){ + dublist = historySeason.HistorySeasonDubLangOverride; + } + if (!string.IsNullOrEmpty(historySeason.SeasonDownloadPath)){ + downloadDirPath = historySeason.SeasonDownloadPath; + } + + if (historyEpisode != null){ + return (historyEpisode, dublist,downloadDirPath); + } + } + } + + return (null, dublist,downloadDirPath); + } + + public List GetDubList(string? seriesId, string? seasonId){ + var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId); + + List dublist = []; + + if (historySeries != null){ + var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId); + if (historySeries.HistorySeriesDubLangOverride.Count > 0){ + dublist = historySeries.HistorySeriesDubLangOverride; + } + + if (historySeason is{ HistorySeasonDubLangOverride.Count: > 0 }){ + dublist = historySeason.HistorySeasonDubLangOverride; + } + } + + return dublist; + } + public async Task UpdateWithEpisode(CrunchyEpisode episodeParam){ @@ -160,6 +212,7 @@ public class History(){ if (historySeason != null){ historySeason.SeasonTitle = episode.SeasonTitle; historySeason.SeasonNum = Helpers.ExtractNumberAfterS(episode.Identifier) ?? episode.SeasonNumber + ""; + historySeason.SpecialSeason = CheckStringForSpecial(episode.Identifier); if (historySeason.EpisodesList.All(e => e.EpisodeId != episode.Id)){ var newHistoryEpisode = new HistoryEpisode{ EpisodeTitle = episode.Identifier.Contains("|M|") ? episode.SeasonTitle : episode.Title, @@ -178,6 +231,7 @@ public class History(){ var newSeason = NewHistorySeason(episode); historySeries.Seasons.Add(newSeason); + newSeason.Init(); } historySeries.UpdateNewEpisodes(); @@ -195,18 +249,33 @@ public class History(){ historySeries.Seasons.Add(newSeason); historySeries.UpdateNewEpisodes(); + historySeries.Init(); + newSeason.Init(); } SortItems(); SortSeasons(historySeries); - MatchHistorySeriesWithSonarr(false); - await MatchHistoryEpisodesWithSonarr(false, historySeries); - UpdateHistoryFile(); + } - public async Task UpdateWithSeasonData(CrunchyEpisodeList seasonData){ + public async Task UpdateWithSeasonData(CrunchyEpisodeList seasonData,bool skippVersionCheck = true){ if (seasonData.Data != null){ + + if (!skippVersionCheck){ + if (seasonData.Data.First().Versions != null){ + var version = seasonData.Data.First().Versions.Find(a => a.Original); + if (version.AudioLocale != seasonData.Data.First().AudioLocale){ + UpdateSeries(seasonData.Data.First().SeriesId, version.SeasonGuid); + return; + } + } else{ + UpdateSeries(seasonData.Data.First().SeriesId, ""); + return; + } + } + + var firstEpisode = seasonData.Data.First(); var seriesId = firstEpisode.SeriesId; var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId); @@ -220,6 +289,7 @@ public class History(){ if (historySeason != null){ historySeason.SeasonTitle = firstEpisode.SeasonTitle; historySeason.SeasonNum = Helpers.ExtractNumberAfterS(firstEpisode.Identifier) ?? firstEpisode.SeasonNumber + ""; + historySeason.SpecialSeason = CheckStringForSpecial(firstEpisode.Identifier); foreach (var crunchyEpisode in seasonData.Data){ var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == crunchyEpisode.Id); @@ -252,6 +322,7 @@ public class History(){ newSeason.EpisodesList.Sort(new NumericStringPropertyComparer()); historySeries.Seasons.Add(newSeason); + newSeason.Init(); } historySeries.UpdateNewEpisodes(); @@ -272,16 +343,13 @@ public class History(){ historySeries.Seasons.Add(newSeason); - historySeries.UpdateNewEpisodes(); + historySeries.Init(); + newSeason.Init(); } SortItems(); SortSeasons(historySeries); - - MatchHistorySeriesWithSonarr(false); - await MatchHistoryEpisodesWithSonarr(false, historySeries); - UpdateHistoryFile(); } } @@ -358,19 +426,19 @@ public class History(){ return; case SortingType.HistorySeriesAddDate: - + var sortedSeriesAddDates = Crunchyroll.Instance.HistoryList .OrderBy(s => sortingDir ? -(s.HistorySeriesAddDate?.Date.Ticks ?? DateTime.MinValue.Ticks) : s.HistorySeriesAddDate?.Date.Ticks ?? DateTime.MaxValue.Ticks) .ThenBy(s => s.SeriesTitle) .ToList(); - + Crunchyroll.Instance.HistoryList.Clear(); Crunchyroll.Instance.HistoryList.AddRange(sortedSeriesAddDates); - + return; } } @@ -388,7 +456,6 @@ public class History(){ } - private string GetSeriesThumbnail(CrSeriesBase series){ // var series = await crunInstance.CrSeries.SeriesById(seriesId); @@ -400,6 +467,10 @@ public class History(){ } private static bool CheckStringForSpecial(string identifier){ + if (string.IsNullOrEmpty(identifier)){ + return false; + } + // Regex pattern to match any sequence that does NOT contain "|S" followed by one or more digits immediately after string pattern = @"^(?!.*\|S\d+).*"; @@ -564,6 +635,9 @@ public class History(){ } } }); + + CfgManager.UpdateHistoryFile(); + } } @@ -675,11 +749,12 @@ public class History(){ public class NumericStringPropertyComparer : IComparer{ public int Compare(HistoryEpisode x, HistoryEpisode y){ - if (int.TryParse(x.Episode, out int xInt) && int.TryParse(y.Episode, out int yInt)){ - return xInt.CompareTo(yInt); + if (double.TryParse(x.Episode, NumberStyles.Any, CultureInfo.InvariantCulture, out double xDouble) && + double.TryParse(y.Episode, NumberStyles.Any, CultureInfo.InvariantCulture, out double yDouble)){ + return xDouble.CompareTo(yDouble); } - // Fall back to string comparison if not parseable as integers - return String.Compare(x.Episode, y.Episode, StringComparison.Ordinal); + // Fall back to string comparison if not parseable as doubles + return string.Compare(x.Episode, y.Episode, StringComparison.Ordinal); } } \ No newline at end of file diff --git a/CRD/Utils/Enums/EnumCollection.cs b/CRD/Utils/Enums/EnumCollection.cs index 36c7c23..7353475 100644 --- a/CRD/Utils/Enums/EnumCollection.cs +++ b/CRD/Utils/Enums/EnumCollection.cs @@ -154,6 +154,9 @@ public enum MediaType{ public enum DownloadMediaType{ [EnumMember(Value = "Video")] Video, + + [EnumMember(Value = "SyncVideo")] + SyncVideo, [EnumMember(Value = "Audio")] Audio, diff --git a/CRD/Utils/Files/CfgManager.cs b/CRD/Utils/Files/CfgManager.cs index 5a1e761..3eb2bb8 100644 --- a/CRD/Utils/Files/CfgManager.cs +++ b/CRD/Utils/Files/CfgManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Compression; using System.Reflection; using CRD.Downloader; using CRD.Utils.Structs; @@ -34,24 +35,41 @@ public class CfgManager{ private static StreamWriter logFile; private static bool isLogModeEnabled = false; + static CfgManager(){ + AppDomain.CurrentDomain.ProcessExit += OnProcessExit; + } + + private static void OnProcessExit(object? sender, EventArgs e){ + DisableLogMode(); + } + public static void EnableLogMode(){ if (!isLogModeEnabled){ - logFile = new StreamWriter(PathLogFile); - logFile.AutoFlush = true; - Console.SetError(logFile); - isLogModeEnabled = true; - Console.Error.WriteLine("Log mode enabled."); + try{ + var fileStream = new FileStream(PathLogFile, FileMode.Append, FileAccess.Write, FileShare.ReadWrite); + logFile = new StreamWriter(fileStream); + logFile.AutoFlush = true; + Console.SetError(logFile); + isLogModeEnabled = true; + Console.Error.WriteLine("Log mode enabled."); + } catch (Exception e){ + Console.Error.WriteLine($"Couldn't enable logging: {e}"); + } } } public static void DisableLogMode(){ if (isLogModeEnabled){ - logFile.Close(); - StreamWriter standardError = new StreamWriter(Console.OpenStandardError()); - standardError.AutoFlush = true; - Console.SetError(standardError); - isLogModeEnabled = false; - Console.Error.WriteLine("Log mode disabled."); + try{ + logFile.Close(); + StreamWriter standardError = new StreamWriter(Console.OpenStandardError()); + standardError.AutoFlush = true; + Console.SetError(standardError); + isLogModeEnabled = false; + Console.Error.WriteLine("Log mode disabled."); + } catch (Exception e){ + Console.Error.WriteLine($"Couldn't disable logging: {e}"); + } } } @@ -127,7 +145,7 @@ public class CfgManager{ File.WriteAllText(PathCrDownloadOptions, yaml); } - + public static void UpdateSettingsFromFile(){ string dirPath = Path.GetDirectoryName(PathCrDownloadOptions) ?? string.Empty; @@ -188,6 +206,10 @@ public class CfgManager{ return properties; } + public static void UpdateHistoryFile(){ + WriteJsonToFile(PathCrHistory, Crunchyroll.Instance.HistoryList); + } + private static object fileLock = new object(); public static void WriteJsonToFile(string pathToFile, object obj){ @@ -199,9 +221,9 @@ public class CfgManager{ } lock (fileLock){ - // Write the JSON string to file using a streaming approach. using (var fileStream = new FileStream(pathToFile, FileMode.Create, FileAccess.Write)) - using (var streamWriter = new StreamWriter(fileStream)) + using (var gzipStream = new GZipStream(fileStream, CompressionLevel.Optimal)) + using (var streamWriter = new StreamWriter(gzipStream)) using (var jsonWriter = new JsonTextWriter(streamWriter){ Formatting = Formatting.Indented }){ var serializer = new JsonSerializer(); serializer.Serialize(jsonWriter, obj); @@ -212,6 +234,39 @@ public class CfgManager{ } } + public static string DecompressJsonFile(string pathToFile){ + try{ + using (var fileStream = new FileStream(pathToFile, FileMode.Open, FileAccess.Read)){ + // Check if the file is compressed + if (IsFileCompressed(fileStream)){ + // Reset the stream position to the beginning + fileStream.Position = 0; + using (var gzipStream = new GZipStream(fileStream, CompressionMode.Decompress)) + using (var streamReader = new StreamReader(gzipStream)){ + return streamReader.ReadToEnd(); + } + } + + // If not compressed, read the file as is + fileStream.Position = 0; + using (var streamReader = new StreamReader(fileStream)){ + return streamReader.ReadToEnd(); + } + } + } catch (Exception ex){ + Console.Error.WriteLine($"An error occurred: {ex.Message}"); + return null; + } + } + + private static bool IsFileCompressed(FileStream fileStream){ + // Check the first two bytes for the GZip header + var buffer = new byte[2]; + fileStream.Read(buffer, 0, 2); + return buffer[0] == 0x1F && buffer[1] == 0x8B; + } + + public static bool CheckIfFileExists(string filePath){ string dirPath = Path.GetDirectoryName(filePath) ?? string.Empty; diff --git a/CRD/Utils/Files/FileNameManager.cs b/CRD/Utils/Files/FileNameManager.cs index 7d37422..1b804a6 100644 --- a/CRD/Utils/Files/FileNameManager.cs +++ b/CRD/Utils/Files/FileNameManager.cs @@ -34,6 +34,7 @@ public class FileNameManager{ string[] parts = replacement.Split(','); string formattedIntegerPart = parts[0].PadLeft(numbers, '0'); replacement = formattedIntegerPart + (parts.Length > 1 ? "," + parts[1] : ""); + replacement = replacement.Replace(",", "."); } else if (variable.Sanitize){ replacement = CleanupFilename(replacement); } diff --git a/CRD/Utils/Helpers.cs b/CRD/Utils/Helpers.cs index 3979a10..7af37a7 100644 --- a/CRD/Utils/Helpers.cs +++ b/CRD/Utils/Helpers.cs @@ -196,6 +196,21 @@ public class Helpers{ } } + public static void DeleteFile(string filePath){ + if (string.IsNullOrEmpty(filePath)){ + return; + } + + try{ + if (File.Exists(filePath)){ + File.Delete(filePath); + } + } catch (Exception ex){ + Console.Error.WriteLine($"Failed to delete file {filePath}. Error: {ex.Message}"); + // Handle exceptions if you need to log them or throw + } + } + public static async Task<(bool IsOk, int ErrorCode)> ExecuteCommandAsyncWorkDir(string type, string bin, string command,string workingDir){ try{ using (var process = new Process()){ diff --git a/CRD/Utils/Muxing/Merger.cs b/CRD/Utils/Muxing/Merger.cs index 34dee2e..4cd9dfa 100644 --- a/CRD/Utils/Muxing/Merger.cs +++ b/CRD/Utils/Muxing/Merger.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Xml; using CRD.Utils.Structs; @@ -11,7 +12,7 @@ using DynamicData; namespace CRD.Utils.Muxing; public class Merger{ - private MergerOptions options; + public MergerOptions options; public Merger(MergerOptions options){ this.options = options; @@ -45,6 +46,10 @@ public class Merger{ } foreach (var aud in options.OnlyAudio){ + if (aud.Delay != null && aud.Delay != 0){ + args.Add($"-itsoffset {aud.Delay}"); + } + args.Add($"-i \"{aud.Path}\""); metaData.Add($"-map {index}:a"); metaData.Add($"-metadata:s:a:{audioIndex} language={aud.Language.Code}"); @@ -53,14 +58,13 @@ public class Merger{ } if (options.Chapters != null && options.Chapters.Count > 0){ - Helpers.ConvertChapterFileForFFMPEG(options.Chapters[0].Path); - + args.Add($"-i \"{options.Chapters[0].Path}\""); metaData.Add($"-map_metadata {index}"); index++; } - + foreach (var sub in options.Subtitles.Select((value, i) => new{ value, i })){ if (sub.value.Delay != null){ args.Add($"-itsoffset -{Math.Ceiling((double)sub.value.Delay * 1000)}ms"); @@ -68,7 +72,7 @@ public class Merger{ args.Add($"-i \"{sub.value.File}\""); } - + if (options.Output.EndsWith(".mkv", StringComparison.OrdinalIgnoreCase)){ if (options.Fonts != null){ int fontIndex = 0; @@ -78,7 +82,7 @@ public class Merger{ } } } - + args.AddRange(metaData); args.AddRange(options.Subtitles.Select((sub, subIndex) => $"-map {subIndex + index}")); args.Add("-c:v copy"); @@ -87,9 +91,8 @@ public class Merger{ args.AddRange(options.Subtitles.Select((sub, subindex) => $"-metadata:s:s:{subindex} title=\"{sub.Language.Language ?? sub.Language.Name}{(sub.ClosedCaption == true ? $" {options.CcTag}" : "")}{(sub.Signs == true ? " Signs" : "")}\" -metadata:s:s:{subindex} language={sub.Language.Code}")); - - if (!string.IsNullOrEmpty(options.VideoTitle)){ + if (!string.IsNullOrEmpty(options.VideoTitle)){ args.Add($"-metadata title=\"{options.VideoTitle}\""); } @@ -125,7 +128,6 @@ public class Merger{ } - public string MkvMerge(){ List args = new List(); @@ -157,8 +159,7 @@ public class Merger{ args.Add("--no-video"); args.Add($"--track-name 0:\"{trackName}\""); args.Add($"--language 0:{aud.Language.Code}"); - - + if (options.Defaults.Audio.Code == aud.Language.Code){ args.Add("--default-track 0"); @@ -166,6 +167,10 @@ public class Merger{ args.Add("--default-track 0:0"); } + if (aud.Delay != null && aud.Delay != 0){ + args.Add($"--sync 0:{aud.Delay}"); + } + args.Add($"\"{aud.Path}\""); } @@ -216,14 +221,65 @@ public class Merger{ if (options.Description is{ Count: > 0 }){ args.Add($"--global-tags \"{options.Description[0].Path}\""); } - - return string.Join(" ", args); } + public async Task ProcessVideo(string baseVideoPath, string compareVideoPath){ + var tempDir = Path.GetTempPath(); //TODO - maybe move this out of temp + var baseFramesDir = Path.Combine(tempDir, "base_frames"); + var compareFramesDir = Path.Combine(tempDir, "compare_frames"); + + Directory.CreateDirectory(baseFramesDir); + Directory.CreateDirectory(compareFramesDir); + + var extractFramesBase = await SyncingHelper.ExtractFrames(baseVideoPath, baseFramesDir, 0, 60); + var extractFramesCompare = await SyncingHelper.ExtractFrames(compareVideoPath, compareFramesDir, 0, 60); + + if (!extractFramesBase.IsOk || !extractFramesCompare.IsOk){ + Console.Error.WriteLine("Failed to extract Frames to Compare"); + return 0; + } + + var baseFrames = Directory.GetFiles(baseFramesDir).Select(fp => new FrameData + { + FilePath = fp, + Time = GetTimeFromFileName(fp, extractFramesBase.frameRate) + }).ToList(); + + var compareFrames = Directory.GetFiles(compareFramesDir).Select(fp => new FrameData + { + FilePath = fp, + Time = GetTimeFromFileName(fp, extractFramesBase.frameRate) + }).ToList(); + + var offset = SyncingHelper.CalculateOffset(baseFrames, compareFrames); + Console.WriteLine($"Calculated offset: {offset} seconds"); + + CleanupDirectory(baseFramesDir); + CleanupDirectory(compareFramesDir); + + return offset; + } + + private static void CleanupDirectory(string dirPath){ + if (Directory.Exists(dirPath)){ + Directory.Delete(dirPath, true); + } + } + + private static double GetTimeFromFileName(string fileName, double frameRate){ + var match = Regex.Match(Path.GetFileName(fileName), @"frame(\d+)"); + if (match.Success){ + return int.Parse(match.Groups[1].Value) / frameRate; // Assuming 30 fps + } + + return 0; + } + + public async Task Merge(string type, string bin){ string command = type switch{ "ffmpeg" => FFmpeg(), @@ -253,28 +309,19 @@ public class Merger{ // Combine all media file lists and iterate through them var allMediaFiles = options.OnlyAudio.Concat(options.OnlyVid) .ToList(); - allMediaFiles.ForEach(file => DeleteFile(file.Path)); - allMediaFiles.ForEach(file => DeleteFile(file.Path + ".resume")); - - options.Description?.ForEach(chapter => DeleteFile(chapter.Path)); - + allMediaFiles.ForEach(file => Helpers.DeleteFile(file.Path)); + allMediaFiles.ForEach(file => Helpers.DeleteFile(file.Path + ".resume")); + + options.Description?.ForEach(chapter => Helpers.DeleteFile(chapter.Path)); + // Delete chapter files if any - options.Chapters?.ForEach(chapter => DeleteFile(chapter.Path)); + options.Chapters?.ForEach(chapter => Helpers.DeleteFile(chapter.Path)); // Delete subtitle files - options.Subtitles.ForEach(subtitle => DeleteFile(subtitle.File)); + options.Subtitles.ForEach(subtitle => Helpers.DeleteFile(subtitle.File)); } - private void DeleteFile(string filePath){ - try{ - if (File.Exists(filePath)){ - File.Delete(filePath); - } - } catch (Exception ex){ - Console.Error.WriteLine($"Failed to delete file {filePath}. Error: {ex.Message}"); - // Handle exceptions if you need to log them or throw - } - } + } public class MergerInput{ @@ -291,6 +338,8 @@ public class SubtitleInput{ public bool? ClosedCaption{ get; set; } public bool? Signs{ get; set; } public int? Delay{ get; set; } + + public DownloadedMedia? RelatedVideoDownloadMedia; } public class ParsedFont{ diff --git a/CRD/Utils/Muxing/SyncingHelper.cs b/CRD/Utils/Muxing/SyncingHelper.cs new file mode 100644 index 0000000..9e0b79d --- /dev/null +++ b/CRD/Utils/Muxing/SyncingHelper.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using CRD.Utils.Structs; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using Image = SixLabors.ImageSharp.Image; + +namespace CRD.Utils.Muxing; + +public class SyncingHelper{ + public static async Task<(bool IsOk, int ErrorCode, double frameRate)> ExtractFrames(string videoPath, string outputDir, double offset, double duration){ + var ffmpegPath = CfgManager.PathFFMPEG; + var arguments = $"-i \"{videoPath}\" -vf \"select='gt(scene,0.1)',showinfo\" -vsync vfr -frame_pts true -t {duration} -ss {offset} \"{outputDir}\\frame%03d.png\""; + + var output = ""; + + try{ + using (var process = new Process()){ + process.StartInfo.FileName = ffmpegPath; + process.StartInfo.Arguments = arguments; + 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}"); + output += e.Data; + } + }; + + process.Start(); + + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + await process.WaitForExitAsync(); + bool isSuccess = process.ExitCode == 0; + double frameRate = ExtractFrameRate(output); + return (IsOk: isSuccess, ErrorCode: process.ExitCode, frameRate); + } + } catch (Exception ex){ + Console.Error.WriteLine($"An error occurred: {ex.Message}"); + return (IsOk: false, ErrorCode: -1, 0); + } + } + + public static double ExtractFrameRate(string ffmpegOutput){ + var match = Regex.Match(ffmpegOutput, @"Stream #0:0.*?(\d+(?:\.\d+)?) fps"); + if (match.Success){ + return double.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); + } + + Console.Error.WriteLine("Failed to extract frame rate from FFmpeg output."); + return 0; + } + + private static double CalculateSSIM(float[] pixels1, float[] pixels2, int width, int height){ + double mean1 = pixels1.Average(); + double mean2 = pixels2.Average(); + + double var1 = 0, var2 = 0, covariance = 0; + int count = pixels1.Length; + + for (int i = 0; i < count; i++){ + var1 += (pixels1[i] - mean1) * (pixels1[i] - mean1); + var2 += (pixels2[i] - mean2) * (pixels2[i] - mean2); + covariance += (pixels1[i] - mean1) * (pixels2[i] - mean2); + } + + var1 /= count - 1; + var2 /= count - 1; + covariance /= count - 1; + + double c1 = 0.01 * 0.01 * 255 * 255; + double c2 = 0.03 * 0.03 * 255 * 255; + + double ssim = ((2 * mean1 * mean2 + c1) * (2 * covariance + c2)) / + ((mean1 * mean1 + mean2 * mean2 + c1) * (var1 + var2 + c2)); + + return ssim; + } + + private static float[] ExtractPixels(Image image, int width, int height){ + float[] pixels = new float[width * height]; + int index = 0; + + image.ProcessPixelRows(accessor => { + for (int y = 0; y < accessor.Height; y++){ + Span row = accessor.GetRowSpan(y); + for (int x = 0; x < row.Length; x++){ + pixels[index++] = row[x].R; + } + } + }); + + return pixels; + } + + public static double ComputeSSIM(string imagePath1, string imagePath2, int targetWidth, int targetHeight){ + using (var image1 = Image.Load(imagePath1)) + using (var image2 = Image.Load(imagePath2)){ + // Preprocess images (resize and convert to grayscale) + image1.Mutate(x => x.Resize(new ResizeOptions{ + Size = new Size(targetWidth, targetHeight), + Mode = ResizeMode.Max + }).Grayscale()); + + image2.Mutate(x => x.Resize(new ResizeOptions{ + Size = new Size(targetWidth, targetHeight), + Mode = ResizeMode.Max + }).Grayscale()); + + // Extract pixel values into arrays + float[] pixels1 = ExtractPixels(image1, targetWidth, targetHeight); + float[] pixels2 = ExtractPixels(image2, targetWidth, targetHeight); + + // Compute SSIM + return CalculateSSIM(pixels1, pixels2, targetWidth, targetHeight); + } + } + + public static bool AreFramesSimilar(string imagePath1, string imagePath2, double ssimThreshold){ + double ssim = ComputeSSIM(imagePath1, imagePath2, 256, 256); + Console.WriteLine($"SSIM: {ssim}"); + return ssim > ssimThreshold; + } + + public static double CalculateOffset(List baseFrames, List compareFrames, double ssimThreshold = 0.9){ + foreach (var baseFrame in baseFrames){ + var matchingFrame = compareFrames.FirstOrDefault(f => AreFramesSimilar(baseFrame.FilePath, f.FilePath, ssimThreshold)); + if (matchingFrame != null){ + Console.WriteLine($"Matched Frame: Base Frame Time: {baseFrame.Time}, Compare Frame Time: {matchingFrame.Time}"); + return baseFrame.Time - matchingFrame.Time; + } else{ + // Console.WriteLine($"No Match Found for Base Frame Time: {baseFrame.Time}"); + Debug.WriteLine($"No Match Found for Base Frame Time: {baseFrame.Time}"); + } + } + + return 0; + } +} \ No newline at end of file diff --git a/CRD/Utils/Structs/CrDownloadOptions.cs b/CRD/Utils/Structs/CrDownloadOptions.cs index 271e3d0..0deeb80 100644 --- a/CRD/Utils/Structs/CrDownloadOptions.cs +++ b/CRD/Utils/Structs/CrDownloadOptions.cs @@ -108,7 +108,7 @@ public class CrDownloadOptions{ [YamlIgnore] public bool? Skipmux{ get; set; } - [YamlIgnore] + [YamlMember(Alias = "mux_sync_dubs", ApplyNamingConventions = false)] public bool SyncTiming{ get; set; } [YamlIgnore] diff --git a/CRD/Utils/Structs/EpisodeStructs.cs b/CRD/Utils/Structs/EpisodeStructs.cs index e74ac7c..44850ac 100644 --- a/CRD/Utils/Structs/EpisodeStructs.cs +++ b/CRD/Utils/Structs/EpisodeStructs.cs @@ -250,7 +250,8 @@ public class CrunchyEpMeta{ public List? AvailableSubs{ get; set; } public string? DownloadPath{ get; set; } - + public List DownloadSubs{ get; set; } =[]; + } public class DownloadProgress{ diff --git a/CRD/Utils/Structs/History/HistoryEpisode.cs b/CRD/Utils/Structs/History/HistoryEpisode.cs index 8c752f7..6d35462 100644 --- a/CRD/Utils/Structs/History/HistoryEpisode.cs +++ b/CRD/Utils/Structs/History/HistoryEpisode.cs @@ -6,6 +6,7 @@ using Newtonsoft.Json; namespace CRD.Utils.Structs.History; public class HistoryEpisode : INotifyPropertyChanged{ + [JsonProperty("episode_title")] public string? EpisodeTitle{ get; set; } @@ -48,7 +49,7 @@ public class HistoryEpisode : INotifyPropertyChanged{ WasDownloaded = !WasDownloaded; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(WasDownloaded))); } - + public void ToggleWasDownloadedSeries(HistorySeries? series){ WasDownloaded = !WasDownloaded; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(WasDownloaded))); @@ -57,14 +58,15 @@ public class HistoryEpisode : INotifyPropertyChanged{ foreach (var historySeason in series.Seasons){ historySeason.UpdateDownloadedSilent(); } + series.UpdateNewEpisodes(); } - - - CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList); + + CfgManager.UpdateHistoryFile(); } public async Task DownloadEpisode(){ - await Crunchyroll.Instance.AddEpisodeToQue(EpisodeId, Crunchyroll.Instance.DefaultLocale, Crunchyroll.Instance.CrunOptions.DubLang); + await Crunchyroll.Instance.AddEpisodeToQue(EpisodeId, string.IsNullOrEmpty(Crunchyroll.Instance.CrunOptions.HistoryLang) ? Crunchyroll.Instance.DefaultLocale : Crunchyroll.Instance.CrunOptions.HistoryLang, + Crunchyroll.Instance.CrunOptions.DubLang); } } \ No newline at end of file diff --git a/CRD/Utils/Structs/History/HistorySeason.cs b/CRD/Utils/Structs/History/HistorySeason.cs index 6711300..e5eaf4a 100644 --- a/CRD/Utils/Structs/History/HistorySeason.cs +++ b/CRD/Utils/Structs/History/HistorySeason.cs @@ -1,6 +1,9 @@ using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; using System.ComponentModel; using System.Linq; +using Avalonia.Controls; using CRD.Downloader; using Newtonsoft.Json; @@ -19,9 +22,6 @@ public class HistorySeason : INotifyPropertyChanged{ [JsonProperty("season_special_season")] public bool? SpecialSeason{ get; set; } - [JsonIgnore] - public string CombinedProperty => SpecialSeason ?? false ? $"Specials {SeasonNum}" : $"Season {SeasonNum}"; - [JsonProperty("season_downloaded_episodes")] public int DownloadedEpisodes{ get; set; } @@ -30,36 +30,126 @@ public class HistorySeason : INotifyPropertyChanged{ [JsonProperty("series_download_path")] public string? SeasonDownloadPath{ get; set; } - + + [JsonProperty("history_season_soft_subs_override")] + public List HistorySeasonSoftSubsOverride{ get; set; } =[]; + + [JsonProperty("history_season_dub_lang_override")] + public List HistorySeasonDubLangOverride{ get; set; } =[]; + + [JsonIgnore] + public string CombinedProperty => SpecialSeason ?? false ? $"Specials {SeasonNum}" : $"Season {SeasonNum}"; + [JsonIgnore] public bool IsExpanded{ get; set; } public event PropertyChangedEventHandler? PropertyChanged; + #region Language Override + + [JsonIgnore] + public string SelectedSubs{ get; set; } = ""; + + [JsonIgnore] + public string SelectedDubs{ get; set; } = ""; + + [JsonIgnore] + public ObservableCollection SelectedSubLang{ get; set; } = new(); + + [JsonIgnore] + public ObservableCollection SelectedDubLang{ get; set; } = new(); + + [JsonIgnore] + public ObservableCollection DubLangList{ get; } = new(){ + }; + + [JsonIgnore] + public ObservableCollection SubLangList{ get; } = new(){ + new StringItem(){ stringValue = "all" }, + new StringItem(){ stringValue = "none" }, + }; + + private void UpdateSubAndDubString(){ + HistorySeasonSoftSubsOverride.Clear(); + HistorySeasonDubLangOverride.Clear(); + + if (SelectedSubLang.Count != 0){ + for (var i = 0; i < SelectedSubLang.Count; i++){ + HistorySeasonSoftSubsOverride.Add(SelectedSubLang[i].stringValue); + } + } + + if (SelectedDubLang.Count != 0){ + for (var i = 0; i < SelectedDubLang.Count; i++){ + HistorySeasonDubLangOverride.Add(SelectedDubLang[i].stringValue); + } + } + + SelectedDubs = string.Join(", ", HistorySeasonDubLangOverride) ?? ""; + SelectedSubs = string.Join(", ", HistorySeasonSoftSubsOverride) ?? ""; + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedSubs))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedDubs))); + + CfgManager.UpdateHistoryFile(); + } + + private void Changes(object? sender, NotifyCollectionChangedEventArgs e){ + UpdateSubAndDubString(); + } + + public void Init(){ + + if (!(SubLangList.Count > 2 || DubLangList.Count > 0)){ + foreach (var languageItem in Languages.languages){ + SubLangList.Add(new StringItem{ stringValue = languageItem.CrLocale }); + DubLangList.Add(new StringItem{ stringValue = languageItem.CrLocale }); + } + } + + + var softSubLang = SubLangList.Where(a => HistorySeasonSoftSubsOverride.Contains(a.stringValue)).ToList(); + var dubLang = DubLangList.Where(a => HistorySeasonDubLangOverride.Contains(a.stringValue)).ToList(); + + SelectedSubLang.Clear(); + foreach (var listBoxItem in softSubLang){ + SelectedSubLang.Add(listBoxItem); + } + + SelectedDubLang.Clear(); + foreach (var listBoxItem in dubLang){ + SelectedDubLang.Add(listBoxItem); + } + + SelectedDubs = string.Join(", ", HistorySeasonDubLangOverride) ?? ""; + SelectedSubs = string.Join(", ", HistorySeasonSoftSubsOverride) ?? ""; + + SelectedSubLang.CollectionChanged += Changes; + SelectedDubLang.CollectionChanged += Changes; + } + + #endregion + public void UpdateDownloaded(string? EpisodeId){ if (!string.IsNullOrEmpty(EpisodeId)){ - EpisodesList.First(e => e.EpisodeId == EpisodeId).ToggleWasDownloaded(); + var episode = EpisodesList.First(e => e.EpisodeId == EpisodeId); + episode.ToggleWasDownloaded(); } DownloadedEpisodes = EpisodesList.FindAll(e => e.WasDownloaded).Count; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DownloadedEpisodes))); - CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList); + CfgManager.UpdateHistoryFile(); } public void UpdateDownloaded(){ DownloadedEpisodes = EpisodesList.FindAll(e => e.WasDownloaded).Count; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DownloadedEpisodes))); - CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList); + CfgManager.UpdateHistoryFile(); } - + public void UpdateDownloadedSilent(){ DownloadedEpisodes = EpisodesList.FindAll(e => e.WasDownloaded).Count; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DownloadedEpisodes))); } - } -public class UpdateDownloadedHistorySeason{ - public string? EpisodeId; - public HistorySeries? HistorySeries; -} diff --git a/CRD/Utils/Structs/History/HistorySeries.cs b/CRD/Utils/Structs/History/HistorySeries.cs index ffaf00a..0f758f2 100644 --- a/CRD/Utils/Structs/History/HistorySeries.cs +++ b/CRD/Utils/Structs/History/HistorySeries.cs @@ -1,10 +1,12 @@ 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.Threading.Tasks; +using Avalonia.Controls; using Avalonia.Media.Imaging; using CRD.Downloader; using CRD.Utils.CustomList; @@ -40,9 +42,6 @@ public class HistorySeries : INotifyPropertyChanged{ [JsonProperty("series_new_episodes")] public int NewEpisodes{ get; set; } - [JsonIgnore] - public Bitmap? ThumbnailImage{ get; set; } - [JsonProperty("series_season_list")] public required RefreshableObservableCollection Seasons{ get; set; } @@ -51,9 +50,18 @@ public class HistorySeries : INotifyPropertyChanged{ [JsonProperty("history_series_add_date")] public DateTime? HistorySeriesAddDate{ get; set; } - + + [JsonProperty("history_series_soft_subs_override")] + public List HistorySeriesSoftSubsOverride{ get; set; } =[]; + + [JsonProperty("history_series_dub_lang_override")] + public List HistorySeriesDubLangOverride{ get; set; } =[]; + public event PropertyChangedEventHandler? PropertyChanged; + [JsonIgnore] + public Bitmap? ThumbnailImage{ get; set; } + [JsonIgnore] public bool FetchingData{ get; set; } @@ -74,6 +82,90 @@ public class HistorySeries : INotifyPropertyChanged{ [JsonIgnore] private bool _editModeEnabled; + #region Language Override + + [JsonIgnore] + public string SelectedSubs{ get; set; } = ""; + + [JsonIgnore] + public string SelectedDubs{ get; set; } = ""; + + [JsonIgnore] + public ObservableCollection SelectedSubLang{ get; set; } = new(); + + [JsonIgnore] + public ObservableCollection SelectedDubLang{ get; set; } = new(); + + + private void UpdateSubAndDubString(){ + HistorySeriesSoftSubsOverride.Clear(); + HistorySeriesDubLangOverride.Clear(); + + if (SelectedSubLang.Count != 0){ + for (var i = 0; i < SelectedSubLang.Count; i++){ + HistorySeriesSoftSubsOverride.Add(SelectedSubLang[i].stringValue); + } + } + + if (SelectedDubLang.Count != 0){ + for (var i = 0; i < SelectedDubLang.Count; i++){ + HistorySeriesDubLangOverride.Add(SelectedDubLang[i].stringValue); + } + } + + SelectedDubs = string.Join(", ", HistorySeriesDubLangOverride) ?? ""; + SelectedSubs = string.Join(", ", HistorySeriesSoftSubsOverride) ?? ""; + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedSubs))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedDubs))); + + CfgManager.UpdateHistoryFile(); + } + + private void Changes(object? sender, NotifyCollectionChangedEventArgs e){ + UpdateSubAndDubString(); + } + + [JsonIgnore] + public ObservableCollection DubLangList{ get; } = new(){ + }; + + [JsonIgnore] + public ObservableCollection SubLangList{ get; } = new(){ + new StringItem(){ stringValue = "all" }, + new StringItem(){ stringValue = "none" }, + }; + + public void Init(){ + if (!(SubLangList.Count > 2 || DubLangList.Count > 0)){ + foreach (var languageItem in Languages.languages){ + SubLangList.Add(new StringItem{ stringValue = languageItem.CrLocale }); + DubLangList.Add(new StringItem{ stringValue = languageItem.CrLocale }); + } + } + + var softSubLang = SubLangList.Where(a => HistorySeriesSoftSubsOverride.Contains(a.stringValue)).ToList(); + var dubLang = DubLangList.Where(a => HistorySeriesDubLangOverride.Contains(a.stringValue)).ToList(); + + SelectedSubLang.Clear(); + foreach (var listBoxItem in softSubLang){ + SelectedSubLang.Add(listBoxItem); + } + + SelectedDubLang.Clear(); + foreach (var listBoxItem in dubLang){ + SelectedDubLang.Add(listBoxItem); + } + + SelectedDubs = string.Join(", ", HistorySeriesDubLangOverride) ?? ""; + SelectedSubs = string.Join(", ", HistorySeriesSoftSubsOverride) ?? ""; + + SelectedSubLang.CollectionChanged += Changes; + SelectedDubLang.CollectionChanged += Changes; + } + + #endregion + public async Task LoadImage(){ try{ using (var client = new HttpClient()){ @@ -165,9 +257,8 @@ public class HistorySeries : INotifyPropertyChanged{ if (objectToRemove != null){ Seasons.Remove(objectToRemove); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Seasons))); + CfgManager.UpdateHistoryFile(); } - - CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList); } public void OpenSonarrPage(){ diff --git a/CRD/Utils/Structs/Languages.cs b/CRD/Utils/Structs/Languages.cs index b521dca..6c26f7e 100644 --- a/CRD/Utils/Structs/Languages.cs +++ b/CRD/Utils/Structs/Languages.cs @@ -111,7 +111,7 @@ public class Languages{ public static LanguageItem Locale2language(string locale){ - LanguageItem? filteredLocale = languages.FirstOrDefault(l => { return l.Locale == locale; }); + LanguageItem? filteredLocale = languages.FirstOrDefault(l => { return l.Locale == locale || l.CrLocale == locale; }); if (filteredLocale != null){ return (LanguageItem)filteredLocale; } else{ diff --git a/CRD/Utils/Structs/Structs.cs b/CRD/Utils/Structs/Structs.cs index d9952ca..ed25488 100644 --- a/CRD/Utils/Structs/Structs.cs +++ b/CRD/Utils/Structs/Structs.cs @@ -9,7 +9,9 @@ public struct AuthData{ } public class DrmAuthData{ - [JsonProperty("custom_data")] public string? CustomData{ get; set; } + [JsonProperty("custom_data")] + public string? CustomData{ get; set; } + public string? Token{ get; set; } } @@ -21,6 +23,7 @@ public struct Meta{ public struct LanguageItem{ [JsonProperty("cr_locale")] public string CrLocale{ get; set; } + public string Locale{ get; set; } public string Code{ get; set; } public string Name{ get; set; } @@ -62,7 +65,7 @@ public struct Episode{ public struct DownloadResponse{ public List Data{ get; set; } public string FileName{ get; set; } - + public string VideoTitle{ get; set; } public bool Error{ get; set; } public string ErrorText{ get; set; } @@ -75,6 +78,8 @@ public class DownloadedMedia : SxItem{ public bool? Cc{ get; set; } public bool? Signs{ get; set; } + + public DownloadedMedia? RelatedVideoDownloadMedia; } public class SxItem{ @@ -85,3 +90,11 @@ public class SxItem{ public Dictionary>? Fonts{ get; set; } } +public class FrameData{ + public string FilePath{ get; set; } + public double Time{ get; set; } +} + +public class StringItem{ + public string stringValue{ get; set; } +} \ No newline at end of file diff --git a/CRD/Utils/UI/UiUpdateDownloadedHistorySeasonConverter.cs b/CRD/Utils/UI/UiUpdateDownloadedHistorySeasonConverter.cs deleted file mode 100644 index 11a7df6..0000000 --- a/CRD/Utils/UI/UiUpdateDownloadedHistorySeasonConverter.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using Avalonia.Data.Converters; -using CRD.Utils.Structs.History; - -namespace CRD.Utils.UI; - -public class UiUpdateDownloadedHistorySeasonConverter : IMultiValueConverter{ - - public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture){ - - if (values[0] is string stringValue1){ - Console.WriteLine(stringValue1); - } - - if (values is[string stringValue, HistorySeries historySeries]){ - - return new UpdateDownloadedHistorySeason{ - EpisodeId = stringValue, - HistorySeries = historySeries - }; - } - - return new UpdateDownloadedHistorySeason{ - EpisodeId = "", - HistorySeries = null - }; - } - -} \ No newline at end of file diff --git a/CRD/Utils/Updater/Updater.cs b/CRD/Utils/Updater/Updater.cs index 5fa1c40..b121821 100644 --- a/CRD/Utils/Updater/Updater.cs +++ b/CRD/Utils/Updater/Updater.cs @@ -1,16 +1,11 @@ using System; using System.ComponentModel; -using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Net.Http; using System.Reflection; using System.Runtime.CompilerServices; using System.Threading.Tasks; -using CRD.Utils.HLS; -using CRD.ViewModels; -using CRD.Views.Utils; -using FluentAvalonia.UI.Controls; using Newtonsoft.Json; namespace CRD.Utils.Updater; diff --git a/CRD/ViewModels/AddDownloadPageViewModel.cs b/CRD/ViewModels/AddDownloadPageViewModel.cs index 8e86735..3a7552c 100644 --- a/CRD/ViewModels/AddDownloadPageViewModel.cs +++ b/CRD/ViewModels/AddDownloadPageViewModel.cs @@ -89,7 +89,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ episode.LoadImage(imageUrl); } } - + SearchItems.Add(episode); } @@ -103,7 +103,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ RaisePropertyChanged(nameof(SearchVisible)); SearchItems.Clear(); } - + partial void OnUrlInputChanged(string value){ if (SearchEnabled){ UpdateSearch(value); @@ -186,7 +186,10 @@ 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, + string.IsNullOrEmpty(locale) + ? string.IsNullOrEmpty(Crunchyroll.Instance.CrunOptions.HistoryLang) ? Crunchyroll.Instance.DefaultLocale : Crunchyroll.Instance.CrunOptions.HistoryLang + : Languages.Locale2language(locale).CrLocale, Crunchyroll.Instance.CrunOptions.DubLang, true); UrlInput = ""; selectedEpisodes.Clear(); SelectedItems.Clear(); @@ -209,7 +212,9 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ ButtonEnabled = false; ShowLoading = true; - var list = await Crunchyroll.Instance.CrSeries.ListSeriesId(id, Languages.Locale2language(locale).CrLocale, new CrunchyMultiDownload(Crunchyroll.Instance.CrunOptions.DubLang, true)); + var list = await Crunchyroll.Instance.CrSeries.ListSeriesId(id, string.IsNullOrEmpty(locale) + ? string.IsNullOrEmpty(Crunchyroll.Instance.CrunOptions.HistoryLang) ? Crunchyroll.Instance.DefaultLocale : Crunchyroll.Instance.CrunOptions.HistoryLang + : Languages.Locale2language(locale).CrLocale, new CrunchyMultiDownload(Crunchyroll.Instance.CrunOptions.DubLang, true)); ShowLoading = false; if (list != null){ currentSeriesList = list; @@ -275,8 +280,8 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ } } - async partial void OnSelectedSearchItemChanged(CrBrowseSeries value){ - if (value == null){ + async partial void OnSelectedSearchItemChanged(CrBrowseSeries? value){ + if (value == null || string.IsNullOrEmpty(value.Id)){ return; } @@ -286,7 +291,9 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ SearchVisible = false; ButtonEnabled = false; ShowLoading = true; - var list = await Crunchyroll.Instance.CrSeries.ListSeriesId(value.Id, "", new CrunchyMultiDownload(Crunchyroll.Instance.CrunOptions.DubLang, true)); + var list = await Crunchyroll.Instance.CrSeries.ListSeriesId(value.Id, + string.IsNullOrEmpty(Crunchyroll.Instance.CrunOptions.HistoryLang) ? Crunchyroll.Instance.DefaultLocale : Crunchyroll.Instance.CrunOptions.HistoryLang, + new CrunchyMultiDownload(Crunchyroll.Instance.CrunOptions.DubLang, true)); ShowLoading = false; if (list != null){ currentSeriesList = list; @@ -296,7 +303,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ 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) + new(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 }); } @@ -351,11 +358,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 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 58af783..5166f2c 100644 --- a/CRD/ViewModels/CalendarPageViewModel.cs +++ b/CRD/ViewModels/CalendarPageViewModel.cs @@ -258,7 +258,7 @@ public partial class CalendarPageViewModel : ViewModelBase{ private async void BuildCustomCalendar(){ ShowLoading = true; - var newEpisodesBase = await Crunchyroll.Instance.CrEpisode.GetNewEpisodes(Crunchyroll.Instance.CrunOptions.HistoryLang, 200); + var newEpisodesBase = await Crunchyroll.Instance.CrEpisode.GetNewEpisodes(Crunchyroll.Instance.CrunOptions.HistoryLang, 200,true); CalendarWeek week = new CalendarWeek(); week.CalendarDays = new List(); @@ -283,7 +283,7 @@ public partial class CalendarPageViewModel : ViewModelBase{ foreach (var crBrowseEpisode in newEpisodes){ var targetDate = FilterByAirDate ? crBrowseEpisode.EpisodeMetadata.EpisodeAirDate : crBrowseEpisode.LastPublic; - if (HideDubs && crBrowseEpisode.EpisodeMetadata.SeasonTitle != null && crBrowseEpisode.EpisodeMetadata.SeasonTitle.EndsWith("Dub)")){ + if (HideDubs && crBrowseEpisode.EpisodeMetadata.SeasonTitle != null && (crBrowseEpisode.EpisodeMetadata.SeasonTitle.EndsWith("Dub)") || crBrowseEpisode.EpisodeMetadata.AudioLocale != Locale.JaJp)){ continue; } @@ -304,8 +304,8 @@ public partial class CalendarPageViewModel : ViewModelBase{ calEpisode.DateTime = targetDate; calEpisode.HasPassed = DateTime.Now > targetDate; 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.SeriesUrl = $"https://www.crunchyroll.com/{Crunchyroll.Instance.CrunOptions.HistoryLang}/series/" + crBrowseEpisode.EpisodeMetadata.SeriesId; + calEpisode.EpisodeUrl = $"https://www.crunchyroll.com/{Crunchyroll.Instance.CrunOptions.HistoryLang}/watch/{crBrowseEpisode.Id}/"; calEpisode.ThumbnailUrl = crBrowseEpisode.Images.Thumbnail.First().First().Source; calEpisode.IsPremiumOnly = crBrowseEpisode.EpisodeMetadata.IsPremiumOnly; calEpisode.IsPremiere = crBrowseEpisode.EpisodeMetadata.Episode == "1"; diff --git a/CRD/ViewModels/DownloadsPageViewModel.cs b/CRD/ViewModels/DownloadsPageViewModel.cs index 2026530..982bcd9 100644 --- a/CRD/ViewModels/DownloadsPageViewModel.cs +++ b/CRD/ViewModels/DownloadsPageViewModel.cs @@ -104,7 +104,7 @@ public partial class DownloadItemModel : INotifyPropertyChanged{ var softSubs = "Softsub: "; - if (Crunchyroll.Instance.CrunOptions.DlSubs.Contains("all")){ + if (epMeta.DownloadSubs.Contains("all")){ if (epMeta.AvailableSubs != null){ foreach (var epMetaAvailableSub in epMeta.AvailableSubs){ softSubs += epMetaAvailableSub + " "; @@ -114,7 +114,7 @@ public partial class DownloadItemModel : INotifyPropertyChanged{ } } - foreach (var crunOptionsDlSub in Crunchyroll.Instance.CrunOptions.DlSubs){ + foreach (var crunOptionsDlSub in epMeta.DownloadSubs){ if (epMeta.AvailableSubs != null && epMeta.AvailableSubs.Contains(crunOptionsDlSub)){ softSubs += crunOptionsDlSub + " "; } diff --git a/CRD/ViewModels/HistoryPageViewModel.cs b/CRD/ViewModels/HistoryPageViewModel.cs index 82cc0b3..e529938 100644 --- a/CRD/ViewModels/HistoryPageViewModel.cs +++ b/CRD/ViewModels/HistoryPageViewModel.cs @@ -84,7 +84,7 @@ public partial class HistoryPageViewModel : ViewModelBase{ [ObservableProperty] public static bool _sortDir = false; - + public HistoryPageViewModel(){ Items = Crunchyroll.Instance.HistoryList; @@ -217,7 +217,6 @@ public partial class HistoryPageViewModel : ViewModelBase{ if (!string.IsNullOrEmpty(value.SonarrSeriesId) && Crunchyroll.Instance.CrunOptions.SonarrProperties is{ SonarrEnabled: true }){ Crunchyroll.Instance.CrHistory.MatchHistoryEpisodesWithSonarr(true, SelectedSeries); - CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList); } @@ -230,9 +229,8 @@ public partial class HistoryPageViewModel : ViewModelBase{ if (objectToRemove != null){ Crunchyroll.Instance.HistoryList.Remove(objectToRemove); Items.Remove(objectToRemove); + CfgManager.UpdateHistoryFile(); } - - CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList); } @@ -291,9 +289,9 @@ public partial class HistoryPageViewModel : ViewModelBase{ if (season != null){ season.SeasonDownloadPath = selectedFolder.Path.LocalPath; + CfgManager.UpdateHistoryFile(); } - - CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList); + } } @@ -316,9 +314,8 @@ public partial class HistoryPageViewModel : ViewModelBase{ if (series != null){ series.SeriesDownloadPath = selectedFolder.Path.LocalPath; + CfgManager.UpdateHistoryFile(); } - - CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList); } } diff --git a/CRD/ViewModels/MainWindowViewModel.cs b/CRD/ViewModels/MainWindowViewModel.cs index fd08265..0cb12b0 100644 --- a/CRD/ViewModels/MainWindowViewModel.cs +++ b/CRD/ViewModels/MainWindowViewModel.cs @@ -48,8 +48,8 @@ public partial class MainWindowViewModel : ViewModelBase{ public async void Init(){ UpdateAvailable = await Updater.Instance.CheckForUpdatesAsync(); - await Crunchyroll.Instance.Init(); - + Crunchyroll.Instance.InitOptions(); + if (Crunchyroll.Instance.CrunOptions.AccentColor != null){ _faTheme.CustomAccentColor = Color.Parse(Crunchyroll.Instance.CrunOptions.AccentColor); } @@ -63,5 +63,8 @@ public partial class MainWindowViewModel : ViewModelBase{ _faTheme.PreferSystemTheme = false; Application.Current.RequestedThemeVariant = ThemeVariant.Light; } + + await Crunchyroll.Instance.Init(); + } } \ No newline at end of file diff --git a/CRD/ViewModels/SeriesPageViewModel.cs b/CRD/ViewModels/SeriesPageViewModel.cs index fe54d8b..8a8c9fe 100644 --- a/CRD/ViewModels/SeriesPageViewModel.cs +++ b/CRD/ViewModels/SeriesPageViewModel.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; +using Avalonia.Controls; using Avalonia.Platform.Storage; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -21,28 +23,34 @@ public partial class SeriesPageViewModel : ViewModelBase{ [ObservableProperty] public static bool _editMode; - + [ObservableProperty] public static bool _sonarrAvailable; - - private IStorageProvider _storageProvider; + + + + + private IStorageProvider? _storageProvider; + public SeriesPageViewModel(){ + + + _selectedSeries = Crunchyroll.Instance.SelectedSeries; if (_selectedSeries.ThumbnailImage == null){ _selectedSeries.LoadImage(); } - + if (!string.IsNullOrEmpty(SelectedSeries.SonarrSeriesId) && Crunchyroll.Instance.CrunOptions.SonarrProperties != null){ SonarrAvailable = SelectedSeries.SonarrSeriesId.Length > 0 && Crunchyroll.Instance.CrunOptions.SonarrProperties.SonarrEnabled; - }else{ + } else{ SonarrAvailable = false; } - } - + [RelayCommand] public async Task OpenFolderDialogAsync(HistorySeason? season){ if (_storageProvider == null){ @@ -62,25 +70,25 @@ public partial class SeriesPageViewModel : ViewModelBase{ if (season != null){ season.SeasonDownloadPath = selectedFolder.Path.LocalPath; + CfgManager.UpdateHistoryFile(); } else{ SelectedSeries.SeriesDownloadPath = selectedFolder.Path.LocalPath; + CfgManager.UpdateHistoryFile(); } - - CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList); } } public void SetStorageProvider(IStorageProvider storageProvider){ _storageProvider = storageProvider ?? throw new ArgumentNullException(nameof(storageProvider)); } - - + + [RelayCommand] public async Task UpdateData(string? season){ await SelectedSeries.FetchData(season); - + SelectedSeries.Seasons.Refresh(); - + // MessageBus.Current.SendMessage(new NavigationMessage(typeof(SeriesPageViewModel), false, true)); } @@ -89,9 +97,8 @@ public partial class SeriesPageViewModel : ViewModelBase{ HistorySeason? objectToRemove = SelectedSeries.Seasons.FirstOrDefault(se => se.SeasonId == season) ?? null; if (objectToRemove != null){ SelectedSeries.Seasons.Remove(objectToRemove); + CfgManager.UpdateHistoryFile(); } - - CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList); MessageBus.Current.SendMessage(new NavigationMessage(typeof(SeriesPageViewModel), false, true)); } @@ -101,7 +108,4 @@ public partial class SeriesPageViewModel : ViewModelBase{ SelectedSeries.UpdateNewEpisodes(); MessageBus.Current.SendMessage(new NavigationMessage(null, true, false)); } - - - } \ No newline at end of file diff --git a/CRD/ViewModels/SettingsPageViewModel.cs b/CRD/ViewModels/SettingsPageViewModel.cs index 30d03b2..c046544 100644 --- a/CRD/ViewModels/SettingsPageViewModel.cs +++ b/CRD/ViewModels/SettingsPageViewModel.cs @@ -16,6 +16,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CRD.Downloader; using CRD.Utils; +using CRD.Utils.CustomList; using CRD.Utils.Sonarr; using CRD.Utils.Structs; using FluentAvalonia.Styling; @@ -51,6 +52,10 @@ public partial class SettingsPageViewModel : ViewModelBase{ [ObservableProperty] private bool _muxToMp4; + + [ObservableProperty] + private bool _syncTimings; + [ObservableProperty] private bool _includeEpisodeDescription; @@ -102,8 +107,7 @@ public partial class SettingsPageViewModel : ViewModelBase{ [ObservableProperty] private ComboBoxItem _selectedDescriptionLang; - - + [ObservableProperty] private string _selectedDubs = "ja-JP"; @@ -268,7 +272,7 @@ public partial class SettingsPageViewModel : ViewModelBase{ new ComboBoxItem(){ Content = "ar-SA" }, }; - public ObservableCollection DubLangList{ get; } = new(){ + public ObservableCollection DubLangList{ get; } = new(){ }; @@ -318,7 +322,7 @@ public partial class SettingsPageViewModel : ViewModelBase{ foreach (var languageItem in Languages.languages){ HardSubLangList.Add(new ComboBoxItem{ Content = languageItem.CrLocale }); SubLangList.Add(new ListBoxItem{ Content = languageItem.CrLocale }); - DubLangList.Add(new ComboBoxItem{ Content = languageItem.CrLocale }); + DubLangList.Add(new ListBoxItem{ Content = languageItem.CrLocale }); DefaultDubLangList.Add(new ComboBoxItem{ Content = languageItem.CrLocale }); DefaultSubLangList.Add(new ComboBoxItem{ Content = languageItem.CrLocale }); } @@ -358,9 +362,7 @@ public partial class SettingsPageViewModel : ViewModelBase{ foreach (var listBoxItem in dubLang){ SelectedDubLang.Add(listBoxItem); } - - UpdateSubAndDubString(); - + var props = options.SonarrProperties; if (props != null){ @@ -383,6 +385,7 @@ public partial class SettingsPageViewModel : ViewModelBase{ DownloadVideoForEveryDub = !options.DlVideoOnce; DownloadChapters = options.Chapters; MuxToMp4 = options.Mp4; + SyncTimings = options.SyncTiming; SkipSubMux = options.SkipSubsMux; LeadingNumbers = options.Numbers; FileName = options.FileName; @@ -417,6 +420,12 @@ public partial class SettingsPageViewModel : ViewModelBase{ FfmpegOptions.Add(new MuxingParam(){ ParamValue = 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; @@ -431,9 +440,7 @@ public partial class SettingsPageViewModel : ViewModelBase{ if (!settingsLoaded){ return; } - - UpdateSubAndDubString(); - + Crunchyroll.Instance.CrunOptions.IncludeVideoDescription = IncludeEpisodeDescription; Crunchyroll.Instance.CrunOptions.VideoTitle = FileTitle; Crunchyroll.Instance.CrunOptions.Novids = !DownloadVideo; @@ -441,6 +448,7 @@ public partial class SettingsPageViewModel : ViewModelBase{ Crunchyroll.Instance.CrunOptions.DlVideoOnce = !DownloadVideoForEveryDub; Crunchyroll.Instance.CrunOptions.Chapters = DownloadChapters; Crunchyroll.Instance.CrunOptions.Mp4 = MuxToMp4; + Crunchyroll.Instance.CrunOptions.SyncTiming = SyncTimings; Crunchyroll.Instance.CrunOptions.SkipSubsMux = SkipSubMux; Crunchyroll.Instance.CrunOptions.Numbers = Math.Clamp((int)(LeadingNumbers ?? 0),0,10); Crunchyroll.Instance.CrunOptions.FileName = FileName; @@ -459,11 +467,11 @@ public partial class SettingsPageViewModel : ViewModelBase{ string descLang = SelectedDescriptionLang.Content + ""; - Crunchyroll.Instance.CrunOptions.DescriptionLang = descLang != "default" ? descLang : ""; + Crunchyroll.Instance.CrunOptions.DescriptionLang = descLang != "default" ? descLang : Crunchyroll.Instance.DefaultLocale; string historyLang = SelectedHistoryLang.Content + ""; - Crunchyroll.Instance.CrunOptions.HistoryLang = historyLang != "default" ? historyLang : ""; + Crunchyroll.Instance.CrunOptions.HistoryLang = historyLang != "default" ? historyLang : Crunchyroll.Instance.DefaultLocale; string hslang = SelectedHSLang.Content + ""; @@ -481,10 +489,7 @@ public partial class SettingsPageViewModel : ViewModelBase{ } Crunchyroll.Instance.CrunOptions.DubLang = dubLangs; - - - - + Crunchyroll.Instance.CrunOptions.QualityAudio = SelectedAudioQuality?.Content + ""; Crunchyroll.Instance.CrunOptions.QualityVideo = SelectedVideoQuality?.Content + ""; Crunchyroll.Instance.CrunOptions.Theme = CurrentAppTheme?.Content + ""; @@ -556,28 +561,7 @@ public partial class SettingsPageViewModel : ViewModelBase{ return ScaledBorderAndShadow[0]; } } - - - private void UpdateSubAndDubString(){ - if (SelectedSubLang.Count == 0){ - SelectedSubs = "none"; - } else{ - SelectedSubs = SelectedSubLang[0].Content.ToString(); - for (var i = 1; i < SelectedSubLang.Count; i++){ - SelectedSubs += "," + SelectedSubLang[i].Content; - } - } - - if (SelectedDubLang.Count == 0){ - SelectedDubs = "none"; - } else{ - SelectedDubs = SelectedDubLang[0].Content.ToString(); - for (var i = 1; i < SelectedDubLang.Count; i++){ - SelectedDubs += "," + SelectedDubLang[i].Content; - } - } - } - + [RelayCommand] public void AddMkvMergeParam(){ MkvMergeOptions.Add(new MuxingParam(){ ParamValue = MkvMergeOption }); @@ -681,13 +665,21 @@ public partial class SettingsPageViewModel : ViewModelBase{ } 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(SelectedSubs) or nameof(SelectedDubs) or nameof(CustomAccentColor) or nameof(ListBoxColor) or nameof(CurrentAppTheme) or nameof(UseCustomAccent) or nameof(LogMode)){ + if (e.PropertyName is nameof(SelectedDubs) or nameof(SelectedSubs) or nameof(CustomAccentColor) or nameof(ListBoxColor) or nameof(CurrentAppTheme) or nameof(UseCustomAccent) or nameof(LogMode)){ return; } diff --git a/CRD/ViewModels/ViewModelBase.cs b/CRD/ViewModels/ViewModelBase.cs index 0d62935..bcc8044 100644 --- a/CRD/ViewModels/ViewModelBase.cs +++ b/CRD/ViewModels/ViewModelBase.cs @@ -1,4 +1,5 @@ -using System.ComponentModel; +using System; +using System.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel; namespace CRD.ViewModels; diff --git a/CRD/Views/HistoryPageView.axaml b/CRD/Views/HistoryPageView.axaml index a95a20d..42268b9 100644 --- a/CRD/Views/HistoryPageView.axaml +++ b/CRD/Views/HistoryPageView.axaml @@ -6,6 +6,7 @@ xmlns:ui="clr-namespace:CRD.Utils.UI" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:history="clr-namespace:CRD.Utils.Structs.History" + xmlns:structs="clr-namespace:CRD.Utils.Structs" x:DataType="vm:HistoryPageViewModel" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="CRD.Views.HistoryPageView"> @@ -13,7 +14,6 @@ - @@ -120,8 +120,8 @@ - - + + @@ -338,7 +338,7 @@ - + Edit + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -481,7 +590,7 @@ Command="{Binding $parent[ScrollViewer].((history:HistorySeries)DataContext).FetchData}" CommandParameter="{Binding SeasonId}"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Edit + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -178,7 +291,7 @@ Command="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).UpdateData}" CommandParameter="{Binding SeasonId}"> - + @@ -197,6 +310,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +