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;
@ -43,148 +43,122 @@ 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){
CrunchyRollEpisodeData episode = new CrunchyRollEpisodeData();
if (crunInstance.CrunOptions.History){
await crunInstance.CrHistory.UpdateWithEpisode(episode);
await crunInstance.CrHistory.UpdateWithEpisode(dlEpisode);
}
// 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 + "")}";
if (!episodes.ContainsKey(episodeKey)){
item = new EpisodeAndLanguage{
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>()
};
episodes[episodeKey] = item;
} else{
item = episodes[episodeKey];
}
if (episode.Versions != null){
foreach (var version in episode.Versions){
if (dlEpisode.Versions != null){
foreach (var version in dlEpisode.Versions){
// Ensure there is only one of the same language
if (item.Langs.All(a => a.CrLocale != version.AudioLocale)){
if (episode.EpisodeAndLanguages.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));
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 (item.Langs.All(a => a.CrLocale != episode.AudioLocale)){
if (episode.EpisodeAndLanguages.Langs.All(a => a.CrLocale != dlEpisode.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));
}
}
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).
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(item.Items[0].Episode)){
newKey = item.Items[0].Episode ?? "SP" + (specialIndex + " " + item.Items[0].Id);
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 + " " + item.Items[0].Id) : epIndex + "")}";
newKey = $"{(isSpecial ? "SP" : 'E')}{(isSpecial ? (specialIndex + " " + episode.EpisodeAndLanguages.Items[0].Id) : episode.EpisodeAndLanguages.Items[0].Episode ?? epIndex + "")}";
}
episodes.Remove(key);
episodes.Add(newKey, item);
episode.Key = newKey;
if (isSpecial){
specialIndex++;
} else{
epIndex++;
}
}
var seasonTitle = episode.EpisodeAndLanguages.Items.FirstOrDefault(a => !Regex.IsMatch(a.SeasonTitle, @"\(\w+ Dub\)")).SeasonTitle
?? Regex.Replace(episode.EpisodeAndLanguages.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd();
var specials = episodes.Where(e => e.Key.StartsWith("SP")).ToList();
var normal = episodes.Where(e => e.Key.StartsWith("E")).ToList();
var title = episode.EpisodeAndLanguages.Items[0].Title;
var seasonNumber = Helpers.ExtractNumberAfterS(episode.EpisodeAndLanguages.Items[0].Identifier) ?? episode.EpisodeAndLanguages.Items[0].SeasonNumber.ToString();
// Combining and sorting episodes with normal first, then specials.
var sortedEpisodes = new Dictionary<string, EpisodeAndLanguage>(normal.Concat(specials));
var languages = episode.EpisodeAndLanguages.Items.Select((a, index) =>
$"{(a.IsPremiumOnly ? "+ " : "")}{episode.EpisodeAndLanguages.Langs.ElementAtOrDefault(index).Name ?? "Unknown"}").ToArray(); //☆
foreach (var kvp in sortedEpisodes){
var key = kvp.Key;
var item = kvp.Value;
Console.WriteLine($"[{episode.Key}] {seasonTitle} - Season {seasonNumber} - {title} [{string.Join(", ", languages)}]");
var seasonTitle = item.Items.FirstOrDefault(a => !Regex.IsMatch(a.SeasonTitle, @"\(\w+ Dub\)")).SeasonTitle
?? Regex.Replace(item.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd();
var title = item.Items[0].Title;
var seasonNumber = Helpers.ExtractNumberAfterS(item.Items[0].Identifier) ?? item.Items[0].SeasonNumber.ToString();
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 < episode.Items.Count; index++){
var item = episode.Items[index];
for (int index = 0; index < episodeP.EpisodeAndLanguages.Items.Count; index++){
var item = episodeP.EpisodeAndLanguages.Items[index];
if (!dubLang.Contains(episode.Langs[index].CrLocale))
if (!dubLang.Contains(episodeP.EpisodeAndLanguages.Langs[index].CrLocale))
continue;
item.HideSeasonTitle = true;
@ -199,15 +173,15 @@ public class CrEpisode(){
item.SeriesTitle = "NO_TITLE";
}
var epNum = key.StartsWith('E') ? key[1..] : key;
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 = 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.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;
@ -224,9 +198,9 @@ public class CrEpisode(){
DownloadSpeed = 0
};
epMeta.AvailableSubs = item.SubtitleLocales;
if (episode.Langs.Count > 0){
if (episodeP.EpisodeAndLanguages.Langs.Count > 0){
epMeta.SelectedDubs = dubLang
.Where(language => episode.Langs.Any(epLang => epLang.CrLocale == language))
.Where(language => episodeP.EpisodeAndLanguages.Langs.Any(epLang => epLang.CrLocale == language))
.ToList();
}
@ -238,22 +212,22 @@ public class CrEpisode(){
}
}
if (ret.TryGetValue(key, out var epMe)){
epMetaData.Lang = episode.Langs[index];
epMe.Data?.Add(epMetaData);
if (retMeta.Data != null){
epMetaData.Lang = episodeP.EpisodeAndLanguages.Langs[index];
retMeta.Data.Add(epMetaData);
} else{
epMetaData.Lang = episode.Langs[index];
epMetaData.Lang = episodeP.EpisodeAndLanguages.Langs[index];
epMeta.Data[0] = epMetaData;
ret.Add(key, epMeta);
retMeta = epMeta;
}
// show ep
item.SeqId = epNum;
}
}
return ret;
return retMeta;
}
}

View File

@ -286,35 +286,49 @@ 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 (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 }){
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 (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;
}
}
}
Queue.Add(crunchyEpMeta);
if (!string.IsNullOrEmpty(historyEpisode.downloadDirPath)){
selected.DownloadPath = historyEpisode.downloadDirPath;
}
}
Queue.Add(selected);
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));
}
}
}
public void AddSeriesToQueue(CrunchySeriesList list, CrunchyMultiDownload data){
@ -324,19 +338,25 @@ public class Crunchyroll{
foreach (var crunchyEpMeta in selected.Values.ToList()){
if (crunchyEpMeta.Data?.First().Playback != null){
if (CrunOptions.History){
var historyEpisode = CrHistory.GetHistoryEpisodeWithDownloadDir(crunchyEpMeta.ShowId, crunchyEpMeta.SeasonId, crunchyEpMeta.Data.First().MediaId);
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 (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);
} else{
failed = true;
@ -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 = ""
};
}
@ -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,7 +1093,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 = "DRM Authentication failed"
};
}
@ -1083,7 +1111,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 = "Couldn't get DRM encryption keys"
};
}
@ -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

@ -90,7 +90,6 @@ public class History(){
var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == episodeId);
if (historyEpisode != null){
return historyEpisode;
}
}
@ -99,6 +98,32 @@ 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;
@ -106,15 +131,13 @@ public class History(){
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){
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;
}
episode = episodeById.Value.Data.First();
}
}
}
@ -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){
@ -341,7 +365,6 @@ public class History(){
}
public void MatchHistorySeriesWithSonarr(bool updateAll){
if (crunInstance.CrunOptions.SonarrProperties is{ SonarrEnabled: false }){
return;
}
@ -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]
@ -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);

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

@ -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;
@ -38,6 +41,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(){
var sonarrProp = Crunchyroll.Instance.CrunOptions.SonarrProperties;

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;
@ -249,15 +251,19 @@ public partial class SettingsPageViewModel : ViewModelBase{
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];
@ -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;

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