Add - Temp folder option to the settings - episodes are downloaded and processed in this folder an moved afterwards https://github.com/Crunchy-DL/Crunchy-Downloader/issues/62

Chg - Renamed {showTitle} to {seasonTitle} make sure to change it if you have it in the file name
Fix - Height of window increased each about 31px  https://github.com/Crunchy-DL/Crunchy-Downloader/issues/89
Fix - History bar now has a different color for dark mode and light mode https://github.com/Crunchy-DL/Crunchy-Downloader/issues/85
Fix - FFMPEG/Mkvmerge/Mp4decrypt errors are now correctly logged
Fix - Crash with separate video files option
This commit is contained in:
Elwador 2024-08-20 18:40:45 +02:00
parent c7a2ef5af7
commit 3a28c0fbd1
14 changed files with 431 additions and 114 deletions

View File

@ -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<string>{ "de-DE" };
options.DlSubs = new List<string>{ "en-US" };
options.Skipmux = false;
options.MkvmergeOptions = new List<string>{ "--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<Merger>();
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<DownloadedMedia> data, CrunchyMuxOptions options, string filename){
#region Temp Files Move
private async Task MoveFromTempFolder(Merger merger, CrunchyEpMeta data, string tempFolderPath, IEnumerable<DownloadedMedia> 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<DownloadedMedia> 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<DownloadResponse> 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
};
}

View File

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

View File

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

View File

@ -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<string, List<DownloadedMedia>> GroupByLanguageWithSubtitles(List<DownloadedMedia> 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;
}
}

View File

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

View File

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

View File

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

View File

@ -68,6 +68,9 @@ public struct DownloadResponse{
public List<DownloadedMedia> 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){

View File

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

View File

@ -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;
@ -46,6 +48,8 @@ public partial class SettingsPageViewModel : ViewModelBase{
[ObservableProperty]
private bool _includeCcSubs;
[ObservableProperty]
private bool _downloadToTempFolder;
[ObservableProperty]
private ComboBoxItem _selectedScaledBorderAndShadow;
@ -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];
@ -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;
@ -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.DownloadSpeedLimit = Math.Clamp((int)(DownloadSpeed ?? 0), 0, 1000000000);
CrunchyrollManager.Instance.CrunOptions.SimultaneousDownloads = Math.Clamp((int)(SimultaneousDownloads ?? 0), 1, 10);
CrunchyrollManager.Instance.CrunOptions.SubsAddScaledBorder = GetScaledBorderAndShadowSelection();
@ -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<string> pathSetter, Func<string> 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,7 +722,6 @@ public partial class SettingsPageViewModel : ViewModelBase{
}
private void Changes(object? sender, NotifyCollectionChangedEventArgs e){
UpdateSettings();
var dubs = SelectedDubLang.Select(item => item.Content?.ToString());
@ -704,17 +729,41 @@ public partial class SettingsPageViewModel : ViewModelBase{
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<ObservableCollection<HistorySeries>>(decompressedJson, CrunchyrollManager.Instance.SettingsJsonSerializerSettings) ??
new ObservableCollection<HistorySeries>();
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){
@ -725,7 +774,6 @@ public partial class SettingsPageViewModel : ViewModelBase{
CfgManager.DisableLogMode();
}
}
}
public class MuxingParam{

View File

@ -30,7 +30,7 @@
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3" Background="#2a2a2a"></StackPanel>
<StackPanel Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3" Background="{DynamicResource ControlAltFillColorQuarternary}"></StackPanel>
<StackPanel Grid.Row="0" Grid.Column="0" Orientation="Horizontal">

View File

@ -14,7 +14,7 @@
<Design.DataContext>
<vm:MainWindowViewModel />
</Design.DataContext>
<Grid Name="mainGrid">
<Grid Name="MainGrid">
<ContentControl x:Name="MainContent">
<Grid RowDefinitions="Auto, *">

View File

@ -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,15 +51,27 @@ 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
@ -177,26 +192,29 @@ public partial class MainWindow : AppWindow{
if (File.Exists(CfgManager.PathWindowSettings)){
var settings = JsonConvert.DeserializeObject<WindowSettings>(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;
}
}
}

View File

@ -189,6 +189,25 @@
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Use Temp Download Folder">
<controls:SettingsExpanderItem.Footer>
<StackPanel Orientation="Horizontal">
<TextBlock IsVisible="{Binding DownloadToTempFolder}" FontSize="15" Opacity="0.8" TextWrapping="NoWrap" Text="{Binding TempDownloadDirPath, Mode=OneWay}" TextAlignment="Center" VerticalAlignment="Center" HorizontalAlignment="Center" />
<Button IsVisible="{Binding DownloadToTempFolder}" Margin="10 0 10 0" FontStyle="Italic"
VerticalAlignment="Center"
Command="{Binding OpenFolderDialogTempFolderAsync}">
<ToolTip.Tip>
<TextBlock Text="Set Download Directory" FontSize="15" />
</ToolTip.Tip>
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Folder" FontSize="18" />
</StackPanel>
</Button>
<CheckBox IsChecked="{Binding DownloadToTempFolder}"> </CheckBox>
</StackPanel>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Download Folder">
<controls:SettingsExpanderItem.Footer>
<StackPanel Orientation="Horizontal">
@ -283,7 +302,7 @@
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Filename"
Description="${showTitle} ${seriesTitle} ${title} ${season} ${episode} ${height} ${width}">
Description="${seriesTitle} ${seasonTitle} ${title} ${season} ${episode} ${height} ${width}">
<controls:SettingsExpanderItem.Footer>
<TextBox Name="FileNameTextBox" HorizontalAlignment="Left" MinWidth="250"
Text="{Binding FileName}" />
@ -343,7 +362,7 @@
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="File title"
Description="${showTitle} ${seriesTitle} ${title} ${season} ${episode} ${height} ${width}">
Description="${seriesTitle} ${seasonTitle} ${title} ${season} ${episode} ${height} ${width}">
<controls:SettingsExpanderItem.Footer>
<TextBox HorizontalAlignment="Left" MinWidth="250"
Text="{Binding FileTitle}" />
@ -356,7 +375,7 @@
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Episode description Language" >
<controls:SettingsExpanderItem Content="Episode description Language">
<controls:SettingsExpanderItem.Footer>
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
ItemsSource="{Binding DescriptionLangList}"