Add - Added download path to download settings

Add - Added download path to history series and also to each season

Fix - When adding an episode it always showed "Added to Queue" even if it wasn't added
This commit is contained in:
Elwador 2024-06-21 03:49:44 +02:00
parent cd4ceea38a
commit 3d74fa7667
12 changed files with 500 additions and 310 deletions

View File

@ -7,6 +7,7 @@ using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;
using Avalonia.Remote.Protocol.Input;
using CRD.Utils;
using CRD.Utils.Structs;
using Newtonsoft.Json;
@ -14,10 +15,9 @@ using Newtonsoft.Json;
namespace CRD.Downloader;
public class CrEpisode(){
private readonly Crunchyroll crunInstance = Crunchyroll.Instance;
public async Task<CrunchyEpisodeList?> ParseEpisodeById(string id,string locale){
public async Task<CrunchyEpisode?> ParseEpisodeById(string id, string locale){
if (crunInstance.CmsToken?.Cms == null){
Console.Error.WriteLine("Missing CMS Access Token");
return null;
@ -26,7 +26,7 @@ public class CrEpisode(){
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
query["preferred_audio_language"] = "ja-JP";
query["locale"] = Languages.Locale2language(locale).CrLocale;
query["locale"] = Languages.Locale2language(locale).CrLocale;
var request = HttpClientReq.CreateRequestMessage($"{Api.Cms}/episodes/{id}", HttpMethod.Get, true, true, query);
@ -43,217 +43,191 @@ public class CrEpisode(){
return null;
}
return epsidoe;
if (epsidoe.Total == 1 && epsidoe.Data != null){
return epsidoe.Data.First();
}
Console.Error.WriteLine("Multiple episodes returned with one ID?");
if (epsidoe.Data != null) return epsidoe.Data.First();
return null;
}
public async Task<CrunchySeriesList> EpisodeData(CrunchyEpisodeList dlEpisodes){
public async Task<CrunchyRollEpisodeData> EpisodeData(CrunchyEpisode dlEpisode){
bool serieshasversions = true;
Dictionary<string, EpisodeAndLanguage> episodes = new Dictionary<string, EpisodeAndLanguage>();
// Dictionary<string, EpisodeAndLanguage> episodes = new Dictionary<string, EpisodeAndLanguage>();
if (dlEpisodes.Data != null){
foreach (var episode in dlEpisodes.Data){
if (crunInstance.CrunOptions.History){
await crunInstance.CrHistory.UpdateWithEpisode(episode);
}
// Prepare the episode array
EpisodeAndLanguage item;
var seasonIdentifier = !string.IsNullOrEmpty(episode.Identifier) ? episode.Identifier.Split('|')[1] : $"S{episode.SeasonNumber}";
var episodeKey = $"{seasonIdentifier}E{episode.Episode ?? (episode.EpisodeNumber + "")}";
CrunchyRollEpisodeData episode = new CrunchyRollEpisodeData();
if (!episodes.ContainsKey(episodeKey)){
item = new EpisodeAndLanguage{
Items = new List<CrunchyEpisode>(),
Langs = new List<LanguageItem>()
};
episodes[episodeKey] = item;
} else{
item = episodes[episodeKey];
}
if (crunInstance.CrunOptions.History){
await crunInstance.CrHistory.UpdateWithEpisode(dlEpisode);
}
if (episode.Versions != null){
foreach (var version in episode.Versions){
// Ensure there is only one of the same language
if (item.Langs.All(a => a.CrLocale != version.AudioLocale)){
// Push to arrays if there are no duplicates of the same language
item.Items.Add(episode);
item.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == version.AudioLocale));
}
}
} else{
// Episode didn't have versions, mark it as such to be logged.
serieshasversions = false;
// Ensure there is only one of the same language
if (item.Langs.All(a => a.CrLocale != episode.AudioLocale)){
// Push to arrays if there are no duplicates of the same language
item.Items.Add(episode);
item.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == episode.AudioLocale));
}
var seasonIdentifier = !string.IsNullOrEmpty(dlEpisode.Identifier) ? dlEpisode.Identifier.Split('|')[1] : $"S{dlEpisode.SeasonNumber}";
episode.Key = $"{seasonIdentifier}E{dlEpisode.Episode ?? (dlEpisode.EpisodeNumber + "")}";
episode.EpisodeAndLanguages = new EpisodeAndLanguage{
Items = new List<CrunchyEpisode>(),
Langs = new List<LanguageItem>()
};
if (dlEpisode.Versions != null){
foreach (var version in dlEpisode.Versions){
// Ensure there is only one of the same language
if (episode.EpisodeAndLanguages.Langs.All(a => a.CrLocale != version.AudioLocale)){
// Push to arrays if there are no duplicates of the same language
episode.EpisodeAndLanguages.Items.Add(dlEpisode);
episode.EpisodeAndLanguages.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == version.AudioLocale));
}
}
} else{
// Episode didn't have versions, mark it as such to be logged.
serieshasversions = false;
// Ensure there is only one of the same language
if (episode.EpisodeAndLanguages.Langs.All(a => a.CrLocale != dlEpisode.AudioLocale)){
// Push to arrays if there are no duplicates of the same language
episode.EpisodeAndLanguages.Items.Add(dlEpisode);
episode.EpisodeAndLanguages.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == dlEpisode.AudioLocale));
}
}
int specialIndex = 1;
int epIndex = 1;
var keys = new List<string>(episodes.Keys); // Copying the keys to a new list to avoid modifying the collection while iterating.
foreach (var key in keys){
EpisodeAndLanguage item = episodes[key];
var isSpecial = !Regex.IsMatch(item.Items[0].Episode ?? string.Empty, @"^\d+$"); // Checking if the episode is not a number (i.e., special).
string newKey;
if (isSpecial && !string.IsNullOrEmpty(item.Items[0].Episode)){
newKey = item.Items[0].Episode ?? "SP" + (specialIndex + " " + item.Items[0].Id);
} else{
newKey = $"{(isSpecial ? "SP" : 'E')}{(isSpecial ? (specialIndex + " " + item.Items[0].Id) : epIndex + "")}";
}
episodes.Remove(key);
episodes.Add(newKey, item);
if (isSpecial){
specialIndex++;
} else{
epIndex++;
}
var isSpecial = !Regex.IsMatch(episode.EpisodeAndLanguages.Items[0].Episode ?? string.Empty, @"^\d+$"); // Checking if the episode is not a number (i.e., special).
string newKey;
if (isSpecial && !string.IsNullOrEmpty(episode.EpisodeAndLanguages.Items[0].Episode)){
newKey = episode.EpisodeAndLanguages.Items[0].Episode ?? "SP" + (specialIndex + " " + episode.EpisodeAndLanguages.Items[0].Id);
} else{
newKey = $"{(isSpecial ? "SP" : 'E')}{(isSpecial ? (specialIndex + " " + episode.EpisodeAndLanguages.Items[0].Id) : episode.EpisodeAndLanguages.Items[0].Episode ?? epIndex + "")}";
}
var specials = episodes.Where(e => e.Key.StartsWith("SP")).ToList();
var normal = episodes.Where(e => e.Key.StartsWith("E")).ToList();
episode.Key = newKey;
// Combining and sorting episodes with normal first, then specials.
var sortedEpisodes = new Dictionary<string, EpisodeAndLanguage>(normal.Concat(specials));
var seasonTitle = episode.EpisodeAndLanguages.Items.FirstOrDefault(a => !Regex.IsMatch(a.SeasonTitle, @"\(\w+ Dub\)")).SeasonTitle
?? Regex.Replace(episode.EpisodeAndLanguages.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd();
foreach (var kvp in sortedEpisodes){
var key = kvp.Key;
var item = kvp.Value;
var title = episode.EpisodeAndLanguages.Items[0].Title;
var seasonNumber = Helpers.ExtractNumberAfterS(episode.EpisodeAndLanguages.Items[0].Identifier) ?? episode.EpisodeAndLanguages.Items[0].SeasonNumber.ToString();
var seasonTitle = item.Items.FirstOrDefault(a => !Regex.IsMatch(a.SeasonTitle, @"\(\w+ Dub\)")).SeasonTitle
?? Regex.Replace(item.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd();
var languages = episode.EpisodeAndLanguages.Items.Select((a, index) =>
$"{(a.IsPremiumOnly ? "+ " : "")}{episode.EpisodeAndLanguages.Langs.ElementAtOrDefault(index).Name ?? "Unknown"}").ToArray(); //☆
var title = item.Items[0].Title;
var seasonNumber = Helpers.ExtractNumberAfterS(item.Items[0].Identifier) ?? item.Items[0].SeasonNumber.ToString();
Console.WriteLine($"[{episode.Key}] {seasonTitle} - Season {seasonNumber} - {title} [{string.Join(", ", languages)}]");
var languages = item.Items.Select((a, index) =>
$"{(a.IsPremiumOnly ? "+ " : "")}{item.Langs.ElementAtOrDefault(index).Name ?? "Unknown"}").ToArray(); //☆
Console.WriteLine($"[{key}] {seasonTitle} - Season {seasonNumber} - {title} [{string.Join(", ", languages)}]");
}
if (!serieshasversions){
Console.WriteLine("Couldn\'t find versions on some episodes, fell back to old method.");
Console.WriteLine("Couldn\'t find versions on episode, fell back to old method.");
}
CrunchySeriesList crunchySeriesList = new CrunchySeriesList();
crunchySeriesList.Data = sortedEpisodes;
crunchySeriesList.List = sortedEpisodes.Select(kvp => {
var key = kvp.Key;
var value = kvp.Value;
var images = (value.Items[0].Images?.Thumbnail ?? new List<List<Image>>{ new List<Image>{ new Image{ Source = "/notFound.png" } } });
var seconds = (int)Math.Floor(value.Items[0].DurationMs / 1000.0);
return new Episode{
E = key.StartsWith("E") ? key.Substring(1) : key,
Lang = value.Langs.Select(a => a.Code).ToList(),
Name = value.Items[0].Title,
Season = Helpers.ExtractNumberAfterS(value.Items[0].Identifier) ?? value.Items[0].SeasonNumber.ToString(),
SeriesTitle = Regex.Replace(value.Items[0].SeriesTitle, @"\(\w+ Dub\)", "").TrimEnd(),
SeasonTitle = Regex.Replace(value.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd(),
EpisodeNum = value.Items[0].EpisodeNumber?.ToString() ?? value.Items[0].Episode ?? "?",
Id = value.Items[0].SeasonId,
Img = images[images.Count / 2].FirstOrDefault().Source,
Description = value.Items[0].Description,
Time = $"{seconds / 60}:{seconds % 60:D2}" // Ensures two digits for seconds.
};
}).ToList();
// crunchySeriesList.Data = sortedEpisodes;
//
//
// var images = (episode.EpisodeAndLanguages.Items[0].Images?.Thumbnail ?? new List<List<Image>>{ new List<Image>{ new Image{ Source = "/notFound.png" } } });
// var seconds = (int)Math.Floor(episode.EpisodeAndLanguages.Items[0].DurationMs / 1000.0);
//
// var newEpisode = new Episode{
// E = episode.Key.StartsWith("E") ? episode.Key.Substring(1) : episode.Key,
// Lang = episode.EpisodeAndLanguages.Langs.Select(a => a.Code).ToList(),
// Name = episode.EpisodeAndLanguages.Items[0].Title,
// Season = Helpers.ExtractNumberAfterS(episode.EpisodeAndLanguages.Items[0].Identifier) ?? episode.EpisodeAndLanguages.Items[0].SeasonNumber.ToString(),
// SeriesTitle = Regex.Replace(episode.EpisodeAndLanguages.Items[0].SeriesTitle, @"\(\w+ Dub\)", "").TrimEnd(),
// SeasonTitle = Regex.Replace(episode.EpisodeAndLanguages.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd(),
// EpisodeNum = episode.EpisodeAndLanguages.Items[0].EpisodeNumber?.ToString() ?? episode.EpisodeAndLanguages.Items[0].Episode ?? "?",
// Id = episode.EpisodeAndLanguages.Items[0].SeasonId,
// Img = images[images.Count / 2].FirstOrDefault().Source,
// Description = episode.EpisodeAndLanguages.Items[0].Description,
// Time = $"{seconds / 60}:{seconds % 60:D2}" // Ensures two digits for seconds.
// };
//
// CrunchySeriesList crunchySeriesList = new CrunchySeriesList();
return crunchySeriesList;
return episode;
}
public Dictionary<string, CrunchyEpMeta> EpisodeMeta(Dictionary<string, EpisodeAndLanguage> eps, List<string> dubLang){
var ret = new Dictionary<string, CrunchyEpMeta>();
public CrunchyEpMeta EpisodeMeta(CrunchyRollEpisodeData episodeP, List<string> dubLang){
// var ret = new Dictionary<string, CrunchyEpMeta>();
var retMeta = new CrunchyEpMeta();
foreach (var kvp in eps){
var key = kvp.Key;
var episode = kvp.Value;
for (int index = 0; index < episodeP.EpisodeAndLanguages.Items.Count; index++){
var item = episodeP.EpisodeAndLanguages.Items[index];
for (int index = 0; index < episode.Items.Count; index++){
var item = episode.Items[index];
if (!dubLang.Contains(episodeP.EpisodeAndLanguages.Langs[index].CrLocale))
continue;
if (!dubLang.Contains(episode.Langs[index].CrLocale))
continue;
item.HideSeasonTitle = true;
if (string.IsNullOrEmpty(item.SeasonTitle) && !string.IsNullOrEmpty(item.SeriesTitle)){
item.SeasonTitle = item.SeriesTitle;
item.HideSeasonTitle = false;
item.HideSeasonNumber = true;
}
if (string.IsNullOrEmpty(item.SeasonTitle) && string.IsNullOrEmpty(item.SeriesTitle)){
item.SeasonTitle = "NO_TITLE";
item.SeriesTitle = "NO_TITLE";
}
var epNum = key.StartsWith('E') ? key[1..] : key;
var images = (item.Images?.Thumbnail ?? new List<List<Image>>{ new List<Image>{ new Image{ Source = "/notFound.png" } } });
Regex dubPattern = new Regex(@"\(\w+ Dub\)");
var epMeta = new CrunchyEpMeta();
epMeta.Data = new List<CrunchyEpMetaData>{ new(){ MediaId = item.Id, Versions = item.Versions, IsSubbed = item.IsSubbed, IsDubbed = item.IsDubbed } };
epMeta.SeriesTitle = episode.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeriesTitle)).SeriesTitle ?? Regex.Replace(episode.Items[0].SeriesTitle, @"\(\w+ Dub\)", "").TrimEnd();
epMeta.SeasonTitle = episode.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeasonTitle)).SeasonTitle ?? Regex.Replace(episode.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd();
epMeta.EpisodeNumber = item.Episode;
epMeta.EpisodeTitle = item.Title;
epMeta.SeasonId = item.SeasonId;
epMeta.Season = Helpers.ExtractNumberAfterS(item.Identifier) ?? item.SeasonNumber + "";
epMeta.ShowId = item.SeriesId;
epMeta.AbsolutEpisodeNumberE = epNum;
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 = item.SubtitleLocales;
if (episode.Langs.Count > 0){
epMeta.SelectedDubs = dubLang
.Where(language => episode.Langs.Any(epLang => epLang.CrLocale == language))
.ToList();
}
var epMetaData = epMeta.Data[0];
if (!string.IsNullOrEmpty(item.StreamsLink)){
epMetaData.Playback = item.StreamsLink;
if (string.IsNullOrEmpty(item.Playback)){
item.Playback = item.StreamsLink;
}
}
if (ret.TryGetValue(key, out var epMe)){
epMetaData.Lang = episode.Langs[index];
epMe.Data?.Add(epMetaData);
} else{
epMetaData.Lang = episode.Langs[index];
epMeta.Data[0] = epMetaData;
ret.Add(key, epMeta);
}
// show ep
item.SeqId = epNum;
item.HideSeasonTitle = true;
if (string.IsNullOrEmpty(item.SeasonTitle) && !string.IsNullOrEmpty(item.SeriesTitle)){
item.SeasonTitle = item.SeriesTitle;
item.HideSeasonTitle = false;
item.HideSeasonNumber = true;
}
if (string.IsNullOrEmpty(item.SeasonTitle) && string.IsNullOrEmpty(item.SeriesTitle)){
item.SeasonTitle = "NO_TITLE";
item.SeriesTitle = "NO_TITLE";
}
var epNum = episodeP.Key.StartsWith('E') ? episodeP.Key[1..] : episodeP.Key;
var images = (item.Images?.Thumbnail ?? new List<List<Image>>{ new List<Image>{ new Image{ Source = "/notFound.png" } } });
Regex dubPattern = new Regex(@"\(\w+ Dub\)");
var epMeta = new CrunchyEpMeta();
epMeta.Data = new List<CrunchyEpMetaData>{ new(){ MediaId = item.Id, Versions = item.Versions, IsSubbed = item.IsSubbed, IsDubbed = item.IsDubbed } };
epMeta.SeriesTitle = episodeP.EpisodeAndLanguages.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeriesTitle)).SeriesTitle ?? Regex.Replace(episodeP.EpisodeAndLanguages.Items[0].SeriesTitle, @"\(\w+ Dub\)", "").TrimEnd();
epMeta.SeasonTitle = episodeP.EpisodeAndLanguages.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeasonTitle)).SeasonTitle ?? Regex.Replace(episodeP.EpisodeAndLanguages.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd();
epMeta.EpisodeNumber = item.Episode;
epMeta.EpisodeTitle = item.Title;
epMeta.SeasonId = item.SeasonId;
epMeta.Season = Helpers.ExtractNumberAfterS(item.Identifier) ?? item.SeasonNumber + "";
epMeta.ShowId = item.SeriesId;
epMeta.AbsolutEpisodeNumberE = epNum;
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 = item.SubtitleLocales;
if (episodeP.EpisodeAndLanguages.Langs.Count > 0){
epMeta.SelectedDubs = dubLang
.Where(language => episodeP.EpisodeAndLanguages.Langs.Any(epLang => epLang.CrLocale == language))
.ToList();
}
var epMetaData = epMeta.Data[0];
if (!string.IsNullOrEmpty(item.StreamsLink)){
epMetaData.Playback = item.StreamsLink;
if (string.IsNullOrEmpty(item.Playback)){
item.Playback = item.StreamsLink;
}
}
if (retMeta.Data != null){
epMetaData.Lang = episodeP.EpisodeAndLanguages.Langs[index];
retMeta.Data.Add(epMetaData);
} else{
epMetaData.Lang = episodeP.EpisodeAndLanguages.Langs[index];
epMeta.Data[0] = epMetaData;
retMeta = epMeta;
}
// show ep
item.SeqId = epNum;
}
return ret;
return retMeta;
}
}

View File

@ -286,34 +286,48 @@ public class Crunchyroll{
if (episodeL != null){
if (episodeL.Value.Data != null && episodeL.Value.Data.First().IsPremiumOnly && !Profile.HasPremium){
if (episodeL.Value.IsPremiumOnly && !Profile.HasPremium){
MessageBus.Current.SendMessage(new ToastMessage($"Episode is a premium episode make sure that you are signed in with an account that has an active premium subscription", ToastType.Error, 3));
return;
}
var sList = await CrEpisode.EpisodeData((CrunchyEpisodeList)episodeL);
var selected = CrEpisode.EpisodeMeta(sList.Data, dubLang);
var metas = selected.Values.ToList();
var sList = await CrEpisode.EpisodeData((CrunchyEpisode)episodeL);
var selected = CrEpisode.EpisodeMeta(sList, dubLang);
foreach (var crunchyEpMeta in metas){
if (CrunOptions.SonarrProperties is{ SonarrEnabled: true, UseSonarrNumbering: true }){
var historyEpisode = CrHistory.GetHistoryEpisode(crunchyEpMeta.ShowId, crunchyEpMeta.SeasonId, crunchyEpMeta.Data.First().MediaId);
if (historyEpisode != null){
if (!string.IsNullOrEmpty(historyEpisode.SonarrEpisodeNumber)){
crunchyEpMeta.EpisodeNumber = historyEpisode.SonarrEpisodeNumber;
}
if (selected.Data is{ Count: > 0 }){
if (CrunOptions.History){
var historyEpisode = CrHistory.GetHistoryEpisodeWithDownloadDir(selected.ShowId, selected.SeasonId, selected.Data.First().MediaId);
if (CrunOptions.SonarrProperties is{ SonarrEnabled: true, UseSonarrNumbering: true }){
if (historyEpisode.historyEpisode != null){
if (!string.IsNullOrEmpty(historyEpisode.historyEpisode.SonarrEpisodeNumber)){
selected.EpisodeNumber = historyEpisode.historyEpisode.SonarrEpisodeNumber;
}
if (!string.IsNullOrEmpty(historyEpisode.SonarrSeasonNumber)){
crunchyEpMeta.Season = historyEpisode.SonarrSeasonNumber;
if (!string.IsNullOrEmpty(historyEpisode.historyEpisode.SonarrSeasonNumber)){
selected.Season = historyEpisode.historyEpisode.SonarrSeasonNumber;
}
}
}
if (!string.IsNullOrEmpty(historyEpisode.downloadDirPath)){
selected.DownloadPath = historyEpisode.downloadDirPath;
}
}
Queue.Add(crunchyEpMeta);
}
Queue.Add(selected);
Console.WriteLine("Added Episode to Queue");
MessageBus.Current.SendMessage(new ToastMessage($"Added episode to the queue", ToastType.Information, 1));
if (selected.Data.Count < dubLang.Count){
Console.WriteLine("Added Episode to Queue but couldn't find all selected dubs");
MessageBus.Current.SendMessage(new ToastMessage($"Added episode to the queue but couldn't find all selected dubs", ToastType.Warning, 2));
} else{
Console.WriteLine("Added Episode to Queue");
MessageBus.Current.SendMessage(new ToastMessage($"Added episode to the queue", ToastType.Information, 1));
}
} else{
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));
}
}
}
@ -324,17 +338,23 @@ public class Crunchyroll{
foreach (var crunchyEpMeta in selected.Values.ToList()){
if (crunchyEpMeta.Data?.First().Playback != null){
if (CrunOptions.SonarrProperties is{ SonarrEnabled: true, UseSonarrNumbering: true }){
var historyEpisode = CrHistory.GetHistoryEpisode(crunchyEpMeta.ShowId, crunchyEpMeta.SeasonId, crunchyEpMeta.Data.First().MediaId);
if (historyEpisode != null){
if (!string.IsNullOrEmpty(historyEpisode.SonarrEpisodeNumber)){
crunchyEpMeta.EpisodeNumber = historyEpisode.SonarrEpisodeNumber;
}
if (CrunOptions.History){
var historyEpisode = CrHistory.GetHistoryEpisodeWithDownloadDir(crunchyEpMeta.ShowId, crunchyEpMeta.SeasonId, crunchyEpMeta.Data.First().MediaId);
if (CrunOptions.SonarrProperties is{ SonarrEnabled: true, UseSonarrNumbering: true }){
if (historyEpisode.historyEpisode != null){
if (!string.IsNullOrEmpty(historyEpisode.historyEpisode.SonarrEpisodeNumber)){
crunchyEpMeta.EpisodeNumber = historyEpisode.historyEpisode.SonarrEpisodeNumber;
}
if (!string.IsNullOrEmpty(historyEpisode.SonarrSeasonNumber)){
crunchyEpMeta.Season = historyEpisode.SonarrSeasonNumber;
if (!string.IsNullOrEmpty(historyEpisode.historyEpisode.SonarrSeasonNumber)){
crunchyEpMeta.Season = historyEpisode.historyEpisode.SonarrSeasonNumber;
}
}
}
if (!string.IsNullOrEmpty(historyEpisode.downloadDirPath)){
crunchyEpMeta.DownloadPath = historyEpisode.downloadDirPath;
}
}
Queue.Add(crunchyEpMeta);
@ -596,7 +616,7 @@ public class Crunchyroll{
return new DownloadResponse{
Data = files,
Error = true,
FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(CfgManager.PathVIDEOS_DIR, fileName)) : "./unknown",
FileName = "./unknown",
ErrorText = "Video Data not found"
};
}
@ -604,11 +624,18 @@ public class Crunchyroll{
bool dlFailed = false;
bool dlVideoOnce = false;
string fileDir = CfgManager.PathVIDEOS_DIR;
if (data.Data != null){
foreach (CrunchyEpMetaData epMeta in data.Data){
Console.WriteLine($"Requesting: [{epMeta.MediaId}] {mediaName}");
fileDir = !string.IsNullOrEmpty(data.DownloadPath) ? data.DownloadPath : !string.IsNullOrEmpty(options.DownloadDirPath) ? options.DownloadDirPath : CfgManager.PathVIDEOS_DIR;
if (!Helpers.IsValidPath(fileDir)){
fileDir = CfgManager.PathVIDEOS_DIR;
}
string currentMediaId = (epMeta.MediaId.Contains(':') ? epMeta.MediaId.Split(':')[1] : epMeta.MediaId);
await CrAuth.RefreshToken(true);
@ -964,12 +991,13 @@ public class Crunchyroll{
Console.WriteLine($"\tServer: {selectedServer}");
Console.WriteLine("Stream URL:" + chosenVideoSegments.segments[0].uri.Split(new[]{ ",.urlset" }, StringSplitOptions.None)[0]);
fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray());
string outFile = Path.Combine(FileNameManager.ParseFileName(options.FileName + "." + (epMeta.Lang?.Name ?? lang.Value.Name), variables, options.Numbers, options.Override).ToArray());
string tempFile = Path.Combine(FileNameManager.ParseFileName($"temp-{(currentVersion.Guid != null ? currentVersion.Guid : currentMediaId)}", variables, options.Numbers, options.Override)
.ToArray());
string tempTsFile = Path.IsPathRooted(tempFile) ? tempFile : Path.Combine(CfgManager.PathVIDEOS_DIR, tempFile);
string tempTsFile = Path.IsPathRooted(tempFile) ? tempFile : Path.Combine(fileDir, tempFile);
bool audioDownloaded = false, videoDownloaded = false;
@ -978,7 +1006,7 @@ public class Crunchyroll{
} else if (options.Novids){
Console.WriteLine("Skipping video download...");
} else{
var videoDownloadResult = await DownloadVideo(chosenVideoSegments, options, outFile, tsFile, tempTsFile, data);
var videoDownloadResult = await DownloadVideo(chosenVideoSegments, options, outFile, tsFile, tempTsFile, data, fileDir);
tsFile = videoDownloadResult.tsFile;
@ -993,7 +1021,7 @@ public class Crunchyroll{
if (chosenAudioSegments.segments.Count > 0 && !options.Noaudio && !dlFailed){
var audioDownloadResult = await DownloadAudio(chosenAudioSegments, options, outFile, tsFile, tempTsFile, data);
var audioDownloadResult = await DownloadAudio(chosenAudioSegments, options, outFile, tsFile, tempTsFile, data, fileDir);
tsFile = audioDownloadResult.tsFile;
@ -1011,7 +1039,7 @@ public class Crunchyroll{
return new DownloadResponse{
Data = files,
Error = dlFailed,
FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(CfgManager.PathVIDEOS_DIR, fileName)) : "./unknown",
FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(fileDir, fileName)) : "./unknown",
ErrorText = ""
};
}
@ -1029,7 +1057,7 @@ public class Crunchyroll{
var assetIdRegexMatch = Regex.Match(chosenVideoSegments.segments[0].uri, @"/assets/(?:p/)?([^_,]+)");
var assetId = assetIdRegexMatch.Success ? assetIdRegexMatch.Groups[1].Value : null;
var sessionId = Helpers.GenerateSessionId();
Console.WriteLine("Decryption Needed, attempting to decrypt");
if (!_widevine.canDecrypt){
@ -1037,7 +1065,7 @@ public class Crunchyroll{
return new DownloadResponse{
Data = files,
Error = dlFailed,
FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(CfgManager.PathVIDEOS_DIR, fileName)) : "./unknown",
FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(fileDir, fileName)) : "./unknown",
ErrorText = "Decryption Needed but couldn't find CDM files"
};
}
@ -1065,16 +1093,16 @@ public class Crunchyroll{
return new DownloadResponse{
Data = files,
Error = dlFailed,
FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(CfgManager.PathVIDEOS_DIR, fileName)) : "./unknown",
FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(fileDir, fileName)) : "./unknown",
ErrorText = "DRM Authentication failed"
};
}
DrmAuthData authData = Helpers.Deserialize<DrmAuthData>(decRequestResponse.ResponseContent, SettingsJsonSerializerSettings) ?? new DrmAuthData();
Dictionary<string, string> authDataDict = new Dictionary<string, string>
{ { "dt-custom-data", authData.CustomData ?? string.Empty },{ "x-dt-auth-token", authData.Token ?? string.Empty } };
var encryptionKeys = await _widevine.getKeys(chosenVideoSegments.pssh, "https://lic.drmtoday.com/license-proxy-widevine/cenc/", authDataDict);
if (encryptionKeys.Count == 0){
@ -1083,11 +1111,11 @@ public class Crunchyroll{
return new DownloadResponse{
Data = files,
Error = dlFailed,
FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(CfgManager.PathVIDEOS_DIR, fileName)) : "./unknown",
FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(fileDir, fileName)) : "./unknown",
ErrorText = "Couldn't get DRM encryption keys"
};
}
if (Path.Exists(CfgManager.PathMP4Decrypt)){
var keyId = BitConverter.ToString(encryptionKeys[0].KeyID).Replace("-", "").ToLower();
@ -1249,7 +1277,7 @@ public class Crunchyroll{
if (Path.IsPathRooted(outFile)){
tsFile = outFile;
} else{
tsFile = Path.Combine(CfgManager.PathVIDEOS_DIR, outFile);
tsFile = Path.Combine(fileDir, outFile);
}
// Check if the path is absolute
@ -1259,7 +1287,7 @@ public class Crunchyroll{
string[] directories = Path.GetDirectoryName(outFile)?.Split(Path.DirectorySeparatorChar) ?? Array.Empty<string>();
// Initialize the cumulative path based on whether the original path is absolute or not
string cumulativePath = isAbsolute ? "" : CfgManager.PathVIDEOS_DIR;
string cumulativePath = isAbsolute ? "" : fileDir;
for (int i = 0; i < directories.Length; i++){
// Build the path incrementally
cumulativePath = Path.Combine(cumulativePath, directories[i]);
@ -1295,7 +1323,7 @@ public class Crunchyroll{
}
if (!options.SkipSubs && options.DlSubs.IndexOf("none") == -1){
await DownloadSubtitles(options, pbData, audDub, fileName, files);
await DownloadSubtitles(options, pbData, audDub, fileName, files, fileDir);
} else{
Console.WriteLine("Subtitles downloading skipped!");
}
@ -1311,12 +1339,12 @@ public class Crunchyroll{
return new DownloadResponse{
Data = files,
Error = dlFailed,
FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(CfgManager.PathVIDEOS_DIR, fileName)) : "./unknown",
FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(fileDir, fileName)) : "./unknown",
ErrorText = ""
};
}
private static async Task DownloadSubtitles(CrDownloadOptions options, PlaybackData pbData, string audDub, string fileName, List<DownloadedMedia> files){
private static async Task DownloadSubtitles(CrDownloadOptions options, PlaybackData pbData, string audDub, string fileName, List<DownloadedMedia> files, string fileDir){
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>();
@ -1354,26 +1382,9 @@ public class Crunchyroll{
var isSigns = langItem.Code == audDub && !subsItem.isCC;
var isCc = subsItem.isCC;
sxData.File = Languages.SubsFile(fileName, index + "", langItem, isCc, options.CcTag, isSigns, subsItem.format);
sxData.Path = Path.Combine(CfgManager.PathVIDEOS_DIR, sxData.File);
sxData.Path = Path.Combine(fileDir, sxData.File);
// Check if the path is absolute
bool isAbsolute = Path.IsPathRooted(sxData.Path);
// Get all directory parts of the path except the last segment (assuming it's a file)
string[] directories = Path.GetDirectoryName(sxData.Path)?.Split(Path.DirectorySeparatorChar) ?? Array.Empty<string>();
// Initialize the cumulative path based on whether the original path is absolute or not
string cumulativePath = isAbsolute ? "" : CfgManager.PathVIDEOS_DIR;
for (int i = 0; i < directories.Length; i++){
// Build the path incrementally
cumulativePath = Path.Combine(cumulativePath, directories[i]);
// Check if the directory exists and create it if it does not
if (!Directory.Exists(cumulativePath)){
Directory.CreateDirectory(cumulativePath);
Console.WriteLine($"Created directory: {cumulativePath}");
}
}
Helpers.EnsureDirectoriesExist(sxData.Path);
// Check if any file matches the specified conditions
if (files.Any(a => a.Type == DownloadMediaType.Subtitle &&
@ -1437,7 +1448,8 @@ public class Crunchyroll{
}
}
private async Task<(bool Ok, PartsData Parts, string tsFile)> DownloadVideo(VideoItem chosenVideoSegments, CrDownloadOptions options, string outFile, string tsFile, string tempTsFile, CrunchyEpMeta data){
private async Task<(bool Ok, PartsData Parts, string tsFile)> DownloadVideo(VideoItem chosenVideoSegments, CrDownloadOptions options, string outFile, string tsFile, string tempTsFile, CrunchyEpMeta data,
string fileDir){
// Prepare for video download
int totalParts = chosenVideoSegments.segments.Count;
int mathParts = (int)Math.Ceiling((double)totalParts / options.Partsize);
@ -1447,28 +1459,10 @@ public class Crunchyroll{
if (Path.IsPathRooted(outFile)){
tsFile = outFile;
} else{
tsFile = Path.Combine(CfgManager.PathVIDEOS_DIR, outFile);
tsFile = Path.Combine(fileDir, outFile);
}
// var split = outFile.Split(Path.DirectorySeparatorChar).AsSpan().Slice(0, -1).ToArray();
// Check if the path is absolute
bool isAbsolute = Path.IsPathRooted(outFile);
// Get all directory parts of the path except the last segment (assuming it's a file)
string[] directories = Path.GetDirectoryName(outFile)?.Split(Path.DirectorySeparatorChar) ?? Array.Empty<string>();
// Initialize the cumulative path based on whether the original path is absolute or not
string cumulativePath = isAbsolute ? "" : CfgManager.PathVIDEOS_DIR;
for (int i = 0; i < directories.Length; i++){
// Build the path incrementally
cumulativePath = Path.Combine(cumulativePath, directories[i]);
// Check if the directory exists and create it if it does not
if (!Directory.Exists(cumulativePath)){
Directory.CreateDirectory(cumulativePath);
Console.WriteLine($"Created directory: {cumulativePath}");
}
}
Helpers.EnsureDirectoriesExist(outFile);
M3U8Json videoJson = new M3U8Json{
Segments = chosenVideoSegments.segments.Cast<dynamic>().ToList()
@ -1489,7 +1483,8 @@ public class Crunchyroll{
return (videoDownloadResult.Ok, videoDownloadResult.Parts, tsFile);
}
private async Task<(bool Ok, PartsData Parts, string tsFile)> DownloadAudio(AudioItem chosenAudioSegments, CrDownloadOptions options, string outFile, string tsFile, string tempTsFile, CrunchyEpMeta data){
private async Task<(bool Ok, PartsData Parts, string tsFile)> DownloadAudio(AudioItem chosenAudioSegments, CrDownloadOptions options, string outFile, string tsFile, string tempTsFile, CrunchyEpMeta data,
string fileDir){
// Prepare for audio download
int totalParts = chosenAudioSegments.segments.Count;
int mathParts = (int)Math.Ceiling((double)totalParts / options.Partsize);
@ -1499,7 +1494,7 @@ public class Crunchyroll{
if (Path.IsPathRooted(outFile)){
tsFile = outFile;
} else{
tsFile = Path.Combine(CfgManager.PathVIDEOS_DIR, outFile);
tsFile = Path.Combine(fileDir, outFile);
}
// Check if the path is absolute
@ -1509,7 +1504,7 @@ public class Crunchyroll{
string[] directories = Path.GetDirectoryName(outFile)?.Split(Path.DirectorySeparatorChar) ?? Array.Empty<string>();
// Initialize the cumulative path based on whether the original path is absolute or not
string cumulativePath = isAbsolute ? "" : CfgManager.PathVIDEOS_DIR;
string cumulativePath = isAbsolute ? "" : fileDir;
for (int i = 0; i < directories.Length; i++){
// Build the path incrementally
cumulativePath = Path.Combine(cumulativePath, directories[i]);

View File

@ -79,7 +79,7 @@ public class History(){
MessageBus.Current.SendMessage(new ToastMessage($"Couldn't update download History", ToastType.Warning, 1));
}
public HistoryEpisode? GetHistoryEpisode(string? seriesId, string? seasonId, string episodeId){
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
@ -90,7 +90,6 @@ public class History(){
var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == episodeId);
if (historyEpisode != null){
return historyEpisode;
}
}
@ -99,21 +98,45 @@ public class History(){
return null;
}
public (HistoryEpisode? historyEpisode, string downloadDirPath) GetHistoryEpisodeWithDownloadDir(string? seriesId, string? seasonId, string episodeId){
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
var downloadDirPath = "";
if (historySeries != null){
var historySeason = historySeries.Seasons.Find(s => s.SeasonId == seasonId);
if (!string.IsNullOrEmpty(historySeries.SeriesDownloadPath)){
downloadDirPath = historySeries.SeriesDownloadPath;
}
if (historySeason != null){
var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == episodeId);
if (!string.IsNullOrEmpty(historySeason.SeasonDownloadPath)){
downloadDirPath = historySeason.SeasonDownloadPath;
}
if (historyEpisode != null){
return (historyEpisode, downloadDirPath);
}
}
}
return (null, downloadDirPath);
}
public async Task UpdateWithEpisode(CrunchyEpisode episodeParam){
var episode = episodeParam;
if (episode.Versions != null){
var version = episode.Versions.Find(a => a.Original);
if (version.AudioLocale != episode.AudioLocale){
var episodeById = await crunInstance.CrEpisode.ParseEpisodeById(version.Guid, "");
if (episodeById?.Data != null){
if (episodeById.Value.Total != 1){
MessageBus.Current.SendMessage(new ToastMessage($"Couldn't update download History", ToastType.Warning, 1));
return;
}
episode = episodeById.Value.Data.First();
var crEpisode = await crunInstance.CrEpisode.ParseEpisodeById(version.Guid, "");
if (crEpisode != null){
episode = crEpisode.Value;
} else{
MessageBus.Current.SendMessage(new ToastMessage($"Couldn't update download History", ToastType.Warning, 1));
return;
}
}
}
@ -182,7 +205,7 @@ public class History(){
}
MatchHistorySeriesWithSonarr(false);
await MatchHistoryEpisodesWithSonarr(false,historySeries);
await MatchHistoryEpisodesWithSonarr(false, historySeries);
UpdateHistoryFile();
}
@ -262,6 +285,7 @@ public class History(){
historySeries.UpdateNewEpisodes();
}
var sortedList = crunInstance.HistoryList.OrderBy(item => item.SeriesTitle).ToList();
crunInstance.HistoryList.Clear();
foreach (var item in sortedList){
@ -269,7 +293,7 @@ public class History(){
}
MatchHistorySeriesWithSonarr(false);
await MatchHistoryEpisodesWithSonarr(false,historySeries);
await MatchHistoryEpisodesWithSonarr(false, historySeries);
UpdateHistoryFile();
}
}
@ -341,11 +365,10 @@ public class History(){
}
public void MatchHistorySeriesWithSonarr(bool updateAll){
if (crunInstance.CrunOptions.SonarrProperties is{ SonarrEnabled: false }){
return;
}
foreach (var historySeries in crunInstance.HistoryList){
if (updateAll || string.IsNullOrEmpty(historySeries.SonarrSeriesId)){
var sonarrSeries = FindClosestMatch(historySeries.SeriesTitle);
@ -362,7 +385,7 @@ public class History(){
if (crunInstance.CrunOptions.SonarrProperties is{ SonarrEnabled: false }){
return;
}
if (!string.IsNullOrEmpty(historySeries.SonarrSeriesId)){
var episodes = await SonarrClient.Instance.GetEpisodes(int.Parse(historySeries.SonarrSeriesId));
@ -551,6 +574,9 @@ public class HistorySeries : INotifyPropertyChanged{
[JsonProperty("series_season_list")]
public required List<HistorySeason> Seasons{ get; set; }
[JsonProperty("series_download_path")]
public string? SeriesDownloadPath{ get; set; }
public event PropertyChangedEventHandler? PropertyChanged;
[JsonIgnore]
@ -636,7 +662,7 @@ public class HistorySeries : INotifyPropertyChanged{
await Crunchyroll.Instance.CrHistory.UpdateSeries(SeriesId, seasonId);
FetchingData = false;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData)));
Crunchyroll.Instance.CrHistory.MatchHistoryEpisodesWithSonarr(false,this);
Crunchyroll.Instance.CrHistory.MatchHistoryEpisodesWithSonarr(false, this);
}
}
@ -662,6 +688,9 @@ public class HistorySeason : INotifyPropertyChanged{
[JsonProperty("season_episode_list")]
public required List<HistoryEpisode> EpisodesList{ get; set; }
[JsonProperty("series_download_path")]
public string? SeasonDownloadPath{ get; set; }
public event PropertyChangedEventHandler? PropertyChanged;
public void UpdateDownloaded(string? EpisodeId){

View File

@ -27,6 +27,64 @@ public class Helpers{
}
}
public static void EnsureDirectoriesExist(string path){
// Check if the path is absolute
bool isAbsolute = Path.IsPathRooted(path);
// Get all directory parts of the path except the last segment (assuming it's a file)
string directoryPath = Path.GetDirectoryName(path);
if (string.IsNullOrEmpty(directoryPath)){
Console.WriteLine("The provided path does not contain any directory information.");
return;
}
// Initialize the cumulative path based on whether the original path is absolute or not
string cumulativePath = isAbsolute ? Path.GetPathRoot(directoryPath) : Environment.CurrentDirectory;
// Get all directory parts
string[] directories = directoryPath.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
// Start the loop from the correct initial index
int startIndex = isAbsolute && directories.Length > 0 && string.IsNullOrEmpty(directories[0]) ? 2 : 0;
for (int i = startIndex; i < directories.Length; i++){
// Skip empty parts (which can occur with UNC paths)
if (string.IsNullOrEmpty(directories[i])){
continue;
}
// Build the path incrementally
cumulativePath = Path.Combine(cumulativePath, directories[i]);
// Check if the directory exists and create it if it does not
if (!Directory.Exists(cumulativePath)){
Directory.CreateDirectory(cumulativePath);
Console.WriteLine($"Created directory: {cumulativePath}");
}
}
}
public static bool IsValidPath(string path){
char[] invalidChars = Path.GetInvalidPathChars();
if (string.IsNullOrWhiteSpace(path)){
return false;
}
if (path.Any(ch => invalidChars.Contains(ch))){
return false;
}
try{
// Use Path.GetFullPath to ensure that the path can be fully qualified
string fullPath = Path.GetFullPath(path);
return true;
} catch (Exception){
return false;
}
}
public static Locale ConvertStringToLocale(string? value){
foreach (Locale locale in Enum.GetValues(typeof(Locale))){
var type = typeof(Locale);
@ -121,7 +179,7 @@ public class Helpers{
}
} catch (Exception ex){
Console.Error.WriteLine($"An error occurred: {ex.Message}");
return (IsOk: false, ErrorCode: -1);
return (IsOk: false, ErrorCode: -1);
}
}

View File

@ -125,4 +125,7 @@ public class CrDownloadOptions{
[YamlMember(Alias = "stream_endpoint", ApplyNamingConventions = false)]
public string? StreamEndpoint{ get; set; }
[YamlMember(Alias = "download_dir_path", ApplyNamingConventions = false)]
public string? DownloadDirPath{ get; set; }
}

View File

@ -50,7 +50,7 @@ public struct CrunchyEpisode{
[JsonProperty("availability_starts")]
public DateTime? AvailabilityStarts{ get; set; }
public Images? Images{ get; set; }
public Images? Images{ get; set; }
[JsonProperty("season_id")]
public string SeasonId{ get; set; }
@ -84,7 +84,7 @@ public struct CrunchyEpisode{
public string Id{ get; set; }
[JsonProperty("media_type")]
public MediaType? MediaType{ get; set; }
public MediaType? 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 ChannelId? ChannelId{ get; set; }
public string? Episode{ get; set; }
@ -143,10 +143,10 @@ public struct CrunchyEpisode{
public int? EpisodeNumber{ get; set; }
[JsonProperty("season_tags")]
public List<object> SeasonTags{ get; set; }
public List<object> SeasonTags{ get; set; }
[JsonProperty("maturity_ratings")]
public List<string> MaturityRatings{ get; set; }
public List<string> MaturityRatings{ get; set; }
[JsonProperty("streams_link")]
public string? StreamsLink{ get; set; }
@ -247,6 +247,9 @@ public class CrunchyEpMeta{
public List<string>? SelectedDubs{ get; set; }
public List<string>? AvailableSubs{ get; set; }
public string? DownloadPath{ get; set; }
}
public class DownloadProgress{
@ -267,4 +270,10 @@ public struct CrunchyEpMetaData{
public List<EpisodeVersion>? Versions{ get; set; }
public bool IsSubbed{ get; set; }
public bool IsDubbed{ get; set; }
}
public struct CrunchyRollEpisodeData{
public string Key{ get; set; }
public EpisodeAndLanguage EpisodeAndLanguages{ get; set; }
}

View File

@ -1,6 +1,7 @@
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CRD.Downloader;
@ -21,6 +22,8 @@ public partial class SeriesPageViewModel : ViewModelBase{
[ObservableProperty]
public static bool _sonarrAvailable;
private IStorageProvider _storageProvider;
public SeriesPageViewModel(){
_selectedSeries = Crunchyroll.Instance.SelectedSeries;
@ -37,6 +40,38 @@ public partial class SeriesPageViewModel : ViewModelBase{
}
}
[RelayCommand]
public async Task OpenFolderDialogAsync(HistorySeason? season){
if (_storageProvider == null){
Console.Error.WriteLine("StorageProvider must be set before using the dialog.");
throw new InvalidOperationException("StorageProvider must be set before using the dialog.");
}
var result = await _storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions{
Title = "Select Folder"
});
if (result.Count > 0){
var selectedFolder = result[0];
// Do something with the selected folder path
Console.WriteLine($"Selected folder: {selectedFolder.Path.LocalPath}");
if (season != null){
season.SeasonDownloadPath = selectedFolder.Path.LocalPath;
} else{
SelectedSeries.SeriesDownloadPath = selectedFolder.Path.LocalPath;
}
CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList);
}
}
public void SetStorageProvider(IStorageProvider storageProvider){
_storageProvider = storageProvider ?? throw new ArgumentNullException(nameof(storageProvider));
}
[RelayCommand]
public void OpenSonarrPage(){

View File

@ -6,9 +6,11 @@ using System.ComponentModel;
using System.Linq;
using System.Net.Mime;
using System.Reflection;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Platform.Storage;
using Avalonia.Styling;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@ -32,13 +34,13 @@ public partial class SettingsPageViewModel : ViewModelBase{
[ObservableProperty]
private bool _downloadChapters = true;
[ObservableProperty]
private bool _addScaledBorderAndShadow = false;
[ObservableProperty]
private ComboBoxItem _selectedScaledBorderAndShadow;
public ObservableCollection<ComboBoxItem> ScaledBorderAndShadow{ get; } = new(){
new ComboBoxItem(){ Content = "ScaledBorderAndShadow: yes" },
new ComboBoxItem(){ Content = "ScaledBorderAndShadow: no" },
@ -46,7 +48,7 @@ public partial class SettingsPageViewModel : ViewModelBase{
[ObservableProperty]
private bool _muxToMp4;
[ObservableProperty]
private bool _downloadVideoForEveryDub;
@ -55,7 +57,7 @@ public partial class SettingsPageViewModel : ViewModelBase{
[ObservableProperty]
private bool _history;
[ObservableProperty]
private int _leadingNumbers;
@ -91,7 +93,7 @@ public partial class SettingsPageViewModel : ViewModelBase{
[ObservableProperty]
private ComboBoxItem _selectedStreamEndpoint;
[ObservableProperty]
private ComboBoxItem _selectedDefaultDubLang;
@ -232,7 +234,7 @@ public partial class SettingsPageViewModel : ViewModelBase{
new ListBoxItem(){ Content = "all" },
new ListBoxItem(){ Content = "none" },
};
public ObservableCollection<ComboBoxItem> StreamEndpoints{ get; } = new(){
new ComboBoxItem(){ Content = "web/firefox" },
new ComboBoxItem(){ Content = "console/switch" },
@ -248,16 +250,20 @@ public partial class SettingsPageViewModel : ViewModelBase{
new ComboBoxItem(){ Content = "android/phone" },
new ComboBoxItem(){ Content = "tv/samsung" },
};
[ObservableProperty]
private string _downloadDirPath;
private readonly FluentAvaloniaTheme _faTheme;
private bool settingsLoaded;
private IStorageProvider _storageProvider;
public SettingsPageViewModel(){
var version = Assembly.GetExecutingAssembly().GetName().Version;
_currentVersion = $"{version?.Major}.{version?.Minor}.{version?.Build}";
_faTheme = App.Current.Styles[0] as FluentAvaloniaTheme;
foreach (var languageItem in Languages.languages){
@ -270,6 +276,8 @@ public partial class SettingsPageViewModel : ViewModelBase{
CrDownloadOptions options = Crunchyroll.Instance.CrunOptions;
DownloadDirPath = string.IsNullOrEmpty(options.DownloadDirPath) ? CfgManager.PathVIDEOS_DIR : options.DownloadDirPath;
ComboBoxItem? hsLang = HardSubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.Hslang) ?? null;
SelectedHSLang = hsLang ?? HardSubLangList[0];
@ -281,7 +289,7 @@ public partial class SettingsPageViewModel : ViewModelBase{
ComboBoxItem? streamEndpoint = StreamEndpoints.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpoint ?? "")) ?? null;
SelectedStreamEndpoint = streamEndpoint ?? StreamEndpoints[0];
var softSubLang = SubLangList.Where(a => options.DlSubs.Contains(a.Content)).ToList();
SelectedSubLang.Clear();
@ -310,7 +318,7 @@ public partial class SettingsPageViewModel : ViewModelBase{
AddScaledBorderAndShadow = options.SubsAddScaledBorder is ScaledBorderAndShadowSelection.ScaledBorderAndShadowNo or ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes;
SelectedScaledBorderAndShadow = GetScaledBorderAndShadowFromOptions(options);
DownloadVideo = !options.Novids;
DownloadAudio = !options.Noaudio;
DownloadVideoForEveryDub = !options.DlVideoOnce;
@ -391,8 +399,8 @@ public partial class SettingsPageViewModel : ViewModelBase{
Crunchyroll.Instance.CrunOptions.DefaultAudio = SelectedDefaultDubLang.Content + "";
Crunchyroll.Instance.CrunOptions.DefaultSub = SelectedDefaultSubLang.Content + "";
Crunchyroll.Instance.CrunOptions.StreamEndpoint = SelectedStreamEndpoint.Content + "";
List<string> dubLangs = new List<string>();
@ -404,7 +412,7 @@ public partial class SettingsPageViewModel : ViewModelBase{
Crunchyroll.Instance.CrunOptions.SimultaneousDownloads = SimultaneousDownloads;
Crunchyroll.Instance.CrunOptions.QualityAudio = SelectedAudioQuality?.Content + "";
Crunchyroll.Instance.CrunOptions.QualityVideo = SelectedVideoQuality?.Content + "";
Crunchyroll.Instance.CrunOptions.Theme = CurrentAppTheme?.Content + "";
@ -458,11 +466,11 @@ public partial class SettingsPageViewModel : ViewModelBase{
if (SelectedScaledBorderAndShadow.Content + "" == "ScaledBorderAndShadow: yes"){
return ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes;
}
if (SelectedScaledBorderAndShadow.Content + "" == "ScaledBorderAndShadow: no"){
return ScaledBorderAndShadowSelection.ScaledBorderAndShadowNo;
}
return ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes;
}
@ -476,8 +484,8 @@ public partial class SettingsPageViewModel : ViewModelBase{
return ScaledBorderAndShadow[0];
}
}
private void UpdateSubAndDubString(){
if (SelectedSubLang.Count == 0){
SelectedSubs = "none";
@ -524,6 +532,32 @@ public partial class SettingsPageViewModel : ViewModelBase{
RaisePropertyChanged(nameof(FfmpegOptions));
}
[RelayCommand]
public async Task OpenFolderDialogAsync(){
if (_storageProvider == null){
Console.Error.WriteLine("StorageProvider must be set before using the dialog.");
throw new InvalidOperationException("StorageProvider must be set before using the dialog.");
}
var result = await _storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions{
Title = "Select Folder"
});
if (result.Count > 0){
var selectedFolder = result[0];
// Do something with the selected folder path
Console.WriteLine($"Selected folder: {selectedFolder.Path.LocalPath}");
Crunchyroll.Instance.CrunOptions.DownloadDirPath = selectedFolder.Path.LocalPath;
DownloadDirPath = string.IsNullOrEmpty(Crunchyroll.Instance.CrunOptions.DownloadDirPath) ? CfgManager.PathVIDEOS_DIR : Crunchyroll.Instance.CrunOptions.DownloadDirPath;
CfgManager.WriteSettingsToFile();
}
}
public void SetStorageProvider(IStorageProvider storageProvider){
_storageProvider = storageProvider ?? throw new ArgumentNullException(nameof(storageProvider));
}
partial void OnCurrentAppThemeChanged(ComboBoxItem? value){
if (value?.Content?.ToString() == "System"){
_faTheme.PreferSystemTheme = true;
@ -617,7 +651,7 @@ public partial class SettingsPageViewModel : ViewModelBase{
partial void OnSelectedVideoQualityChanged(ComboBoxItem? value){
UpdateSettings();
}
partial void OnHistoryChanged(bool value){
UpdateSettings();
}

View File

@ -58,10 +58,16 @@ public partial class MainWindow : AppWindow{
if (message.Refresh){
navigationStack.Pop();
var viewModel = Activator.CreateInstance(message.ViewModelType);
if (viewModel is SeriesPageViewModel){
((SeriesPageViewModel)viewModel).SetStorageProvider(StorageProvider);
}
navigationStack.Push(viewModel);
nv.Content = viewModel;
} else if (!message.Back && message.ViewModelType != null){
var viewModel = Activator.CreateInstance(message.ViewModelType);
if (viewModel is SeriesPageViewModel){
((SeriesPageViewModel)viewModel).SetStorageProvider(StorageProvider);
}
navigationStack.Push(viewModel);
nv.Content = viewModel;
} else{
@ -119,7 +125,9 @@ public partial class MainWindow : AppWindow{
selectedNavVieItem = selectedItem;
break;
case "Settings":
navView.Content = Activator.CreateInstance(typeof(SettingsPageViewModel));
var viewModel = (SettingsPageViewModel)Activator.CreateInstance(typeof(SettingsPageViewModel));
viewModel.SetStorageProvider(StorageProvider);
navView.Content = viewModel;
selectedNavVieItem = selectedItem;
break;
case "UpdateAvailable":

View File

@ -66,7 +66,19 @@
<StackPanel Orientation="Horizontal">
<Button Command="{Binding UpdateData}" Margin="0 0 5 10">Fetch Series</Button>
<ToggleButton IsChecked="{Binding EditMode}" Margin="0 0 0 10">Edit</ToggleButton>
<ToggleButton IsChecked="{Binding EditMode}" Margin="0 0 5 10">Edit</ToggleButton>
<Button Margin="0 0 5 10" FontStyle="Italic"
VerticalAlignment="Center"
Command="{Binding OpenFolderDialogAsync}">
<ToolTip.Tip>
<TextBlock Text="{Binding SelectedSeries.SeriesDownloadPath}" FontSize="15" />
</ToolTip.Tip>
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Folder" FontSize="18" />
</StackPanel>
</Button>
</StackPanel>
@ -111,10 +123,12 @@
IsVisible="{Binding $parent[ItemsControl].((vm:SeriesPageViewModel)DataContext).SonarrAvailable}">
<controls:ImageIcon IsVisible="{Binding SonarrHasFile}" Source="../Assets/sonarr.png" Width="25"
<controls:ImageIcon IsVisible="{Binding SonarrHasFile}"
Source="../Assets/sonarr.png" Width="25"
Height="25" />
<controls:ImageIcon IsVisible="{Binding !SonarrHasFile}" Source="../Assets/sonarr_inactive.png" Width="25"
<controls:ImageIcon IsVisible="{Binding !SonarrHasFile}"
Source="../Assets/sonarr_inactive.png" Width="25"
Height="25" />
@ -171,6 +185,19 @@
<controls:SymbolIcon Symbol="Refresh" FontSize="18" />
</StackPanel>
</Button>
<Button Margin="10 0 0 0" FontStyle="Italic"
VerticalAlignment="Center"
Command="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).OpenFolderDialogAsync}"
CommandParameter="{Binding .}">
<ToolTip.Tip>
<TextBlock Text="{Binding SeasonDownloadPath}" FontSize="15" />
</ToolTip.Tip>
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Folder" FontSize="18" />
</StackPanel>
</Button>
<Button Margin="10 0 0 0" FontStyle="Italic"
VerticalAlignment="Center"
Command="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).RemoveSeason}"

View File

@ -145,6 +145,25 @@
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Download Folder">
<controls:SettingsExpanderItem.Footer>
<StackPanel Orientation="Horizontal">
<TextBlock FontSize="15" Opacity="0.8" TextWrapping="NoWrap" Text="{Binding DownloadDirPath, Mode=OneWay}" TextAlignment="Center" VerticalAlignment="Center" HorizontalAlignment="Center" />
<Button Margin="10 0 0 0" FontStyle="Italic"
VerticalAlignment="Center"
Command="{Binding OpenFolderDialogAsync}">
<ToolTip.Tip>
<TextBlock Text="Set Download Directory" FontSize="15" />
</ToolTip.Tip>
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Folder" FontSize="18" />
</StackPanel>
</Button>
</StackPanel>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Simultaneous Downloads">
<controls:SettingsExpanderItem.Footer>
<controls:NumberBox Minimum="0" Maximum="5"

View File

@ -19,5 +19,4 @@ public partial class SettingsPageView : UserControl{
Crunchyroll.Instance.RefreshSonarr();
}
}
}