Elwador 2024-08-09 23:16:13 +02:00
parent 3f79e45131
commit dff195ce1f
37 changed files with 1127 additions and 407 deletions

View File

@ -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){

View File

@ -15,11 +15,6 @@ public class CrEpisode(){
private readonly CrunchyrollManager crunInstance = CrunchyrollManager.Instance;
public async Task<CrunchyEpisode?> 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";

View File

@ -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<CrunchyMovie?> 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<CrunchyMovieList>(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<string> 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<List<Image>>{ new List<Image>{ new Image{ Source = "/notFound.png" } } });
var epMeta = new CrunchyEpMeta();
epMeta.Data = new List<CrunchyEpMetaData>{ 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<string>();
epMeta.Description = episodeP.Description;
return epMeta;
}
}

View File

@ -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<CrunchyMusicVideo?> ParseMusicVideoByIdAsync(string id, string crLocale, bool forcedLang = false){
return await ParseMediaByIdAsync(id, crLocale, forcedLang, "music/music_videos");
}
public async Task<CrunchyMusicVideo?> ParseConcertByIdAsync(string id, string crLocale, bool forcedLang = false){
return await ParseMediaByIdAsync(id, crLocale, forcedLang, "music/concerts");
}
public async Task<CrunchyMusicVideoList?> 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<CrunchyMusicVideo>();
if (concerts.Data != null){
musicVideos.Data.AddRange(concerts.Data);
}
return musicVideos;
}
private async Task<CrunchyMusicVideo?> 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<CrunchyMusicVideoList> 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<CrunchyMusicVideoList>(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<Image>{ new Image{ Source = "/notFound.png" } });
var epMeta = new CrunchyEpMeta();
epMeta.Data = new List<CrunchyEpMetaData>{ 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<string>();
epMeta.Description = episodeP.Description;
epMeta.Music = true;
return epMeta;
}
}

View File

@ -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;
@ -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",
@ -173,7 +179,7 @@ public class CrunchyrollManager{
}
}
SonarrClient.Instance.RefreshSonarr();
await SonarrClient.Instance.RefreshSonarr();
}
}
@ -241,8 +247,6 @@ public class CrunchyrollManager{
},
res.FileName);
}
} else{
await MuxStreams(res.Data,
new CrunchyMuxOptions{
@ -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<SubtitleInfo> subsData = pbData.Meta.Subtitles.Values.ToList();
List<SubtitleInfo> capsData = pbData.Meta.ClosedCaptions?.Values.ToList() ?? new List<SubtitleInfo>();
List<Caption> capsData = pbData.Meta.Captions?.Values.ToList() ?? new List<Caption>();
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(@"(?<start>\d{2}:\d{2}:\d{2}\.\d{3})\s-->\s(?<end>\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,101 +1537,77 @@ 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<Dictionary<string, Dictionary<string, StreamDetails>>>() };
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<Dictionary<string, Dictionary<string, StreamDetails>>>()
};
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<Dictionary<string, Dictionary<string, StreamDetails>>>() };
temppbData.Data.Add(new Dictionary<string, Dictionary<string, StreamDetails>>());
CrunchyStreamData? playStream = JsonConvert.DeserializeObject<CrunchyStreamData>(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<string>{ 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);
if (!playbackRequestResponse.IsOk){
playbackRequestResponse = await HandleStreamErrorsAsync(playbackRequestResponse, mediaGuidId, playbackEndpoint);
}
playbackRequestResponse = await HttpClientReq.Instance.SendHttpRequest(playbackRequest);
if (playbackRequestResponse.IsOk){
temppbData = await ProcessPlaybackResponseAsync(playbackRequestResponse.ResponseContent, mediaId, mediaGuidId);
} else{
Console.Error.WriteLine("Fallback Request Stream URLs FAILED!");
}
}
if (!playbackRequestResponse.IsOk && playbackRequestResponse.ResponseContent != string.Empty){
var s = playbackRequestResponse.ResponseContent;
var error = StreamError.FromJson(s);
if (error != null && error.IsTooManyActiveStreamsError()){
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);
}
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);
}
return await SendPlaybackRequestAsync(endpoint);
}
if (playbackRequestResponse.IsOk){
temppbData = new PlaybackData{ Total = 0, Data = new List<Dictionary<string, Dictionary<string, StreamDetails>>>() };
temppbData.Data.Add(new Dictionary<string, Dictionary<string, StreamDetails>>());
return response;
}
CrunchyStreamData? playStream = JsonConvert.DeserializeObject<CrunchyStreamData>(playbackRequestResponse.ResponseContent, SettingsJsonSerializerSettings);
CrunchyStreams derivedPlayCrunchyStreams = new CrunchyStreams();
if (playStream != null){
if (playStream.Token != null) await HttpClientReq.DeAuthVideo(mediaGuidId, playStream.Token);
private async Task<PlaybackData> ProcessPlaybackResponseAsync(string responseContent, string mediaId, string mediaGuidId){
var temppbData = new PlaybackData{
Total = 0,
Data = new List<Dictionary<string, Dictionary<string, StreamDetails>>>()
};
if (playStream.HardSubs != null)
var playStream = JsonConvert.DeserializeObject<CrunchyStreamData>(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{
@ -1585,39 +1615,43 @@ public class CrunchyrollManager{
HardsubLocale = stream.Hlang
};
}
}
derivedPlayCrunchyStreams[""] = new StreamDetails{
Url = playStream.Url,
HardsubLocale = Locale.DefaulT
};
if (temppbData.Data != null){
temppbData.Data[0]["drm_adaptive_dash"] = derivedPlayCrunchyStreams;
temppbData.Data.Add(new Dictionary<string, Dictionary<string, StreamDetails>>{
{ "drm_adaptive_dash", derivedPlayCrunchyStreams }
});
temppbData.Total = 1;
}
temppbData.Meta = new PlaybackMeta(){ AudioLocale = playStream.AudioLocale, Versions = playStream.Versions, Bifs = new List<string>{ playStream.Bifs }, MediaId = mediaId };
temppbData.Meta = new PlaybackMeta{
AudioLocale = playStream.AudioLocale,
Versions = playStream.Versions,
Bifs = new List<string>{ playStream.Bifs },
MediaId = mediaId,
Captions = playStream.Captions,
Subtitles = new Subtitles()
};
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 });
}
}
} else{
Console.Error.WriteLine("'Fallback Request Stream URLs FAILED!'");
ok = playbackRequestResponse.IsOk;
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 (IsOk: ok, pbData: temppbData, error: ok ? "" : playbackRequestResponse.ResponseContent);
return temppbData;
}
#endregion
private async Task ParseChapters(string currentMediaId, List<string> compiledChapters){
var showRequest = HttpClientReq.CreateRequestMessage($"https://static.crunchyroll.com/skip-events/production/{currentMediaId}.json", HttpMethod.Get, true, true, null);
@ -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
});

