diff --git a/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs b/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs index 01854e2..eb5190d 100644 --- a/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs +++ b/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs @@ -98,12 +98,12 @@ public class CrunchyrollManager{ options.Force = "Y"; options.FileName = "${seriesTitle} - S${season}E${episode} [${height}p]"; options.Partsize = 10; - options.DlSubs = new List{ "de-DE" }; + options.DlSubs = new List{ "en-US" }; options.Skipmux = false; options.MkvmergeOptions = new List{ "--no-date", "--disable-track-statistics-tags", "--engage no_variable_data" }; options.FfmpegOptions = new(); options.DefaultAudio = "ja-JP"; - options.DefaultSub = "de-DE"; + options.DefaultSub = "en-US"; options.CcTag = "CC"; options.FsRetryTime = 5; options.Numbers = 2; @@ -223,15 +223,16 @@ public class CrunchyrollManager{ QueueManager.Instance.Queue.Refresh(); + var fileNameAndPath = CrunOptions.DownloadToTempFolder ? Path.Combine(res.TempFolderPath, res.FileName) : Path.Combine(res.FolderPath, res.FileName); if (CrunOptions is{ DlVideoOnce: false, KeepDubsSeperate: true }){ var groupByDub = Helpers.GroupByLanguageWithSubtitles(res.Data); - + var mergers = new List(); foreach (var keyValue in groupByDub){ - await MuxStreams(keyValue.Value, + var result = await MuxStreams(keyValue.Value, new CrunchyMuxOptions{ FfmpegOptions = options.FfmpegOptions, SkipSubMux = options.SkipSubsMux, - Output = res.FileName, + Output = fileNameAndPath, Mp4 = options.Mp4, VideoTitle = res.VideoTitle, Novids = options.Novids, @@ -245,14 +246,23 @@ public class CrunchyrollManager{ KeepAllVideos = true, MuxDescription = options.IncludeVideoDescription }, - res.FileName); + fileNameAndPath); + + if (result is{ merger: not null, isMuxed: true }){ + mergers.Add(result.merger); + } + } + + foreach (var merger in mergers){ + merger.CleanUp(); + await MoveFromTempFolder(merger, data, res.TempFolderPath, res.Data.Where(e => e.Type == DownloadMediaType.Subtitle)); } } else{ - await MuxStreams(res.Data, + var result = await MuxStreams(res.Data, new CrunchyMuxOptions{ FfmpegOptions = options.FfmpegOptions, SkipSubMux = options.SkipSubsMux, - Output = res.FileName, + Output = fileNameAndPath, Mp4 = options.Mp4, VideoTitle = res.VideoTitle, Novids = options.Novids, @@ -266,9 +276,16 @@ public class CrunchyrollManager{ KeepAllVideos = true, MuxDescription = options.IncludeVideoDescription }, - res.FileName); + fileNameAndPath); + + if (result is{ merger: not null, isMuxed: true }){ + result.merger.CleanUp(); + + await MoveFromTempFolder(result.merger, data, res.TempFolderPath, res.Data.Where(e => e.Type == DownloadMediaType.Subtitle)); + } } + data.DownloadProgress = new DownloadProgress(){ IsDownloading = true, Done = true, @@ -296,7 +313,85 @@ public class CrunchyrollManager{ return true; } - private async Task MuxStreams(List data, CrunchyMuxOptions options, string filename){ + #region Temp Files Move + + private async Task MoveFromTempFolder(Merger merger, CrunchyEpMeta data, string tempFolderPath, IEnumerable subtitles){ + if (!CrunOptions.DownloadToTempFolder) return; + + data.DownloadProgress = new DownloadProgress{ + IsDownloading = true, + Done = true, + Percent = 100, + Time = 0, + DownloadSpeed = 0, + Doing = "Moving Files" + }; + + QueueManager.Instance.Queue.Refresh(); + + if (string.IsNullOrEmpty(tempFolderPath) || !Directory.Exists(tempFolderPath)){ + Console.WriteLine("Invalid or non-existent temp folder path."); + return; + } + + // Move the main output file + await MoveFile(merger.options.Output, tempFolderPath, data.DownloadPath); + + // Move the subtitle files + if (CrunOptions.SkipSubsMux){ + foreach (var downloadedMedia in subtitles){ + await MoveFile(downloadedMedia.Path ?? string.Empty, tempFolderPath, data.DownloadPath); + } + } + } + + private async Task MoveFile(string sourcePath, string tempFolderPath, string downloadPath){ + if (string.IsNullOrEmpty(sourcePath) || !File.Exists(sourcePath)){ + Console.Error.WriteLine("Source file does not exist or path is invalid."); + return; + } + + if (!sourcePath.StartsWith(tempFolderPath)){ + Console.Error.WriteLine("Source file is not located in the temp folder."); + return; + } + + try{ + var fileName = sourcePath[tempFolderPath.Length..].TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + var destinationFolder = !string.IsNullOrEmpty(downloadPath) + ? downloadPath + : !string.IsNullOrEmpty(CrunOptions.DownloadDirPath) + ? CrunOptions.DownloadDirPath + : CfgManager.PathVIDEOS_DIR; + + var destinationPath = Path.Combine(destinationFolder, fileName); + + string destinationDirectory = Path.GetDirectoryName(destinationPath); + if (string.IsNullOrEmpty(destinationDirectory)){ + Console.WriteLine("Invalid destination directory path."); + return; + } + + await Task.Run(() => { + if (!Directory.Exists(destinationDirectory)){ + Directory.CreateDirectory(destinationDirectory); + } + }); + + await Task.Run(() => File.Move(sourcePath, destinationPath)); + Console.WriteLine($"File moved to {destinationPath}"); + } catch (IOException ex){ + Console.Error.WriteLine($"An error occurred while moving the file: {ex.Message}"); + } catch (UnauthorizedAccessException ex){ + Console.Error.WriteLine($"Access denied while moving the file: {ex.Message}"); + } catch (Exception ex){ + Console.Error.WriteLine($"An unexpected error occurred: {ex.Message}"); + } + } + + #endregion + + private async Task<(Merger? merger, bool isMuxed)> MuxStreams(List data, CrunchyMuxOptions options, string filename){ var muxToMp3 = false; if (options.Novids == true || data.FindAll(a => a.Type == DownloadMediaType.Video).Count == 0){ @@ -305,7 +400,7 @@ public class CrunchyrollManager{ muxToMp3 = true; } else{ Console.WriteLine("Skip muxing since no videos are downloaded"); - return; + return (null, false); } } @@ -333,7 +428,8 @@ public class CrunchyrollManager{ bool muxDesc = false; if (options.MuxDescription){ - if (File.Exists($"{filename}.xml")){ + var descriptionPath = data.Where(a => a.Type == DownloadMediaType.Description).First().Path; + if (File.Exists(descriptionPath)){ muxDesc = true; } else{ Console.Error.WriteLine("No xml description file found to mux description"); @@ -409,9 +505,7 @@ public class CrunchyrollManager{ isMuxed = true; } - if (isMuxed && options.NoCleanup == false){ - merger.CleanUp(); - } + return (merger, isMuxed); } private async Task DownloadMediaList(CrunchyEpMeta data, CrDownloadOptions options){ @@ -506,13 +600,18 @@ public class CrunchyrollManager{ 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; + string currentMediaId = (epMeta.MediaId.Contains(':') ? epMeta.MediaId.Split(':')[1] : epMeta.MediaId); + + fileDir = CrunOptions.DownloadToTempFolder ? !string.IsNullOrEmpty(CrunOptions.DownloadTempDirPath) + ? Path.Combine(CrunOptions.DownloadTempDirPath, Helpers.GetValidFolderName(currentMediaId)) + : Path.Combine(CfgManager.PathTEMP_DIR, Helpers.GetValidFolderName(currentMediaId)) : + !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); @@ -607,7 +706,7 @@ public class CrunchyrollManager{ variables.Add(new Variable("episode", (double.TryParse(data.EpisodeNumber, NumberStyles.Any, CultureInfo.InvariantCulture, out double episodeNum) ? (object)Math.Round(episodeNum, 1) : data.AbsolutEpisodeNumberE) ?? string.Empty, false)); variables.Add(new Variable("seriesTitle", data.SeriesTitle ?? string.Empty, true)); - variables.Add(new Variable("showTitle", data.SeasonTitle ?? string.Empty, true)); + variables.Add(new Variable("seasonTitle", data.SeasonTitle ?? string.Empty, true)); variables.Add(new Variable("season", !string.IsNullOrEmpty(data.Season) ? Math.Round(double.Parse(data.Season, CultureInfo.InvariantCulture), 1) : 0, false)); if (pbStreams?.Keys != null){ @@ -1275,18 +1374,34 @@ public class CrunchyrollManager{ Type = DownloadMediaType.Description, Path = fullPath, }); + } else{ + if (files.All(e => e.Type != DownloadMediaType.Description)){ + files.Add(new DownloadedMedia{ + Type = DownloadMediaType.Description, + Path = fullPath, + }); + } } - Console.WriteLine($"{fileName} has been created with the description."); + Console.WriteLine($"{fileName}.xml has been created with the description."); + } + + var tempFolderPath = ""; + if (CrunOptions.DownloadToTempFolder){ + tempFolderPath = fileDir; + fileDir = !string.IsNullOrEmpty(data.DownloadPath) ? data.DownloadPath : + !string.IsNullOrEmpty(options.DownloadDirPath) ? options.DownloadDirPath : CfgManager.PathVIDEOS_DIR; } return new DownloadResponse{ Data = files, Error = dlFailed, - FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(fileDir, fileName)) : "./unknown", + FileName = fileName.Length > 0 ? fileName : "unknown - " + Guid.NewGuid(), ErrorText = "", - VideoTitle = FileNameManager.ParseFileName(options.VideoTitle ?? "", variables, options.Numbers, options.Override).Last() + VideoTitle = FileNameManager.ParseFileName(options.VideoTitle ?? "", variables, options.Numbers, options.Override).Last(), + FolderPath = fileDir, + TempFolderPath = tempFolderPath }; } diff --git a/CRD/Utils/Files/CfgManager.cs b/CRD/Utils/Files/CfgManager.cs index ec16219..4aa38a3 100644 --- a/CRD/Utils/Files/CfgManager.cs +++ b/CRD/Utils/Files/CfgManager.cs @@ -19,21 +19,22 @@ namespace CRD.Utils; public class CfgManager{ private static string WorkingDirectory = Directory.GetCurrentDirectory(); - public static readonly string PathCrToken = WorkingDirectory + "/config/cr_token.yml"; - public static readonly string PathCrDownloadOptions = WorkingDirectory + "/config/settings.yml"; - public static readonly string PathCrHistory = WorkingDirectory + "/config/history.json"; - public static readonly string PathWindowSettings= WorkingDirectory + "/config/windowSettings.json"; + public static readonly string PathCrToken = Path.Combine(WorkingDirectory, "config", "cr_token.yml"); + public static readonly string PathCrDownloadOptions = Path.Combine(WorkingDirectory, "config", "settings.yml"); + public static readonly string PathCrHistory = Path.Combine(WorkingDirectory, "config", "history.json"); + public static readonly string PathWindowSettings = Path.Combine(WorkingDirectory, "config", "windowSettings.json"); - public static readonly string PathFFMPEG = WorkingDirectory + "/lib/ffmpeg.exe"; - public static readonly string PathMKVMERGE = WorkingDirectory + "/lib/mkvmerge.exe"; - public static readonly string PathMP4Decrypt = WorkingDirectory + "/lib/mp4decrypt.exe"; + public static readonly string PathFFMPEG = Path.Combine(WorkingDirectory, "lib", "ffmpeg.exe"); + public static readonly string PathMKVMERGE = Path.Combine(WorkingDirectory, "lib", "mkvmerge.exe"); + public static readonly string PathMP4Decrypt = Path.Combine(WorkingDirectory, "lib", "mp4decrypt.exe"); - public static readonly string PathWIDEVINE_DIR = WorkingDirectory + "/widevine/"; + public static readonly string PathWIDEVINE_DIR = Path.Combine(WorkingDirectory, "widevine"); - public static readonly string PathVIDEOS_DIR = WorkingDirectory + "/video/"; - public static readonly string PathFONTS_DIR = WorkingDirectory + "/video/"; + public static readonly string PathVIDEOS_DIR = Path.Combine(WorkingDirectory, "video"); + public static readonly string PathTEMP_DIR = Path.Combine(WorkingDirectory, "temp"); + public static readonly string PathFONTS_DIR = Path.Combine(WorkingDirectory, "video"); - public static readonly string PathLogFile = WorkingDirectory + "/logfile.txt"; + public static readonly string PathLogFile = Path.Combine(WorkingDirectory, "logfile.txt"); private static StreamWriter logFile; private static bool isLogModeEnabled = false; @@ -210,6 +211,10 @@ public class CfgManager{ } public static void UpdateHistoryFile(){ + if (!CrunchyrollManager.Instance.CrunOptions.History){ + return; + } + WriteJsonToFile(PathCrHistory, CrunchyrollManager.Instance.HistoryList); } diff --git a/CRD/Utils/Files/FileNameManager.cs b/CRD/Utils/Files/FileNameManager.cs index 1b804a6..6733b22 100644 --- a/CRD/Utils/Files/FileNameManager.cs +++ b/CRD/Utils/Files/FileNameManager.cs @@ -49,6 +49,7 @@ public class FileNameManager{ if (overrides == null){ return variables; } + foreach (var item in overrides){ int index = item.IndexOf('='); if (index == -1){ @@ -107,4 +108,39 @@ public class FileNameManager{ return filename; } + + + public static void DeleteEmptyFolders(string rootFolderPath){ + if (string.IsNullOrEmpty(rootFolderPath) || !Directory.Exists(rootFolderPath)){ + Console.WriteLine("Invalid directory path."); + return; + } + + DeleteEmptyFoldersRecursive(rootFolderPath, isRoot: true); + } + + private static bool DeleteEmptyFoldersRecursive(string folderPath, bool isRoot = false){ + bool isFolderEmpty = true; + + try{ + foreach (var directory in Directory.GetDirectories(folderPath)){ + // Recursively delete empty subfolders + if (!DeleteEmptyFoldersRecursive(directory)){ + isFolderEmpty = false; + } + } + + // Check if the current folder is empty (no files and no non-deleted subfolders) + if (!isRoot && isFolderEmpty && Directory.GetFiles(folderPath).Length == 0){ + Directory.Delete(folderPath); + Console.WriteLine($"Deleted empty folder: {folderPath}"); + return true; + } + + return false; + } catch (Exception ex){ + Console.WriteLine($"An error occurred while deleting folder {folderPath}: {ex.Message}"); + return false; + } + } } \ No newline at end of file diff --git a/CRD/Utils/Helpers.cs b/CRD/Utils/Helpers.cs index c91ff51..9205921 100644 --- a/CRD/Utils/Helpers.cs +++ b/CRD/Utils/Helpers.cs @@ -9,6 +9,7 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using Avalonia.Media.Imaging; +using CRD.Downloader.Crunchyroll; using CRD.Utils.JsonConv; using CRD.Utils.Structs; using CRD.Utils.Structs.Crunchyroll.Music; @@ -212,7 +213,7 @@ public class Helpers{ process.ErrorDataReceived += (sender, e) => { if (!string.IsNullOrEmpty(e.Data)){ - Console.WriteLine($"{e.Data}"); + Console.Error.WriteLine($"{e.Data}"); } }; @@ -223,7 +224,6 @@ public class Helpers{ await process.WaitForExitAsync(); - // Define success condition more appropriately based on the application bool isSuccess = process.ExitCode == 0; return (IsOk: isSuccess, ErrorCode: process.ExitCode); @@ -245,7 +245,6 @@ public class Helpers{ } } catch (Exception ex){ Console.Error.WriteLine($"Failed to delete file {filePath}. Error: {ex.Message}"); - // Handle exceptions if you need to log them or throw } } @@ -268,7 +267,7 @@ public class Helpers{ process.ErrorDataReceived += (sender, e) => { if (!string.IsNullOrEmpty(e.Data)){ - Console.WriteLine($"{e.Data}"); + Console.Error.WriteLine($"{e.Data}"); } }; @@ -279,7 +278,6 @@ public class Helpers{ await process.WaitForExitAsync(); - // Define success condition more appropriately based on the application bool isSuccess = process.ExitCode == 0; return (IsOk: isSuccess, ErrorCode: process.ExitCode); @@ -351,7 +349,7 @@ public class Helpers{ } public static string? ExtractNumberAfterS(string input){ - // Define the regular expression pattern to match |S followed by a number and optionally C followed by another number + // Regular expression pattern to match |S followed by a number and optionally C followed by another number string pattern = @"\|S(\d+)(?:C(\d+))?"; Match match = Regex.Match(input, pattern); @@ -380,7 +378,6 @@ public class Helpers{ } } } catch (Exception ex){ - // Handle exceptions Console.Error.WriteLine("Failed to load image: " + ex.Message); } @@ -388,7 +385,13 @@ public class Helpers{ } public static Dictionary> GroupByLanguageWithSubtitles(List allMedia){ + //Group by language var languageGroups = allMedia + .Where(media => + !string.IsNullOrEmpty(media.Lang.CrLocale) || + (media.Type == DownloadMediaType.Subtitle && media.RelatedVideoDownloadMedia != null && + !string.IsNullOrEmpty(media.RelatedVideoDownloadMedia.Lang.CrLocale)) + ) .GroupBy(media => { if (media.Type == DownloadMediaType.Subtitle && media.RelatedVideoDownloadMedia != null){ return media.RelatedVideoDownloadMedia.Lang.CrLocale; @@ -398,7 +401,34 @@ public class Helpers{ }) .ToDictionary(group => group.Key, group => group.ToList()); + //Find and add Description media to each group + var descriptionMedia = allMedia.Where(media => media.Type == DownloadMediaType.Description).ToList(); + + foreach (var description in descriptionMedia){ + foreach (var group in languageGroups.Values){ + group.Add(description); + } + } + return languageGroups; } - + + public static string GetValidFolderName(string folderName){ + // Get the invalid characters for a folder name + char[] invalidChars = Path.GetInvalidFileNameChars(); + + // Check if the folder name contains any invalid characters + bool isValid = !folderName.Any(c => invalidChars.Contains(c)); + + // Check for reserved names on Windows + string[] reservedNames =["CON", "PRN", "AUX", "NUL", "COM1", "LPT1"]; + bool isReservedName = reservedNames.Contains(folderName.ToUpperInvariant()); + + if (isValid && !isReservedName && folderName.Length <= 255){ + return folderName; // Return the original folder name if it's valid + } + + string uuid = Guid.NewGuid().ToString(); + return uuid; + } } \ No newline at end of file diff --git a/CRD/Utils/Muxing/Merger.cs b/CRD/Utils/Muxing/Merger.cs index cd36f00..24069fe 100644 --- a/CRD/Utils/Muxing/Merger.cs +++ b/CRD/Utils/Muxing/Merger.cs @@ -36,6 +36,8 @@ public class Merger{ var audioIndex = 0; var hasVideo = false; + args.Add("-loglevel warning"); + if (!options.mp3){ foreach (var vid in options.OnlyVid){ if (!hasVideo || options.KeepAllVideos == true){ @@ -325,7 +327,7 @@ public class Merger{ allMediaFiles.ForEach(file => Helpers.DeleteFile(file.Path)); allMediaFiles.ForEach(file => Helpers.DeleteFile(file.Path + ".resume")); - options.Description?.ForEach(chapter => Helpers.DeleteFile(chapter.Path)); + options.Description?.ForEach(description => Helpers.DeleteFile(description.Path)); // Delete chapter files if any options.Chapters?.ForEach(chapter => Helpers.DeleteFile(chapter.Path)); diff --git a/CRD/Utils/Sonarr/SonarrClient.cs b/CRD/Utils/Sonarr/SonarrClient.cs index 9015099..cab689d 100644 --- a/CRD/Utils/Sonarr/SonarrClient.cs +++ b/CRD/Utils/Sonarr/SonarrClient.cs @@ -67,6 +67,14 @@ public class SonarrClient{ } } + public async Task RefreshSonarrLite(){ + await CheckSonarrSettings(); + if (CrunchyrollManager.Instance.CrunOptions.SonarrProperties is{ SonarrEnabled: true }){ + SonarrSeries = await GetSeries(); + CrunchyrollManager.Instance.History.MatchHistorySeriesWithSonarr(true); + } + } + public void SetApiUrl(){ if (CrunchyrollManager.Instance.CrunOptions.SonarrProperties != null) properties = CrunchyrollManager.Instance.CrunOptions.SonarrProperties; diff --git a/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs b/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs index 8dd17df..ac7878b 100644 --- a/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs +++ b/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs @@ -180,6 +180,12 @@ public class CrDownloadOptions{ [YamlMember(Alias = "download_dir_path", ApplyNamingConventions = false)] public string? DownloadDirPath{ get; set; } + [YamlMember(Alias = "download_temp_dir_path", ApplyNamingConventions = false)] + public string? DownloadTempDirPath{ get; set; } + + [YamlMember(Alias = "download_to_temp_folder", ApplyNamingConventions = false)] + public bool DownloadToTempFolder{ get; set; } + [YamlMember(Alias = "history_page_properties", ApplyNamingConventions = false)] public HistoryPageProperties? HistoryPageProperties{ get; set; } diff --git a/CRD/Utils/Structs/Structs.cs b/CRD/Utils/Structs/Structs.cs index 4677062..6a5c0b1 100644 --- a/CRD/Utils/Structs/Structs.cs +++ b/CRD/Utils/Structs/Structs.cs @@ -68,6 +68,9 @@ public struct DownloadResponse{ public List Data{ get; set; } public string FileName{ get; set; } + public string FolderPath{ get; set; } + public string TempFolderPath{ get; set; } + public string VideoTitle{ get; set; } public bool Error{ get; set; } public string ErrorText{ get; set; } @@ -101,12 +104,14 @@ public class StringItem{ public string stringValue{ get; set; } } -public class WindowSettings{ - public double Width{ get; set; } - public double Height{ get; set; } - public int ScreenIndex{ get; set; } - public int PosX{ get; set; } - public int PosY{ get; set; } +public class WindowSettings +{ + public double Width { get; set; } + public double Height { get; set; } + public int ScreenIndex { get; set; } + public int PosX { get; set; } + public int PosY { get; set; } + public bool IsMaximized { get; set; } } public class ToastMessage(string message, ToastType type, int i){ diff --git a/CRD/ViewModels/MainWindowViewModel.cs b/CRD/ViewModels/MainWindowViewModel.cs index 4bc6507..0f5a753 100644 --- a/CRD/ViewModels/MainWindowViewModel.cs +++ b/CRD/ViewModels/MainWindowViewModel.cs @@ -4,6 +4,7 @@ using System.Net.Http; using System.Reflection; using System.Threading.Tasks; using Avalonia; +using Avalonia.Controls.Chrome; using Avalonia.Media; using Avalonia.Styling; using CommunityToolkit.Mvvm.ComponentModel; @@ -56,7 +57,7 @@ public partial class MainWindowViewModel : ViewModelBase{ if (CrunchyrollManager.Instance.CrunOptions.AccentColor != null){ _faTheme.CustomAccentColor = Color.Parse(CrunchyrollManager.Instance.CrunOptions.AccentColor); } - + if (CrunchyrollManager.Instance.CrunOptions.Theme == "System"){ _faTheme.PreferSystemTheme = true; } else if (CrunchyrollManager.Instance.CrunOptions.Theme == "Dark"){ diff --git a/CRD/ViewModels/SettingsPageViewModel.cs b/CRD/ViewModels/SettingsPageViewModel.cs index 709ef2a..50ceab3 100644 --- a/CRD/ViewModels/SettingsPageViewModel.cs +++ b/CRD/ViewModels/SettingsPageViewModel.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; +using System.IO; using System.Linq; using System.Net.Mime; using System.Reflection; @@ -20,6 +21,7 @@ using CRD.Utils; using CRD.Utils.CustomList; using CRD.Utils.Sonarr; using CRD.Utils.Structs; +using CRD.Utils.Structs.History; using FluentAvalonia.Styling; namespace CRD.ViewModels; @@ -42,10 +44,12 @@ public partial class SettingsPageViewModel : ViewModelBase{ [ObservableProperty] private bool _includeSignSubs; - + [ObservableProperty] private bool _includeCcSubs; - + + [ObservableProperty] + private bool _downloadToTempFolder; [ObservableProperty] private ComboBoxItem _selectedScaledBorderAndShadow; @@ -57,13 +61,13 @@ public partial class SettingsPageViewModel : ViewModelBase{ [ObservableProperty] private bool _muxToMp4; - + [ObservableProperty] private bool _syncTimings; [ObservableProperty] private bool _defaultSubSigns; - + [ObservableProperty] private bool _defaultSubForcedDisplay; @@ -72,7 +76,7 @@ public partial class SettingsPageViewModel : ViewModelBase{ [ObservableProperty] private bool _downloadVideoForEveryDub; - + [ObservableProperty] private bool _keepDubsSeparate; @@ -81,7 +85,7 @@ public partial class SettingsPageViewModel : ViewModelBase{ [ObservableProperty] private bool _history; - + [ObservableProperty] private bool _historyAddSpecials; @@ -126,7 +130,7 @@ public partial class SettingsPageViewModel : ViewModelBase{ [ObservableProperty] private ComboBoxItem _selectedDescriptionLang; - + [ObservableProperty] private string _selectedDubs = "ja-JP"; @@ -326,6 +330,9 @@ public partial class SettingsPageViewModel : ViewModelBase{ [ObservableProperty] private string _downloadDirPath; + [ObservableProperty] + private string _tempDownloadDirPath; + private readonly FluentAvaloniaTheme _faTheme; private bool settingsLoaded; @@ -349,6 +356,7 @@ public partial class SettingsPageViewModel : ViewModelBase{ CrDownloadOptions options = CrunchyrollManager.Instance.CrunOptions; DownloadDirPath = string.IsNullOrEmpty(options.DownloadDirPath) ? CfgManager.PathVIDEOS_DIR : options.DownloadDirPath; + TempDownloadDirPath = string.IsNullOrEmpty(options.DownloadTempDirPath) ? CfgManager.PathTEMP_DIR : options.DownloadTempDirPath; ComboBoxItem? descriptionLang = DescriptionLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.DescriptionLang) ?? null; SelectedDescriptionLang = descriptionLang ?? DescriptionLangList[0]; @@ -381,7 +389,7 @@ public partial class SettingsPageViewModel : ViewModelBase{ foreach (var listBoxItem in dubLang){ SelectedDubLang.Add(listBoxItem); } - + var props = options.SonarrProperties; if (props != null){ @@ -407,6 +415,7 @@ public partial class SettingsPageViewModel : ViewModelBase{ DownloadVideo = !options.Novids; DownloadAudio = !options.Noaudio; DownloadVideoForEveryDub = !options.DlVideoOnce; + DownloadToTempFolder = options.DownloadToTempFolder; KeepDubsSeparate = options.KeepDubsSeperate; DownloadChapters = options.Chapters; MuxToMp4 = options.Mp4; @@ -445,10 +454,10 @@ public partial class SettingsPageViewModel : ViewModelBase{ FfmpegOptions.Add(new MuxingParam(){ ParamValue = ffmpegParam }); } } - + var dubs = SelectedDubLang.Select(item => item.Content?.ToString()); SelectedDubs = string.Join(", ", dubs) ?? ""; - + var subs = SelectedSubLang.Select(item => item.Content?.ToString()); SelectedSubs = string.Join(", ", subs) ?? ""; @@ -466,6 +475,7 @@ public partial class SettingsPageViewModel : ViewModelBase{ return; } + CrunchyrollManager.Instance.CrunOptions.DownloadToTempFolder = DownloadToTempFolder; CrunchyrollManager.Instance.CrunOptions.DefaultSubSigns = DefaultSubSigns; CrunchyrollManager.Instance.CrunOptions.DefaultSubForcedDisplay = DefaultSubForcedDisplay; CrunchyrollManager.Instance.CrunOptions.IncludeVideoDescription = IncludeEpisodeDescription; @@ -480,12 +490,12 @@ public partial class SettingsPageViewModel : ViewModelBase{ CrunchyrollManager.Instance.CrunOptions.Mp4 = MuxToMp4; CrunchyrollManager.Instance.CrunOptions.SyncTiming = SyncTimings; CrunchyrollManager.Instance.CrunOptions.SkipSubsMux = SkipSubMux; - CrunchyrollManager.Instance.CrunOptions.Numbers = Math.Clamp((int)(LeadingNumbers ?? 0),0,10); + CrunchyrollManager.Instance.CrunOptions.Numbers = Math.Clamp((int)(LeadingNumbers ?? 0), 0, 10); CrunchyrollManager.Instance.CrunOptions.FileName = FileName; - CrunchyrollManager.Instance.CrunOptions.IncludeSignsSubs = IncludeSignSubs; - CrunchyrollManager.Instance.CrunOptions.IncludeCcSubs = IncludeCcSubs; - CrunchyrollManager.Instance.CrunOptions.DownloadSpeedLimit = Math.Clamp((int)(DownloadSpeed ?? 0),0,1000000000); - CrunchyrollManager.Instance.CrunOptions.SimultaneousDownloads = Math.Clamp((int)(SimultaneousDownloads ?? 0),1,10); + CrunchyrollManager.Instance.CrunOptions.IncludeSignsSubs = IncludeSignSubs; + CrunchyrollManager.Instance.CrunOptions.IncludeCcSubs = IncludeCcSubs; + CrunchyrollManager.Instance.CrunOptions.DownloadSpeedLimit = Math.Clamp((int)(DownloadSpeed ?? 0), 0, 1000000000); + CrunchyrollManager.Instance.CrunOptions.SimultaneousDownloads = Math.Clamp((int)(SimultaneousDownloads ?? 0), 1, 10); CrunchyrollManager.Instance.CrunOptions.SubsAddScaledBorder = GetScaledBorderAndShadowSelection(); @@ -520,7 +530,7 @@ public partial class SettingsPageViewModel : ViewModelBase{ } CrunchyrollManager.Instance.CrunOptions.DubLang = dubLangs; - + CrunchyrollManager.Instance.CrunOptions.QualityAudio = SelectedAudioQuality?.Content + ""; CrunchyrollManager.Instance.CrunOptions.QualityVideo = SelectedVideoQuality?.Content + ""; CrunchyrollManager.Instance.CrunOptions.Theme = CurrentAppTheme?.Content + ""; @@ -592,7 +602,7 @@ public partial class SettingsPageViewModel : ViewModelBase{ return ScaledBorderAndShadow[0]; } } - + [RelayCommand] public void AddMkvMergeParam(){ MkvMergeOptions.Add(new MuxingParam(){ ParamValue = MkvMergeOption }); @@ -621,22 +631,38 @@ public partial class SettingsPageViewModel : ViewModelBase{ [RelayCommand] public async Task OpenFolderDialogAsync(){ + await OpenFolderDialogAsyncInternal( + pathSetter: (path) => CrunchyrollManager.Instance.CrunOptions.DownloadDirPath = path, + pathGetter: () => CrunchyrollManager.Instance.CrunOptions.DownloadDirPath, + defaultPath: CfgManager.PathVIDEOS_DIR + ); + } + + [RelayCommand] + public async Task OpenFolderDialogTempFolderAsync(){ + await OpenFolderDialogAsyncInternal( + pathSetter: (path) => CrunchyrollManager.Instance.CrunOptions.DownloadTempDirPath = path, + pathGetter: () => CrunchyrollManager.Instance.CrunOptions.DownloadTempDirPath, + defaultPath: CfgManager.PathTEMP_DIR + ); + } + + private async Task OpenFolderDialogAsyncInternal(Action pathSetter, Func pathGetter, string defaultPath){ 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}"); - CrunchyrollManager.Instance.CrunOptions.DownloadDirPath = selectedFolder.Path.LocalPath; - DownloadDirPath = string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.DownloadDirPath) ? CfgManager.PathVIDEOS_DIR : CrunchyrollManager.Instance.CrunOptions.DownloadDirPath; + pathSetter(selectedFolder.Path.LocalPath); + var finalPath = string.IsNullOrEmpty(pathGetter()) ? defaultPath : pathGetter(); + pathSetter(finalPath); CfgManager.WriteSettingsToFile(); } } @@ -696,27 +722,50 @@ public partial class SettingsPageViewModel : ViewModelBase{ } private void Changes(object? sender, NotifyCollectionChangedEventArgs e){ - UpdateSettings(); - + var dubs = SelectedDubLang.Select(item => item.Content?.ToString()); SelectedDubs = string.Join(", ", dubs) ?? ""; - + var subs = SelectedSubLang.Select(item => item.Content?.ToString()); SelectedSubs = string.Join(", ", subs) ?? ""; - } - + protected override void OnPropertyChanged(PropertyChangedEventArgs e){ base.OnPropertyChanged(e); - if (e.PropertyName is nameof(SelectedDubs) or nameof(SelectedSubs) or nameof(CustomAccentColor) or nameof(ListBoxColor) or nameof(CurrentAppTheme) or nameof(UseCustomAccent) or nameof(LogMode)){ + if (e.PropertyName is nameof(SelectedDubs) or nameof(SelectedSubs) or nameof(CustomAccentColor) or nameof(ListBoxColor) or nameof(CurrentAppTheme) or nameof(UseCustomAccent) or nameof(LogMode)){ return; } - + UpdateSettings(); + + if (e.PropertyName is nameof(History)){ + if (CrunchyrollManager.Instance.CrunOptions.History){ + if (File.Exists(CfgManager.PathCrHistory)){ + var decompressedJson = CfgManager.DecompressJsonFile(CfgManager.PathCrHistory); + if (!string.IsNullOrEmpty(decompressedJson)){ + CrunchyrollManager.Instance.HistoryList = Helpers.Deserialize>(decompressedJson, CrunchyrollManager.Instance.SettingsJsonSerializerSettings) ?? + new ObservableCollection(); + + foreach (var historySeries in CrunchyrollManager.Instance.HistoryList){ + historySeries.Init(); + foreach (var historySeriesSeason in historySeries.Seasons){ + historySeriesSeason.Init(); + } + } + } else{ + CrunchyrollManager.Instance.HistoryList =[]; + } + } + + _ = SonarrClient.Instance.RefreshSonarrLite(); + } else{ + CrunchyrollManager.Instance.HistoryList =[]; + } + } } - + partial void OnLogModeChanged(bool value){ UpdateSettings(); if (value){ @@ -725,7 +774,6 @@ public partial class SettingsPageViewModel : ViewModelBase{ CfgManager.DisableLogMode(); } } - } public class MuxingParam{ diff --git a/CRD/Views/HistoryPageView.axaml b/CRD/Views/HistoryPageView.axaml index f1ddb25..0062ec3 100644 --- a/CRD/Views/HistoryPageView.axaml +++ b/CRD/Views/HistoryPageView.axaml @@ -30,7 +30,7 @@ - + diff --git a/CRD/Views/MainWindow.axaml b/CRD/Views/MainWindow.axaml index ea35a4f..66d653c 100644 --- a/CRD/Views/MainWindow.axaml +++ b/CRD/Views/MainWindow.axaml @@ -14,7 +14,7 @@ - + diff --git a/CRD/Views/MainWindow.axaml.cs b/CRD/Views/MainWindow.axaml.cs index 1ef236c..263ceab 100644 --- a/CRD/Views/MainWindow.axaml.cs +++ b/CRD/Views/MainWindow.axaml.cs @@ -3,9 +3,12 @@ using System.Collections.Generic; using System.IO; using Avalonia; using Avalonia.Controls; +using Avalonia.Interactivity; using Avalonia.Markup.Xaml; +using Avalonia.Media; using Avalonia.Platform; using CRD.Utils; +using CRD.Utils.Files; using CRD.Utils.Structs; using CRD.Utils.Updater; using CRD.ViewModels; @@ -48,17 +51,29 @@ public partial class MainWindow : AppWindow{ private object selectedNavVieItem; + private const int TitleBarHeightAdjustment = 31; + + private PixelPoint _restorePosition; + private Size _restoreSize; + public MainWindow(){ AvaloniaXamlLoader.Load(this); InitializeComponent(); + ExtendClientAreaTitleBarHeightHint = TitleBarHeightAdjustment; + TitleBar.Height = TitleBarHeightAdjustment; + TitleBar.ExtendsContentIntoTitleBar = true; + TitleBar.TitleBarHitTestType = TitleBarHitTestType.Complex; + Opened += OnOpened; Closing += OnClosing; - TitleBar.ExtendsContentIntoTitleBar = true; - TitleBar.TitleBarHitTestType = TitleBarHitTestType.Complex; + PropertyChanged += OnWindowStateChanged; + PositionChanged += OnPositionChanged; + SizeChanged += OnSizeChanged; + //select first element as default var nv = this.FindControl("NavView"); nv.SelectedItem = nv.MenuItems.ElementAt(0); @@ -177,26 +192,29 @@ public partial class MainWindow : AppWindow{ if (File.Exists(CfgManager.PathWindowSettings)){ var settings = JsonConvert.DeserializeObject(File.ReadAllText(CfgManager.PathWindowSettings)); if (settings != null){ - Width = settings.Width; - Height = settings.Height; - var screens = Screens.All; if (settings.ScreenIndex >= 0 && settings.ScreenIndex < screens.Count){ var screen = screens[settings.ScreenIndex]; var screenBounds = screen.Bounds; - var topLeft = screenBounds.TopLeft; - var bottomRight = screenBounds.BottomRight; + // Restore the position first + Position = new PixelPoint(settings.PosX, settings.PosY + TitleBarHeightAdjustment); - if (settings.PosX >= topLeft.X && settings.PosX <= bottomRight.X - Width && - settings.PosY >= topLeft.Y && settings.PosY <= bottomRight.Y - Height){ - Position = new PixelPoint(settings.PosX, settings.PosY); - } else{ - Position = new PixelPoint(topLeft.X, topLeft.Y + 31); - } - } else{ - var primaryScreen = screens?[0].Bounds ?? new PixelRect(0, 0, 1000, 600); // Default size if no screens - Position = new PixelPoint(primaryScreen.TopLeft.X, primaryScreen.TopLeft.Y + 31); + // Restore the size + Width = settings.Width; + Height = settings.Height - TitleBarHeightAdjustment; + + // Set restore size and position for non-maximized state + _restoreSize = new Size(settings.Width, settings.Height); + _restorePosition = new PixelPoint(settings.PosX, settings.PosY + TitleBarHeightAdjustment); + + // Ensure the window is on the correct screen before maximizing + Position = new PixelPoint(settings.PosX, settings.PosY+ TitleBarHeightAdjustment); + } + + if (settings.IsMaximized){ + // Maximize the window after setting its position on the correct screen + WindowState = WindowState.Maximized; } } } @@ -214,13 +232,37 @@ public partial class MainWindow : AppWindow{ } var settings = new WindowSettings{ - Width = Width, - Height = Height, + Width = this.WindowState == WindowState.Maximized ? _restoreSize.Width : Width, + Height = this.WindowState == WindowState.Maximized ? _restoreSize.Height : Height, ScreenIndex = screenIndex, - PosX = Position.X, - PosY = Position.Y + PosX = this.WindowState == WindowState.Maximized ? _restorePosition.X : Position.X, + PosY = this.WindowState == WindowState.Maximized ? _restorePosition.Y : Position.Y, + IsMaximized = this.WindowState == WindowState.Maximized }; File.WriteAllText(CfgManager.PathWindowSettings, JsonConvert.SerializeObject(settings, Formatting.Indented)); } + + private void OnWindowStateChanged(object sender, AvaloniaPropertyChangedEventArgs e){ + if (e.Property == Window.WindowStateProperty){ + if (WindowState == WindowState.Normal){ + // When the window is restored to normal, use the stored restore size and position + Width = _restoreSize.Width; + Height = _restoreSize.Height; + Position = _restorePosition; + } + } + } + + private void OnPositionChanged(object sender, PixelPointEventArgs e){ + if (WindowState == WindowState.Normal){ + _restorePosition = e.Point; + } + } + + private void OnSizeChanged(object sender, SizeChangedEventArgs e){ + if (WindowState == WindowState.Normal){ + _restoreSize = e.NewSize; + } + } } \ No newline at end of file diff --git a/CRD/Views/SettingsPageView.axaml b/CRD/Views/SettingsPageView.axaml index b253cf4..bf3f69b 100644 --- a/CRD/Views/SettingsPageView.axaml +++ b/CRD/Views/SettingsPageView.axaml @@ -12,7 +12,7 @@ - + @@ -50,7 +50,7 @@ @@ -125,7 +125,7 @@ - + @@ -149,13 +149,13 @@ - + - + @@ -168,7 +168,7 @@ IconSource="Download" Description="Adjust download settings" IsExpanded="False"> - + @@ -178,7 +178,7 @@ HorizontalAlignment="Stretch" /> - + @@ -189,6 +189,25 @@ + + + + + + + + + + @@ -229,7 +248,7 @@ - + @@ -283,7 +302,7 @@ + Description="${seriesTitle} ${seasonTitle} ${title} ${season} ${episode} ${height} ${width}"> @@ -332,10 +351,10 @@ - + - + @@ -343,7 +362,7 @@ + Description="${seriesTitle} ${seasonTitle} ${title} ${season} ${episode} ${height} ${width}"> @@ -355,8 +374,8 @@ - - + +