Add - Added movie support https://github.com/Crunchy-DL/Crunchy-Downloader/issues/64
Add - Added Artist/Concerts/Music video support https://github.com/Crunchy-DL/Crunchy-Downloader/issues/67 https://github.com/Crunchy-DL/Crunchy-Downloader/issues/46 Add - Added CC support https://github.com/Crunchy-DL/Crunchy-Downloader/issues/65 Fix - "No active subscrition" text shown with funimation sub https://github.com/Crunchy-DL/Crunchy-Downloader/issues/56
This commit is contained in:
parent
3f79e45131
commit
dff195ce1f
|
@ -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){
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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; }
|
||||
}
|
|
@ -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; }
|
||||
|
|
@ -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; }
|
||||
}
|
|
@ -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; }
|
|
@ -1,6 +1,6 @@
|
|||
using System;
|
||||
|
||||
namespace CRD.Utils.Structs;
|
||||
namespace CRD.Utils.Structs.Crunchyroll;
|
||||
|
||||
public class CrToken{
|
||||
public string? access_token { get; set; }
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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{
|
|
@ -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; }
|
||||
|
||||
}
|
|
@ -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; }
|
|
@ -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")]
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 + " "));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue