diff --git a/CRD/Downloader/Crunchyroll/CRAuth.cs b/CRD/Downloader/Crunchyroll/CRAuth.cs index 052b453..4fada42 100644 --- a/CRD/Downloader/Crunchyroll/CRAuth.cs +++ b/CRD/Downloader/Crunchyroll/CRAuth.cs @@ -6,7 +6,10 @@ using System.Net.Http.Headers; using System.Threading.Tasks; using CRD.Utils; using CRD.Utils.Structs; +using CRD.Utils.Structs.Crunchyroll; +using CRD.Views; using Newtonsoft.Json; +using ReactiveUI; namespace CRD.Downloader.Crunchyroll; @@ -75,6 +78,8 @@ public class CrAuth{ if (response.IsOk){ JsonTokenToFileAndVariable(response.ResponseContent); + } else{ + MessageBus.Current.SendMessage(new ToastMessage($"Failed to login - {response.ResponseContent.Substring(0,response.ResponseContent.Length < 200 ? response.ResponseContent.Length : 200)}", ToastType.Error, 10)); } if (crunInstance.Token?.refresh_token != null){ diff --git a/CRD/Downloader/Crunchyroll/CrEpisode.cs b/CRD/Downloader/Crunchyroll/CrEpisode.cs index 2f360f3..53a58ec 100644 --- a/CRD/Downloader/Crunchyroll/CrEpisode.cs +++ b/CRD/Downloader/Crunchyroll/CrEpisode.cs @@ -15,11 +15,6 @@ public class CrEpisode(){ private readonly CrunchyrollManager crunInstance = CrunchyrollManager.Instance; public async Task ParseEpisodeById(string id, string crLocale, bool forcedLang = false){ - // if (crunInstance.CmsToken?.Cms == null){ - // Console.Error.WriteLine("Missing CMS Access Token"); - // return null; - // } - NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query); query["preferred_audio_language"] = "ja-JP"; diff --git a/CRD/Downloader/Crunchyroll/CrMovies.cs b/CRD/Downloader/Crunchyroll/CrMovies.cs new file mode 100644 index 0000000..2aba63e --- /dev/null +++ b/CRD/Downloader/Crunchyroll/CrMovies.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Net.Http; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Web; +using CRD.Utils; +using CRD.Utils.Structs; + +namespace CRD.Downloader.Crunchyroll; + +public class CrMovies{ + private readonly CrunchyrollManager crunInstance = CrunchyrollManager.Instance; + + public async Task ParseMovieById(string id, string crLocale, bool forcedLang = false){ + NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query); + + query["preferred_audio_language"] = "ja-JP"; + if (!string.IsNullOrEmpty(crLocale)){ + query["locale"] = crLocale; + if (forcedLang){ + query["force_locale"] = crLocale; + } + } + + + var request = HttpClientReq.CreateRequestMessage($"{Api.Cms}/movies/{id}", HttpMethod.Get, true, true, query); + + var response = await HttpClientReq.Instance.SendHttpRequest(request); + + if (!response.IsOk){ + Console.Error.WriteLine("Movie Request Failed"); + return null; + } + + CrunchyMovieList movie = Helpers.Deserialize(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings); + + if (movie.Total < 1){ + return null; + } + + if (movie.Total == 1 && movie.Data != null){ + return movie.Data.First(); + } + + Console.Error.WriteLine("Multiple movie returned with one ID?"); + if (movie.Data != null) return movie.Data.First(); + return null; + } + + + public CrunchyEpMeta? EpisodeMeta(CrunchyMovie episodeP, List dubLang){ + + if (!string.IsNullOrEmpty(episodeP.AudioLocale) && !dubLang.Contains(episodeP.AudioLocale)){ + Console.Error.WriteLine("Movie not available in the selected dub lang"); + return null; + } + + var images = (episodeP.Images?.Thumbnail ?? new List>{ new List{ new Image{ Source = "/notFound.png" } } }); + + var epMeta = new CrunchyEpMeta(); + epMeta.Data = new List{ new(){ MediaId = episodeP.Id, Versions = null, IsSubbed = episodeP.IsSubbed, IsDubbed = episodeP.IsDubbed } }; + epMeta.SeriesTitle = "Movie"; + epMeta.SeasonTitle = ""; + epMeta.EpisodeNumber = ""; + epMeta.EpisodeTitle = episodeP.Title; + epMeta.SeasonId = ""; + epMeta.Season = ""; + epMeta.ShowId = ""; + epMeta.AbsolutEpisodeNumberE = ""; + epMeta.Image = images[images.Count / 2].FirstOrDefault().Source; + epMeta.DownloadProgress = new DownloadProgress(){ + IsDownloading = false, + Done = false, + Error = false, + Percent = 0, + Time = 0, + DownloadSpeed = 0 + }; + epMeta.AvailableSubs = new List(); + epMeta.Description = episodeP.Description; + + return epMeta; + } +} \ No newline at end of file diff --git a/CRD/Downloader/Crunchyroll/CrMusic.cs b/CRD/Downloader/Crunchyroll/CrMusic.cs new file mode 100644 index 0000000..4261df2 --- /dev/null +++ b/CRD/Downloader/Crunchyroll/CrMusic.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using System.Web; +using CRD.Utils; +using CRD.Utils.Structs; +using CRD.Utils.Structs.Crunchyroll.Music; + +namespace CRD.Downloader.Crunchyroll; + +public class CrMusic{ + private readonly CrunchyrollManager crunInstance = CrunchyrollManager.Instance; + + public async Task ParseMusicVideoByIdAsync(string id, string crLocale, bool forcedLang = false){ + return await ParseMediaByIdAsync(id, crLocale, forcedLang, "music/music_videos"); + } + + public async Task ParseConcertByIdAsync(string id, string crLocale, bool forcedLang = false){ + return await ParseMediaByIdAsync(id, crLocale, forcedLang, "music/concerts"); + } + + public async Task ParseArtistMusicVideosByIdAsync(string id, string crLocale, bool forcedLang = false){ + var musicVideosTask = FetchMediaListAsync($"{Api.Content}/music/artists/{id}/music_videos", crLocale, forcedLang); + var concertsTask = FetchMediaListAsync($"{Api.Content}/music/artists/{id}/concerts", crLocale, forcedLang); + + await Task.WhenAll(musicVideosTask, concertsTask); + + var musicVideos = await musicVideosTask; + var concerts = await concertsTask; + + musicVideos.Total += concerts.Total; + musicVideos.Data ??= new List(); + + if (concerts.Data != null){ + musicVideos.Data.AddRange(concerts.Data); + } + + return musicVideos; + } + + private async Task ParseMediaByIdAsync(string id, string crLocale, bool forcedLang, string endpoint){ + var mediaList = await FetchMediaListAsync($"{Api.Content}/{endpoint}/{id}", crLocale, forcedLang); + + switch (mediaList.Total){ + case < 1: + return null; + case 1 when mediaList.Data != null: + return mediaList.Data.First(); + default: + Console.Error.WriteLine($"Multiple items returned for endpoint {endpoint} with ID {id}"); + return mediaList.Data?.First(); + } + } + + private async Task FetchMediaListAsync(string url, string crLocale, bool forcedLang){ + var query = CreateQueryParameters(crLocale, forcedLang); + var request = HttpClientReq.CreateRequestMessage(url, HttpMethod.Get, true, true, query); + + var response = await HttpClientReq.Instance.SendHttpRequest(request); + + if (!response.IsOk){ + Console.Error.WriteLine($"Request to {url} failed"); + return new CrunchyMusicVideoList(); + } + + return Helpers.Deserialize(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings); + } + + private NameValueCollection CreateQueryParameters(string crLocale, bool forcedLang){ + var query = HttpUtility.ParseQueryString(string.Empty); + query["preferred_audio_language"] = "ja-JP"; + + if (!string.IsNullOrEmpty(crLocale)){ + query["locale"] = crLocale; + + if (forcedLang){ + query["force_locale"] = crLocale; + } + } + + return query; + } + + + public CrunchyEpMeta EpisodeMeta(CrunchyMusicVideo episodeP){ + var images = (episodeP.Images?.Thumbnail ?? new List{ new Image{ Source = "/notFound.png" } }); + + var epMeta = new CrunchyEpMeta(); + epMeta.Data = new List{ new(){ MediaId = episodeP.Id, Versions = null } }; + epMeta.SeriesTitle = "Music"; + epMeta.SeasonTitle = episodeP.DisplayArtistName; + epMeta.EpisodeNumber = episodeP.SequenceNumber + ""; + epMeta.EpisodeTitle = episodeP.Title; + epMeta.SeasonId = ""; + epMeta.Season = ""; + epMeta.ShowId = ""; + epMeta.AbsolutEpisodeNumberE = ""; + epMeta.Image = images[images.Count / 2].Source; + epMeta.DownloadProgress = new DownloadProgress(){ + IsDownloading = false, + Done = false, + Error = false, + Percent = 0, + Time = 0, + DownloadSpeed = 0 + }; + epMeta.AvailableSubs = new List(); + epMeta.Description = episodeP.Description; + epMeta.Music = true; + + return epMeta; + } +} \ No newline at end of file diff --git a/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs b/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs index 63d4c70..d70025a 100644 --- a/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs +++ b/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs @@ -18,6 +18,7 @@ using CRD.Utils.HLS; using CRD.Utils.Muxing; using CRD.Utils.Sonarr; using CRD.Utils.Structs; +using CRD.Utils.Structs.Crunchyroll; using CRD.Utils.Structs.History; using CRD.Views; using Newtonsoft.Json; @@ -44,7 +45,7 @@ public class CrunchyrollManager{ #endregion public CrBrowseSeriesBase? AllCRSeries; - + public string DefaultLocale = "en-US"; @@ -57,6 +58,8 @@ public class CrunchyrollManager{ public CrAuth CrAuth; public CrEpisode CrEpisode; public CrSeries CrSeries; + public CrMovies CrMovies; + public CrMusic CrMusic; public History History; #region Singelton @@ -101,7 +104,7 @@ public class CrunchyrollManager{ options.FfmpegOptions = new(); options.DefaultAudio = "ja-JP"; options.DefaultSub = "de-DE"; - options.CcTag = "cc"; + options.CcTag = "CC"; options.FsRetryTime = 5; options.Numbers = 2; options.Timeout = 15000; @@ -129,8 +132,11 @@ public class CrunchyrollManager{ CrAuth = new CrAuth(); CrEpisode = new CrEpisode(); CrSeries = new CrSeries(); + CrMovies = new CrMovies(); + CrMusic = new CrMusic(); History = new History(); + Profile = new CrProfile{ Username = "???", Avatar = "003-cr-hime-excited.png", @@ -172,8 +178,8 @@ public class CrunchyrollManager{ HistoryList =[]; } } - - SonarrClient.Instance.RefreshSonarr(); + + await SonarrClient.Instance.RefreshSonarr(); } } @@ -216,7 +222,7 @@ public class CrunchyrollManager{ }; QueueManager.Instance.Queue.Refresh(); - + if (CrunOptions is{ DlVideoOnce: false, KeepDubsSeperate: true }){ var groupByDub = Helpers.GroupByLanguageWithSubtitles(res.Data); @@ -239,10 +245,8 @@ public class CrunchyrollManager{ KeepAllVideos = true, MuxDescription = options.IncludeVideoDescription }, - res.FileName); + res.FileName); } - - } else{ await MuxStreams(res.Data, new CrunchyMuxOptions{ @@ -262,9 +266,9 @@ public class CrunchyrollManager{ KeepAllVideos = true, MuxDescription = options.IncludeVideoDescription }, - res.FileName); + res.FileName); } - + data.DownloadProgress = new DownloadProgress(){ IsDownloading = true, Done = true, @@ -567,7 +571,7 @@ public class CrunchyrollManager{ #endregion - var fetchPlaybackData = await FetchPlaybackData(mediaId, mediaGuid, epMeta); + var fetchPlaybackData = await FetchPlaybackData(mediaId, mediaGuid, data.Music); if (!fetchPlaybackData.IsOk){ if (!fetchPlaybackData.IsOk && fetchPlaybackData.error != string.Empty){ @@ -604,7 +608,7 @@ public class CrunchyrollManager{ (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)); + variables.Add(new Variable("season", !string.IsNullOrEmpty(data.Season) ? Math.Round(double.Parse(data.Season, CultureInfo.InvariantCulture), 1) : 0, false)); if (pbStreams?.Keys != null){ foreach (var key in pbStreams.Keys){ @@ -1290,7 +1294,7 @@ public class CrunchyrollManager{ 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(); + List capsData = pbData.Meta.Captions?.Values.ToList() ?? new List(); var subsDataMapped = subsData.Select(s => { var subLang = Languages.FixAndFindCrLc((s.Locale ?? Locale.DefaulT).GetEnumMemberValue()); return new{ @@ -1303,7 +1307,7 @@ public class CrunchyrollManager{ }).ToList(); var capsDataMapped = capsData.Select(s => { - var subLang = Languages.FixAndFindCrLc((s.Locale ?? Locale.DefaulT).GetEnumMemberValue()); + var subLang = Languages.FixAndFindCrLc((s.Language ?? Locale.DefaulT).GetEnumMemberValue()); return new{ format = s.Format, url = s.Url, @@ -1317,6 +1321,9 @@ public class CrunchyrollManager{ var subsArr = Languages.SortSubtitles(subsDataMapped, "language"); + int playResX = 1280; + int playResY = 720; + foreach (var subsItem in subsArr){ var index = subsArr.IndexOf(subsItem); var langItem = subsItem.locale; @@ -1334,7 +1341,7 @@ public class CrunchyrollManager{ if (files.Any(a => a.Type == DownloadMediaType.Subtitle && (a.Language.CrLocale == langItem.CrLocale || a.Language.Locale == langItem.Locale) && a.Cc == isCc && - a.Signs == isSigns) || (!options.IncludeSignsSubs && isSigns)){ + a.Signs == isSigns) || (!options.IncludeSignsSubs && isSigns) || (!options.IncludeCcSubs && isCc)){ continue; } @@ -1345,9 +1352,20 @@ public class CrunchyrollManager{ if (subsAssReqResponse.IsOk){ if (subsItem.format == "ass"){ + // Define regular expressions to match PlayResX and PlayResY + Regex playResXPattern = new Regex(@"PlayResX:\s*(\d+)"); + Regex playResYPattern = new Regex(@"PlayResY:\s*(\d+)"); + + // Find matches + Match matchX = playResXPattern.Match(subsAssReqResponse.ResponseContent); + Match matchY = playResYPattern.Match(subsAssReqResponse.ResponseContent); + + playResX = int.Parse(matchX.Groups[1].Value); + playResY = int.Parse(matchY.Groups[1].Value); + subsAssReqResponse.ResponseContent = '\ufeff' + subsAssReqResponse.ResponseContent; var sBodySplit = subsAssReqResponse.ResponseContent.Split(new[]{ "\r\n" }, StringSplitOptions.None).ToList(); - // Insert 'ScaledBorderAndShadow' after the second line + if (sBodySplit.Count > 2){ if (options.SubsAddScaledBorder == ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes){ sBodySplit.Insert(2, "ScaledBorderAndShadow: yes"); @@ -1356,11 +1374,8 @@ public class CrunchyrollManager{ } } - - // Rejoin the lines back into a single string subsAssReqResponse.ResponseContent = string.Join("\r\n", sBodySplit); - // Extract the title from the second line and remove 'Title: ' prefix if (sBodySplit.Count > 1){ sxData.Title = sBodySplit[1].Replace("Title: ", ""); sxData.Title = $"{langItem.Language} / {sxData.Title}"; @@ -1368,7 +1383,46 @@ public class CrunchyrollManager{ sxData.Fonts = FontsManager.Instance.GetDictFromKeyList(keysList); } } else if (subsItem.format == "vtt"){ - // TODO + var assBuilder = new StringBuilder(); + + assBuilder.AppendLine("[Script Info]"); + assBuilder.AppendLine("Title: CC Subtitle"); + assBuilder.AppendLine("ScriptType: v4.00+"); + assBuilder.AppendLine("WrapStyle: 0"); + assBuilder.AppendLine("PlayResX: " + playResX); + assBuilder.AppendLine("PlayResY: " + playResY); + assBuilder.AppendLine("Timer: 0.0"); + assBuilder.AppendLine(); + assBuilder.AppendLine("[V4+ Styles]"); + assBuilder.AppendLine("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, " + + "Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding"); + assBuilder.AppendLine("Style: Default,Arial,20,&H00FFFFFF,&H00FFFFFF,&H00000000,&H64000000,-1,0,0,0,100,100,0,0,1,2,0,2,0000,0000,0020,1"); + assBuilder.AppendLine(); + assBuilder.AppendLine("[Events]"); + assBuilder.AppendLine("Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text"); + + // Parse the VTT content + var lines = subsAssReqResponse.ResponseContent.Split(new[]{ Environment.NewLine }, StringSplitOptions.None); + Regex timePattern = new Regex(@"(?\d{2}:\d{2}:\d{2}\.\d{3})\s-->\s(?\d{2}:\d{2}:\d{2}\.\d{3})"); + + for (int i = 0; i < lines.Length; i++){ + Match match = timePattern.Match(lines[i]); + + if (match.Success){ + string startTime = Helpers.ConvertTimeFormat(match.Groups["start"].Value); + string endTime = Helpers.ConvertTimeFormat(match.Groups["end"].Value); + string dialogue = Helpers.ExtractDialogue(lines, i + 1); + + assBuilder.AppendLine($"Dialogue: 0,{startTime},{endTime},Default,,0000,0000,0000,,{dialogue}"); + } + } + + subsAssReqResponse.ResponseContent = assBuilder.ToString(); + + sxData.Title = $"{langItem.Name} / CC Subtitle"; + var keysList = FontsManager.ExtractFontsFromAss(subsAssReqResponse.ResponseContent); + sxData.Fonts = FontsManager.Instance.GetDictFromKeyList(keysList); + sxData.Path = sxData.Path.Replace("vtt", "ass"); } File.WriteAllText(sxData.Path, subsAssReqResponse.ResponseContent); @@ -1483,141 +1537,121 @@ public class CrunchyrollManager{ return (audioDownloadResult.Ok, audioDownloadResult.Parts, tsFile); } - private async Task<(bool IsOk, PlaybackData pbData, string error)> FetchPlaybackData(string mediaId, string mediaGuidId, CrunchyEpMetaData epMeta){ - PlaybackData temppbData = new PlaybackData{ Total = 0, Data = new List>>() }; - bool ok = true; + #region Fetch Playback Data - HttpRequestMessage playbackRequest; - (bool IsOk, string ResponseContent) playbackRequestResponse; + private async Task<(bool IsOk, PlaybackData pbData, string error)> FetchPlaybackData(string mediaId, string mediaGuidId, bool music){ + var temppbData = new PlaybackData{ + Total = 0, + Data = new List>>() + }; + var playbackEndpoint = $"https://cr-play-service.prd.crunchyrollsvc.com/v1/{(music ? "music/" : "")}{mediaGuidId}/{CrunOptions.StreamEndpoint}/play"; + var playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint); - playbackRequest = HttpClientReq.CreateRequestMessage($"https://cr-play-service.prd.crunchyrollsvc.com/v1/{mediaGuidId}/{CrunOptions.StreamEndpoint}/play", HttpMethod.Get, true, false, null); - - playbackRequestResponse = await HttpClientReq.Instance.SendHttpRequest(playbackRequest); - - if (!playbackRequestResponse.IsOk && playbackRequestResponse.ResponseContent != string.Empty){ - var s = playbackRequestResponse.ResponseContent; - var error = StreamError.FromJson(s); - if (error != null && error.IsTooManyActiveStreamsError()){ - foreach (var errorActiveStream in error.ActiveStreams){ - await HttpClientReq.DeAuthVideo(errorActiveStream.ContentId, errorActiveStream.Token); - } - - playbackRequest = HttpClientReq.CreateRequestMessage($"https://cr-play-service.prd.crunchyrollsvc.com/v1/{mediaGuidId}/{CrunOptions.StreamEndpoint}/play", HttpMethod.Get, true, false, null); - playbackRequestResponse = await HttpClientReq.Instance.SendHttpRequest(playbackRequest); - } + if (!playbackRequestResponse.IsOk){ + playbackRequestResponse = await HandleStreamErrorsAsync(playbackRequestResponse, mediaGuidId, playbackEndpoint); } if (playbackRequestResponse.IsOk){ - temppbData = new PlaybackData{ Total = 0, Data = new List>>() }; - temppbData.Data.Add(new Dictionary>()); - - CrunchyStreamData? playStream = JsonConvert.DeserializeObject(playbackRequestResponse.ResponseContent, SettingsJsonSerializerSettings); - CrunchyStreams derivedPlayCrunchyStreams = new CrunchyStreams(); - if (playStream != null){ - if (playStream.Token != null) await HttpClientReq.DeAuthVideo(mediaGuidId, playStream.Token); - - if (playStream.HardSubs != null) - foreach (var hardsub in playStream.HardSubs){ - var stream = hardsub.Value; - derivedPlayCrunchyStreams[hardsub.Key] = new StreamDetails{ - Url = stream.Url, - HardsubLocale = stream.Hlang - }; - } - - derivedPlayCrunchyStreams[""] = new StreamDetails{ - Url = playStream.Url, - HardsubLocale = Locale.DefaulT - }; - - if (temppbData.Data != null){ - temppbData.Data[0]["drm_adaptive_dash"] = derivedPlayCrunchyStreams; - temppbData.Total = 1; - } - - temppbData.Meta = new PlaybackMeta(){ AudioLocale = playStream.AudioLocale, Versions = playStream.Versions, Bifs = new List{ playStream.Bifs }, MediaId = mediaId }; - - if (playStream.Captions != null){ - temppbData.Meta.Captions = playStream.Captions; - } - - temppbData.Meta.Subtitles = new Subtitles(); - foreach (var playStreamSubtitle in playStream.Subtitles){ - Subtitle sub = playStreamSubtitle.Value; - temppbData.Meta.Subtitles.Add(playStreamSubtitle.Key, new SubtitleInfo(){ Format = sub.Format, Locale = sub.Locale, Url = sub.Url }); - } - } + temppbData = await ProcessPlaybackResponseAsync(playbackRequestResponse.ResponseContent, mediaId, mediaGuidId); } else{ Console.WriteLine("Request Stream URLs FAILED! Attempting fallback"); + playbackEndpoint = $"https://cr-play-service.prd.crunchyrollsvc.com/v1/{(music ? "music/" : "")}{mediaGuidId}/web/firefox/play"; + playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint); - playbackRequest = HttpClientReq.CreateRequestMessage($"https://cr-play-service.prd.crunchyrollsvc.com/v1/{mediaGuidId}/web/firefox/play", HttpMethod.Get, true, false, null); - - playbackRequestResponse = await HttpClientReq.Instance.SendHttpRequest(playbackRequest); - - if (!playbackRequestResponse.IsOk && playbackRequestResponse.ResponseContent != string.Empty){ - var s = playbackRequestResponse.ResponseContent; - var error = StreamError.FromJson(s); - if (error != null && error.IsTooManyActiveStreamsError()){ - foreach (var errorActiveStream in error.ActiveStreams){ - await HttpClientReq.DeAuthVideo(errorActiveStream.ContentId, errorActiveStream.Token); - } - - playbackRequest = HttpClientReq.CreateRequestMessage($"https://cr-play-service.prd.crunchyrollsvc.com/v1/{mediaGuidId}/web/firefox/play", HttpMethod.Get, true, false, null); - playbackRequestResponse = await HttpClientReq.Instance.SendHttpRequest(playbackRequest); - } + if (!playbackRequestResponse.IsOk){ + playbackRequestResponse = await HandleStreamErrorsAsync(playbackRequestResponse, mediaGuidId, playbackEndpoint); } if (playbackRequestResponse.IsOk){ - temppbData = new PlaybackData{ Total = 0, Data = new List>>() }; - temppbData.Data.Add(new Dictionary>()); - - CrunchyStreamData? playStream = JsonConvert.DeserializeObject(playbackRequestResponse.ResponseContent, SettingsJsonSerializerSettings); - CrunchyStreams derivedPlayCrunchyStreams = new CrunchyStreams(); - if (playStream != null){ - if (playStream.Token != null) await HttpClientReq.DeAuthVideo(mediaGuidId, playStream.Token); - - if (playStream.HardSubs != null) - foreach (var hardsub in playStream.HardSubs){ - var stream = hardsub.Value; - derivedPlayCrunchyStreams[hardsub.Key] = new StreamDetails{ - Url = stream.Url, - HardsubLocale = stream.Hlang - }; - } - - derivedPlayCrunchyStreams[""] = new StreamDetails{ - Url = playStream.Url, - HardsubLocale = Locale.DefaulT - }; - - if (temppbData.Data != null){ - temppbData.Data[0]["drm_adaptive_dash"] = derivedPlayCrunchyStreams; - temppbData.Total = 1; - } - - temppbData.Meta = new PlaybackMeta(){ AudioLocale = playStream.AudioLocale, Versions = playStream.Versions, Bifs = new List{ playStream.Bifs }, MediaId = mediaId }; - - if (playStream.Captions != null){ - temppbData.Meta.Captions = playStream.Captions; - } - - temppbData.Meta.Subtitles = new Subtitles(); - foreach (var playStreamSubtitle in playStream.Subtitles){ - Subtitle sub = playStreamSubtitle.Value; - temppbData.Meta.Subtitles.Add(playStreamSubtitle.Key, new SubtitleInfo(){ Format = sub.Format, Locale = sub.Locale, Url = sub.Url }); - } - } + temppbData = await ProcessPlaybackResponseAsync(playbackRequestResponse.ResponseContent, mediaId, mediaGuidId); } else{ - Console.Error.WriteLine("'Fallback Request Stream URLs FAILED!'"); - ok = playbackRequestResponse.IsOk; + Console.Error.WriteLine("Fallback Request Stream URLs FAILED!"); } } - - return (IsOk: ok, pbData: temppbData, error: ok ? "" : playbackRequestResponse.ResponseContent); + return (IsOk: playbackRequestResponse.IsOk, pbData: temppbData, error: playbackRequestResponse.IsOk ? "" : playbackRequestResponse.ResponseContent); } + private async Task<(bool IsOk, string ResponseContent)> SendPlaybackRequestAsync(string endpoint){ + var request = HttpClientReq.CreateRequestMessage(endpoint, HttpMethod.Get, true, false, null); + return await HttpClientReq.Instance.SendHttpRequest(request); + } + + private async Task<(bool IsOk, string ResponseContent)> HandleStreamErrorsAsync((bool IsOk, string ResponseContent) response, string mediaGuidId, string endpoint){ + if (response.IsOk || string.IsNullOrEmpty(response.ResponseContent)) return response; + + var error = StreamError.FromJson(response.ResponseContent); + if (error?.IsTooManyActiveStreamsError() == true){ + foreach (var errorActiveStream in error.ActiveStreams){ + await HttpClientReq.DeAuthVideo(errorActiveStream.ContentId, errorActiveStream.Token); + } + + return await SendPlaybackRequestAsync(endpoint); + } + + return response; + } + + private async Task ProcessPlaybackResponseAsync(string responseContent, string mediaId, string mediaGuidId){ + var temppbData = new PlaybackData{ + Total = 0, + Data = new List>>() + }; + + var playStream = JsonConvert.DeserializeObject(responseContent, SettingsJsonSerializerSettings); + if (playStream == null) return temppbData; + + if (!string.IsNullOrEmpty(playStream.Token)){ + await HttpClientReq.DeAuthVideo(mediaGuidId, playStream.Token); + } + + var derivedPlayCrunchyStreams = new CrunchyStreams(); + if (playStream.HardSubs != null){ + foreach (var hardsub in playStream.HardSubs){ + var stream = hardsub.Value; + derivedPlayCrunchyStreams[hardsub.Key] = new StreamDetails{ + Url = stream.Url, + HardsubLocale = stream.Hlang + }; + } + } + + derivedPlayCrunchyStreams[""] = new StreamDetails{ + Url = playStream.Url, + HardsubLocale = Locale.DefaulT + }; + + temppbData.Data.Add(new Dictionary>{ + { "drm_adaptive_dash", derivedPlayCrunchyStreams } + }); + temppbData.Total = 1; + + temppbData.Meta = new PlaybackMeta{ + AudioLocale = playStream.AudioLocale, + Versions = playStream.Versions, + Bifs = new List{ playStream.Bifs }, + MediaId = mediaId, + Captions = playStream.Captions, + Subtitles = new Subtitles() + }; + + if (playStream.Subtitles != null){ + foreach (var subtitle in playStream.Subtitles){ + temppbData.Meta.Subtitles.Add(subtitle.Key, new SubtitleInfo{ + Format = subtitle.Value.Format, + Locale = subtitle.Value.Locale, + Url = subtitle.Value.Url + }); + } + } + + return temppbData; + } + + #endregion + + private async Task ParseChapters(string currentMediaId, List compiledChapters){ var showRequest = HttpClientReq.CreateRequestMessage($"https://static.crunchyroll.com/skip-events/production/{currentMediaId}.json", HttpMethod.Get, true, true, null); @@ -1629,11 +1663,11 @@ public class CrunchyrollManager{ try{ JObject jObject = JObject.Parse(showRequestResponse.ResponseContent); - + if (jObject.TryGetValue("lastUpdate", out JToken lastUpdateToken)){ chapterData.lastUpdate = lastUpdateToken.ToObject(); } - + if (jObject.TryGetValue("mediaId", out JToken mediaIdToken)){ chapterData.mediaId = mediaIdToken.ToObject(); } @@ -1659,6 +1693,7 @@ public class CrunchyrollManager{ if (a.start.HasValue && b.start.HasValue){ return a.start.Value.CompareTo(b.start.Value); } + return 0; // Both values are null, they are considered equal }); diff --git a/CRD/Downloader/History.cs b/CRD/Downloader/History.cs index 60b6b4f..37dae84 100644 --- a/CRD/Downloader/History.cs +++ b/CRD/Downloader/History.cs @@ -527,7 +527,7 @@ public class History(){ return newSeason; } - public void MatchHistorySeriesWithSonarr(bool updateAll){ + public async void MatchHistorySeriesWithSonarr(bool updateAll){ if (crunInstance.CrunOptions.SonarrProperties is{ SonarrEnabled: false }){ return; } @@ -654,7 +654,7 @@ public class History(){ } } - private string GetNextAirDate(List episodes){ + public string GetNextAirDate(List episodes){ DateTime today = DateTime.UtcNow.Date; // Check if any episode air date matches today diff --git a/CRD/Downloader/QueueManager.cs b/CRD/Downloader/QueueManager.cs index 8d9413e..9e44b85 100644 --- a/CRD/Downloader/QueueManager.cs +++ b/CRD/Downloader/QueueManager.cs @@ -75,7 +75,7 @@ public class QueueManager{ downloadItem.Refresh(); } else{ downloadItem = new DownloadItemModel(crunchyEpMeta); - downloadItem.LoadImage(); + _ = downloadItem.LoadImage(); DownloadItemModels.Add(downloadItem); } @@ -86,7 +86,7 @@ public class QueueManager{ } - public async Task CRAddEpisodeToQue(string epId, string crLocale, List dubLang, bool updateHistory = false){ + public async Task CrAddEpisodeToQueue(string epId, string crLocale, List dubLang, bool updateHistory = false){ await CrunchyrollManager.Instance.CrAuth.RefreshToken(true); var episodeL = await CrunchyrollManager.Instance.CrEpisode.ParseEpisodeById(epId, crLocale); @@ -156,10 +156,60 @@ public class QueueManager{ Console.WriteLine("Episode couldn't be added to Queue"); MessageBus.Current.SendMessage(new ToastMessage($"Couldn't add episode to the queue with current dub settings", ToastType.Error, 2)); } + } else{ + Console.WriteLine("Couldn't find episode trying to find movie with id"); + + var movie = await CrunchyrollManager.Instance.CrMovies.ParseMovieById(epId, crLocale); + + if (movie != null){ + var movieMeta = CrunchyrollManager.Instance.CrMovies.EpisodeMeta(movie, dubLang); + + if (movieMeta != null){ + movieMeta.DownloadSubs = CrunchyrollManager.Instance.CrunOptions.DlSubs; + Queue.Add(movieMeta); + + Console.WriteLine("Added Movie to Queue"); + MessageBus.Current.SendMessage(new ToastMessage($"Added Movie to Queue", ToastType.Information, 1)); + } + } } } - public async Task CRAddSeriesToQueue(CrunchySeriesList list, CrunchyMultiDownload data){ + + public void CrAddEpMetaToQueue(CrunchyEpMeta epMeta){ + Queue.Add(epMeta); + MessageBus.Current.SendMessage(new ToastMessage($"Added episode to the queue", ToastType.Information, 1)); + } + + public async Task CrAddMusicVideoToQueue(string epId){ + await CrunchyrollManager.Instance.CrAuth.RefreshToken(true); + + var musicVideo = await CrunchyrollManager.Instance.CrMusic.ParseMusicVideoByIdAsync(epId, ""); + + if (musicVideo != null){ + var musicVideoMeta = CrunchyrollManager.Instance.CrMusic.EpisodeMeta(musicVideo); + Queue.Add(musicVideoMeta); + MessageBus.Current.SendMessage(new ToastMessage($"Added music video to the queue", ToastType.Information, 1)); + } + + + } + + public async Task CrAddConcertToQueue(string epId){ + await CrunchyrollManager.Instance.CrAuth.RefreshToken(true); + + var concert = await CrunchyrollManager.Instance.CrMusic.ParseConcertByIdAsync(epId, ""); + + if (concert != null){ + var concertMeta = CrunchyrollManager.Instance.CrMusic.EpisodeMeta(concert); + Queue.Add(concertMeta); + MessageBus.Current.SendMessage(new ToastMessage($"Added concert to the queue", ToastType.Information, 1)); + } + + } + + + public async Task CrAddSeriesToQueue(CrunchySeriesList list, CrunchyMultiDownload data){ var selected = CrunchyrollManager.Instance.CrSeries.ItemSelectMultiDub(list.Data, data.DubLang, data.But, data.AllEpisodes, data.E); bool failed = false; diff --git a/CRD/Utils/Enums/EnumCollection.cs b/CRD/Utils/Enums/EnumCollection.cs index 4b5695d..2c25fc0 100644 --- a/CRD/Utils/Enums/EnumCollection.cs +++ b/CRD/Utils/Enums/EnumCollection.cs @@ -64,37 +64,37 @@ public enum Locale{ [EnumMember(Value = "id-ID")] IdId, - + [EnumMember(Value = "en-IN")] EnIn, - + [EnumMember(Value = "pt-PT")] PtPt, - + [EnumMember(Value = "zh-TW")] ZhTw, - + [EnumMember(Value = "ca-ES")] CaEs, - + [EnumMember(Value = "pl-PL")] PlPl, - + [EnumMember(Value = "th-TH")] ThTh, - + [EnumMember(Value = "ta-IN")] TaIn, - + [EnumMember(Value = "ms-MY")] MsMy, - + [EnumMember(Value = "vi-VN")] ViVn, - + [EnumMember(Value = "te-IN")] TeIn, - + [EnumMember(Value = "id-ID")] idID, } @@ -117,12 +117,6 @@ public static class EnumExtensions{ } } -[DataContract] -public enum ChannelId{ - [EnumMember(Value = "crunchyroll")] - Crunchyroll, -} - [DataContract] public enum ImageType{ [EnumMember(Value = "poster_tall")] @@ -138,23 +132,11 @@ public enum ImageType{ Thumbnail, } -[DataContract] -public enum MaturityRating{ - [EnumMember(Value = "TV-14")] - Tv14, -} - -[DataContract] -public enum MediaType{ - [EnumMember(Value = "episode")] - Episode, -} - [DataContract] public enum DownloadMediaType{ [EnumMember(Value = "Video")] Video, - + [EnumMember(Value = "SyncVideo")] SyncVideo, @@ -166,7 +148,7 @@ public enum DownloadMediaType{ [EnumMember(Value = "Subtitle")] Subtitle, - + [EnumMember(Value = "Description")] Description, } @@ -185,8 +167,10 @@ public enum HistoryViewType{ public enum SortingType{ [EnumMember(Value = "Series Title")] SeriesTitle, + [EnumMember(Value = "Next Air Date")] NextAirDate, + [EnumMember(Value = "History Series Add Date")] HistorySeriesAddDate, } @@ -194,14 +178,26 @@ public enum SortingType{ public enum FilterType{ [EnumMember(Value = "All")] All, + [EnumMember(Value = "Missing Episodes")] MissingEpisodes, + [EnumMember(Value = "Missing Episodes Sonarr")] MissingEpisodesSonarr, + [EnumMember(Value = "Continuing Only")] ContinuingOnly, } +public enum CrunchyUrlType{ + Artist, + MusicVideo, + Concert, + Episode, + Series, + Unknown +} + public enum SonarrCoverType{ Banner, FanArt, @@ -219,5 +215,5 @@ public enum SonarrStatus{ Continuing, Upcoming, Ended, - Deleted + Deleted }; \ No newline at end of file diff --git a/CRD/Utils/Files/CfgManager.cs b/CRD/Utils/Files/CfgManager.cs index 84cd932..7b8a175 100644 --- a/CRD/Utils/Files/CfgManager.cs +++ b/CRD/Utils/Files/CfgManager.cs @@ -6,6 +6,7 @@ using System.Reflection; using CRD.Downloader; using CRD.Downloader.Crunchyroll; using CRD.Utils.Structs; +using CRD.Utils.Structs.Crunchyroll; using Newtonsoft.Json; using YamlDotNet.Core; using YamlDotNet.Core.Events; diff --git a/CRD/Utils/Helpers.cs b/CRD/Utils/Helpers.cs index b18f389..659f839 100644 --- a/CRD/Utils/Helpers.cs +++ b/CRD/Utils/Helpers.cs @@ -5,10 +5,12 @@ using System.IO; using System.Linq; using System.Net.Http; using System.Runtime.Serialization; +using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using Avalonia.Media.Imaging; using CRD.Utils.Structs; +using CRD.Utils.Structs.Crunchyroll.Music; using Newtonsoft.Json; namespace CRD.Utils; @@ -30,6 +32,36 @@ public class Helpers{ } } + public static string ConvertTimeFormat(string time){ + var timeParts = time.Split(':', '.'); + int hours = int.Parse(timeParts[0]); + int minutes = int.Parse(timeParts[1]); + int seconds = int.Parse(timeParts[2]); + int milliseconds = int.Parse(timeParts[3]); + + return $"{hours}:{minutes:D2}:{seconds:D2}.{milliseconds / 10:D2}"; + } + + public static string ExtractDialogue(string[] lines, int startLine){ + var dialogueBuilder = new StringBuilder(); + + for (int i = startLine; i < lines.Length && !string.IsNullOrWhiteSpace(lines[i]); i++){ + if (!lines[i].Contains("-->") && !lines[i].StartsWith("STYLE")){ + string line = lines[i].Trim(); + // Remove HTML tags and keep the inner text + line = Regex.Replace(line, @"<[^>]+>", ""); + dialogueBuilder.Append(line + "\\N"); + } + } + + // Remove the last newline character + if (dialogueBuilder.Length > 0){ + dialogueBuilder.Length -= 2; // Remove the last "\N" + } + + return dialogueBuilder.ToString(); + } + public static void OpenUrl(string url){ try{ Process.Start(new ProcessStartInfo{ @@ -378,4 +410,7 @@ public class Helpers{ return languageGroups; } + + + } \ No newline at end of file diff --git a/CRD/Utils/Http/HttpClientReq.cs b/CRD/Utils/Http/HttpClientReq.cs index 952c61c..e55c308 100644 --- a/CRD/Utils/Http/HttpClientReq.cs +++ b/CRD/Utils/Http/HttpClientReq.cs @@ -48,6 +48,7 @@ public class HttpClientReq{ // client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0"); client.DefaultRequestHeaders.UserAgent.ParseAdd("Crunchyroll/1.9.0 Nintendo Switch/18.1.0.0 UE4/4.27"); + // client.DefaultRequestHeaders.UserAgent.ParseAdd("Crunchyroll/3.60.0 Android/9 okhttp/4.12.0"); } @@ -126,6 +127,7 @@ public static class Api{ public static readonly string Search = ApiBeta + "/content/v2/discover/search"; public static readonly string Browse = ApiBeta + "/content/v2/discover/browse"; public static readonly string Cms = ApiBeta + "/content/v2/cms"; + public static readonly string Content = ApiBeta + "/content/v2"; 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"; diff --git a/CRD/Utils/Sonarr/SonarrClient.cs b/CRD/Utils/Sonarr/SonarrClient.cs index 6071c4a..4aeaa7b 100644 --- a/CRD/Utils/Sonarr/SonarrClient.cs +++ b/CRD/Utils/Sonarr/SonarrClient.cs @@ -50,11 +50,20 @@ public class SonarrClient{ httpClient = new HttpClient(); } - public async void RefreshSonarr(){ + public async Task RefreshSonarr(){ await CheckSonarrSettings(); if (CrunchyrollManager.Instance.CrunOptions.SonarrProperties is{ SonarrEnabled: true }){ SonarrSeries = await GetSeries(); CrunchyrollManager.Instance.History.MatchHistorySeriesWithSonarr(true); + + foreach (var historySeries in CrunchyrollManager.Instance.HistoryList){ + if (historySeries.SonarrSeriesId != null){ + List? episodes = await GetEpisodes(int.Parse(historySeries.SonarrSeriesId)); + historySeries.SonarrNextAirDate = CrunchyrollManager.Instance.History.GetNextAirDate(episodes); + } + } + + } } diff --git a/CRD/Utils/Structs/CalendarStructs.cs b/CRD/Utils/Structs/CalendarStructs.cs index a79b848..082aa52 100644 --- a/CRD/Utils/Structs/CalendarStructs.cs +++ b/CRD/Utils/Structs/CalendarStructs.cs @@ -51,7 +51,7 @@ public partial class CalendarEpisode : INotifyPropertyChanged{ if (match.Success){ var locale = match.Groups[1].Value; // Capture the locale part var id = match.Groups[2].Value; // Capture the ID part - QueueManager.Instance.CRAddEpisodeToQue(id, Languages.Locale2language(locale).CrLocale, CrunchyrollManager.Instance.CrunOptions.DubLang, true); + QueueManager.Instance.CrAddEpisodeToQueue(id, Languages.Locale2language(locale).CrLocale, CrunchyrollManager.Instance.CrunOptions.DubLang, true); } } diff --git a/CRD/Utils/Structs/CrCmsToken.cs b/CRD/Utils/Structs/CrCmsToken.cs deleted file mode 100644 index 6ec205b..0000000 --- a/CRD/Utils/Structs/CrCmsToken.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using Newtonsoft.Json; - -namespace CRD.Utils.Structs; - -public class CrCmsToken{ - [JsonProperty("cms")] public CmsTokenB Cms{ get; set; } - [JsonProperty("cms_beta")] public CmsTokenB CmsBeta{ get; set; } - [JsonProperty("cms_web")] public CmsTokenB CmsWeb{ get; set; } - - [JsonProperty("service_available")] public bool ServiceAvailable{ get; set; } - - [JsonProperty("default_marketing_opt_in")] - public bool DefaultMarketingOptIn{ get; set; } -} - -public struct CmsTokenB{ - public string Bucket{ get; set; } - public string Policy{ get; set; } - public string Signature{ get; set; } - [JsonProperty("key_pair_id")] public string KeyPairId{ get; set; } - - public DateTime Expires{ get; set; } -} \ No newline at end of file diff --git a/CRD/Utils/Structs/CrDownloadOptions.cs b/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs similarity index 98% rename from CRD/Utils/Structs/CrDownloadOptions.cs rename to CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs index 04422f5..8dd17df 100644 --- a/CRD/Utils/Structs/CrDownloadOptions.cs +++ b/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs @@ -69,6 +69,9 @@ public class CrDownloadOptions{ [YamlMember(Alias = "include_signs_subs", ApplyNamingConventions = false)] public bool IncludeSignsSubs{ get; set; } + [YamlMember(Alias = "include_cc_subs", ApplyNamingConventions = false)] + public bool IncludeCcSubs{ get; set; } + [YamlMember(Alias = "mux_mp4", ApplyNamingConventions = false)] public bool Mp4{ get; set; } diff --git a/CRD/Utils/Structs/Crunchyroll/CrMovie.cs b/CRD/Utils/Structs/Crunchyroll/CrMovie.cs new file mode 100644 index 0000000..d6a9143 --- /dev/null +++ b/CRD/Utils/Structs/Crunchyroll/CrMovie.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace CRD.Utils.Structs; + +public struct CrunchyMovieList{ + public int Total{ get; set; } + public List? Data{ get; set; } + public Meta Meta{ get; set; } +} + +public class CrunchyMovie{ + [JsonProperty("channel_id")] + public string? ChannelId{ get; set; } + + [JsonProperty("content_descriptors")] + public List ContentDescriptors{ get; set; } + + [JsonProperty("mature_blocked")] + public bool MatureBlocked{ get; set; } + + [JsonProperty("is_premium_only")] + public bool IsPremiumOnly{ get; set; } + + [JsonProperty("is_mature")] + public bool IsMature{ get; set; } + + [JsonProperty("free_available_date")] + public DateTime? FreeAvailableDate{ get; set; } + + [JsonProperty("premium_available_date")] + public DateTime? PremiumAvailableDate{ get; set; } + + [JsonProperty("availability_starts")] + public DateTime? AvailabilityStarts{ get; set; } + + [JsonProperty("availability_ends")] + public DateTime? AvailabilityEnds{ get; set; } + + [JsonProperty("maturity_ratings")] + public List MaturityRatings{ get; set; } + + [JsonProperty("movie_listing_title")] + public string? MovieListingTitle{ get; set; } + + public string Id{ get; set; } + + public string Title{ get; set; } + + [JsonProperty("duration_ms")] + public int DurationMs{ get; set; } + + [JsonProperty("listing_id")] + public string ListingId{ get; set; } + + [JsonProperty("available_date")] + public DateTime? AvailableDate{ get; set; } + + [JsonProperty("is_subbed")] + public bool IsSubbed{ get; set; } + + public string Slug{ get; set; } + + [JsonProperty("available_offline")] + public bool AvailableOffline{ get; set; } + + [JsonProperty("availability_notes")] + public string AvailabilityNotes{ get; set; } + + [JsonProperty("closed_captions_available")] + public bool ClosedCaptionsAvailable{ get; set; } + + [JsonProperty("audio_locale")] + public string AudioLocale{ get; set; } + + [JsonProperty("is_dubbed")] + public bool IsDubbed{ get; set; } + + [JsonProperty("streams_link")] + public string? StreamsLink{ get; set; } + + [JsonProperty("slug_title")] + public string SlugTitle{ get; set; } + + public string Description{ get; set; } + + public Images? Images{ get; set; } + + [JsonProperty("media_type")] + public string? MediaType{ get; set; } + + [JsonProperty("extended_maturity_rating")] + public Dictionary ExtendedMaturityRating{ get; set; } + + [JsonProperty("premium_date")] + public DateTime? PremiumDate{ get; set; } +} \ No newline at end of file diff --git a/CRD/Utils/Structs/CrProfile.cs b/CRD/Utils/Structs/Crunchyroll/CrProfile.cs similarity index 98% rename from CRD/Utils/Structs/CrProfile.cs rename to CRD/Utils/Structs/Crunchyroll/CrProfile.cs index 1494119..333c538 100644 --- a/CRD/Utils/Structs/CrProfile.cs +++ b/CRD/Utils/Structs/Crunchyroll/CrProfile.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using Newtonsoft.Json; -namespace CRD.Utils.Structs; +namespace CRD.Utils.Structs.Crunchyroll; public class CrProfile{ public string? Avatar{ get; set; } diff --git a/CRD/Utils/Structs/CrToken.cs b/CRD/Utils/Structs/Crunchyroll/CrToken.cs similarity index 91% rename from CRD/Utils/Structs/CrToken.cs rename to CRD/Utils/Structs/Crunchyroll/CrToken.cs index 1749018..1249349 100644 --- a/CRD/Utils/Structs/CrToken.cs +++ b/CRD/Utils/Structs/Crunchyroll/CrToken.cs @@ -1,6 +1,6 @@ using System; -namespace CRD.Utils.Structs; +namespace CRD.Utils.Structs.Crunchyroll; public class CrToken{ public string? access_token { get; set; } diff --git a/CRD/Utils/Structs/CrunchyStreamData.cs b/CRD/Utils/Structs/Crunchyroll/CrunchyStreamData.cs similarity index 94% rename from CRD/Utils/Structs/CrunchyStreamData.cs rename to CRD/Utils/Structs/Crunchyroll/CrunchyStreamData.cs index 74cd577..7f6c14d 100644 --- a/CRD/Utils/Structs/CrunchyStreamData.cs +++ b/CRD/Utils/Structs/Crunchyroll/CrunchyStreamData.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Newtonsoft.Json; -namespace CRD.Utils.Structs; +namespace CRD.Utils.Structs.Crunchyroll; public class CrunchyStreamData{ public string? AssetId{ get; set; } @@ -20,7 +20,7 @@ public class CrunchyStreamData{ public class Caption{ public string? Format{ get; set; } - public string? Language{ get; set; } + public Locale? Language{ get; set; } public string? Url{ get; set; } } diff --git a/CRD/Utils/Structs/CrBrowseEpisode.cs b/CRD/Utils/Structs/Crunchyroll/Episode/CrBrowseEpisode.cs similarity index 100% rename from CRD/Utils/Structs/CrBrowseEpisode.cs rename to CRD/Utils/Structs/Crunchyroll/Episode/CrBrowseEpisode.cs diff --git a/CRD/Utils/Structs/EpisodeStructs.cs b/CRD/Utils/Structs/Crunchyroll/Episode/EpisodeStructs.cs similarity index 98% rename from CRD/Utils/Structs/EpisodeStructs.cs rename to CRD/Utils/Structs/Crunchyroll/Episode/EpisodeStructs.cs index 9dce293..c3a010d 100644 --- a/CRD/Utils/Structs/EpisodeStructs.cs +++ b/CRD/Utils/Structs/Crunchyroll/Episode/EpisodeStructs.cs @@ -84,7 +84,7 @@ public struct CrunchyEpisode{ public string Id{ get; set; } [JsonProperty("media_type")] - public MediaType? MediaType{ get; set; } + public string? MediaType{ get; set; } [JsonProperty("availability_ends")] public DateTime? AvailabilityEnds{ get; set; } @@ -95,7 +95,7 @@ public struct CrunchyEpisode{ public string Playback{ get; set; } [JsonProperty("channel_id")] - public ChannelId? ChannelId{ get; set; } + public string? ChannelId{ get; set; } public string? Episode{ get; set; } @@ -251,6 +251,8 @@ public class CrunchyEpMeta{ public string? DownloadPath{ get; set; } public List DownloadSubs{ get; set; } =[]; + + public bool Music{ get; set; } } diff --git a/CRD/Utils/Structs/Crunchyroll/Music/CrMusicVideo.cs b/CRD/Utils/Structs/Crunchyroll/Music/CrMusicVideo.cs new file mode 100644 index 0000000..7ad3ce3 --- /dev/null +++ b/CRD/Utils/Structs/Crunchyroll/Music/CrMusicVideo.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace CRD.Utils.Structs.Crunchyroll.Music; + +public struct CrunchyMusicVideoList{ + public int Total{ get; set; } + public List? Data{ get; set; } + public Meta Meta{ get; set; } +} + +public class CrunchyMusicVideo{ + + [JsonProperty("copyright")] + public string? Copyright{ get; set; } + + [JsonProperty("hash")] + public string? Hash{ get; set; } + + [JsonProperty("availability")] + public MusicVideoAvailability? Availability{ get; set; } + + [JsonProperty("isMature")] + public bool IsMature{ get; set; } + + [JsonProperty("maturityRatings")] + public object? MaturityRatings{ get; set; } + + [JsonProperty("title")] + public string? Title{ get; set; } + + [JsonProperty("artists")] + public object? Artists{ get; set; } + + [JsonProperty("displayArtistNameRequired")] + public bool DisplayArtistNameRequired{ get; set; } + + [JsonProperty("streams_link")] + public string? StreamsLink{ get; set; } + + [JsonProperty("matureBlocked")] + public bool MatureBlocked{ get; set; } + + [JsonProperty("originalRelease")] + public DateTime? OriginalRelease{ get; set; } + + [JsonProperty("sequenceNumber")] + public int SequenceNumber{ get; set; } + + [JsonProperty("type")] + public string? Type{ get; set; } + + [JsonProperty("animeIds")] + public List? AnimeIds{ get; set; } + + [JsonProperty("description")] + public string? Description{ get; set; } + + [JsonProperty("durationMs")] + public int DurationMs{ get; set; } + + [JsonProperty("licensor")] + public string? Licensor{ get; set; } + + [JsonProperty("slug")] + public string? Slug{ get; set; } + + [JsonProperty("artist")] + public MusicVideoArtist? Artist{ get; set; } + + [JsonProperty("isPremiumOnly")] + public bool IsPremiumOnly{ get; set; } + + [JsonProperty("isPublic")] + public bool IsPublic{ get; set; } + + [JsonProperty("publishDate")] + public DateTime? PublishDate{ get; set; } + + [JsonProperty("displayArtistName")] + public string? DisplayArtistName{ get; set; } + + [JsonProperty("genres")] + public object? genres{ get; set; } + + [JsonProperty("readyToPublish")] + public bool ReadyToPublish{ get; set; } + + [JsonProperty("id")] + public string? Id{ get; set; } + + [JsonProperty("createdAt")] + public DateTime? CreatedAt{ get; set; } + + public MusicImages? Images{ get; set; } + + [JsonProperty("updatedAt")] + public DateTime? UpdatedAt{ get; set; } + +} + +public struct MusicImages{ + [JsonProperty("poster_tall")] + public List? PosterTall{ get; set; } + + [JsonProperty("poster_wide")] + public List? PosterWide{ get; set; } + + [JsonProperty("promo_image")] + public List? PromoImage{ get; set; } + + public List? Thumbnail{ get; set; } +} + +public struct MusicVideoArtist{ + [JsonProperty("id")] + public string? Id{ get; set; } + [JsonProperty("name")] + public string? Name{ get; set; } + [JsonProperty("slug")] + public string? Slug{ get; set; } + +} + +public struct MusicVideoAvailability{ + [JsonProperty("endDate")] + public DateTime? EndDate{ get; set; } + [JsonProperty("startDate")] + public DateTime? StartDate{ get; set; } + +} \ No newline at end of file diff --git a/CRD/Utils/Structs/Playback.cs b/CRD/Utils/Structs/Crunchyroll/Playback.cs similarity index 98% rename from CRD/Utils/Structs/Playback.cs rename to CRD/Utils/Structs/Crunchyroll/Playback.cs index 02e5254..2b152b8 100644 --- a/CRD/Utils/Structs/Playback.cs +++ b/CRD/Utils/Structs/Crunchyroll/Playback.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Newtonsoft.Json; -namespace CRD.Utils.Structs; +namespace CRD.Utils.Structs.Crunchyroll; public class PlaybackData{ public int Total{ get; set; } diff --git a/CRD/Utils/Structs/CrBrowseSeries.cs b/CRD/Utils/Structs/Crunchyroll/Series/CrBrowseSeries.cs similarity index 100% rename from CRD/Utils/Structs/CrBrowseSeries.cs rename to CRD/Utils/Structs/Crunchyroll/Series/CrBrowseSeries.cs diff --git a/CRD/Utils/Structs/CrSearchSeries.cs b/CRD/Utils/Structs/Crunchyroll/Series/CrSearchSeries.cs similarity index 100% rename from CRD/Utils/Structs/CrSearchSeries.cs rename to CRD/Utils/Structs/Crunchyroll/Series/CrSearchSeries.cs diff --git a/CRD/Utils/Structs/CrSeriesBase.cs b/CRD/Utils/Structs/Crunchyroll/Series/CrSeriesBase.cs similarity index 100% rename from CRD/Utils/Structs/CrSeriesBase.cs rename to CRD/Utils/Structs/Crunchyroll/Series/CrSeriesBase.cs diff --git a/CRD/Utils/Structs/CrSeriesSearch.cs b/CRD/Utils/Structs/Crunchyroll/Series/CrSeriesSearch.cs similarity index 100% rename from CRD/Utils/Structs/CrSeriesSearch.cs rename to CRD/Utils/Structs/Crunchyroll/Series/CrSeriesSearch.cs diff --git a/CRD/Utils/Structs/StreamLimits.cs b/CRD/Utils/Structs/Crunchyroll/StreamLimits.cs similarity index 98% rename from CRD/Utils/Structs/StreamLimits.cs rename to CRD/Utils/Structs/Crunchyroll/StreamLimits.cs index 0da9a8e..b9401d3 100644 --- a/CRD/Utils/Structs/StreamLimits.cs +++ b/CRD/Utils/Structs/Crunchyroll/StreamLimits.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Text.Json.Serialization; using Newtonsoft.Json; -namespace CRD.Utils.Structs; +namespace CRD.Utils.Structs.Crunchyroll; public class StreamError{ [JsonPropertyName("error")] diff --git a/CRD/Utils/Structs/History/HistoryEpisode.cs b/CRD/Utils/Structs/History/HistoryEpisode.cs index 83996e2..7e050f0 100644 --- a/CRD/Utils/Structs/History/HistoryEpisode.cs +++ b/CRD/Utils/Structs/History/HistoryEpisode.cs @@ -67,7 +67,7 @@ public class HistoryEpisode : INotifyPropertyChanged{ } public async Task DownloadEpisode(){ - await QueueManager.Instance.CRAddEpisodeToQue(EpisodeId, string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.HistoryLang) ? CrunchyrollManager.Instance.DefaultLocale : CrunchyrollManager.Instance.CrunOptions.HistoryLang, + await QueueManager.Instance.CrAddEpisodeToQueue(EpisodeId, string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.HistoryLang) ? CrunchyrollManager.Instance.DefaultLocale : CrunchyrollManager.Instance.CrunOptions.HistoryLang, CrunchyrollManager.Instance.CrunOptions.DubLang); } } \ No newline at end of file diff --git a/CRD/Utils/Structs/PlaybackDataAndroid.cs b/CRD/Utils/Structs/PlaybackDataAndroid.cs deleted file mode 100644 index 43e8447..0000000 --- a/CRD/Utils/Structs/PlaybackDataAndroid.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; - -namespace CRD.Utils.Structs; - -public class PlaybackDataAndroid{ - public string __class__{ get; set; } - public string __href__{ get; set; } - public string __resource_key__{ get; set; } - public Links __links__{ get; set; } - public Dictionary __actions__{ get; set; } - public string media_id{ get; set; } - public Locale audio_locale{ get; set; } - public Subtitles subtitles{ get; set; } - public Subtitles closed_captions{ get; set; } - public List>> streams{ get; set; } - public List bifs{ get; set; } - public List versions{ get; set; } - public Dictionary captions{ get; set; } -} \ No newline at end of file diff --git a/CRD/ViewModels/AccountPageViewModel.cs b/CRD/ViewModels/AccountPageViewModel.cs index b131e5f..84ae245 100644 --- a/CRD/ViewModels/AccountPageViewModel.cs +++ b/CRD/ViewModels/AccountPageViewModel.cs @@ -74,7 +74,7 @@ public partial class AccountPageViewModel : ViewModelBase{ UnknownEndDate = true; } - if (CrunchyrollManager.Instance.Profile.Subscription?.NextRenewalDate != null){ + if (CrunchyrollManager.Instance.Profile.Subscription?.NextRenewalDate != null && !UnknownEndDate){ _targetTime = CrunchyrollManager.Instance.Profile.Subscription.NextRenewalDate; _timer = new DispatcherTimer{ Interval = TimeSpan.FromSeconds(1) diff --git a/CRD/ViewModels/AddDownloadPageViewModel.cs b/CRD/ViewModels/AddDownloadPageViewModel.cs index c596646..2397904 100644 --- a/CRD/ViewModels/AddDownloadPageViewModel.cs +++ b/CRD/ViewModels/AddDownloadPageViewModel.cs @@ -16,6 +16,7 @@ using CRD.Downloader; using CRD.Downloader.Crunchyroll; using CRD.Utils; using CRD.Utils.Structs; +using CRD.Utils.Structs.Crunchyroll.Music; using CRD.Views; using DynamicData; using FluentAvalonia.Core; @@ -51,7 +52,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ [ObservableProperty] private bool _searchVisible = true; - + [ObservableProperty] private bool _slectSeasonVisible = false; @@ -76,6 +77,8 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ private CrunchySeriesList? currentSeriesList; + private CrunchyMusicVideoList? currentMusicVideoList; + private bool CurrentSeasonFullySelected = false; private readonly SemaphoreSlim _updateSearchSemaphore = new SemaphoreSlim(1, 1); @@ -114,152 +117,287 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ SearchItems.Clear(); } + #region UrlInput + partial void OnUrlInputChanged(string value){ if (SearchEnabled){ - UpdateSearch(value); - ButtonText = "Select Searched Series"; - ButtonEnabled = false; + _ = UpdateSearch(value); + SetButtonProperties("Select Searched Series", false); } else if (UrlInput.Length > 9){ - if (UrlInput.Contains("/watch/concert/") || UrlInput.Contains("/artist/")){ - MessageBus.Current.SendMessage(new ToastMessage("Concerts / Artists not implemented yet", ToastType.Error, 1)); - } else if (UrlInput.Contains("/watch/")){ - //Episode - ButtonText = "Add Episode to Queue"; - ButtonEnabled = true; - SearchVisible = false; - SlectSeasonVisible = false; - } else if (UrlInput.Contains("/series/")){ - //Series - ButtonText = "List Episodes"; - ButtonEnabled = true; - SearchVisible = false; - SlectSeasonVisible = false; - } else{ - ButtonEnabled = false; - SearchVisible = true; - SlectSeasonVisible = false; - } + EvaluateUrlInput(); } else{ - ButtonText = "Enter Url"; - ButtonEnabled = false; - SearchVisible = true; - SlectSeasonVisible = false; + SetButtonProperties("Enter Url", false); + SetVisibility(true, false); } } - partial void OnSearchEnabledChanged(bool value){ - if (SearchEnabled){ - ButtonText = "Select Searched Series"; - ButtonEnabled = false; - } else{ - ButtonText = "Enter Url"; - ButtonEnabled = false; - } + private void EvaluateUrlInput(){ + var (buttonText, isButtonEnabled) = DetermineButtonTextAndState(); + + SetButtonProperties(buttonText, isButtonEnabled); + SetVisibility(false, false); } + private (string, bool) DetermineButtonTextAndState(){ + return UrlInput switch{ + _ when UrlInput.Contains("/artist/") => ("List Episodes", true), + _ when UrlInput.Contains("/watch/musicvideo/") => ("Add Music Video to Queue", true), + _ when UrlInput.Contains("/watch/concert/") => ("Add Concert to Queue", true), + _ when UrlInput.Contains("/watch/") => ("Add Episode to Queue", true), + _ when UrlInput.Contains("/series/") => ("List Episodes", true), + _ => ("Unknown", false), + }; + } + + private void SetButtonProperties(string text, bool isEnabled){ + ButtonText = text; + ButtonEnabled = isEnabled; + } + + private void SetVisibility(bool isSearchVisible, bool isSelectSeasonVisible){ + SearchVisible = isSearchVisible; + SlectSeasonVisible = isSelectSeasonVisible; + } + + #endregion + + + partial void OnSearchEnabledChanged(bool value){ + ButtonText = SearchEnabled ? "Select Searched Series" : "Enter Url"; + ButtonEnabled = false; + } + + #region OnButtonPress + [RelayCommand] public async void OnButtonPress(){ - if ((selectedEpisodes.Count > 0 || SelectedItems.Count > 0 || AddAllEpisodes)){ + if (HasSelectedItemsOrEpisodes()){ Console.WriteLine("Added to Queue"); - if (SelectedItems.Count > 0){ - foreach (var selectedItem in SelectedItems){ - if (!selectedEpisodes.Contains(selectedItem.AbsolutNum)){ - selectedEpisodes.Add(selectedItem.AbsolutNum); - } - } + if (currentMusicVideoList != null){ + AddSelectedMusicVideosToQueue(); + } else{ + AddSelectedEpisodesToQueue(); } - if (currentSeriesList != null){ - await QueueManager.Instance.CRAddSeriesToQueue(currentSeriesList.Value, new CrunchyMultiDownload(CrunchyrollManager.Instance.CrunOptions.DubLang, AddAllEpisodes, false, selectedEpisodes)); - } - - - UrlInput = ""; - selectedEpisodes.Clear(); - SelectedItems.Clear(); - Items.Clear(); - currentSeriesList = null; - SeasonList.Clear(); - episodesBySeason.Clear(); - AllButtonEnabled = false; - AddAllEpisodes = false; - ButtonText = "Enter Url"; - ButtonEnabled = false; - SearchVisible = true; - SlectSeasonVisible = false; + ResetState(); } else if (UrlInput.Length > 9){ - episodesBySeason.Clear(); - SeasonList.Clear(); - if (UrlInput.Contains("/watch/concert/") || UrlInput.Contains("/artist/")){ - MessageBus.Current.SendMessage(new ToastMessage("Concerts / Artists not implemented yet", ToastType.Error, 1)); - } else if (UrlInput.Contains("/watch/")){ - //Episode - - var match = Regex.Match(UrlInput, "/([^/]+)/watch/([^/]+)"); - - if (match.Success){ - var locale = match.Groups[1].Value; // Capture the locale part - var id = match.Groups[2].Value; // Capture the ID part - QueueManager.Instance.CRAddEpisodeToQue(id, - string.IsNullOrEmpty(locale) - ? string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.HistoryLang) ? CrunchyrollManager.Instance.DefaultLocale : CrunchyrollManager.Instance.CrunOptions.HistoryLang - : Languages.Locale2language(locale).CrLocale, CrunchyrollManager.Instance.CrunOptions.DubLang, true); - UrlInput = ""; - selectedEpisodes.Clear(); - SelectedItems.Clear(); - Items.Clear(); - currentSeriesList = null; - SeasonList.Clear(); - episodesBySeason.Clear(); - } - } else if (UrlInput.Contains("/series/")){ - //Series - var match = Regex.Match(UrlInput, "/([^/]+)/series/([^/]+)"); - - if (match.Success){ - var locale = match.Groups[1].Value; // Capture the locale part - var id = match.Groups[2].Value; // Capture the ID part - - if (id.Length != 9){ - return; - } - - ButtonEnabled = false; - ShowLoading = true; - var list = await CrunchyrollManager.Instance.CrSeries.ListSeriesId(id, string.IsNullOrEmpty(locale) - ? string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.HistoryLang) ? CrunchyrollManager.Instance.DefaultLocale : CrunchyrollManager.Instance.CrunOptions.HistoryLang - : Languages.Locale2language(locale).CrLocale, new CrunchyMultiDownload(CrunchyrollManager.Instance.CrunOptions.DubLang, true)); - ShowLoading = false; - if (list != null){ - currentSeriesList = list; - foreach (var episode in currentSeriesList.Value.List){ - if (episodesBySeason.ContainsKey("S" + episode.Season)){ - episodesBySeason["S" + episode.Season].Add(new ItemModel(episode.Img, episode.Description, episode.Time, episode.Name, "S" + episode.Season, episode.EpisodeNum.StartsWith("SP") ? episode.EpisodeNum : "E" + episode.EpisodeNum, episode.E, - episode.Lang)); - } else{ - episodesBySeason.Add("S" + episode.Season, new List{ - new ItemModel(episode.Img, episode.Description, episode.Time, episode.Name, "S" + episode.Season, episode.EpisodeNum.StartsWith("SP") ? episode.EpisodeNum : "E" + episode.EpisodeNum, episode.E, episode.Lang) - }); - SeasonList.Add(new ComboBoxItem{ Content = "S" + episode.Season }); - } - } - - CurrentSelectedSeason = SeasonList[0]; - ButtonEnabled = false; - AllButtonEnabled = true; - SlectSeasonVisible = true; - ButtonText = "Select Episodes"; - } else{ - ButtonEnabled = true; - } - } - } + await HandleUrlInputAsync(); } else{ - Console.Error.WriteLine("Unnkown input"); + Console.Error.WriteLine("Unknown input"); } } + private bool HasSelectedItemsOrEpisodes(){ + return selectedEpisodes.Count > 0 || SelectedItems.Count > 0 || AddAllEpisodes; + } + + private void AddSelectedMusicVideosToQueue(){ + if (SelectedItems.Count > 0){ + var musicClass = CrunchyrollManager.Instance.CrMusic; + foreach (var selectedItem in SelectedItems){ + var music = currentMusicVideoList.Value.Data?.FirstOrDefault(ele => ele.Id == selectedItem.Id); + + if (music != null){ + var meta = musicClass.EpisodeMeta(music); + QueueManager.Instance.CrAddEpMetaToQueue(meta); + } + } + } + } + + private async void AddSelectedEpisodesToQueue(){ + AddItemsToSelectedEpisodes(); + + if (currentSeriesList != null){ + await QueueManager.Instance.CrAddSeriesToQueue( + currentSeriesList.Value, + new CrunchyMultiDownload( + CrunchyrollManager.Instance.CrunOptions.DubLang, + AddAllEpisodes, + false, + selectedEpisodes)); + } + } + + private void AddItemsToSelectedEpisodes(){ + foreach (var selectedItem in SelectedItems){ + if (!selectedEpisodes.Contains(selectedItem.AbsolutNum)){ + selectedEpisodes.Add(selectedItem.AbsolutNum); + } + } + } + + private void ResetState(){ + currentMusicVideoList = null; + UrlInput = ""; + selectedEpisodes.Clear(); + SelectedItems.Clear(); + Items.Clear(); + currentSeriesList = null; + SeasonList.Clear(); + episodesBySeason.Clear(); + AllButtonEnabled = false; + AddAllEpisodes = false; + ButtonText = "Enter Url"; + ButtonEnabled = false; + SearchVisible = true; + SlectSeasonVisible = false; + } + + private async Task HandleUrlInputAsync(){ + episodesBySeason.Clear(); + SeasonList.Clear(); + + var matchResult = ExtractLocaleAndIdFromUrl(); + + if (matchResult is (string locale, string id)){ + switch (GetUrlType()){ + case CrunchyUrlType.Artist: + await HandleArtistUrlAsync(locale, id); + break; + case CrunchyUrlType.MusicVideo: + HandleMusicVideoUrl(id); + break; + case CrunchyUrlType.Concert: + HandleConcertUrl(id); + break; + case CrunchyUrlType.Episode: + HandleEpisodeUrl(locale, id); + break; + case CrunchyUrlType.Series: + await HandleSeriesUrlAsync(locale, id); + break; + default: + Console.Error.WriteLine("Unknown input"); + break; + } + } + } + + private (string locale, string id)? ExtractLocaleAndIdFromUrl(){ + var match = Regex.Match(UrlInput, "/([^/]+)/(?:artist|watch|series)(?:/(?:musicvideo|concert))?/([^/]+)/?"); + return match.Success ? (match.Groups[1].Value, match.Groups[2].Value) : null; + } + + private CrunchyUrlType GetUrlType(){ + return UrlInput switch{ + _ when UrlInput.Contains("/artist/") => CrunchyUrlType.Artist, + _ when UrlInput.Contains("/watch/musicvideo/") => CrunchyUrlType.MusicVideo, + _ when UrlInput.Contains("/watch/concert/") => CrunchyUrlType.Concert, + _ when UrlInput.Contains("/watch/") => CrunchyUrlType.Episode, + _ when UrlInput.Contains("/series/") => CrunchyUrlType.Series, + _ => CrunchyUrlType.Unknown, + }; + } + + private async Task HandleArtistUrlAsync(string locale, string id){ + SetLoadingState(true); + + var list = await CrunchyrollManager.Instance.CrMusic.ParseArtistMusicVideosByIdAsync( + id, DetermineLocale(locale), true); + + SetLoadingState(false); + + if (list != null){ + currentMusicVideoList = list; + PopulateItemsFromMusicVideoList(); + UpdateUiForSelection(); + } + } + + private void HandleMusicVideoUrl(string id){ + _ = QueueManager.Instance.CrAddMusicVideoToQueue(id); + ResetState(); + } + + private void HandleConcertUrl(string id){ + _ = QueueManager.Instance.CrAddConcertToQueue(id); + ResetState(); + } + + private void HandleEpisodeUrl(string locale, string id){ + _ = QueueManager.Instance.CrAddEpisodeToQueue( + id, DetermineLocale(locale), + CrunchyrollManager.Instance.CrunOptions.DubLang, true); + ResetState(); + } + + private async Task HandleSeriesUrlAsync(string locale, string id){ + SetLoadingState(true); + + var list = await CrunchyrollManager.Instance.CrSeries.ListSeriesId( + id, DetermineLocale(locale), + new CrunchyMultiDownload(CrunchyrollManager.Instance.CrunOptions.DubLang, true)); + + SetLoadingState(false); + + if (list != null){ + currentSeriesList = list; + PopulateEpisodesBySeason(); + UpdateUiForSelection(); + } else{ + ButtonEnabled = true; + } + } + + private void PopulateItemsFromMusicVideoList(){ + if (currentMusicVideoList?.Data != null){ + foreach (var episode in currentMusicVideoList.Value.Data){ + var imageUrl = episode.Images?.Thumbnail?.FirstOrDefault().Source ?? ""; + var time = $"{(episode.DurationMs / 1000) / 60}:{(episode.DurationMs / 1000) % 60:D2}"; + + var newItem = new ItemModel(episode.Id ?? "", imageUrl, episode.Description ?? "", time, episode.Title ?? "", "", + episode.SequenceNumber.ToString(), episode.SequenceNumber.ToString(), new List()); + + newItem.LoadImage(imageUrl); + Items.Add(newItem); + } + } + } + + private void PopulateEpisodesBySeason(){ + foreach (var episode in currentSeriesList?.List ?? Enumerable.Empty()){ + var seasonKey = "S" + episode.Season; + var itemModel = new ItemModel( + episode.Id, episode.Img, episode.Description, episode.Time, episode.Name, seasonKey, + episode.EpisodeNum.StartsWith("SP") ? episode.EpisodeNum : "E" + episode.EpisodeNum, + episode.E, episode.Lang); + + if (!episodesBySeason.ContainsKey(seasonKey)){ + episodesBySeason[seasonKey] = new List{ itemModel }; + SeasonList.Add(new ComboBoxItem{ Content = seasonKey }); + } else{ + episodesBySeason[seasonKey].Add(itemModel); + } + } + + CurrentSelectedSeason = SeasonList.First(); + } + + private string DetermineLocale(string locale){ + return string.IsNullOrEmpty(locale) + ? (string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.HistoryLang) + ? CrunchyrollManager.Instance.DefaultLocale + : CrunchyrollManager.Instance.CrunOptions.HistoryLang) + : Languages.Locale2language(locale).CrLocale; + } + + private void SetLoadingState(bool isLoading){ + ButtonEnabled = !isLoading; + ShowLoading = isLoading; + } + + private void UpdateUiForSelection(){ + ButtonEnabled = false; + AllButtonEnabled = true; + SlectSeasonVisible = false; + ButtonText = "Select Episodes"; + } + + #endregion + + [RelayCommand] public void OnSelectSeasonPressed(){ if (CurrentSeasonFullySelected){ @@ -326,11 +464,28 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ } } + + #region SearchItemSelection + async partial void OnSelectedSearchItemChanged(CrBrowseSeries? value){ - if (value == null || string.IsNullOrEmpty(value.Id)){ + if (value is null || string.IsNullOrEmpty(value.Id)){ return; } + UpdateUiForSearchSelection(); + + var list = await FetchSeriesListAsync(value.Id); + + if (list != null){ + currentSeriesList = list; + SearchPopulateEpisodesBySeason(); + UpdateUiForEpisodeSelection(); + } else{ + ButtonEnabled = true; + } + } + + private void UpdateUiForSearchSelection(){ SearchPopupVisible = false; RaisePropertyChanged(nameof(SearchVisible)); SearchItems.Clear(); @@ -338,33 +493,58 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ SlectSeasonVisible = true; ButtonEnabled = false; ShowLoading = true; - var list = await CrunchyrollManager.Instance.CrSeries.ListSeriesId(value.Id, - string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.HistoryLang) ? CrunchyrollManager.Instance.DefaultLocale : CrunchyrollManager.Instance.CrunOptions.HistoryLang, - new CrunchyMultiDownload(CrunchyrollManager.Instance.CrunOptions.DubLang, true)); - ShowLoading = false; - if (list != null){ - currentSeriesList = list; - foreach (var episode in currentSeriesList.Value.List){ - if (episodesBySeason.ContainsKey("S" + episode.Season)){ - episodesBySeason["S" + episode.Season].Add(new ItemModel(episode.Img, episode.Description, episode.Time, episode.Name, "S" + episode.Season, episode.EpisodeNum.StartsWith("SP") ? episode.EpisodeNum : "E" + episode.EpisodeNum, episode.E, - episode.Lang)); - } else{ - episodesBySeason.Add("S" + episode.Season, new List{ - new(episode.Img, episode.Description, episode.Time, episode.Name, "S" + episode.Season, episode.EpisodeNum.StartsWith("SP") ? episode.EpisodeNum : "E" + episode.EpisodeNum, episode.E, episode.Lang) - }); - SeasonList.Add(new ComboBoxItem{ Content = "S" + episode.Season }); - } - } - - CurrentSelectedSeason = SeasonList[0]; - ButtonEnabled = false; - AllButtonEnabled = true; - ButtonText = "Select Episodes"; - } else{ - ButtonEnabled = true; - } } + private async Task FetchSeriesListAsync(string seriesId){ + var locale = string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.HistoryLang) + ? CrunchyrollManager.Instance.DefaultLocale + : CrunchyrollManager.Instance.CrunOptions.HistoryLang; + + return await CrunchyrollManager.Instance.CrSeries.ListSeriesId( + seriesId, + locale, + new CrunchyMultiDownload(CrunchyrollManager.Instance.CrunOptions.DubLang, true)); + } + + private void SearchPopulateEpisodesBySeason(){ + if (currentSeriesList?.List == null){ + return; + } + + foreach (var episode in currentSeriesList.Value.List){ + var seasonKey = "S" + episode.Season; + var episodeModel = new ItemModel( + episode.Id, + episode.Img, + episode.Description, + episode.Time, + episode.Name, + seasonKey, + episode.EpisodeNum.StartsWith("SP") ? episode.EpisodeNum : "E" + episode.EpisodeNum, + episode.E, + episode.Lang); + + if (!episodesBySeason.ContainsKey(seasonKey)){ + episodesBySeason[seasonKey] = new List{ episodeModel }; + SeasonList.Add(new ComboBoxItem{ Content = seasonKey }); + } else{ + episodesBySeason[seasonKey].Add(episodeModel); + } + } + + CurrentSelectedSeason = SeasonList.First(); + } + + private void UpdateUiForEpisodeSelection(){ + ShowLoading = false; + ButtonEnabled = false; + AllButtonEnabled = true; + ButtonText = "Select Episodes"; + } + + #endregion + + partial void OnCurrentSelectedSeasonChanged(ComboBoxItem? value){ if (value == null){ return; @@ -399,7 +579,8 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ } } -public class ItemModel(string imageUrl, string description, string time, string title, string season, string episode, string absolutNum, List availableAudios) : INotifyPropertyChanged{ +public class ItemModel(string id, string imageUrl, string description, string time, string title, string season, string episode, string absolutNum, List availableAudios) : INotifyPropertyChanged{ + public string Id{ get; set; } = id; public string ImageUrl{ get; set; } = imageUrl; public Bitmap? ImageBitmap{ get; set; } public string Title{ get; set; } = title; diff --git a/CRD/ViewModels/DownloadsPageViewModel.cs b/CRD/ViewModels/DownloadsPageViewModel.cs index 538a556..4278553 100644 --- a/CRD/ViewModels/DownloadsPageViewModel.cs +++ b/CRD/ViewModels/DownloadsPageViewModel.cs @@ -29,12 +29,13 @@ public partial class DownloadsPageViewModel : ViewModelBase{ AutoDownload = CrunchyrollManager.Instance.CrunOptions.AutoDownload; RemoveFinished = CrunchyrollManager.Instance.CrunOptions.RemoveFinishedDownload; } - + partial void OnAutoDownloadChanged(bool value){ CrunchyrollManager.Instance.CrunOptions.AutoDownload = value; if (value){ QueueManager.Instance.UpdateDownloadListItems(); } + CfgManager.WriteSettingsToFile(); } @@ -68,7 +69,8 @@ public partial class DownloadItemModel : INotifyPropertyChanged{ epMeta = epMetaF; ImageUrl = epMeta.Image; - Title = epMeta.SeriesTitle + " - S" + epMeta.Season + "E" + (epMeta.EpisodeNumber != string.Empty ? epMeta.EpisodeNumber : epMeta.AbsolutEpisodeNumberE) + " - " + epMeta.EpisodeTitle; + Title = epMeta.SeriesTitle + (!string.IsNullOrEmpty(epMeta.Season) ? " - S" + epMeta.Season + "E" + (epMeta.EpisodeNumber != string.Empty ? epMeta.EpisodeNumber : epMeta.AbsolutEpisodeNumberE) : "") + " - " + + epMeta.EpisodeTitle; isDownloading = epMeta.DownloadProgress.IsDownloading || Done; Done = epMeta.DownloadProgress.Done; @@ -84,14 +86,11 @@ public partial class DownloadItemModel : INotifyPropertyChanged{ } private string GetDubString(){ - var dubs = "Dub: "; - - if (epMeta.SelectedDubs != null) - foreach (var crunOptionsDlDub in epMeta.SelectedDubs){ - dubs += crunOptionsDlDub + " "; - } - - return dubs; + if (epMeta.SelectedDubs == null || epMeta.SelectedDubs.Count < 1){ + return ""; + } + + return epMeta.SelectedDubs.Aggregate("Dub: ", (current, crunOptionsDlDub) => current + (crunOptionsDlDub + " ")); } private string GetSubtitleString(){ @@ -105,15 +104,15 @@ public partial class DownloadItemModel : INotifyPropertyChanged{ return hardSubs; } + if (epMeta.DownloadSubs.Count < 1){ + return ""; + } + var softSubs = "Softsub: "; if (epMeta.DownloadSubs.Contains("all")){ if (epMeta.AvailableSubs != null){ - foreach (var epMetaAvailableSub in epMeta.AvailableSubs){ - softSubs += epMetaAvailableSub + " "; - } - - return softSubs; + return epMeta.AvailableSubs.Aggregate(softSubs, (current, epMetaAvailableSub) => current + (epMetaAvailableSub + " ")); } } diff --git a/CRD/ViewModels/MainWindowViewModel.cs b/CRD/ViewModels/MainWindowViewModel.cs index 0cddf7a..4bc6507 100644 --- a/CRD/ViewModels/MainWindowViewModel.cs +++ b/CRD/ViewModels/MainWindowViewModel.cs @@ -20,6 +20,9 @@ public partial class MainWindowViewModel : ViewModelBase{ [ObservableProperty] private bool _updateAvailable = true; + + [ObservableProperty] + private bool _finishedLoading = false; public MainWindowViewModel(){ _faTheme = App.Current.Styles[0] as FluentAvaloniaTheme; @@ -65,5 +68,7 @@ public partial class MainWindowViewModel : ViewModelBase{ } await CrunchyrollManager.Instance.Init(); + + FinishedLoading = true; } } \ No newline at end of file diff --git a/CRD/ViewModels/SettingsPageViewModel.cs b/CRD/ViewModels/SettingsPageViewModel.cs index ae4fc53..709ef2a 100644 --- a/CRD/ViewModels/SettingsPageViewModel.cs +++ b/CRD/ViewModels/SettingsPageViewModel.cs @@ -41,7 +41,11 @@ public partial class SettingsPageViewModel : ViewModelBase{ private bool _addScaledBorderAndShadow = false; [ObservableProperty] - private bool _includeSignSubs = false; + private bool _includeSignSubs; + + [ObservableProperty] + private bool _includeCcSubs; + [ObservableProperty] private ComboBoxItem _selectedScaledBorderAndShadow; @@ -399,6 +403,7 @@ public partial class SettingsPageViewModel : ViewModelBase{ IncludeEpisodeDescription = options.IncludeVideoDescription; FileTitle = options.VideoTitle ?? ""; IncludeSignSubs = options.IncludeSignsSubs; + IncludeCcSubs = options.IncludeCcSubs; DownloadVideo = !options.Novids; DownloadAudio = !options.Noaudio; DownloadVideoForEveryDub = !options.DlVideoOnce; @@ -478,6 +483,7 @@ public partial class SettingsPageViewModel : ViewModelBase{ CrunchyrollManager.Instance.CrunOptions.Numbers = Math.Clamp((int)(LeadingNumbers ?? 0),0,10); CrunchyrollManager.Instance.CrunOptions.FileName = FileName; CrunchyrollManager.Instance.CrunOptions.IncludeSignsSubs = IncludeSignSubs; + CrunchyrollManager.Instance.CrunOptions.IncludeCcSubs = IncludeCcSubs; CrunchyrollManager.Instance.CrunOptions.DownloadSpeedLimit = Math.Clamp((int)(DownloadSpeed ?? 0),0,1000000000); CrunchyrollManager.Instance.CrunOptions.SimultaneousDownloads = Math.Clamp((int)(SimultaneousDownloads ?? 0),1,10); diff --git a/CRD/Views/MainWindow.axaml b/CRD/Views/MainWindow.axaml index 93689d9..ea35a4f 100644 --- a/CRD/Views/MainWindow.axaml +++ b/CRD/Views/MainWindow.axaml @@ -60,7 +60,7 @@ IconSource="Add" /> - diff --git a/CRD/Views/SettingsPageView.axaml b/CRD/Views/SettingsPageView.axaml index ab2b05a..39c8b19 100644 --- a/CRD/Views/SettingsPageView.axaml +++ b/CRD/Views/SettingsPageView.axaml @@ -125,6 +125,12 @@ + + + + + +