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:
parent
c7a2ef5af7
commit
3a28c0fbd1
|
@ -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,8 +276,15 @@ 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,
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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; }
|
||||
|
||||
|
|
|
@ -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 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){
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
@ -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,7 +729,6 @@ public partial class SettingsPageViewModel : ViewModelBase{
|
|||
|
||||
var subs = SelectedSubLang.Select(item => item.Content?.ToString());
|
||||
SelectedSubs = string.Join(", ", subs) ?? "";
|
||||
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged(PropertyChangedEventArgs e){
|
||||
|
@ -715,6 +739,31 @@ public partial class SettingsPageViewModel : ViewModelBase{
|
|||
}
|
||||
|
||||
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{
|
||||
|
|
|
@ -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">
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<Design.DataContext>
|
||||
<vm:MainWindowViewModel />
|
||||
</Design.DataContext>
|
||||
<Grid Name="mainGrid">
|
||||
<Grid Name="MainGrid">
|
||||
<ContentControl x:Name="MainContent">
|
||||
<Grid RowDefinitions="Auto, *">
|
||||
|
||||
|
|
|
@ -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);
|
||||
// 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);
|
||||
}
|
||||
} 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);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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}" />
|
||||
|
|
Loading…
Reference in New Issue