View File

@ -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<SonarrEpisode> episodes){
public string GetNextAirDate(List<SonarrEpisode> episodes){
DateTime today = DateTime.UtcNow.Date;
// Check if any episode air date matches today

View File

@ -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<string> dubLang, bool updateHistory = false){
public async Task CrAddEpisodeToQueue(string epId, string crLocale, List<string> 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;

View File

@ -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,18 +132,6 @@ 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")]
@ -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,

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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";

View File

@ -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<SonarrEpisode>? episodes = await GetEpisodes(int.Parse(historySeries.SonarrSeriesId));
historySeries.SonarrNextAirDate = CrunchyrollManager.Instance.History.GetNextAirDate(episodes);
}
}
}
}

View File

@ -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);
}
}

View File

@ -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; }
}

View File

@ -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; }

View File

@ -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<CrunchyMovie>? Data{ get; set; }
public Meta Meta{ get; set; }
}
public class CrunchyMovie{
[JsonProperty("channel_id")]
public string? ChannelId{ get; set; }
[JsonProperty("content_descriptors")]
public List<string> 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<string> 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<string, object> ExtendedMaturityRating{ get; set; }
[JsonProperty("premium_date")]
public DateTime? PremiumDate{ get; set; }
}

View File

@ -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; }

View File

@ -1,6 +1,6 @@
using System;
namespace CRD.Utils.Structs;
namespace CRD.Utils.Structs.Crunchyroll;
public class CrToken{
public string? access_token { get; set; }

View File

@ -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; }
}

View File

@ -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; }
@ -252,6 +252,8 @@ public class CrunchyEpMeta{
public string? DownloadPath{ get; set; }
public List<string> DownloadSubs{ get; set; } =[];
public bool Music{ get; set; }
}
public class DownloadProgress{

View File

@ -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<CrunchyMusicVideo>? 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<string>? 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<Image>? PosterTall{ get; set; }
[JsonProperty("poster_wide")]
public List<Image>? PosterWide{ get; set; }
[JsonProperty("promo_image")]
public List<Image>? PromoImage{ get; set; }
public List<Image>? 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; }
}

