From ae0f936ff5e09a4c52df44947fb4ddc7e06690ef Mon Sep 17 00:00:00 2001 From: Elwador <75888166+Elwador@users.noreply.github.com> Date: Tue, 6 May 2025 18:31:44 +0200 Subject: [PATCH] Add - Added **toggle to count missing history episodes** instead of just new episodes Add - Added **Fast Add button** to the Seasons tab Chg - Changed **"Couldn't sync dubs" message** to include the specific failed dubs and added more details to the log Chg - Changed **history episode addition** to maintain order (slightly slower when adding but prevents queue mixing) Chg - Changed **language sorting** for improved clarity Chg - Changed **subtitle locale handling** to use the actual locale instead of Crunchyroll's local language tag Chg - Changed **filename format** when downloading all dubs to separate files to include the locale in the filename Fix - Fixed **download retries not being logged** Fix - Fixed **dubbed episodes added from the calendar** not updating correctly in the history Fix - Fixed **DRM authentication issue** --- CRD/Downloader/Crunchyroll/CrEpisode.cs | 16 +- CRD/Downloader/Crunchyroll/CrSeries.cs | 34 --- .../Crunchyroll/CrunchyrollManager.cs | 156 ++++------ .../CrunchyrollSettingsViewModel.cs | 8 +- CRD/Utils/DRM/Widevine.cs | 8 +- CRD/Utils/Enums/EnumCollection.cs | 2 +- CRD/Utils/HLS/HLSDownloader.cs | 12 +- CRD/Utils/Helpers.cs | 22 +- CRD/Utils/Http/HttpClientReq.cs | 10 +- CRD/Utils/Muxing/Merger.cs | 4 + .../Structs/Crunchyroll/CrDownloadOptions.cs | 3 + .../Crunchyroll/Episode/EpisodeStructs.cs | 10 + CRD/Utils/Structs/Crunchyroll/Playback.cs | 28 +- CRD/Utils/Structs/HelperClasses.cs | 2 +- CRD/Utils/Structs/History/HistorySeries.cs | 288 +++++++----------- CRD/Utils/Structs/Languages.cs | 72 +++-- CRD/ViewModels/HistoryPageViewModel.cs | 28 +- CRD/ViewModels/SeriesPageViewModel.cs | 26 +- .../UpcomingSeasonsPageViewModel.cs | 10 +- .../Utils/GeneralSettingsViewModel.cs | 59 ++-- CRD/Views/UpcomingSeasonsPageView.axaml | 57 +++- CRD/Views/Utils/GeneralSettingsView.axaml | 8 +- 22 files changed, 415 insertions(+), 448 deletions(-) diff --git a/CRD/Downloader/Crunchyroll/CrEpisode.cs b/CRD/Downloader/Crunchyroll/CrEpisode.cs index 051bd07..f298019 100644 --- a/CRD/Downloader/Crunchyroll/CrEpisode.cs +++ b/CRD/Downloader/Crunchyroll/CrEpisode.cs @@ -82,13 +82,7 @@ public class CrEpisode(){ if (episode.EpisodeAndLanguages.Langs.All(a => a.CrLocale != version.AudioLocale)){ // Push to arrays if there are no duplicates of the same language episode.EpisodeAndLanguages.Items.Add(dlEpisode); - episode.EpisodeAndLanguages.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == version.AudioLocale) ?? new LanguageItem{ - CrLocale = "und", - Locale = "un", - Code = "und", - Name = string.Empty, - Language = string.Empty - }); + episode.EpisodeAndLanguages.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == version.AudioLocale) ?? Languages.DEFAULT_lang); } } } else{ @@ -98,13 +92,7 @@ public class CrEpisode(){ if (episode.EpisodeAndLanguages.Langs.All(a => a.CrLocale != dlEpisode.AudioLocale)){ // Push to arrays if there are no duplicates of the same language episode.EpisodeAndLanguages.Items.Add(dlEpisode); - episode.EpisodeAndLanguages.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == dlEpisode.AudioLocale) ?? new LanguageItem{ - CrLocale = "und", - Locale = "un", - Code = "und", - Name = string.Empty, - Language = string.Empty - }); + episode.EpisodeAndLanguages.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == dlEpisode.AudioLocale) ?? Languages.DEFAULT_lang); } } diff --git a/CRD/Downloader/Crunchyroll/CrSeries.cs b/CRD/Downloader/Crunchyroll/CrSeries.cs index 1015eeb..7d53662 100644 --- a/CRD/Downloader/Crunchyroll/CrSeries.cs +++ b/CRD/Downloader/Crunchyroll/CrSeries.cs @@ -343,40 +343,6 @@ public class CrSeries{ return episodeList; } - public Dictionary> ParseSeriesResult(CrSeriesSearch seasonsList){ - var ret = new Dictionary>(); - int i = 0; - - if (seasonsList.Data == null) return ret; - - foreach (var item in seasonsList.Data){ - i++; - foreach (var lang in Languages.languages){ - int seasonNumber = item.SeasonNumber; - if (item.Versions != null){ - seasonNumber = i; - } - - if (!ret.ContainsKey(seasonNumber)){ - ret[seasonNumber] = new Dictionary(); - } - - if (item.Title.Contains($"({lang.Name} Dub)") || item.Title.Contains($"({lang.Name})")){ - ret[seasonNumber][lang.Code] = item; - } else if (item.IsSubbed && !item.IsDubbed && lang.Code == "jpn"){ - ret[seasonNumber][lang.Code] = item; - } else if (item.IsDubbed && lang.Code == "eng" && !Languages.languages.Any(a => (item.Title.Contains($"({a.Name})") || item.Title.Contains($"({a.Name} Dub)")))){ - // Dubbed with no more infos will be treated as eng dubs - ret[seasonNumber][lang.Code] = item; - } else if (item.AudioLocale == lang.CrLocale){ - ret[seasonNumber][lang.Code] = item; - } - } - } - - return ret; - } - public async Task ParseSeriesById(string id, string? crLocale, bool forced = false){ await crunInstance.CrAuth.RefreshToken(true); NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query); diff --git a/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs b/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs index f3d9bd8..7cf5b7e 100644 --- a/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs +++ b/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs @@ -4,6 +4,7 @@ using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; using System.IO; +using System.IO.Compression; using System.Linq; using System.Net.Http; using System.Runtime.InteropServices; @@ -296,6 +297,7 @@ public class CrunchyrollManager{ await CrAuth.AuthAnonymous(); } + if (CrunOptions.History){ if (File.Exists(CfgManager.PathCrHistory)){ var decompressedJson = CfgManager.DecompressJsonFile(CfgManager.PathCrHistory); @@ -329,6 +331,12 @@ public class CrunchyrollManager{ await SonarrClient.Instance.RefreshSonarr(); } + + //Fix hslang - can be removed in a future version + var lang = Languages.Locale2language(CrunOptions.Hslang); + if (lang != Languages.DEFAULT_lang){ + CrunOptions.Hslang = lang.CrLocale; + } } @@ -363,6 +371,7 @@ public class CrunchyrollManager{ if (options.SkipMuxing == false){ bool syncError = false; bool muxError = false; + var notSyncedDubs = ""; data.DownloadProgress = new DownloadProgress(){ IsDownloading = true, @@ -382,6 +391,7 @@ public class CrunchyrollManager{ ? Path.Combine(res.TempFolderPath ?? string.Empty, res.FileName ?? string.Empty) : Path.Combine(res.FolderPath ?? string.Empty, res.FileName ?? string.Empty); if (options is{ DlVideoOnce: false, KeepDubsSeperate: true }){ + var groupByDub = Helpers.GroupByLanguageWithSubtitles(res.Data); var mergers = new List(); foreach (var keyValue in groupByDub){ @@ -391,7 +401,7 @@ public class CrunchyrollManager{ SubLangList = options.DlSubs, FfmpegOptions = options.FfmpegOptions, SkipSubMux = options.SkipSubsMux, - Output = fileNameAndPath, + Output = fileNameAndPath + $".{keyValue.Value.First().Lang.Locale}", Mp4 = options.Mp4, MuxFonts = options.MuxFonts, VideoTitle = res.VideoTitle, @@ -411,7 +421,7 @@ public class CrunchyrollManager{ CcSubsMuxingFlag = options.CcSubsMuxingFlag, SignsSubsAsForced = options.SignsSubsAsForced, }, - fileNameAndPath, data); + fileNameAndPath + $".{keyValue.Value.First().Lang.Locale}", data); if (result is{ merger: not null, isMuxed: true }){ mergers.Add(result.merger); @@ -479,6 +489,7 @@ public class CrunchyrollManager{ fileNameAndPath, data); syncError = result.syncError; + notSyncedDubs = result.notSyncedDubs; muxError = !result.isMuxed; if (result is{ merger: not null, isMuxed: true }){ @@ -512,7 +523,7 @@ public class CrunchyrollManager{ Percent = 100, Time = 0, DownloadSpeed = 0, - Doing = (muxError ? "Muxing Failed" : "Done") + (syncError ? " - Couldn't sync dubs" : "") + Doing = (muxError ? "Muxing Failed" : "Done") + (syncError ? $" - Couldn't sync dubs ({notSyncedDubs})" : "") }; if (CrunOptions.RemoveFinishedDownload && !syncError){ @@ -551,7 +562,8 @@ public class CrunchyrollManager{ QueueManager.Instance.Queue.Refresh(); if (options.History && data.Data is{ Count: > 0 } && (options.HistoryIncludeCrArtists && data.Music || !data.Music)){ - History.SetAsDownloaded(data.SeriesId, data.SeasonId, data.Data.First().MediaId); + var ids = data.Data.First().GetOriginalIds(); + History.SetAsDownloaded(data.SeriesId, ids.seasonID ?? data.SeasonId, ids.guid ?? data.Data.First().MediaId); } if (options.MarkAsWatched && data.Data is{ Count: > 0 }){ @@ -648,7 +660,7 @@ public class CrunchyrollManager{ #endregion - private async Task<(Merger? merger, bool isMuxed, bool syncError)> MuxStreams(List data, CrunchyMuxOptions options, string filename, CrunchyEpMeta crunchyEpMeta){ + private async Task<(Merger? merger, bool isMuxed, bool syncError, string notSyncedDubs)> MuxStreams(List data, CrunchyMuxOptions options, string filename, CrunchyEpMeta crunchyEpMeta){ var muxToMp3 = false; if (options.Novids == true || data.FindAll(a => a.Type == DownloadMediaType.Video).Count == 0){ @@ -657,7 +669,7 @@ public class CrunchyrollManager{ muxToMp3 = true; } else{ Console.WriteLine("Skip muxing since no videos are downloaded"); - return (null, false, false); + return (null, false, false, ""); } } @@ -733,6 +745,8 @@ public class CrunchyrollManager{ } bool isMuxed, syncError = false; + List notSyncedDubs =[]; + if (options is{ SyncTiming: true, DlVideoOnce: true }){ crunchyEpMeta.DownloadProgress = new DownloadProgress(){ @@ -755,6 +769,7 @@ public class CrunchyrollManager{ if (delay <= -100){ syncError = true; + notSyncedDubs.Add(syncVideo.Lang.CrLocale ?? syncVideo.Language.CrLocale); continue; } @@ -794,7 +809,7 @@ public class CrunchyrollManager{ isMuxed = await merger.Merge("ffmpeg", CfgManager.PathFFMPEG); } - return (merger, isMuxed, syncError); + return (merger, isMuxed, syncError, string.Join(", ", notSyncedDubs)); } private async Task DownloadMediaList(CrunchyEpMeta data, CrDownloadOptions options){ @@ -974,7 +989,7 @@ public class CrunchyrollManager{ List compiledChapters = new List(); - if (options.Chapters){ + if (options.Chapters && !data.OnlySubs){ await ParseChapters(mediaGuid, compiledChapters); if (compiledChapters.Count == 0 && primaryVersion.MediaGuid != null && mediaGuid != primaryVersion.MediaGuid){ @@ -1052,18 +1067,16 @@ public class CrunchyrollManager{ if (pbStreams?.Keys != null){ var pb = pbStreams.Select(v => { - v.Value.HardsubLang = v.Value.HardsubLocale != null - ? Languages.FixAndFindCrLc(v.Value.HardsubLocale.GetEnumMemberValue()).Locale - : null; - if (v.Value.HardsubLocale != null && v.Value.HardsubLang != null && !hsLangs.Contains(v.Value.HardsubLocale.GetEnumMemberValue())){ - hsLangs.Add(v.Value.HardsubLang); + if (v.Value is{ IsHardsubbed: true, HardsubLocale: not null } && v.Value.HardsubLocale != Locale.DefaulT && !hsLangs.Contains(v.Value.HardsubLang.CrLocale)){ + hsLangs.Add(v.Value.HardsubLang.CrLocale); } return new StreamDetailsPop{ Url = v.Value.Url, + IsHardsubbed = v.Value.IsHardsubbed, HardsubLocale = v.Value.HardsubLocale, HardsubLang = v.Value.HardsubLang, - AudioLang = v.Value.AudioLang, + AudioLang = pbData.Meta?.AudioLocale ?? Languages.DEFAULT_lang, Type = v.Value.Type, Format = "drm_adaptive_dash", }; @@ -1083,16 +1096,16 @@ public class CrunchyrollManager{ }; } - var audDub = ""; + var audDub = Languages.DEFAULT_lang; if (pbData.Meta != null){ - audDub = Languages.FindLang(Languages.FixLanguageTag((pbData.Meta.AudioLocale ?? Locale.DefaulT).GetEnumMemberValue())).Code; + audDub = pbData.Meta.AudioLocale; } hsLangs = Languages.SortTags(hsLangs); streams = streams.Select(s => { s.AudioLang = audDub; - s.HardsubLang = string.IsNullOrEmpty(s.HardsubLang) ? "-" : s.HardsubLang; + s.HardsubLang = s.HardsubLang; s.Type = $"{s.Format}/{s.AudioLang}/{s.HardsubLang}"; return s; }).ToList(); @@ -1102,21 +1115,15 @@ public class CrunchyrollManager{ if (options.Hslang != "none"){ if (hsLangs.IndexOf(options.Hslang) > -1){ Console.WriteLine($"Selecting stream with {Languages.Locale2language(options.Hslang).Language} hardsubs"); - streams = streams.Where((s) => s.HardsubLang != "-" && s.HardsubLang == options.Hslang).ToList(); + streams = streams.Where((s) => s.IsHardsubbed && s.HardsubLang.CrLocale == options.Hslang).ToList(); } else{ - Console.Error.WriteLine($"Selected stream with {Languages.Locale2language(options.Hslang).CrLocale} hardsubs not available"); + Console.Error.WriteLine($"Selected stream with {options.Hslang} hardsubs not available"); if (hsLangs.Count > 0){ Console.Error.WriteLine("Try hardsubs stream: " + string.Join(", ", hsLangs)); } if (dlVideoOnce && options.DlVideoOnce){ - streams = streams.Where((s) => { - if (s.HardsubLang != "-"){ - return false; - } - - return true; - }).ToList(); + streams = streams.Where((s) => !s.IsHardsubbed).ToList(); } else{ if (hsLangs.Count > 0){ var dialog = new ContentDialog(){ @@ -1141,7 +1148,7 @@ public class CrunchyrollManager{ if (hsLangs.IndexOf(selectedValue) > -1){ Console.WriteLine($"Selecting stream with {Languages.Locale2language(selectedValue).Language} hardsubs"); - streams = streams.Where((s) => s.HardsubLang != "-" && s.HardsubLang == selectedValue).ToList(); + streams = streams.Where((s) => s.IsHardsubbed && s.HardsubLang?.CrLocale == selectedValue).ToList(); data.Hslang = selectedValue; } } else{ @@ -1167,13 +1174,7 @@ public class CrunchyrollManager{ } } } else{ - streams = streams.Where((s) => { - if (s.HardsubLang != "-"){ - return false; - } - - return true; - }).ToList(); + streams = streams.Where((s) => !s.IsHardsubbed).ToList(); if (streams.Count < 1){ Console.Error.WriteLine("Raw streams not available!"); @@ -1205,7 +1206,7 @@ public class CrunchyrollManager{ } string tsFile = ""; - var videoDownloadMedia = new DownloadedMedia(); + var videoDownloadMedia = new DownloadedMedia(){ Lang = Languages.DEFAULT_lang }; if (!dlFailed && curStream != null && options is not{ Novids: true, Noaudio: true }){ var streamPlaylistsReq = HttpClientReq.CreateRequestMessage(curStream.Url ?? string.Empty, HttpMethod.Get, true, true, null); @@ -1231,7 +1232,7 @@ public class CrunchyrollManager{ //Parse MPD Playlists var crLocal = ""; if (pbData.Meta != null){ - crLocal = Languages.FixLanguageTag((pbData.Meta.AudioLocale ?? Locale.DefaulT).GetEnumMemberValue()); + crLocal = pbData.Meta.AudioLocale.CrLocale; } MPDParsed streamPlaylists = MPDParser.Parse(streamPlaylistsReqResponse.ResponseContent, Languages.FindLang(crLocal), matchedUrl); @@ -1348,10 +1349,10 @@ public class CrunchyrollManager{ variables.Add(new Variable("width", chosenVideoSegments.quality.width, false)); if (string.IsNullOrEmpty(data.Resolution)) data.Resolution = chosenVideoSegments.quality.height + "p"; - LanguageItem? lang = Languages.languages.FirstOrDefault(a => a.Code == curStream.AudioLang); + LanguageItem? lang = Languages.languages.FirstOrDefault(a => a.CrLocale == curStream.AudioLang.CrLocale); if (lang == null){ - Console.Error.WriteLine($"Unable to find language for code {curStream.AudioLang}"); - MainWindow.Instance.ShowError($"Unable to find language for code {curStream.AudioLang}"); + Console.Error.WriteLine($"Unable to find language for code {curStream.AudioLang.CrLocale}"); + MainWindow.Instance.ShowError($"Unable to find language for code {curStream.AudioLang.CrLocale}"); return new DownloadResponse{ Data = new List(), Error = true, @@ -1465,10 +1466,6 @@ public class CrunchyrollManager{ }; QueueManager.Instance.Queue.Refresh(); - var assetIdRegexMatch = Regex.Match(chosenVideoSegments.segments[0].uri, @"/assets/(?:p/)?([^_,]+)"); - var assetId = assetIdRegexMatch.Success ? assetIdRegexMatch.Groups[1].Value : null; - var sessionId = Helpers.GenerateSessionId(); - Console.WriteLine("Decryption Needed, attempting to decrypt"); if (!_widevine.canDecrypt){ @@ -1481,40 +1478,10 @@ public class CrunchyrollManager{ }; } - - var reqBodyData = new{ - accounting_id = "crunchyroll", - asset_id = assetId, - session_id = sessionId, - user_id = Token?.account_id - }; - - var json = JsonConvert.SerializeObject(reqBodyData); - var reqBody = new StringContent(json, Encoding.UTF8, "application/json"); - - var decRequest = HttpClientReq.CreateRequestMessage($"{ApiUrls.DRM}", HttpMethod.Post, false, false, null); - decRequest.Content = reqBody; - - var decRequestResponse = await HttpClientReq.Instance.SendHttpRequest(decRequest); - - if (!decRequestResponse.IsOk){ - Console.Error.WriteLine("Request to DRM Authentication failed: "); - MainWindow.Instance.ShowError("Request to DRM Authentication failed"); - dlFailed = true; - return new DownloadResponse{ - Data = files, - Error = dlFailed, - FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(fileDir, fileName)) : "./unknown", - ErrorText = "DRM Authentication failed" - }; - } - - DrmAuthData authData = Helpers.Deserialize(decRequestResponse.ResponseContent, SettingsJsonSerializerSettings) ?? new DrmAuthData(); - Dictionary authDataDict = new Dictionary - { { "dt-custom-data", authData.CustomData ?? string.Empty },{ "x-dt-auth-token", authData.Token ?? string.Empty } }; - - var encryptionKeys = await _widevine.getKeys(chosenVideoSegments.pssh, "https://lic.drmtoday.com/license-proxy-widevine/cenc/", authDataDict); + { { "authorization", "Bearer " + Token?.access_token },{"x-cr-content-id", mediaGuid},{"x-cr-video-token", pbData.Meta?.Token ?? string.Empty} }; + + var encryptionKeys = await _widevine.getKeys(chosenVideoSegments.pssh, ApiUrls.WidevineLicenceUrl, authDataDict); if (encryptionKeys.Count == 0){ Console.Error.WriteLine("Failed to get encryption keys"); @@ -1527,7 +1494,6 @@ public class CrunchyrollManager{ }; } - if (Path.Exists(CfgManager.PathMP4Decrypt) || Path.Exists(CfgManager.PathShakaPackager)){ var keyId = BitConverter.ToString(encryptionKeys[0].KeyID).Replace("-", "").ToLower(); var key = BitConverter.ToString(encryptionKeys[0].Bytes).Replace("-", "").ToLower(); @@ -1610,6 +1576,7 @@ public class CrunchyrollManager{ Type = syncTimingDownload ? DownloadMediaType.SyncVideo : DownloadMediaType.Video, Path = $"{tsFile}.video.m4s", Lang = lang, + Language = lang, IsPrimary = isPrimary }; files.Add(videoDownloadMedia); @@ -1694,6 +1661,7 @@ public class CrunchyrollManager{ Type = syncTimingDownload ? DownloadMediaType.SyncVideo : DownloadMediaType.Video, Path = $"{tsFile}.video.m4s", Lang = lang, + Language = lang, IsPrimary = isPrimary }; files.Add(videoDownloadMedia); @@ -1753,13 +1721,7 @@ public class CrunchyrollManager{ } // Finding language by code - var lang = Languages.languages.FirstOrDefault(l => l.Code == curStream?.AudioLang) ?? new LanguageItem{ - CrLocale = "und", - Locale = "un", - Code = "und", - Name = string.Empty, - Language = string.Empty - }; + var lang = Languages.languages.FirstOrDefault(l => l == curStream?.AudioLang) ?? Languages.DEFAULT_lang; if (lang.Code == "und"){ Console.Error.WriteLine($"Unable to find language for code {curStream?.AudioLang}"); } @@ -1793,7 +1755,7 @@ public class CrunchyrollManager{ } } - if (options.IncludeVideoDescription){ + if (options.IncludeVideoDescription && !data.OnlySubs){ string fullPath = (Path.IsPathRooted(fileName) ? fileName : Path.Combine(fileDir, fileName)) + ".xml"; if (!File.Exists(fullPath)){ @@ -1821,6 +1783,7 @@ public class CrunchyrollManager{ files.Add(new DownloadedMedia{ Type = DownloadMediaType.Description, Path = fullPath, + Lang = Languages.DEFAULT_lang }); data.downloadedFiles.Add(fullPath); } else{ @@ -1828,6 +1791,7 @@ public class CrunchyrollManager{ files.Add(new DownloadedMedia{ Type = DownloadMediaType.Description, Path = fullPath, + Lang = Languages.DEFAULT_lang }); data.downloadedFiles.Add(fullPath); } @@ -1858,7 +1822,7 @@ public class CrunchyrollManager{ }; } - private static async Task DownloadSubtitles(CrDownloadOptions options, PlaybackData pbData, string audDub, string fileName, List files, string fileDir, CrunchyEpMeta data, + private static async Task DownloadSubtitles(CrDownloadOptions options, PlaybackData pbData, LanguageItem audDub, string fileName, List files, string fileDir, CrunchyEpMeta data, DownloadedMedia videoDownloadMedia){ if (pbData.Meta != null && (pbData.Meta.Subtitles is{ Count: > 0 } || pbData.Meta.Captions is{ Count: > 0 })){ List subsData = pbData.Meta.Subtitles?.Values.ToList() ??[]; @@ -1894,7 +1858,7 @@ public class CrunchyrollManager{ var langItem = subsItem.locale; var sxData = new SxItem(); sxData.Language = langItem; - var isSigns = langItem.Code == audDub && !subsItem.isCC; + var isSigns = langItem.CrLocale == audDub.CrLocale && !subsItem.isCC; var isCc = subsItem.isCC; sxData.File = Languages.SubsFile(fileName, index + "", langItem, isCc, options.CcTag, isSigns, subsItem.format, !(data.DownloadSubs.Count == 1 && !data.DownloadSubs.Contains("all"))); @@ -2127,7 +2091,7 @@ public class CrunchyrollManager{ Data = new Dictionary() }; - var playbackEndpoint = $"https://cr-play-service.prd.crunchyrollsvc.com/v2/{(music ? "music/" : "")}{mediaGuidId}/{options.StreamEndpoint}/play"; + var playbackEndpoint = $"{ApiUrls.Playback}/{(music ? "music/" : "")}{mediaGuidId}/{options.StreamEndpoint}/play"; var playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint); if (!playbackRequestResponse.IsOk){ @@ -2138,7 +2102,7 @@ public class CrunchyrollManager{ temppbData = await ProcessPlaybackResponseAsync(playbackRequestResponse.ResponseContent, mediaId, mediaGuidId); } else{ Console.WriteLine("Request Stream URLs FAILED! Attempting fallback"); - playbackEndpoint = $"https://cr-play-service.prd.crunchyrollsvc.com/v2/{(music ? "music/" : "")}{mediaGuidId}/web/firefox/play"; + playbackEndpoint = $"{ApiUrls.Playback}/{(music ? "music/" : "")}{mediaGuidId}/web/firefox/play"; playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint); if (!playbackRequestResponse.IsOk){ @@ -2190,30 +2154,36 @@ public class CrunchyrollManager{ var derivedPlayCrunchyStreams = new CrunchyStreams(); if (playStream.HardSubs != null){ + //hlang "none" is no hardsube same url as the default url foreach (var hardsub in playStream.HardSubs){ var stream = hardsub.Value; derivedPlayCrunchyStreams[hardsub.Key] = new StreamDetails{ Url = stream.Url, - HardsubLocale = stream.Hlang + IsHardsubbed = true, + HardsubLocale = stream.Hlang, + HardsubLang = Languages.FixAndFindCrLc((stream.Hlang ?? Locale.DefaulT).GetEnumMemberValue()) }; } } derivedPlayCrunchyStreams[""] = new StreamDetails{ Url = playStream.Url, - HardsubLocale = Locale.DefaulT + IsHardsubbed = false, + HardsubLocale = Locale.DefaulT, + HardsubLang = Languages.DEFAULT_lang }; temppbData.Data = derivedPlayCrunchyStreams; temppbData.Total = 1; temppbData.Meta = new PlaybackMeta{ - AudioLocale = playStream.AudioLocale, + AudioLocale = Languages.FindLang(playStream.AudioLocale != null ? playStream.AudioLocale.GetEnumMemberValue() : ""), Versions = playStream.Versions, Bifs = new List{ playStream.Bifs ?? "" }, MediaId = mediaId, Captions = playStream.Captions, - Subtitles = new Subtitles() + Subtitles = new Subtitles(), + Token = playStream.Token, }; if (playStream.Subtitles != null){ diff --git a/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs b/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs index b548880..366f215 100644 --- a/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs +++ b/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs @@ -270,7 +270,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ ComboBoxItem? descriptionLang = DescriptionLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.DescriptionLang) ?? null; SelectedDescriptionLang = descriptionLang ?? DescriptionLangList[0]; - ComboBoxItem? hsLang = HardSubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == Languages.Locale2language(options.Hslang).CrLocale) ?? null; + ComboBoxItem? hsLang = HardSubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.Hslang) ?? null; SelectedHSLang = hsLang ?? HardSubLangList[0]; ComboBoxItem? defaultDubLang = DefaultDubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.DefaultAudio ?? "")) ?? null; @@ -416,10 +416,8 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ string descLang = SelectedDescriptionLang.Content + ""; CrunchyrollManager.Instance.CrunOptions.DescriptionLang = descLang != "default" ? descLang : CrunchyrollManager.Instance.DefaultLocale; - - string hslang = SelectedHSLang.Content + ""; - - CrunchyrollManager.Instance.CrunOptions.Hslang = hslang != "none" ? Languages.FindLang(hslang).Locale : hslang; + + CrunchyrollManager.Instance.CrunOptions.Hslang = SelectedHSLang.Content + ""; CrunchyrollManager.Instance.CrunOptions.DefaultAudio = SelectedDefaultDubLang.Content + ""; CrunchyrollManager.Instance.CrunOptions.DefaultSub = SelectedDefaultSubLang.Content + ""; diff --git a/CRD/Utils/DRM/Widevine.cs b/CRD/Utils/DRM/Widevine.cs index 2e472b4..f5f1651 100644 --- a/CRD/Utils/DRM/Widevine.cs +++ b/CRD/Utils/DRM/Widevine.cs @@ -41,10 +41,10 @@ public class Widevine{ if (Directory.Exists(CfgManager.PathWIDEVINE_DIR)){ foreach (var file in Directory.EnumerateFiles(CfgManager.PathWIDEVINE_DIR)){ var fileInfo = new FileInfo(file); - + if (fileInfo.Length >= 1024 * 8 || fileInfo.Attributes.HasFlag(FileAttributes.Directory)) continue; - + string fileContents = File.ReadAllText(file, Encoding.UTF8); if (IsPrivateKey(fileContents)){ @@ -107,7 +107,9 @@ public class Widevine{ } var licenceReq = ses.GetLicenseRequest(); - playbackRequest2.Content = new ByteArrayContent(licenceReq); + var content = new ByteArrayContent(licenceReq); + content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream"); + playbackRequest2.Content = content; var response = await HttpClientReq.Instance.SendHttpRequest(playbackRequest2); diff --git a/CRD/Utils/Enums/EnumCollection.cs b/CRD/Utils/Enums/EnumCollection.cs index 817ede7..5d76d44 100644 --- a/CRD/Utils/Enums/EnumCollection.cs +++ b/CRD/Utils/Enums/EnumCollection.cs @@ -41,7 +41,7 @@ public enum SeriesType{ public enum Locale{ [EnumMember(Value = "")] DefaulT, - + [EnumMember(Value = "un")] Unknown, diff --git a/CRD/Utils/HLS/HLSDownloader.cs b/CRD/Utils/HLS/HLSDownloader.cs index f5aab85..df1f869 100644 --- a/CRD/Utils/HLS/HLSDownloader.cs +++ b/CRD/Utils/HLS/HLSDownloader.cs @@ -43,7 +43,7 @@ public class HlsDownloader{ Offset = options.Offset ?? 0, BaseUrl = options.BaseUrl, SkipInit = options.SkipInit ?? false, - Timeout = options.Timeout ?? 60 * 1000, + Timeout = options.Timeout ?? 15 * 1000, CheckPartLength = true, IsResume = options.Offset.HasValue && options.Offset.Value > 0, BytesDownloaded = 0, @@ -449,17 +449,17 @@ public class HlsDownloader{ // Log retry attempts string partType = isKey ? "Key" : "Part"; int partIndx = partIndex + 1 + segOffset; - Console.WriteLine($"{partType} {partIndx}: Attempt {attempt + 1} to retrieve data failed."); - Console.WriteLine($"\tError: {ex.Message}"); + Console.Error.WriteLine($"{partType} {partIndx}: Attempt {attempt + 1} to retrieve data failed."); + Console.Error.WriteLine($"\tError: {ex.Message}"); if (attempt == retryCount) throw; // rethrow after last retry await Task.Delay(_data.WaitTime); }catch (Exception ex) { - Console.WriteLine($"Unexpected exception at part {partIndex + 1 + segOffset}:"); - Console.WriteLine($"\tType: {ex.GetType()}"); - Console.WriteLine($"\tMessage: {ex.Message}"); + Console.Error.WriteLine($"Unexpected exception at part {partIndex + 1 + segOffset}:"); + Console.Error.WriteLine($"\tType: {ex.GetType()}"); + Console.Error.WriteLine($"\tMessage: {ex.Message}"); throw; } } diff --git a/CRD/Utils/Helpers.cs b/CRD/Utils/Helpers.cs index 216b5bb..bfba120 100644 --- a/CRD/Utils/Helpers.cs +++ b/CRD/Utils/Helpers.cs @@ -240,7 +240,7 @@ public class Helpers{ if (e.Data.StartsWith("Error:")){ Console.Error.WriteLine(e.Data); } else{ - Console.WriteLine(e.Data); + Console.WriteLine(e.Data); } } }; @@ -604,29 +604,29 @@ public class Helpers{ public static Dictionary> GroupByLanguageWithSubtitles(List allMedia){ //Group by language var languageGroups = allMedia - .Where(media => - !string.IsNullOrEmpty(media.Lang.CrLocale) || - (media.Type == DownloadMediaType.Subtitle && media.RelatedVideoDownloadMedia != null && - !string.IsNullOrEmpty(media.RelatedVideoDownloadMedia.Lang.CrLocale)) + .Where(media => media.Type != DownloadMediaType.Description && + (!string.IsNullOrEmpty(media.Lang?.CrLocale) || + (media is{ Type: DownloadMediaType.Subtitle, RelatedVideoDownloadMedia: not null } && + !string.IsNullOrEmpty(media.RelatedVideoDownloadMedia.Lang?.CrLocale))) ) .GroupBy(media => { - if (media.Type == DownloadMediaType.Subtitle && media.RelatedVideoDownloadMedia != null){ - return media.RelatedVideoDownloadMedia.Lang.CrLocale; + if (media is{ Type: DownloadMediaType.Subtitle, RelatedVideoDownloadMedia: not null }){ + return media.RelatedVideoDownloadMedia.Lang?.CrLocale ?? "und"; } - return media.Lang.CrLocale; + return media.Lang?.CrLocale ?? "und"; }) .ToDictionary(group => group.Key, group => group.ToList()); //Find and add Description media to each group var descriptionMedia = allMedia.Where(media => media.Type == DownloadMediaType.Description).ToList(); - foreach (var description in descriptionMedia){ + if (descriptionMedia.Count > 0){ foreach (var group in languageGroups.Values){ - group.Add(description); + group.Add(descriptionMedia[0]); } } - + return languageGroups; } diff --git a/CRD/Utils/Http/HttpClientReq.cs b/CRD/Utils/Http/HttpClientReq.cs index 1a92e65..d610e12 100644 --- a/CRD/Utils/Http/HttpClientReq.cs +++ b/CRD/Utils/Http/HttpClientReq.cs @@ -263,14 +263,22 @@ public static class ApiUrls{ public static string Cms => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/content/v2/cms"; public static string Content => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/content/v2"; + public static string Playback => "https://cr-play-service.prd.crunchyrollsvc.com/v2"; + //https://www.crunchyroll.com/playback/v2 + //https://cr-play-service.prd.crunchyrollsvc.com/v2 + public static string Subscription => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/subs/v3/subscriptions/"; public static readonly string BetaBrowse = ApiBeta + "/content/v1/browse"; public static readonly string BetaCms = ApiBeta + "/cms/v2"; public static readonly string DRM = ApiBeta + "/drm/v1/auth"; + public static readonly string WidevineLicenceUrl = "https://www.crunchyroll.com/license/v1/license/widevine"; + //https://lic.drmtoday.com/license-proxy-widevine/cenc/ + //https://lic.staging.drmtoday.com/license-proxy-widevine/cenc/ + public static string authBasicMob = "Basic eHVuaWh2ZWRidDNtYmlzdWhldnQ6MWtJUzVkeVR2akUwX3JxYUEzWWVBaDBiVVhVbXhXMTE="; - public static readonly string MobileUserAgent = "Crunchyroll/3.79.0 Android/15 okhttp/4.12.0"; + public static readonly string MobileUserAgent = "Crunchyroll/3.81.8 Android/15 okhttp/4.12.0"; public static readonly string FirefoxUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0"; } \ No newline at end of file diff --git a/CRD/Utils/Muxing/Merger.cs b/CRD/Utils/Muxing/Merger.cs index 4282efd..49333e7 100644 --- a/CRD/Utils/Muxing/Merger.cs +++ b/CRD/Utils/Muxing/Merger.cs @@ -361,6 +361,10 @@ public class Merger{ case < 0.1: return startOffset; case > 1: + Console.Error.WriteLine($"Couldn't sync dub:"); + Console.Error.WriteLine($"\tStart offset: {startOffset} seconds"); + Console.Error.WriteLine($"\tEnd offset: {endOffset} seconds"); + Console.Error.WriteLine($"\tVideo length difference: {lengthDiff} seconds"); return -100; default: return endOffset; diff --git a/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs b/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs index 76c75ec..f9e0633 100644 --- a/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs +++ b/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs @@ -63,6 +63,9 @@ public class CrDownloadOptions{ [JsonProperty("history")] public bool History{ get; set; } + + [JsonProperty("history_count_missing")] + public bool HistoryCountMissing { get; set; } [JsonProperty("history_include_cr_artists")] public bool HistoryIncludeCrArtists{ get; set; } diff --git a/CRD/Utils/Structs/Crunchyroll/Episode/EpisodeStructs.cs b/CRD/Utils/Structs/Crunchyroll/Episode/EpisodeStructs.cs index 541e2e2..5f0bff7 100644 --- a/CRD/Utils/Structs/Crunchyroll/Episode/EpisodeStructs.cs +++ b/CRD/Utils/Structs/Crunchyroll/Episode/EpisodeStructs.cs @@ -402,6 +402,16 @@ public class CrunchyEpMetaData{ public List? Versions{ get; set; } public bool IsSubbed{ get; set; } public bool IsDubbed{ get; set; } + + public (string? seasonID, string? guid) GetOriginalIds(){ + var version = Versions?.FirstOrDefault(a => a.Original); + if (version != null && !string.IsNullOrEmpty(version.Guid) && !string.IsNullOrEmpty(version.SeasonGuid)){ + return (version.SeasonGuid, version.Guid); + } + + return (null, null); + } + } public class CrunchyRollEpisodeData{ diff --git a/CRD/Utils/Structs/Crunchyroll/Playback.cs b/CRD/Utils/Structs/Crunchyroll/Playback.cs index 2c29a27..9130c05 100644 --- a/CRD/Utils/Structs/Crunchyroll/Playback.cs +++ b/CRD/Utils/Structs/Crunchyroll/Playback.cs @@ -16,7 +16,9 @@ public class StreamDetails{ public string? Url{ get; set; } [JsonProperty("hardsub_lang")] - public string? HardsubLang{ get; set; } + public required LanguageItem HardsubLang{ get; set; } + + public bool IsHardsubbed{ get; set; } [JsonProperty("audio_lang")] public string? AudioLang{ get; set; } @@ -24,6 +26,18 @@ public class StreamDetails{ public string? Type{ get; set; } } +public class StreamDetailsPop{ + public Locale? HardsubLocale{ get; set; } + public string? Url{ get; set; } + public required LanguageItem HardsubLang{ get; set; } + + public bool IsHardsubbed{ get; set; } + + public required LanguageItem AudioLang{ get; set; } + public string? Type{ get; set; } + public string? Format{ get; set; } +} + public class PlaybackMeta{ [JsonProperty("media_id")] public string? MediaId{ get; set; } @@ -33,12 +47,14 @@ public class PlaybackMeta{ public List? Versions{ get; set; } [JsonProperty("audio_locale")] - public Locale? AudioLocale{ get; set; } + public LanguageItem AudioLocale{ get; set; } [JsonProperty("closed_captions")] public Subtitles? ClosedCaptions{ get; set; } public Dictionary? Captions{ get; set; } + + public string? Token{ get; set; } } public class SubtitleInfo{ @@ -71,11 +87,3 @@ public class PlaybackVersion{ public string? Variant{ get; set; } } -public class StreamDetailsPop{ - public Locale? HardsubLocale{ get; set; } - public string? Url{ get; set; } - public string? HardsubLang{ get; set; } - public string? AudioLang{ get; set; } - public string? Type{ get; set; } - public string? Format{ get; set; } -} \ No newline at end of file diff --git a/CRD/Utils/Structs/HelperClasses.cs b/CRD/Utils/Structs/HelperClasses.cs index a484dd7..b5114c1 100644 --- a/CRD/Utils/Structs/HelperClasses.cs +++ b/CRD/Utils/Structs/HelperClasses.cs @@ -82,7 +82,7 @@ public class DownloadResponse{ public class DownloadedMedia : SxItem{ public DownloadMediaType Type{ get; set; } - public LanguageItem Lang{ get; set; } + public required LanguageItem Lang{ get; set; } public bool IsPrimary{ get; set; } public bool? Cc{ get; set; } diff --git a/CRD/Utils/Structs/History/HistorySeries.cs b/CRD/Utils/Structs/History/HistorySeries.cs index 49ca10b..cf9c241 100644 --- a/CRD/Utils/Structs/History/HistorySeries.cs +++ b/CRD/Utils/Structs/History/HistorySeries.cs @@ -22,7 +22,7 @@ public class HistorySeries : INotifyPropertyChanged{ [JsonProperty("series_type")] public SeriesType SeriesType{ get; set; } = SeriesType.Unknown; - + [JsonProperty("series_is_inactive")] public bool IsInactive{ get; set; } @@ -107,10 +107,10 @@ public class HistorySeries : INotifyPropertyChanged{ [JsonIgnore] public string SeriesFolderPath{ get; set; } - + [JsonIgnore] public bool SeriesFolderPathExists{ get; set; } - + #region Settings Override [JsonIgnore] @@ -225,9 +225,9 @@ public class HistorySeries : INotifyPropertyChanged{ SelectedSubLang.CollectionChanged += Changes; SelectedDubLang.CollectionChanged += Changes; - + UpdateSeriesFolderPath(); - + Loading = false; } @@ -249,113 +249,83 @@ public class HistorySeries : INotifyPropertyChanged{ public void UpdateNewEpisodes(){ int count = 0; + bool foundWatched = false; - var historyAddSpecials = CrunchyrollManager.Instance.CrunOptions.HistoryAddSpecials; - var sonarrEnabled = SeriesType != SeriesType.Artist && CrunchyrollManager.Instance.CrunOptions.SonarrProperties != null && CrunchyrollManager.Instance.CrunOptions.SonarrProperties.SonarrEnabled && - !string.IsNullOrEmpty(SonarrSeriesId); + var options = CrunchyrollManager.Instance.CrunOptions; - var sonarrSkipUnmonitored = CrunchyrollManager.Instance.CrunOptions.HistorySkipUnmonitored; + bool historyAddSpecials = options.HistoryAddSpecials; + bool sonarrEnabled = SeriesType != SeriesType.Artist && + options.SonarrProperties?.SonarrEnabled == true && + !string.IsNullOrEmpty(SonarrSeriesId); + bool skipUnmonitored = options.HistorySkipUnmonitored; + bool countMissing = options.HistoryCountMissing; + bool useSonarrCounting = options.HistoryCountSonarr; - if (sonarrEnabled && CrunchyrollManager.Instance.CrunOptions.HistoryCountSonarr){ - for (int i = Seasons.Count - 1; i >= 0; i--){ - var season = Seasons[i]; + for (int i = Seasons.Count - 1; i >= 0; i--){ + var season = Seasons[i]; + var episodes = season.EpisodesList; - if (season.SpecialSeason == true){ - if (historyAddSpecials){ - var episodes = season.EpisodesList; - for (int j = episodes.Count - 1; j >= 0; j--){ - if (sonarrSkipUnmonitored && !episodes[j].SonarrIsMonitored){ - continue; - } + if (season.SpecialSeason == true){ + if (historyAddSpecials){ + for (int j = episodes.Count - 1; j >= 0; j--){ + var ep = episodes[j]; - if (!string.IsNullOrEmpty(episodes[j].SonarrEpisodeId) && !episodes[j].SonarrHasFile){ - count++; - } + if (skipUnmonitored && sonarrEnabled && !ep.SonarrIsMonitored){ + continue; } + + if (ShouldCountEpisode(ep, sonarrEnabled && useSonarrCounting, countMissing, false)){ + count++; + } + } + } + + continue; + } + + + for (int j = episodes.Count - 1; j >= 0; j--){ + var ep = episodes[j]; + + if (skipUnmonitored && sonarrEnabled && !ep.SonarrIsMonitored){ + continue; + } + + if (ep.SpecialEpisode){ + if (historyAddSpecials && ShouldCountEpisode(ep, sonarrEnabled && useSonarrCounting, countMissing, false)){ + count++; } continue; } - var episodesList = season.EpisodesList; - for (int j = episodesList.Count - 1; j >= 0; j--){ - var episode = episodesList[j]; - - if (sonarrSkipUnmonitored && !episode.SonarrIsMonitored){ - continue; - } - - if (episode.SpecialEpisode){ - if (historyAddSpecials && !episode.SonarrHasFile){ - count++; - } - - continue; - } - - if (!string.IsNullOrEmpty(episode.SonarrEpisodeId) && !episode.SonarrHasFile){ - count++; + if (ShouldCountEpisode(ep, sonarrEnabled && useSonarrCounting, countMissing, foundWatched)){ + count++; + } else{ + foundWatched = true; + //if not count specials break + if (!historyAddSpecials && !countMissing){ + break; } } } - } else{ - for (int i = Seasons.Count - 1; i >= 0; i--){ - var season = Seasons[i]; - if (season.SpecialSeason == true){ - if (historyAddSpecials){ - var episodes = season.EpisodesList; - for (int j = episodes.Count - 1; j >= 0; j--){ - if (sonarrEnabled && sonarrSkipUnmonitored && !episodes[j].SonarrIsMonitored){ - continue; - } - - if (!episodes[j].WasDownloaded){ - count++; - } - } - } - - continue; - } - - var episodesList = season.EpisodesList; - for (int j = episodesList.Count - 1; j >= 0; j--){ - var episode = episodesList[j]; - - if (sonarrEnabled && sonarrSkipUnmonitored && !episode.SonarrIsMonitored){ - continue; - } - - if (episode.SpecialEpisode){ - if (historyAddSpecials && !episode.WasDownloaded){ - count++; - } - - continue; - } - - if (!episode.WasDownloaded && !foundWatched){ - count++; - } else{ - foundWatched = true; - if (!historyAddSpecials){ - break; - } - } - } - - if (foundWatched && !historyAddSpecials){ - break; - } + if (foundWatched && !historyAddSpecials && !countMissing){ + break; } } - NewEpisodes = count; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NewEpisodes))); } + private bool ShouldCountEpisode(HistoryEpisode episode, bool useSonarr, bool countMissing, bool foundWatched){ + if (useSonarr) + return !string.IsNullOrEmpty(episode.SonarrEpisodeId) && !episode.SonarrHasFile; + + return !episode.WasDownloaded && (!foundWatched || countMissing); + } + public void SetFetchingData(){ FetchingData = true; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData))); @@ -363,104 +333,65 @@ public class HistorySeries : INotifyPropertyChanged{ public async Task AddNewMissingToDownloads(){ bool foundWatched = false; - var historyAddSpecials = CrunchyrollManager.Instance.CrunOptions.HistoryAddSpecials; - var sonarrEnabled = SeriesType != SeriesType.Artist && CrunchyrollManager.Instance.CrunOptions.SonarrProperties != null && CrunchyrollManager.Instance.CrunOptions.SonarrProperties.SonarrEnabled && - !string.IsNullOrEmpty(SonarrSeriesId); + var options = CrunchyrollManager.Instance.CrunOptions; - var sonarrSkipUnmonitored = CrunchyrollManager.Instance.CrunOptions.HistorySkipUnmonitored; + bool historyAddSpecials = options.HistoryAddSpecials; + bool sonarrEnabled = SeriesType != SeriesType.Artist && + options.SonarrProperties?.SonarrEnabled == true && + !string.IsNullOrEmpty(SonarrSeriesId); + bool skipUnmonitored = options.HistorySkipUnmonitored; + bool countMissing = options.HistoryCountMissing; + bool useSonarrCounting = options.HistoryCountSonarr; - if (sonarrEnabled && CrunchyrollManager.Instance.CrunOptions.HistoryCountSonarr){ - for (int i = Seasons.Count - 1; i >= 0; i--){ - var season = Seasons[i]; + for (int i = Seasons.Count - 1; i >= 0; i--){ + var season = Seasons[i]; + var episodes = season.EpisodesList; - if (season.SpecialSeason == true){ - if (historyAddSpecials){ - var episodes = season.EpisodesList; - for (int j = episodes.Count - 1; j >= 0; j--){ - if (sonarrSkipUnmonitored && !episodes[j].SonarrIsMonitored){ - continue; - } + if (season.SpecialSeason == true){ + if (historyAddSpecials){ + for (int j = episodes.Count - 1; j >= 0; j--){ + var ep = episodes[j]; - if (!string.IsNullOrEmpty(episodes[j].SonarrEpisodeId) && !episodes[j].SonarrHasFile){ - await Seasons[i].EpisodesList[j].DownloadEpisode(); - } + if (skipUnmonitored && sonarrEnabled && !ep.SonarrIsMonitored){ + continue; } + + if (ShouldCountEpisode(ep, sonarrEnabled && useSonarrCounting, countMissing, false)){ + await ep.DownloadEpisode(); + } + } + } + + continue; + } + + for (int j = episodes.Count - 1; j >= 0; j--){ + var ep = episodes[j]; + + if (skipUnmonitored && sonarrEnabled && !ep.SonarrIsMonitored){ + continue; + } + + if (ep.SpecialEpisode){ + if (historyAddSpecials && ShouldCountEpisode(ep, sonarrEnabled && useSonarrCounting, countMissing, false)){ + await ep.DownloadEpisode(); } continue; } - var episodesList = season.EpisodesList; - for (int j = episodesList.Count - 1; j >= 0; j--){ - var episode = episodesList[j]; - - if (sonarrEnabled && sonarrSkipUnmonitored && !episode.SonarrIsMonitored){ - continue; - } - - if (episode.SpecialEpisode){ - if (historyAddSpecials && !episode.SonarrHasFile){ - await Seasons[i].EpisodesList[j].DownloadEpisode(); - } - - continue; - } - - if (!string.IsNullOrEmpty(episode.SonarrEpisodeId) && !episode.SonarrHasFile){ - await Seasons[i].EpisodesList[j].DownloadEpisode(); + if (ShouldCountEpisode(ep, sonarrEnabled && useSonarrCounting, countMissing, foundWatched)){ + await ep.DownloadEpisode(); + } else{ + foundWatched = true; + if (!historyAddSpecials && !countMissing){ + break; } } } - } else{ - for (int i = Seasons.Count - 1; i >= 0; i--){ - var season = Seasons[i]; - if (season.SpecialSeason == true){ - if (historyAddSpecials){ - var episodes = season.EpisodesList; - for (int j = episodes.Count - 1; j >= 0; j--){ - if (sonarrSkipUnmonitored && !episodes[j].SonarrIsMonitored){ - continue; - } - - if (!episodes[j].WasDownloaded){ - await Seasons[i].EpisodesList[j].DownloadEpisode(); - } - } - } - - continue; - } - - var episodesList = season.EpisodesList; - for (int j = episodesList.Count - 1; j >= 0; j--){ - var episode = episodesList[j]; - - if (sonarrEnabled && sonarrSkipUnmonitored && !episode.SonarrIsMonitored){ - continue; - } - - if (episode.SpecialEpisode){ - if (historyAddSpecials && !episode.WasDownloaded){ - await Seasons[i].EpisodesList[j].DownloadEpisode(); - } - - continue; - } - - if (!episode.WasDownloaded && !foundWatched){ - await Seasons[i].EpisodesList[j].DownloadEpisode(); - } else{ - foundWatched = true; - if (!historyAddSpecials){ - break; - } - } - } - - if (foundWatched && !historyAddSpecials){ - break; - } + if (foundWatched && !historyAddSpecials && !countMissing){ + break; } } } @@ -470,7 +401,7 @@ public class HistorySeries : INotifyPropertyChanged{ FetchingData = true; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData))); var isOk = true; - + switch (SeriesType){ case SeriesType.Artist: try{ @@ -503,7 +434,7 @@ public class HistorySeries : INotifyPropertyChanged{ UpdateNewEpisodes(); FetchingData = false; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData))); - + return isOk; } @@ -536,7 +467,7 @@ public class HistorySeries : INotifyPropertyChanged{ break; } } - + public void UpdateSeriesFolderPath(){ var season = Seasons.FirstOrDefault(season => !string.IsNullOrEmpty(season.SeasonDownloadPath)); @@ -587,8 +518,7 @@ public class HistorySeries : INotifyPropertyChanged{ SeriesFolderPathExists = true; } } - + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SeriesFolderPathExists))); } - } \ No newline at end of file diff --git a/CRD/Utils/Structs/Languages.cs b/CRD/Utils/Structs/Languages.cs index fa81222..9c5baf0 100644 --- a/CRD/Utils/Structs/Languages.cs +++ b/CRD/Utils/Structs/Languages.cs @@ -9,35 +9,53 @@ namespace CRD.Utils.Structs; public class Languages{ public static readonly LanguageItem[] languages ={ new(){ CrLocale = "ja-JP", Locale = "ja", Code = "jpn", Name = "Japanese" }, - new(){ CrLocale = "en-US", Locale = "en", Code = "eng", Name = "English" }, + new(){ CrLocale = "de-DE", Locale = "de", Code = "deu", Name = "German" }, + + new(){ CrLocale = "en-US", Locale = "en", Code = "eng", Name = "English" }, new(){ CrLocale = "en-IN", Locale = "en-IN", Code = "eng", Name = "English (India)" }, - new(){ CrLocale = "es-LA", Locale = "es-LA", Code = "spa", Name = "Spanish", Language = "Latin American Spanish" }, + new(){ CrLocale = "es-419", Locale = "es-419", Code = "spa-419", Name = "Spanish", Language = "Latin American Spanish" }, new(){ CrLocale = "es-ES", Locale = "es-ES", Code = "spa-ES", Name = "Castilian", Language = "European Spanish" }, + new(){ CrLocale = "pt-BR", Locale = "pt-BR", Code = "por", Name = "Portuguese", Language = "Brazilian Portuguese" }, new(){ CrLocale = "pt-PT", Locale = "pt-PT", Code = "por", Name = "Portuguese (Portugal)", Language = "Portugues (Portugal)" }, + new(){ CrLocale = "fr-FR", Locale = "fr", Code = "fra", Name = "French" }, - new(){ CrLocale = "ar-ME", Locale = "ar", Code = "ara-ME", Name = "Arabic" }, - new(){ CrLocale = "ar-SA", Locale = "ar", Code = "ara", Name = "Arabic (Saudi Arabia)" }, new(){ CrLocale = "it-IT", Locale = "it", Code = "ita", Name = "Italian" }, - new(){ CrLocale = "ru-RU", Locale = "ru", Code = "rus", Name = "Russian" }, - new(){ CrLocale = "tr-TR", Locale = "tr", Code = "tur", Name = "Turkish" }, - new(){ CrLocale = "hi-IN", Locale = "hi", Code = "hin", Name = "Hindi" }, - // new(){ locale = "zh", code = "cmn", name = "Chinese (Mandarin, PRC)" }, - new(){ CrLocale = "zh-CN", Locale = "zh-CN", Code = "zho", Name = "Chinese (Mainland China)" }, - new(){ CrLocale = "zh-TW", Locale = "zh-TW", Code = "chi", Name = "Chinese (Taiwan)" }, - new(){ CrLocale = "zh-HK", Locale = "zh-HK", Code = "zho-HK", Name = "Chinese (Hong Kong)" }, - new(){ CrLocale = "ko-KR", Locale = "ko", Code = "kor", Name = "Korean" }, - new(){ CrLocale = "ca-ES", Locale = "ca-ES", Code = "cat", Name = "Catalan" }, new(){ CrLocale = "pl-PL", Locale = "pl-PL", Code = "pol", Name = "Polish" }, - new(){ CrLocale = "th-TH", Locale = "th-TH", Code = "tha", Name = "Thai", Language = "ไทย" }, - new(){ CrLocale = "ta-IN", Locale = "ta-IN", Code = "tam", Name = "Tamil (India)", Language = "தமிழ்" }, - new(){ CrLocale = "ms-MY", Locale = "ms-MY", Code = "may", Name = "Malay (Malaysia)", Language = "Bahasa Melayu" }, - new(){ CrLocale = "vi-VN", Locale = "vi-VN", Code = "vie", Name = "Vietnamese", Language = "Tiếng Việt" }, + new(){ CrLocale = "id-ID", Locale = "id-ID", Code = "ind", Name = "Indonesian", Language = "Bahasa Indonesia" }, + new(){ CrLocale = "ms-MY", Locale = "ms-MY", Code = "may", Name = "Malay (Malaysia)", Language = "Bahasa Melayu" }, + + new(){ CrLocale = "ca-ES", Locale = "ca-ES", Code = "cat", Name = "Catalan" }, + + new(){ CrLocale = "vi-VN", Locale = "vi-VN", Code = "vie", Name = "Vietnamese", Language = "Tiếng Việt" }, + new(){ CrLocale = "tr-TR", Locale = "tr", Code = "tur", Name = "Turkish" }, + new(){ CrLocale = "ru-RU", Locale = "ru", Code = "rus", Name = "Russian" }, + + new(){ CrLocale = "ar-SA", Locale = "ar-SA", Code = "ara", Name = "Arabic" }, + new(){ CrLocale = "hi-IN", Locale = "hi", Code = "hin", Name = "Hindi" }, + new(){ CrLocale = "ta-IN", Locale = "ta-IN", Code = "tam", Name = "Tamil (India)", Language = "தமிழ்" }, new(){ CrLocale = "te-IN", Locale = "te-IN", Code = "tel", Name = "Telugu (India)", Language = "తెలుగు" }, + + new(){ CrLocale = "zh-CN", Locale = "zh-CN", Code = "zho", Name = "Chinese (Mainland China)" }, + new(){ CrLocale = "zh-HK", Locale = "zh-HK", Code = "zho-HK", Name = "Chinese (Hong Kong)" }, + new(){ CrLocale = "zh-TW", Locale = "zh-TW", Code = "chi", Name = "Chinese (Taiwan)" }, + + new(){ CrLocale = "ko-KR", Locale = "ko", Code = "kor", Name = "Korean" }, + new(){ CrLocale = "th-TH", Locale = "th-TH", Code = "tha", Name = "Thai", Language = "ไทย" }, + }; + + public static readonly LanguageItem DEFAULT_lang = new LanguageItem{ + CrLocale = "und", + Locale = "un", + Code = "und", + Name = string.Empty, + Language = string.Empty + }; + public static List SortListByLangList(List langList){ var orderMap = languages.Select((value, index) => new{ Value = value.CrLocale, Index = index }) @@ -65,7 +83,7 @@ public class Languages{ public static LanguageItem FixAndFindCrLc(string cr_locale){ if (string.IsNullOrEmpty(cr_locale)){ - return new LanguageItem(); + return DEFAULT_lang; } string str = FixLanguageTag(cr_locale); @@ -77,7 +95,7 @@ public class Languages{ string fileName = $"{fnOutput}"; if (addIndexAndLangCode){ - fileName += $".{langItem.CrLocale}"; //.{subsIndex} + fileName += $".{langItem.Locale}"; //.{subsIndex} } //removed .{langItem.language} from file name at end @@ -123,13 +141,7 @@ public class Languages{ if (lang?.CrLocale != null){ return lang; } else{ - return new LanguageItem{ - CrLocale = "und", - Locale = "un", - Code = "und", - Name = string.Empty, - Language = string.Empty - }; + return DEFAULT_lang; } } @@ -139,13 +151,7 @@ public class Languages{ if (filteredLocale != null){ return (LanguageItem)filteredLocale; } else{ - return new LanguageItem{ - CrLocale = "und", - Locale = "un", - Code = "und", - Name = string.Empty, - Language = string.Empty - }; + return DEFAULT_lang; } } diff --git a/CRD/ViewModels/HistoryPageViewModel.cs b/CRD/ViewModels/HistoryPageViewModel.cs index bd5081c..8386aa0 100644 --- a/CRD/ViewModels/HistoryPageViewModel.cs +++ b/CRD/ViewModels/HistoryPageViewModel.cs @@ -501,28 +501,30 @@ public partial class HistoryPageViewModel : ViewModelBase{ [RelayCommand] public async Task DownloadSeasonAll(HistorySeason season){ - var downloadTasks = season.EpisodesList - .Select(episode => episode.DownloadEpisode()); - - await Task.WhenAll(downloadTasks); + foreach (var episode in season.EpisodesList){ + await episode.DownloadEpisode(); + } } [RelayCommand] public async Task DownloadSeasonMissing(HistorySeason season){ - var downloadTasks = season.EpisodesList - .Where(episode => !episode.WasDownloaded) - .Select(episode => episode.DownloadEpisode()); + var missingEpisodes = season.EpisodesList + .Where(episode => !episode.WasDownloaded).ToList(); - await Task.WhenAll(downloadTasks); + if (missingEpisodes.Count == 0){ + MessageBus.Current.SendMessage(new ToastMessage($"There are no missing episodes", ToastType.Error, 3)); + } else{ + foreach (var episode in missingEpisodes){ + await episode.DownloadEpisode(); + } + } } [RelayCommand] public async Task DownloadSeasonMissingSonarr(HistorySeason season){ - var downloadTasks = season.EpisodesList - .Where(episode => !episode.SonarrHasFile) - .Select(episode => episode.DownloadEpisode()); - - await Task.WhenAll(downloadTasks); + foreach (var episode in season.EpisodesList.Where(episode => !episode.SonarrHasFile)){ + await episode.DownloadEpisode(); + } } [RelayCommand] diff --git a/CRD/ViewModels/SeriesPageViewModel.cs b/CRD/ViewModels/SeriesPageViewModel.cs index f035e21..d44d3b0 100644 --- a/CRD/ViewModels/SeriesPageViewModel.cs +++ b/CRD/ViewModels/SeriesPageViewModel.cs @@ -136,32 +136,30 @@ public partial class SeriesPageViewModel : ViewModelBase{ [RelayCommand] public async Task DownloadSeasonAll(HistorySeason season){ - var downloadTasks = season.EpisodesList - .Select(episode => episode.DownloadEpisode()); - - await Task.WhenAll(downloadTasks); + foreach (var episode in season.EpisodesList){ + await episode.DownloadEpisode(); + } } [RelayCommand] public async Task DownloadSeasonMissing(HistorySeason season){ - var downloadTasks = season.EpisodesList - .Where(episode => !episode.WasDownloaded) - .Select(episode => episode.DownloadEpisode()).ToList(); + var missingEpisodes = season.EpisodesList + .Where(episode => !episode.WasDownloaded).ToList(); - if (downloadTasks.Count == 0){ + if (missingEpisodes.Count == 0){ MessageBus.Current.SendMessage(new ToastMessage($"There are no missing episodes", ToastType.Error, 3)); } else{ - await Task.WhenAll(downloadTasks); + foreach (var episode in missingEpisodes){ + await episode.DownloadEpisode(); + } } } [RelayCommand] public async Task DownloadSeasonMissingSonarr(HistorySeason season){ - var downloadTasks = season.EpisodesList - .Where(episode => !episode.SonarrHasFile) - .Select(episode => episode.DownloadEpisode()); - - await Task.WhenAll(downloadTasks); + foreach (var episode in season.EpisodesList.Where(episode => !episode.SonarrHasFile)){ + await episode.DownloadEpisode(); + } } [RelayCommand] diff --git a/CRD/ViewModels/UpcomingSeasonsPageViewModel.cs b/CRD/ViewModels/UpcomingSeasonsPageViewModel.cs index f847156..f5cf727 100644 --- a/CRD/ViewModels/UpcomingSeasonsPageViewModel.cs +++ b/CRD/ViewModels/UpcomingSeasonsPageViewModel.cs @@ -146,6 +146,9 @@ public partial class UpcomingPageViewModel : ViewModelBase{ [ObservableProperty] private int _selectedIndex; + [ObservableProperty] + private bool _quickAddMode; + [ObservableProperty] private bool _isLoading; @@ -251,6 +254,11 @@ public partial class UpcomingPageViewModel : ViewModelBase{ [RelayCommand] public async Task AddToHistory(AnilistSeries series){ + if (ProgramManager.Instance.FetchingData){ + MessageBus.Current.SendMessage(new ToastMessage($"History still loading", ToastType.Warning, 3)); + return; + } + if (!string.IsNullOrEmpty(series.CrunchyrollID)){ if (CrunchyrollManager.Instance.CrunOptions.History){ series.IsInHistory = true; @@ -431,7 +439,7 @@ public partial class UpcomingPageViewModel : ViewModelBase{ } public void SelectionChangedOfSeries(AnilistSeries? value){ - if (value != null) value.IsExpanded = !value.IsExpanded; + if (value != null && !QuickAddMode) value.IsExpanded = !value.IsExpanded; SelectedSeries = null; SelectedIndex = -1; } diff --git a/CRD/ViewModels/Utils/GeneralSettingsViewModel.cs b/CRD/ViewModels/Utils/GeneralSettingsViewModel.cs index acca121..dd79a3c 100644 --- a/CRD/ViewModels/Utils/GeneralSettingsViewModel.cs +++ b/CRD/ViewModels/Utils/GeneralSettingsViewModel.cs @@ -35,6 +35,9 @@ public partial class GeneralSettingsViewModel : ViewModelBase{ [ObservableProperty] private bool _history; + + [ObservableProperty] + private bool _historyCountMissing; [ObservableProperty] private bool _historyIncludeCrArtists; @@ -259,6 +262,7 @@ public partial class GeneralSettingsViewModel : ViewModelBase{ ProxyUsername = options.ProxyUsername ?? ""; ProxyPassword = options.ProxyPassword ?? ""; ProxyPort = options.ProxyPort; + HistoryCountMissing = options.HistoryCountMissing; HistoryIncludeCrArtists = options.HistoryIncludeCrArtists; HistoryAddSpecials = options.HistoryAddSpecials; HistorySkipUnmonitored = options.HistorySkipUnmonitored; @@ -287,42 +291,45 @@ public partial class GeneralSettingsViewModel : ViewModelBase{ return; } - CrunchyrollManager.Instance.CrunOptions.DownloadFinishedPlaySound = DownloadFinishedPlaySound; - - CrunchyrollManager.Instance.CrunOptions.BackgroundImageBlurRadius = Math.Clamp((BackgroundImageBlurRadius ?? 0), 0, 40); - CrunchyrollManager.Instance.CrunOptions.BackgroundImageOpacity = Math.Clamp((BackgroundImageOpacity ?? 0), 0, 1); + var settings = CrunchyrollManager.Instance.CrunOptions; - CrunchyrollManager.Instance.CrunOptions.RetryAttempts = Math.Clamp((int)(RetryAttempts ?? 0), 1, 10); - CrunchyrollManager.Instance.CrunOptions.RetryDelay = Math.Clamp((int)(RetryDelay ?? 0), 1, 30); + settings.DownloadFinishedPlaySound = DownloadFinishedPlaySound; - CrunchyrollManager.Instance.CrunOptions.DownloadToTempFolder = DownloadToTempFolder; - CrunchyrollManager.Instance.CrunOptions.HistoryAddSpecials = HistoryAddSpecials; - CrunchyrollManager.Instance.CrunOptions.HistoryIncludeCrArtists = HistoryIncludeCrArtists; - CrunchyrollManager.Instance.CrunOptions.HistorySkipUnmonitored = HistorySkipUnmonitored; - CrunchyrollManager.Instance.CrunOptions.HistoryCountSonarr = HistoryCountSonarr; - CrunchyrollManager.Instance.CrunOptions.DownloadSpeedLimit = Math.Clamp((int)(DownloadSpeed ?? 0), 0, 1000000000); - CrunchyrollManager.Instance.CrunOptions.SimultaneousDownloads = Math.Clamp((int)(SimultaneousDownloads ?? 0), 1, 10); + settings.BackgroundImageBlurRadius = Math.Clamp((BackgroundImageBlurRadius ?? 0), 0, 40); + settings.BackgroundImageOpacity = Math.Clamp((BackgroundImageOpacity ?? 0), 0, 1); + + settings.RetryAttempts = Math.Clamp((int)(RetryAttempts ?? 0), 1, 10); + settings.RetryDelay = Math.Clamp((int)(RetryDelay ?? 0), 1, 30); - CrunchyrollManager.Instance.CrunOptions.ProxyEnabled = ProxyEnabled; - CrunchyrollManager.Instance.CrunOptions.ProxySocks = ProxySocks; - CrunchyrollManager.Instance.CrunOptions.ProxyHost = ProxyHost; - CrunchyrollManager.Instance.CrunOptions.ProxyPort = Math.Clamp((int)(ProxyPort ?? 0), 0, 65535); - CrunchyrollManager.Instance.CrunOptions.ProxyUsername = ProxyUsername; - CrunchyrollManager.Instance.CrunOptions.ProxyPassword = ProxyPassword; + settings.DownloadToTempFolder = DownloadToTempFolder; + settings.HistoryCountMissing = HistoryCountMissing; + settings.HistoryAddSpecials = HistoryAddSpecials; + settings.HistoryIncludeCrArtists = HistoryIncludeCrArtists; + settings.HistorySkipUnmonitored = HistorySkipUnmonitored; + settings.HistoryCountSonarr = HistoryCountSonarr; + settings.DownloadSpeedLimit = Math.Clamp((int)(DownloadSpeed ?? 0), 0, 1000000000); + settings.SimultaneousDownloads = Math.Clamp((int)(SimultaneousDownloads ?? 0), 1, 10); + + settings.ProxyEnabled = ProxyEnabled; + settings.ProxySocks = ProxySocks; + settings.ProxyHost = ProxyHost; + settings.ProxyPort = Math.Clamp((int)(ProxyPort ?? 0), 0, 65535); + settings.ProxyUsername = ProxyUsername; + settings.ProxyPassword = ProxyPassword; string historyLang = SelectedHistoryLang.Content + ""; - CrunchyrollManager.Instance.CrunOptions.HistoryLang = historyLang != "default" ? historyLang : CrunchyrollManager.Instance.DefaultLocale; + settings.HistoryLang = historyLang != "default" ? historyLang : CrunchyrollManager.Instance.DefaultLocale; - CrunchyrollManager.Instance.CrunOptions.Theme = CurrentAppTheme?.Content + ""; + settings.Theme = CurrentAppTheme?.Content + ""; if (_faTheme.CustomAccentColor != (Application.Current?.PlatformSettings?.GetColorValues().AccentColor1)){ - CrunchyrollManager.Instance.CrunOptions.AccentColor = _faTheme.CustomAccentColor.ToString(); + settings.AccentColor = _faTheme.CustomAccentColor.ToString(); } else{ - CrunchyrollManager.Instance.CrunOptions.AccentColor = string.Empty; + settings.AccentColor = string.Empty; } - CrunchyrollManager.Instance.CrunOptions.History = History; + settings.History = History; var props = new SonarrProperties(); @@ -339,9 +346,9 @@ public partial class GeneralSettingsViewModel : ViewModelBase{ props.ApiKey = SonarrApiKey; - CrunchyrollManager.Instance.CrunOptions.SonarrProperties = props; + settings.SonarrProperties = props; - CrunchyrollManager.Instance.CrunOptions.LogMode = LogMode; + settings.LogMode = LogMode; CfgManager.WriteCrSettings(); } diff --git a/CRD/Views/UpcomingSeasonsPageView.axaml b/CRD/Views/UpcomingSeasonsPageView.axaml index 74aff37..0b8aa97 100644 --- a/CRD/Views/UpcomingSeasonsPageView.axaml +++ b/CRD/Views/UpcomingSeasonsPageView.axaml @@ -57,7 +57,17 @@ - + + + + + + + + - +