From 3b06574b62a280ed225cbc05959c8441134a6d00 Mon Sep 17 00:00:00 2001 From: Sarahtonin Date: Fri, 3 Apr 2026 21:23:13 +0200 Subject: [PATCH] It works! Sorry for lack of pushes. :p --- Config.toml | 21 ++-- DirConfig.toml | 2 +- README.md | 4 +- SaraSortd.py | 263 +++++++++++++++++++++++++++++++++++-------------- 4 files changed, 202 insertions(+), 88 deletions(-) diff --git a/Config.toml b/Config.toml index 6b9cdab..60eac92 100644 --- a/Config.toml +++ b/Config.toml @@ -2,10 +2,10 @@ Title = "SaraSortd Config" SafeMode = 1 SilentMode = 0 CheckInput = 10 -OutputDir = [ "Log",] +DotFiles = 1 [Variables] -NextNum = "*" +NextNum = "!" NextChar = "&" Parent = "@" OrgFileName = "~" @@ -20,20 +20,21 @@ VarCall = "%%" [DirectoryPaths] RootDir = "/home/sarah/TESTDIR" -InputDir = [ "%RootDir%",] -OutputDir = [ "%RootDir%/Output",] +InputDir = [ "%RootDir%"] +OutputDir = [ "%RootDir%/Output"] LogDir = "%RootDir%/.Logs" FailedDir = "%RootDir%/.Failed" [Names] -LogFileName = "[y].[m].[d]._*" +LogFileName = "[y].[m].[d]._!" DirConfName = ".@Config" [Log] All = "[h]:[n]:[s] | " -Start = "Started: SaraSortd" +Start = "Started SaraSortd." NotStart = "Error: Failed to start SaraSortd." -Sorted = "Sorted: %% to %%." -ValueSet = "Set: %% to be %% in %%." -WrongType = "Error: %% is not a %%." -Unset = "Error: %% is not set." \ No newline at end of file +MatchPattern = "Checking %%." +Sorted = "Sorted %%." +NotSorted = "Error: Failed to sort %%. No valid output directory." +ValueSet = "Set %%." +Unset = "Error: %% is not set." diff --git a/DirConfig.toml b/DirConfig.toml index 46473c5..2cc808c 100644 --- a/DirConfig.toml +++ b/DirConfig.toml @@ -1,7 +1,7 @@ Title = "Unset" Files = [ - {Pattern = "?", NewFileName = "~^", NextNum = 1, NextChar = "A"}, + {Pattern = "*.png", NewFileName = "~^", NextNum = 1, NextChar = "A"}, ] LastFile = "Unset" diff --git a/README.md b/README.md index 0282744..2383583 100644 --- a/README.md +++ b/README.md @@ -69,8 +69,8 @@ Title = "Unset" # NOTE: Do not change the title. Files = [ {Pattern = "?", NewFileName = "~^", NextNum = 1, NextChar = "A"}, ] -# Pattern: the pattern used to sort files here. ? is a wildcard. -# E.G: if Pattern is "?.png", all files ending with .png will end up here. +# Pattern: the pattern used to sort files here. * is a wildcard, ? is a one-character wildcard. +# E.G: if Pattern is "*.png", all files ending with .png will end up here. # NewFileName: the pattern for the new names of the files that get sorted here. diff --git a/SaraSortd.py b/SaraSortd.py index dc17c0f..9db7326 100644 --- a/SaraSortd.py +++ b/SaraSortd.py @@ -1,4 +1,6 @@ -import toml, os, datetime, re, shutil +import toml, os, datetime, re, shutil, fnmatch, time + +ConfPath = "./Config.toml" Conf = None ConfVars = None @@ -15,10 +17,10 @@ def Clear(): os.system('clear') -def LoadConf(): +def LoadGlobalConf(): global Conf, ConfVars, ConfDirs, ConfNames, ConfLog - LocalConf = toml.load("Config.toml") + LocalConf = toml.load(ConfPath) Conf = LocalConf ConfVars = Conf["Variables"] ConfDirs = Conf["DirectoryPaths"] @@ -26,44 +28,34 @@ def LoadConf(): ConfLog = Conf["Log"] -def CheckConf(): - for Parameter, Value in ConfVars.items(): - if (Value == None or Value == "Unset"): - TextOutput = Parse(ConfLog["Unset"], Parameter) - LogWrite(TextOutput) - Speak(TextOutput) - Error() - - for Parameter, Value in ConfDirs.items(): - if (Value == None or Value == "Unset"): - TextOutput = Parse(ConfLog["Unset"], Parameter) - LogWrite(TextOutput) - Speak(TextOutput) - Error() - - for Parameter, Value in ConfNames.items(): - if (Value == None or Value == "Unset"): - TextOutput = Parse(ConfLog["Unset"], Parameter) - LogWrite(TextOutput) - Speak(TextOutput) - Error() +def CheckConf(Path = ConfPath): + ConfData = toml.load(Path) - for Parameter, Value in ConfLog.items(): - if (Value == None or Value == "Unset"): - TextOutput = Parse(ConfLog["Unset"], Parameter) - LogWrite(TextOutput) - Speak(TextOutput) - Error() + def CheckDict(Data): + for Key, Value in Data.items(): + if isinstance(Value, dict): + CheckDict(Value) + else: + if Value is None or Value == "Unset": + TextOutput = Parse(String = ConfLog["Unset"], Path = Path, VarCall = Key) + LogWrite(TextOutput) + Speak(TextOutput) + Error() + + CheckDict(ConfData) -def GetConf(Parameter): - if Parameter in Conf: - return (Conf[Parameter]) +def GetConf(Parameter, *Paths): + for Path in Paths: + ConfData = toml.load(Path) - for Key, Value in Conf.items(): - if (isinstance(Value, dict)): - if (Parameter in Value): - return Value[Parameter] + if Parameter in ConfData: + return (ConfData[Parameter]) + + for Key, Value in ConfData.items(): + if (isinstance(Value, dict)): + if (Parameter in Value): + return Value[Parameter] return None @@ -71,23 +63,40 @@ def GetConf(Parameter): def UpdateConf(Path, Parameter, Value, Append = False): ConfData = toml.load(Path) - if (not Append): - ConfData[Parameter] = Value + Section = None + for Table, Data in ConfData.items(): + if (isinstance(Data, dict) and Parameter in Data): + Section = Table + break + + if (Section == None): + TargetData = ConfData else: - if (Parameter not in ConfData): - ConfData[Parameter] = [] + TargetData = ConfData[Section] + + if (not Append): + TargetData[Parameter] = Value + + else: + if (Parameter not in TargetData): + TargetData[Parameter] = [] - elif (not isinstance(ConfData[Parameter], list)): - TextOutput = Parse(ConfLog["WrongType"], Parameter, "list") - LogWrite(TextOutput) - Speak(TextOutput) - - if (Value not in ConfData[Parameter]): - ConfData[Parameter].append(Value) + if (Value not in TargetData[Parameter]): + TargetData[Parameter].append(Value) + + if (Section == None): + ConfData = TargetData + + else: + ConfData[Section] = TargetData with open(Path, 'w', encoding='utf-8') as File: toml.dump(ConfData, File) + + TextOutput = Parse(String = ConfLog["ValueSet"], VarCall = f"{Parameter} to {Value}") + LogWrite(TextOutput) + Speak(TextOutput) def Error(): @@ -101,36 +110,47 @@ def Speak(Text): print(Text) -def Parse(String = None, Var = None, Path = None): +def Parse(String = None, Path = ConfPath, + NextNum = None, + NextChar = None, + Parent = None, + OrgFile = None, + VarCall = None): + def Replacer(Match): Content = Match.group(1) if (Content == None or Content == ""): - if (Var != None): - return str(Var) + if (VarCall != None): + return str(VarCall) else: return "" - Value = GetConf(Content) + Value = GetConf(Content, Path) if (Value != None): return Value Now = datetime.datetime.now() + if (OrgFile != None): + OrgFileName, OrgFileType = os.path.splitext(OrgFile) + else: + OrgFileName = None + OrgFileType = None VarValues = { - ConfVars["NextNum"]: "NOT ADDED", - ConfVars["NextChar"]: "NOT ADDED", - ConfVars["Parent"]: Var, - ConfVars["OrgFileName"]: Var, - ConfVars["OrgFileType"]: Var, + ConfVars["NextNum"]: NextNum, + ConfVars["NextChar"]: NextChar, + ConfVars["Parent"]: Parent, + ConfVars["OrgFileName"]: OrgFileName, + ConfVars["OrgFileType"]: OrgFileType, ConfVars["Year"]: str(Now.year), ConfVars["Month"]: str(Now.month), ConfVars["Day"]: str(Now.day), ConfVars["Hour"]: str(Now.hour), ConfVars["Minute"]: str(Now.minute), ConfVars["Second"]: str(Now.second), - ConfVars["VarCall"]: Var + ConfVars["VarCall"]: VarCall } for Key, Value in VarValues.items(): @@ -146,52 +166,126 @@ def Parse(String = None, Var = None, Path = None): return String -def Clone(Source, Destination, NewName): - shutil.copy(Source, Destination) +def Clone(Source, Destination, NewName, Delete = False): + Dir(Destination, Output = False, CopyConf = False) + shutil.copy2(Source, Destination) - NewPath = f"{Destination}/{NewName}" - shutil.move(f"{Destination}/{Source}", NewPath) + BaseName = os.path.basename(Source) + shutil.move(os.path.join(Destination, BaseName), os.path.join(Destination, NewName)) + + if (Delete): + os.remove(Source) def Dir(Path, Output = True, CopyConf = True): - ParentName = os.path.basename(os.path.dirname(Path)) + ParentName = os.path.basename(Path) os.makedirs(Path, exist_ok=True) if (CopyConf): - Clone("DirConfig.toml", Path, f"{Parse(ConfNames["DirConfName"], ParentName)}.toml") + Clone("DirConfig.toml", Path, f"{Parse(String = ConfNames["DirConfName"], Path = ConfPath, Parent = ParentName)}.toml") - UpdateConf(f"{Path}/{Parse(ConfNames["DirConfName"], ParentName)}.toml", "ParentDir", ParentName) - UpdateConf(f"{Path}/{Parse(ConfNames["DirConfName"], ParentName)}.toml", "Title", f"{ParentName} Config") + UpdateConf(f"{Path}/{Parse(String = ConfNames["DirConfName"], Path = ConfPath, Parent = ParentName)}.toml", "ParentDir", ParentName) + UpdateConf(f"{Path}/{Parse(String = ConfNames["DirConfName"], Path = ConfPath, Parent = ParentName)}.toml", "Title", f"{ParentName} Config") if (Output): - UpdateConf("Config.toml", "OutputDir", Output, True) + ParsedPath = Parse(String = Path) + ParsedOutputs = [Parse(String = Dir) for Dir in ConfDirs["OutputDir"]] + if (ParsedPath not in ParsedOutputs): + UpdateConf(ConfPath, "OutputDir", Path, True) def LogWrite(Text): if (ConfDirs["LogDir"] != None and ConfNames != "Unset"): if (ConfNames["LogFileName"] != None and ConfNames["LogFileName"] != "Unset"): - Dir(Parse(ConfDirs["LogDir"]), "Log") - LogFile = f"{Parse(ConfDirs["LogDir"])}/{Parse(ConfNames["LogFileName"])}.log" - with open(LogFile, "w", encoding="utf-8") as File: - File.write(f"{Parse(ConfLog["All"])}{Text}\n") + Dir(Parse(String = ConfDirs["LogDir"], Path = ConfPath), Output = False, CopyConf = False) + LogFile = f"{Parse(String = ConfDirs["LogDir"], Path = ConfPath)}/{Parse(String = ConfNames["LogFileName"], Path = ConfPath)}.log" + with open(LogFile, "a", encoding="utf-8", buffering=1) as File: + File.write(f"{Parse(String = ConfLog["All"], Path = ConfPath)}{Text}\n") + + +def DecideNewPath(FilePath): + UnsortedFile = os.path.basename(FilePath) + + for Output in ConfDirs["OutputDir"]: + Output = Parse(String = Output, Path = ConfPath) + + ParentName = os.path.basename(Output) + + DirConfName = Parse(String = ConfNames["DirConfName"], Path = ConfPath, Parent = ParentName) + DirConfPath = f"{Output}/{DirConfName}.toml" + + if not os.path.exists(DirConfPath): + continue + + FileConf = GetConf("Files", DirConfPath) + + if not FileConf: + continue + + for File in FileConf: + Pattern = File["Pattern"] + + TextOutput = Parse(ConfLog["MatchPattern"], VarCall = f"{UnsortedFile} against {Pattern}") + LogWrite(TextOutput) + Speak(TextOutput) + + if fnmatch.fnmatch(UnsortedFile, Pattern): + NewFileName = File["NewFileName"] + NewFileName = Parse(String = NewFileName, Path = DirConfPath, OrgFile = UnsortedFile) + return f"{Output}/{NewFileName}" + + +def Sort(FilePath): + NewPath = DecideNewPath(FilePath) + + if (NewPath == None): + Dir(Parse(String = ConfDirs["FailedDir"]), Output = False, CopyConf = False) + NewName = os.path.basename(FilePath) + Clone(FilePath, Parse(String = ConfDirs["FailedDir"]), NewName, True) + return + + NewDirPath = os.path.dirname(NewPath) + + NextNum = GetConf("NextNum", f"{NewDirPath}/{Parse(String = ConfNames["DirConfName"], Parent = os.path.basename(NewDirPath))}.toml") + NextChar = GetConf("NextChar", f"{NewDirPath}/{Parse(String = ConfNames["DirConfName"], Parent = os.path.basename(NewDirPath))}.toml") + + NewName = Parse(os.path.basename(FilePath), NextNum = NextNum, NextChar = NextChar) + + try: + Clone(FilePath, NewDirPath, NewName, True) + + TextOutput = Parse(String = ConfLog["Sorted"], VarCall = f"{FilePath} to {NewDirPath}/{NewName}") + LogWrite(TextOutput) + Speak(TextOutput) + + + except Exception: + Dir(Parse(String = ConfDirs["FailedDir"]), Output = False, CopyConf = False) + NewName = os.path.basename(FilePath) + Clone(FilePath, Parse(String = ConfDirs["FailedDir"]), NewName, True) + + TextOutput = Parse(String = ConfLog["NotSorted"], VarCall = FilePath) + LogWrite(TextOutput) + Speak(TextOutput) + return def Init(): global Conf, ConfVars, ConfDirs, ConfNames, ConfLog - LoadConf() + LoadGlobalConf() if (Conf != None): CheckConf() if (Start): - TextOutput = Parse(ConfLog["Start"]) + TextOutput = Parse(String = ConfLog["Start"], Path = ConfPath) LogWrite(TextOutput) Speak(TextOutput) Main() else: - TextOutput = Parse(ConfLog["NotStart"]) + TextOutput = Parse(String = ConfLog["NotStart"]) LogWrite(TextOutput) Speak(TextOutput) @@ -200,8 +294,27 @@ def Init(): def Main(): + for Output in ConfDirs["OutputDir"]: + Output = Parse(String = Output) + Dir(Output) + while True: - pass + for Input in ConfDirs["InputDir"]: + InputPath = Parse(Input) + + if not os.path.exists(InputPath): + continue + + for FileName in os.listdir(InputPath): + FilePath = os.path.join(InputPath, FileName) + + if os.path.isfile(FilePath): + FileName = os.path.basename(FilePath) + if (FileName.startswith(".") and Conf["DotFiles"] == 0): + continue + Sort(FilePath) + + time.sleep(Conf.get("CheckInput", 10)) Init() \ No newline at end of file