View File

@ -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; }

View File

@ -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")]

View File

@ -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);
}
}

View File

@ -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<object, object> __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<Dictionary<string, Dictionary<string, StreamDetails>>> streams{ get; set; }
public List<string> bifs{ get; set; }
public List<PlaybackVersion> versions{ get; set; }
public Dictionary<string, object> captions{ get; set; }
}

View File

@ -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)

View File

@ -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;
@ -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,55 +117,110 @@ 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;
EvaluateUrlInput();
} else{
ButtonEnabled = false;
SearchVisible = true;
SlectSeasonVisible = false;
}
} else{
ButtonText = "Enter Url";
ButtonEnabled = false;
SearchVisible = true;
SlectSeasonVisible = false;
SetButtonProperties("Enter Url", false);
SetVisibility(true, 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){
if (SearchEnabled){
ButtonText = "Select Searched Series";
ButtonEnabled = false;
} else{
ButtonText = "Enter Url";
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 (currentMusicVideoList != null){
AddSelectedMusicVideosToQueue();
} else{
AddSelectedEpisodesToQueue();
}
ResetState();
} else if (UrlInput.Length > 9){
await HandleUrlInputAsync();
} else{
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);
@ -170,11 +228,8 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
}
}
if (currentSeriesList != null){
await QueueManager.Instance.CRAddSeriesToQueue(currentSeriesList.Value, new CrunchyMultiDownload(CrunchyrollManager.Instance.CrunOptions.DubLang, AddAllEpisodes, false, selectedEpisodes));
}
private void ResetState(){
currentMusicVideoList = null;
UrlInput = "";
selectedEpisodes.Clear();
SelectedItems.Clear();
@ -188,78 +243,161 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
ButtonEnabled = false;
SearchVisible = true;
SlectSeasonVisible = false;
} 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;
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;
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<ItemModel>{
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";
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<string>());
newItem.LoadImage(imageUrl);
Items.Add(newItem);
}
}
}
private void PopulateEpisodesBySeason(){
foreach (var episode in currentSeriesList?.List ?? Enumerable.Empty<Episode>()){
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>{ itemModel };
SeasonList.Add(new ComboBoxItem{ Content = seasonKey });
} else{
Console.Error.WriteLine("Unnkown input");
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,
}
private async Task<CrunchySeriesList?> 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));
ShowLoading = false;
if (list != null){
currentSeriesList = list;
}
private void SearchPopulateEpisodesBySeason(){
if (currentSeriesList?.List == null){
return;
}
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));
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<ItemModel>{ episodeModel };
SeasonList.Add(new ComboBoxItem{ Content = seasonKey });
} else{
episodesBySeason.Add("S" + episode.Season, new List<ItemModel>{
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 });
episodesBySeason[seasonKey].Add(episodeModel);
}
}
CurrentSelectedSeason = SeasonList[0];
CurrentSelectedSeason = SeasonList.First();
}
private void UpdateUiForEpisodeSelection(){
ShowLoading = false;
ButtonEnabled = false;
AllButtonEnabled = true;
ButtonText = "Select Episodes";
} else{
ButtonEnabled = true;
}
}
#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<string> availableAudios) : INotifyPropertyChanged{
public class ItemModel(string id, string imageUrl, string description, string time, string title, string season, string episode, string absolutNum, List<string> 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;

View File

@ -35,6 +35,7 @@ public partial class DownloadsPageViewModel : ViewModelBase{
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 + " ";
if (epMeta.SelectedDubs == null || epMeta.SelectedDubs.Count < 1){
return "";
}
return dubs;
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 + " "));
}
}

View File

@ -21,6 +21,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;
}
}

View File

@ -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);

View File

@ -60,7 +60,7 @@
IconSource="Add" />
<ui:NavigationViewItem Classes="SampleAppNav" Content="Calendar" Tag="Calendar"
IconSource="Calendar" />
<ui:NavigationViewItem Classes="SampleAppNav" Content="History" Tag="History"
<ui:NavigationViewItem IsEnabled="{Binding FinishedLoading}" Classes="SampleAppNav" Content="History" Tag="History"
IconSource="Library" />
</ui:NavigationView.MenuItems>
<ui:NavigationView.FooterMenuItems>

View File

@ -126,6 +126,12 @@
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Include CC Subtitles ">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding IncludeCcSubs}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
</controls:SettingsExpander>
<controls:SettingsExpander Header="History"