Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6a5b28631 | ||
|
|
4a52120b1a | ||
|
|
234889ba6d | ||
|
|
4389a1c76e | ||
|
|
aef32f6f3b | ||
|
|
78216fc352 | ||
|
|
16e6b7196e | ||
|
|
ec9e97e099 | ||
|
|
70a36d8b96 | ||
|
|
19ceec218f | ||
|
|
2cc72b1730 | ||
|
|
2bf4457981 | ||
|
|
5c39166a66 | ||
|
|
6a6e7dd494 | ||
|
|
a83b2e3c94 | ||
|
|
9be5938a87 | ||
|
|
4e00617e09 | ||
|
|
c5612654f8 | ||
|
|
324d06d6b4 | ||
|
|
b5afad3895 | ||
|
|
a314614055 | ||
|
|
3aa28f76cf | ||
|
|
89f4e59041 | ||
|
|
dc60a9828e | ||
|
|
4901d346f2 |
No files matched your search
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 18 KiB |
@@ -1,8 +1,8 @@
|
|||||||
Title = "SaraSortd Config"
|
Title = "SaraSortd Config"
|
||||||
Version = "6.1"
|
Version = "7.5"
|
||||||
SafeMode = 1
|
SafeMode = 1
|
||||||
SilentMode = 0
|
SilentMode = 0
|
||||||
CheckInput = 5
|
SortTimer = 1
|
||||||
DotFiles = 0
|
DotFiles = 0
|
||||||
Logging = 1
|
Logging = 1
|
||||||
OverwriteDirConf = 0
|
OverwriteDirConf = 0
|
||||||
@@ -22,11 +22,14 @@ Second = "[s]"
|
|||||||
VarCall = "%%"
|
VarCall = "%%"
|
||||||
|
|
||||||
[DirectoryPaths]
|
[DirectoryPaths]
|
||||||
RootDir = "C:/Users/StigOscarAadalen-Aas/OneDrive - Telemark fylkeskommune/Desktop/Testdir"
|
RootDir = "Unset"
|
||||||
InputDir = [ "%RootDir%/Input" ]
|
InputDir = [ "%RootDir%/Input" ]
|
||||||
OutputDir = [ "%RootDir%/Output" ]
|
OutputDir = [ "%RootDir%/Output" ]
|
||||||
LogDir = "%RootDir%/.Logs"
|
LogDir = "%RootDir%/.Logs"
|
||||||
FailedDir = "%RootDir%/.Failed"
|
FailedDir = "%RootDir%/.Failed"
|
||||||
|
DirConfDir = ""
|
||||||
|
|
||||||
|
[DirConfs]
|
||||||
|
|
||||||
[Names]
|
[Names]
|
||||||
LogFileName = "[y].[m].[d]"
|
LogFileName = "[y].[m].[d]"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Title = "Unset"
|
Title = "Unset"
|
||||||
Version = "6.1"
|
Version = "7.5"
|
||||||
ParentDir = "Unset"
|
ParentDir = "Unset"
|
||||||
SortDirs = 0
|
SortDirs = 0
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
Title = "Unset"
|
Title = "Unset"
|
||||||
Version = "6.1"
|
Version = "7.5"
|
||||||
LastFile = "Unset"
|
LastFile = "Unset"
|
||||||
ParentDir = "Unset"
|
ParentDir = "Unset"
|
||||||
ValidInputDirs = ["*"]
|
ValidInputDirs = ["*"]
|
||||||
FileLimit = 0
|
FileLimit = 0
|
||||||
DeleteOrg = 1
|
DeleteOrg = 1
|
||||||
|
Unzip = []
|
||||||
|
|
||||||
[[Files]]
|
[[Files]]
|
||||||
Pattern = "*"
|
Pattern = "*"
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
<img align="center" src="Banners/SaraSortd.png" width="100%" alt="SaraSortd"/>
|
<img align="center" src="https://git.srhtnin.garden/sarah/RepoAssets/raw/branch/main/SaraSortd/Banners/SaraSortd.png" width="100%" alt="SaraSortd: a file and directory sorting daemon built with python."/>
|
||||||
(Version = "6.1")
|
|
||||||
|
(Version = "7.5")
|
||||||
|
|
||||||
A python daemon that can automatically sort your files to your liking without using AI.
|
|
||||||
|
|
||||||
It's made by Sarah, hence the name. :3
|
It's made by Sarah, hence the name. :3
|
||||||
|
|
||||||
If the first number in the version goes up, it means that old config files will not work with the new SaraSortd.py.
|
If the first number in the version goes up, it means that old config files will not work with the new SaraSortd.py.
|
||||||
|
|
||||||
If the second number in the version goes up, something new got added, but your old config files will still work just fine.
|
If the second number in the version goes up, something new got added, but your old config files will still work just fine.
|
||||||
|
|
||||||
<img align="center" src="Banners/Usage.png" width="100%" alt="Usage"/>
|
---
|
||||||
|
|
||||||
|
<img align="center" src="https://git.srhtnin.garden/sarah/RepoAssets/raw/branch/main/SaraSortd/Banners/Usage.png" width="100%" alt="Usage"/>
|
||||||
|
|
||||||
|
|
||||||
SaraSortd is for people who don't want to manually sort files, people who want to organise files, and people who want an automatic naming standard for their files.
|
SaraSortd is for people who don't want to manually sort files, people who want to organise files, and people who want an automatic naming standard for their files.
|
||||||
|
|
||||||
@@ -21,54 +23,65 @@ Example use-cases:
|
|||||||
|
|
||||||
- Regularly cloning important files into a backup directory.
|
- Regularly cloning important files into a backup directory.
|
||||||
|
|
||||||
<img align="center" src="Banners/Dependencies.png" width="100%" alt="Dependencies"/>
|
---
|
||||||
|
|
||||||
|
<img align="center" src="https://git.srhtnin.garden/sarah/RepoAssets/raw/branch/main/SaraSortd/Banners/Dependencies.png" width="100%" alt="Dependencies"/>
|
||||||
|
|
||||||
|
|
||||||
python
|
python
|
||||||
|
|
||||||
toml: Use ```pip install toml``` to install toml.
|
toml: Use ```pip install toml``` to install toml.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
<img align="center" src="Banners/Installation.png" width="100%" alt="Installation"/>
|
<img align="center" src="https://git.srhtnin.garden/sarah/RepoAssets/raw/branch/main/SaraSortd/Banners/Installation.png" width="100%" alt="Installation"/>
|
||||||
|
|
||||||
|
|
||||||
|
Here's an [installation video](https://youtu.be/QkMZVSJ-PLQ?si=Nd20jDyHT7CN6Ztb) on Linux.
|
||||||
|
|
||||||
Here's an [installation video](https://youtu.be/ZMIaOhZA0kE).
|
|
||||||
Here's a step-by-step tutorial (in case you don't want to watch the video).
|
Here's a step-by-step tutorial (in case you don't want to watch the video).
|
||||||
|
|
||||||
1. Install ZIP-file.
|
1. Install ZIP-file by clicking the green "Code" button on the top right, then clicking "Download Zip".
|
||||||
2. Unzip ZIP-file wherever you want the program to live.
|
2. Unzip ZIP-file wherever you want the program to live.
|
||||||
3. Navigate to the new SaraSortd directory in your terminal.
|
3. Edit your config files to your liking. Make sure to edit your paths! (E.G. RootDir = "/home/janedoe/Programs/SaraSortd")
|
||||||
4. Edit your config files to your liking.
|
4. Navigate to the new SaraSortd directory in your terminal (CMD / PowerShell on Windows).
|
||||||
5. Run ```python SaraSortd.py``` to run the program. (This will make new directories for you.)
|
5. Run ```python SaraSortd.py``` to run the program. (This will make new directories for you.)
|
||||||
6. Edit your output directories' configs.
|
6. Edit your new output directories' configs.
|
||||||
|
|
||||||
<img align="center" src="Banners/ConfigInfo.png" width="100%" alt="Config Info"/>
|
---
|
||||||
|
|
||||||
|
<img align="center" src="https://git.srhtnin.garden/sarah/RepoAssets/raw/branch/main/SaraSortd/Banners/ConfigInfo.png" width="100%" alt="Config Info"/>
|
||||||
|
|
||||||
|
<img align="center" src="https://git.srhtnin.garden/sarah/RepoAssets/raw/branch/main/SaraSortd/Banners/GlobalConf.png" width="100%" alt="GlobalConf"/>
|
||||||
|
|
||||||
<img align="center" src="Banners/GlobalConf.png" width="100%" alt="GlobalConf"/>
|
|
||||||
|
|
||||||
```
|
```
|
||||||
Title = "SaraSortd Config" # NOTE: Do not change the title. This is so that the program knows which config file is which.
|
Title = "SaraSortd Config" # NOTE: Do not change the title. This is so that the program knows which config file is which.
|
||||||
|
|
||||||
Version = "6.1" # Just the version number for the config file.
|
Version = "7.5" # Just the version number for the config file.
|
||||||
|
|
||||||
SafeMode = 1 # If SaraSortd should stop if an error with the config appears. 1 is stop, 0 is continue.
|
SafeMode = 1 # If SaraSortd should stop if an error with the config appears. 1 is stop, 0 is continue.
|
||||||
|
|
||||||
SilentMode = 0 # If SaraSortd should write things it does, to the terminal.
|
SilentMode = 0 # If SaraSortd should write things it does, to the terminal.
|
||||||
|
|
||||||
CheckInput = 3 # How many seconds it takes for SaraSortd to check input directories for new files to sort.
|
SortTimer = 3 # How many seconds it takes for SaraSortd to check input directories for new files to sort.
|
||||||
|
|
||||||
DotFiles = 0 # If SaraSortd should sort files that begin with ".". This is mainly for linux users, since hidden files always have "." in front of them.
|
DotFiles = 0 # If SaraSortd should sort files that begin with ".". This is mainly for linux users, since hidden files always have "." in front of them.
|
||||||
|
# Files that are still downloading are never sorted or even checked (since 7.4): anything ending in .part (Firefox), .crdownload (Chrome and other Chromium browsers) or .download (Safari), and a file next to its own unfinished download (like the empty "video.mp4" Firefox makes beside "video.mp4.part"). They're sorted as usual once the download finishes.
|
||||||
|
|
||||||
Logging = 1 # If SaraSortd's logging system should be active. If this is 0, nothing will be logged into files.
|
Logging = 1 # If SaraSortd's logging system should be active. If this is 0, nothing will be logged into files.
|
||||||
|
|
||||||
OverwriteDirConf = 0 # If SaraSortd should overwrite directory configs with the config templates. 0 is no, 1 is yes.
|
OverwriteDirConf = 0 # If SaraSortd should overwrite directory configs with the config templates. 0 is no, 1 is yes.
|
||||||
|
|
||||||
[Variables] # Only use these in config files, not the actual file names themselves.
|
[Variables] # Only use these in config files, not the actual file names themselves.
|
||||||
NextNum = "*" # The next available number. If 1 already exists, 2 will be used.
|
# Directory paths (InputDir, OutputDir, LogDir, FailedDir, ValidInputDirs, DirConfDir, [DirConfs]) only use %Name% lookups and the date and time variables (since 7.3). The single-character variables below are about the file being sorted, so a path like "%RootDir%/Stuff&Things" stays as it is. A "~" at the start of a path is your home directory.
|
||||||
|
NextNum = "!" # The next available number. If 1 already exists, 2 will be used.
|
||||||
|
|
||||||
NextChar = "&" # The next available character. If A already exists, B will be used.
|
NextChar = "&" # The next available character. If A already exists, B will be used.
|
||||||
|
|
||||||
Parent = "@" # The name of the parent directory.
|
Parent = "@" # The name of the parent directory.
|
||||||
|
|
||||||
OrgFileName = "\~" # The original name of the sorted file. If "blahblah.txt" gets sorted, "~" would be equal to "blahblah".
|
OrgFileName = "~" # The original name of the sorted file. If "blahblah.txt" gets sorted, "~" would be equal to "blahblah".
|
||||||
|
|
||||||
OrgFileType = "^" # The original file type of the sorted file. If "example.png" gets sorted, "^" would be equal to ".png".
|
OrgFileType = "^" # The original file type of the sorted file. If "example.png" gets sorted, "^" would be equal to ".png".
|
||||||
|
|
||||||
@@ -92,15 +105,25 @@ RootDir = "Unset" # A variable directory. This doesn't NEED to exist for the pro
|
|||||||
InputDir = ["%RootDir%"] # If files get put here, they'll be sorted. This is a list, so you can add multiple. E.G: ["%RootDir%", "%RootDir%/input"]
|
InputDir = ["%RootDir%"] # If files get put here, they'll be sorted. This is a list, so you can add multiple. E.G: ["%RootDir%", "%RootDir%/input"]
|
||||||
|
|
||||||
OutputDir = ["%RootDir%/Output"] # Where sorted files go. This is a list, so you can add multiple. E.G: ["%RootDir%/Output", "%RootDir%/Output2"]. The output directories have separate config files.
|
OutputDir = ["%RootDir%/Output"] # Where sorted files go. This is a list, so you can add multiple. E.G: ["%RootDir%/Output", "%RootDir%/Output2"]. The output directories have separate config files.
|
||||||
|
# Outputs are tried in this order, first with their specific patterns. Catch-all patterns ("*") are only a fallback for files no specific pattern took (since 7.3), so an output that takes everything can be anywhere in the list.
|
||||||
|
|
||||||
LogDir = "%RootDir%/.Logs" # Where log files from SaraSortd go.
|
LogDir = "%RootDir%/.Logs" # Where log files from SaraSortd go.
|
||||||
|
|
||||||
FailedDir ="%RootDir%/.Failed" # For files that failed to be sorted.
|
FailedDir ="%RootDir%/.Failed" # For files that failed to be sorted.
|
||||||
|
|
||||||
[Names]
|
DirConfDir = "" # Optional (since 7.2). Where directory configs live. If this is "", every directory keeps its config inside itself. If it's set, the configs (and history files) of all directories go here instead, so the directories stay clean. Relative paths count from GlobalConf.toml's own folder. E.G: DirConfDir = "Dirs"
|
||||||
LogFileName = "[y].[m].[d]._*" # Name for log files.
|
# Each config is named after its directory's path below RootDir, with "/" turned into "_", and InDirConfName/OutDirConfName applied without the leading ".". E.G: "%RootDir%/Pictures/Cats" gets "Pictures_CatsConfig.toml".
|
||||||
|
|
||||||
DirConfName = ".@Config" # Name for config files for directories.
|
[DirConfs] # Optional (since 7.2). A config file for one directory, wherever you like. These win over DirConfDir. Relative paths count from GlobalConf.toml's own folder. E.G:
|
||||||
|
# "%RootDir%/Downloads" = "Configs/Downloads.toml"
|
||||||
|
# "%RootDir%/Pictures" = { In = "Configs/PicturesIn.toml", Out = "Configs/PicturesOut.toml" } # Separate input and output configs.
|
||||||
|
|
||||||
|
[Names]
|
||||||
|
LogFileName = "[y].[m].[d]" # Name for log files.
|
||||||
|
|
||||||
|
InDirConfName = ".@Config" # Name for config files for input directories.
|
||||||
|
|
||||||
|
OutDirConfName = ".@Config" # Name for config files for output directories. If a directory is both an input and an output and both names are the same, it uses one config file for both: its [[Files]] are then both the input's and the output's rules, and SaraSortd gives the file every key both roles need (since 7.3).
|
||||||
|
|
||||||
FileHistoryName = ".@History" # Name for file history files. This keeps track of sorted files if an input and output are the same directory.
|
FileHistoryName = ".@History" # Name for file history files. This keeps track of sorted files if an input and output are the same directory.
|
||||||
|
|
||||||
@@ -137,16 +160,17 @@ Unzipping = "Unzipping: %%." # When a directory gets unzipped.
|
|||||||
NoPermission = "Error: No permission to %%." # When SaraSortd lacks permission to do something.
|
NoPermission = "Error: No permission to %%." # When SaraSortd lacks permission to do something.
|
||||||
# %% is the action that the program had no permission to do. E.G. "remove directory /home/bob/exampledir".
|
# %% is the action that the program had no permission to do. E.G. "remove directory /home/bob/exampledir".
|
||||||
```
|
```
|
||||||
|
---
|
||||||
|
|
||||||
|
<img align="center" src="https://git.srhtnin.garden/sarah/RepoAssets/raw/branch/main/SaraSortd/Banners/OutputDirConf.png" width="100%" alt="OutputDirConf"/>
|
||||||
|
|
||||||
<img align="center" src="Banners/OutputDirConf.png" width="100%" alt="OutputDirConf"/>
|
|
||||||
|
|
||||||
The output directory config references the SaraSortd config (GlobalConf.toml). This means that you can, for example, use the character assigned to "NextNum" in GlobalConf.toml in this config (OutputDirConf.toml). This applies to all Variables in the SaraSortd config (GlobalConf.toml).
|
The output directory config references the SaraSortd config (GlobalConf.toml). This means that you can, for example, use the character assigned to "NextNum" in GlobalConf.toml in this config (OutputDirConf.toml). This applies to all Variables in the SaraSortd config (GlobalConf.toml).
|
||||||
|
|
||||||
```
|
```
|
||||||
Title = "Unset" # NOTE: Do not change the title. This is so that the program knows which config file is which.
|
Title = "Unset" # NOTE: Do not change the title. This is so that the program knows which config file is which.
|
||||||
|
|
||||||
Version = "6.1" # The version number of the config.
|
Version = "7.5" # The version number of the config.
|
||||||
|
|
||||||
LastFile = "Unset" # The last file that was sorted to this directory. It gets automatically updated whenever a file gets sorted to this directory.
|
LastFile = "Unset" # The last file that was sorted to this directory. It gets automatically updated whenever a file gets sorted to this directory.
|
||||||
|
|
||||||
@@ -158,6 +182,9 @@ FileLimit = 0 # The max limit of files a directory can have. If this is 0, then
|
|||||||
|
|
||||||
DeleteOrg = 1 # If SaraSortd should delete the original file or not. If this is 1, then sorted files will be 'moved' to an output directory. If not, they will be 'copied' to an output directory.
|
DeleteOrg = 1 # If SaraSortd should delete the original file or not. If this is 1, then sorted files will be 'moved' to an output directory. If not, they will be 'copied' to an output directory.
|
||||||
|
|
||||||
|
Unzip = [] # What type of files this output directory should try to unzip. E.G:
|
||||||
|
# Unzip = ["zip", "tar.gz"] would unzip .zip files and .tar files. This pairs insanely well with input directories' config's "SortDirs" parameter.
|
||||||
|
|
||||||
[[Files]] # The double brackets means that you can add multiple types of files to be sorted here. Just copy [[Files]] and all the parameters under it, and paste it under or over this section, then you may change the values of the new parameters. E.G:
|
[[Files]] # The double brackets means that you can add multiple types of files to be sorted here. Just copy [[Files]] and all the parameters under it, and paste it under or over this section, then you may change the values of the new parameters. E.G:
|
||||||
|
|
||||||
# [[Files]]
|
# [[Files]]
|
||||||
@@ -187,18 +214,23 @@ NextChar = "A" # Used to keep track of the next available letter for file names.
|
|||||||
CaseSensitive = 0 # Whether this "Pattern" is case-sensitive or not. 1 is yes, 0 is no. E.G: if CaseSensitive = 1 and this "Pattern" is "*.PNG", a file named "picture.png" won't get sorted to this directory. If CaseSensitive = 0 and this "Pattern" is "*.PNG", a file named "picture.png" *will* get sorted to this directory.
|
CaseSensitive = 0 # Whether this "Pattern" is case-sensitive or not. 1 is yes, 0 is no. E.G: if CaseSensitive = 1 and this "Pattern" is "*.PNG", a file named "picture.png" won't get sorted to this directory. If CaseSensitive = 0 and this "Pattern" is "*.PNG", a file named "picture.png" *will* get sorted to this directory.
|
||||||
|
|
||||||
Overwrite = 1 # Whether this specific file type can be overwritten by others of the same type. 0 is no, 1 is yes.
|
Overwrite = 1 # Whether this specific file type can be overwritten by others of the same type. 0 is no, 1 is yes.
|
||||||
|
|
||||||
|
# ValidInputDirs = ["%RootDir%/Downloads"] # Optional (since 7.1). Like the ValidInputDirs above, but only for this [[Files]] entry, so different input directories can send different file types here. Without it, this entry takes files from every input directory the ValidInputDirs above allows.
|
||||||
```
|
```
|
||||||
|
|
||||||
<img align="center" src="Banners/InputDirConf.png" width="100%" alt="InputDirConf"/>
|
---
|
||||||
|
|
||||||
|
<img align="center" src="https://git.srhtnin.garden/sarah/RepoAssets/raw/branch/main/SaraSortd/Banners/InputDirConf.png" width="100%" alt="InputDirConf"/>
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
Title = "Unset" # NOTE: Do not change the title. This is so that the program knows which config file is which.
|
Title = "Unset" # NOTE: Do not change the title. This is so that the program knows which config file is which.
|
||||||
|
|
||||||
Version = "6.1"
|
Version = "7.5"
|
||||||
|
|
||||||
ParentDir = "Unset" # The name of this directory. It gets automatically updated when this config gets copied to an output directory.
|
ParentDir = "Unset" # The name of this directory. It gets automatically updated when this config gets copied to an output directory.
|
||||||
|
|
||||||
SortDirs = 0
|
SortDirs = 0 # If the input directory should be allowed to sort directories within it. Automatically uses .zip to move directories.
|
||||||
|
|
||||||
[[Files]] # The double brackets means that you can add multiple types of files to be sorted here. Just copy [[Files]] and all the parameters under it, and paste it under or over this section, then you may change the values of the new parameters.
|
[[Files]] # The double brackets means that you can add multiple types of files to be sorted here. Just copy [[Files]] and all the parameters under it, and paste it under or over this section, then you may change the values of the new parameters.
|
||||||
|
|
||||||
@@ -206,3 +238,5 @@ Pattern = "*" # A pattern used to decide if a file should be sorted or not. * is
|
|||||||
|
|
||||||
CaseSensitive = 0 # Whether Pattern is case-sensitive or not. If CaseSensitive = 1 and your pattern is "*.PNG", then a file named "example.png" won't be sorted.
|
CaseSensitive = 0 # Whether Pattern is case-sensitive or not. If CaseSensitive = 1 and your pattern is "*.PNG", then a file named "example.png" won't be sorted.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
@@ -6,7 +6,7 @@ import shutil
|
|||||||
import time
|
import time
|
||||||
import toml
|
import toml
|
||||||
|
|
||||||
# Version = "6.1"
|
# Version = "7.5"
|
||||||
|
|
||||||
ConfPath = "./GlobalConf.toml"
|
ConfPath = "./GlobalConf.toml"
|
||||||
|
|
||||||
@@ -151,8 +151,10 @@ def Parse(
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
Value = GetConf(Content, Path)
|
Value = GetConf(Content, Path)
|
||||||
if Value is not None:
|
if Value is None:
|
||||||
return Value
|
return Match.group(0) # Unknown names are left as they are (they used to crash).
|
||||||
|
|
||||||
|
return ResolveLookups(str(Value), Path)
|
||||||
|
|
||||||
Now = datetime.datetime.now()
|
Now = datetime.datetime.now()
|
||||||
if OrgFile is not None:
|
if OrgFile is not None:
|
||||||
@@ -189,10 +191,110 @@ def Parse(
|
|||||||
return String
|
return String
|
||||||
|
|
||||||
|
|
||||||
|
def ResolveLookups(String, Path = ConfPath, Depth = 0):
|
||||||
|
# %Name% lookups, also inside the values they bring in (since 7.3).
|
||||||
|
# E.G: ImageDir = "%RootDir%/Images" used as "%ImageDir%" gives the full path. Unknown names are left as they are.
|
||||||
|
def Replacer(Match):
|
||||||
|
Value = GetConf(Match.group(1), Path) if Match.group(1) else None
|
||||||
|
if Value is None or Depth >= 10:
|
||||||
|
return Match.group(0)
|
||||||
|
|
||||||
|
return ResolveLookups(str(Value), Path, Depth + 1)
|
||||||
|
|
||||||
|
VarCall0 = re.escape(ConfVars["VarCall"][0])
|
||||||
|
VarCall1 = re.escape(ConfVars["VarCall"][1])
|
||||||
|
return re.sub(f"{VarCall0}(.*?){VarCall1}", Replacer, String)
|
||||||
|
|
||||||
|
|
||||||
|
def ParsePath(String):
|
||||||
|
# Directory paths only get %Name% lookups and the date and time variables (since 7.3). The single-character
|
||||||
|
# variables (NextNum, NextChar, Parent, OrgFileName, OrgFileType) are about the file being sorted, so a path
|
||||||
|
# like "~/Stuff&Things" stays as it is. A "~" at the start is your home directory.
|
||||||
|
Now = datetime.datetime.now()
|
||||||
|
DateValues = {
|
||||||
|
ConfVars["Year"]: str(Now.year),
|
||||||
|
ConfVars["Month"]: str(Now.month).rjust(2, "0"),
|
||||||
|
ConfVars["Day"]: str(Now.day).rjust(2, "0"),
|
||||||
|
ConfVars["Hour"]: str(Now.hour).rjust(2, "0"),
|
||||||
|
ConfVars["Minute"]: str(Now.minute).rjust(2, "0"),
|
||||||
|
ConfVars["Second"]: str(Now.second).rjust(2, "0"),
|
||||||
|
}
|
||||||
|
|
||||||
|
for Key, Value in DateValues.items():
|
||||||
|
if Key and Key in String:
|
||||||
|
String = String.replace(Key, Value)
|
||||||
|
|
||||||
|
return os.path.expanduser(ResolveLookups(String))
|
||||||
|
|
||||||
|
|
||||||
|
def ConfFolder(PathText):
|
||||||
|
# Relative paths in GlobalConf.toml (DirConfDir, [DirConfs]) count from GlobalConf.toml's own folder.
|
||||||
|
PathText = ParsePath(PathText)
|
||||||
|
if os.path.isabs(PathText):
|
||||||
|
return os.path.normpath(PathText)
|
||||||
|
return os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(ConfPath)), PathText))
|
||||||
|
|
||||||
|
|
||||||
|
def Slashes(Path):
|
||||||
|
# Windows paths use "\\" (and configs may mix both); compare and name them with "/" only.
|
||||||
|
return Path.replace("\\", "/")
|
||||||
|
|
||||||
|
|
||||||
|
def FlatName(Path):
|
||||||
|
# A directory's name for files kept outside it: its path below RootDir (or its whole path), "/" turned into "_".
|
||||||
|
# E.G: "%RootDir%/Pictures/Cats" becomes "Pictures_Cats". On Windows a drive's ":" is dropped (not allowed in names).
|
||||||
|
Root = Slashes(ParsePath(ConfDirs.get("RootDir", ""))).rstrip("/")
|
||||||
|
Path = Slashes(Path).rstrip("/")
|
||||||
|
if Root and Path.startswith(Root + "/"):
|
||||||
|
Relative = Path[len(Root) + 1:]
|
||||||
|
else:
|
||||||
|
Relative = Path.strip("/") or os.path.basename(Path)
|
||||||
|
return Relative.replace(":", "").replace("/", "_")
|
||||||
|
|
||||||
|
|
||||||
|
def CentralConf(Path, Kind):
|
||||||
|
# Since 7.2, a directory's config can live outside it, like SaraTag's Lexicons:
|
||||||
|
# [DirConfs] names a file per directory ("dir" = "file", or "dir" = { In = "file", Out = "file" }),
|
||||||
|
# and DirConfDir holds the configs of every directory [DirConfs] doesn't name. None means inside the directory.
|
||||||
|
for Key, Value in (Conf.get("DirConfs") or {}).items():
|
||||||
|
if Slashes(ParsePath(Key)).rstrip("/") == Slashes(Path).rstrip("/"):
|
||||||
|
if isinstance(Value, dict):
|
||||||
|
Value = Value.get(Kind)
|
||||||
|
if Value:
|
||||||
|
return ConfFolder(Value)
|
||||||
|
|
||||||
|
if ConfDirs.get("DirConfDir"):
|
||||||
|
NameKey = "InDirConfName" if Kind == "In" else "OutDirConfName"
|
||||||
|
Name = Parse(String = ConfNames[NameKey], Parent = FlatName(Path)).lstrip(".")
|
||||||
|
return f"{ConfFolder(ConfDirs['DirConfDir'])}/{Name}.toml"
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def DirConfPath(Path, Kind):
|
||||||
|
# The config of an input ("In") or output ("Out") directory.
|
||||||
|
Central = CentralConf(Path, Kind)
|
||||||
|
if Central:
|
||||||
|
return Central
|
||||||
|
|
||||||
|
NameKey = "InDirConfName" if Kind == "In" else "OutDirConfName"
|
||||||
|
return f"{Path}/{Parse(String = ConfNames[NameKey], Path = ConfPath, Parent = os.path.basename(Path))}.toml"
|
||||||
|
|
||||||
|
|
||||||
|
def HistoryPath(Path):
|
||||||
|
# The history file of a directory that sorts into itself. It goes next to the directory's config
|
||||||
|
# when that's kept outside the directory.
|
||||||
|
Central = CentralConf(Path, "In") or CentralConf(Path, "Out")
|
||||||
|
if Central:
|
||||||
|
Name = Parse(String = ConfNames["FileHistoryName"], Parent = FlatName(Path)).lstrip(".")
|
||||||
|
return f"{os.path.dirname(Central)}/{Name}.toml"
|
||||||
|
|
||||||
|
return f"{Path}/{Parse(String = ConfNames['FileHistoryName'], Parent = os.path.basename(Path))}.toml"
|
||||||
|
|
||||||
|
|
||||||
def Clone(Source, Destination, NewName, Delete = False):
|
def Clone(Source, Destination, NewName, Delete = False):
|
||||||
if (os.path.dirname(Source) == Destination):
|
if (os.path.dirname(Source) == Destination):
|
||||||
FileHistoryPath = f"{Destination}/{Parse(ConfNames["FileHistoryName"], Parent = os.path.basename(Destination))}.toml"
|
FileHistoryPath = HistoryPath(Destination)
|
||||||
print("FileHistoryPath:",FileHistoryPath)
|
|
||||||
|
|
||||||
if not os.path.exists(FileHistoryPath):
|
if not os.path.exists(FileHistoryPath):
|
||||||
with open(FileHistoryPath, "w", encoding = "utf-8") as HistoryFile:
|
with open(FileHistoryPath, "w", encoding = "utf-8") as HistoryFile:
|
||||||
@@ -204,61 +306,66 @@ def Clone(Source, Destination, NewName, Delete = False):
|
|||||||
with open(FileHistoryPath, "w", encoding = "utf-8") as HistoryFile:
|
with open(FileHistoryPath, "w", encoding = "utf-8") as HistoryFile:
|
||||||
toml.dump(HistoryFileData, HistoryFile)
|
toml.dump(HistoryFileData, HistoryFile)
|
||||||
|
|
||||||
os.rename(Source, os.path.join(Destination, NewName))
|
os.replace(Source, os.path.join(Destination, NewName)) # overwrites on Windows too, like rename on Linux
|
||||||
return
|
return
|
||||||
|
|
||||||
Dir(Destination, Output = False, CopyConf = False)
|
Dir(Destination, Output = False, CopyConf = False)
|
||||||
shutil.copy2(Source, Destination)
|
# Straight to the new name: copying under the old name first could land on a file that's already
|
||||||
|
# there (E.G. a template, when DirConfDir is GlobalConf.toml's own folder).
|
||||||
BaseName = os.path.basename(Source)
|
shutil.copy2(Source, os.path.join(Destination, NewName))
|
||||||
shutil.move(os.path.join(Destination, BaseName), os.path.join(Destination, NewName))
|
|
||||||
|
|
||||||
if Delete:
|
if Delete:
|
||||||
os.remove(Source)
|
os.remove(Source)
|
||||||
|
|
||||||
|
|
||||||
|
def CompleteDirConf(DirConf, Template, Kind):
|
||||||
|
# Gives an existing directory config the keys it's missing from the template (since 7.3). A directory that is
|
||||||
|
# both an input and an output shares one config by default, so the second role's keys used to be missing and
|
||||||
|
# SaraSortd crashed. Output configs' [[Files]] entries also get the output keys (NewFileName, NextNum, ...).
|
||||||
|
Data = toml.load(DirConf)
|
||||||
|
TemplateData = toml.load(Template)
|
||||||
|
Changed = False
|
||||||
|
|
||||||
|
for Key, Value in TemplateData.items():
|
||||||
|
if Key != "Files" and Key not in Data:
|
||||||
|
Data[Key] = Value
|
||||||
|
Changed = True
|
||||||
|
|
||||||
|
if Kind == "Out":
|
||||||
|
Defaults = (TemplateData.get("Files") or [{}])[0]
|
||||||
|
for Entry in Data.get("Files", []):
|
||||||
|
for Key, Value in Defaults.items():
|
||||||
|
if Key not in Entry:
|
||||||
|
Entry[Key] = Value
|
||||||
|
Changed = True
|
||||||
|
|
||||||
|
if Changed:
|
||||||
|
with open(DirConf, "w", encoding = "utf-8") as File:
|
||||||
|
toml.dump(Data, File)
|
||||||
|
|
||||||
|
|
||||||
def Dir(Path, Output = True, CopyConf = "OutputDirConf.toml"):
|
def Dir(Path, Output = True, CopyConf = "OutputDirConf.toml"):
|
||||||
ParentName = os.path.basename(Path)
|
ParentName = os.path.basename(Path)
|
||||||
os.makedirs(Path, exist_ok=True)
|
os.makedirs(Path, exist_ok=True)
|
||||||
|
|
||||||
if CopyConf:
|
if CopyConf:
|
||||||
if "Out" in CopyConf:
|
for Kind in ("Out", "In"):
|
||||||
NewDirConf = f"{Parse(String = ConfNames['OutDirConfName'], Path = ConfPath, Parent = ParentName)}.toml"
|
if Kind not in CopyConf:
|
||||||
|
continue
|
||||||
|
|
||||||
if Conf["OverwriteDirConf"] == 1 or not os.path.exists(os.path.join(f"{Path}/{NewDirConf}")):
|
DirConf = DirConfPath(Path, Kind)
|
||||||
Clone(CopyConf, Path, NewDirConf)
|
|
||||||
UpdateConf(
|
|
||||||
f"{Path}/{Parse(String = ConfNames['OutDirConfName'], Path = ConfPath, Parent = ParentName)}.toml",
|
|
||||||
"ParentDir",
|
|
||||||
ParentName,
|
|
||||||
)
|
|
||||||
|
|
||||||
UpdateConf(
|
if Conf["OverwriteDirConf"] == 1 or not os.path.exists(DirConf):
|
||||||
f"{Path}/{Parse(String = ConfNames['OutDirConfName'], Path = ConfPath, Parent = ParentName)}.toml",
|
Clone(CopyConf, os.path.dirname(DirConf), os.path.basename(DirConf))
|
||||||
"Title",
|
UpdateConf(DirConf, "ParentDir", ParentName)
|
||||||
f"{ParentName} Config",
|
UpdateConf(DirConf, "Title", f"{ParentName} Config")
|
||||||
)
|
|
||||||
|
|
||||||
if "In" in CopyConf:
|
else:
|
||||||
NewDirConf = f"{Parse(String = ConfNames['InDirConfName'], Path = ConfPath, Parent = ParentName)}.toml"
|
CompleteDirConf(DirConf, CopyConf, Kind)
|
||||||
|
|
||||||
if Conf["OverwriteDirConf"] == 1 or not os.path.exists(os.path.join(f"{Path}/{NewDirConf}")):
|
|
||||||
Clone(CopyConf, Path, NewDirConf)
|
|
||||||
UpdateConf(
|
|
||||||
f"{Path}/{Parse(String = ConfNames['InDirConfName'], Path = ConfPath, Parent = ParentName)}.toml",
|
|
||||||
"ParentDir",
|
|
||||||
ParentName,
|
|
||||||
)
|
|
||||||
|
|
||||||
UpdateConf(
|
|
||||||
f"{Path}/{Parse(String = ConfNames['InDirConfName'], Path = ConfPath, Parent = ParentName)}.toml",
|
|
||||||
"Title",
|
|
||||||
f"{ParentName} Config",
|
|
||||||
)
|
|
||||||
|
|
||||||
if Output:
|
if Output:
|
||||||
ParsedPath = Parse(String = Path)
|
ParsedPath = ParsePath(Path)
|
||||||
ParsedOutputs = [Parse(String = Dir) for Dir in ConfDirs["OutputDir"]]
|
ParsedOutputs = [ParsePath(Dir) for Dir in ConfDirs["OutputDir"]]
|
||||||
if ParsedPath not in ParsedOutputs:
|
if ParsedPath not in ParsedOutputs:
|
||||||
UpdateConf(ConfPath, "OutputDir", Path, True)
|
UpdateConf(ConfPath, "OutputDir", Path, True)
|
||||||
|
|
||||||
@@ -270,27 +377,62 @@ def LogWrite(Text):
|
|||||||
ConfNames["LogFileName"] is not None
|
ConfNames["LogFileName"] is not None
|
||||||
and ConfNames["LogFileName"] != "Unset"
|
and ConfNames["LogFileName"] != "Unset"
|
||||||
):
|
):
|
||||||
Dir(Parse(String = ConfDirs["LogDir"], Path = ConfPath), Output = False, CopyConf = False)
|
Dir(ParsePath(ConfDirs["LogDir"]), Output = False, CopyConf = False)
|
||||||
LogFile = f"{Parse(String = ConfDirs['LogDir'], Path = ConfPath)}/{Parse(String = ConfNames['LogFileName'], Path = ConfPath)}.log"
|
LogFile = f"{ParsePath(ConfDirs['LogDir'])}/{Parse(String = ConfNames['LogFileName'], Path = ConfPath)}.log"
|
||||||
with open(LogFile, "a", encoding = "utf-8", buffering = 1) as File:
|
with open(LogFile, "a", encoding = "utf-8", buffering = 1) as File:
|
||||||
File.write(f"{Parse(String = ConfLog['All'], Path = ConfPath)}{Text}\n")
|
File.write(f"{Parse(String = ConfLog['All'], Path = ConfPath)}{Text}\n")
|
||||||
|
|
||||||
|
|
||||||
|
def FileInputValid(File, FilePath, ParentName):
|
||||||
|
# A [[Files]] entry can have its own ValidInputDirs (since 7.1), so one output directory can take
|
||||||
|
# different patterns from different input directories. Without one, the entry takes files from
|
||||||
|
# every input directory the output directory's own ValidInputDirs allows.
|
||||||
|
ValidInputDirs = File.get("ValidInputDirs")
|
||||||
|
if ValidInputDirs is None:
|
||||||
|
return True
|
||||||
|
|
||||||
|
for ValidInputDir in ValidInputDirs:
|
||||||
|
if fnmatch.fnmatchcase(os.path.dirname(FilePath), ParsePath(ValidInputDir)):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# Files that are still downloading (since 7.4): Firefox's .part, Chromium's .crdownload and Safari's .download.
|
||||||
|
# SaraSortd never touches them, not even to check them against patterns.
|
||||||
|
PartialDownloadSuffixes = (".part", ".crdownload", ".download")
|
||||||
|
|
||||||
|
|
||||||
|
def IsPartialDownload(FilePath):
|
||||||
|
if os.path.basename(FilePath).lower().endswith(PartialDownloadSuffixes):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# A file next to its own unfinished download, E.G. the empty "video.mp4" Firefox makes beside
|
||||||
|
# "video.mp4.part" and fills in at the end. Moving it would break the download.
|
||||||
|
return any(os.path.exists(FilePath + Suffix) for Suffix in PartialDownloadSuffixes)
|
||||||
|
|
||||||
|
|
||||||
|
def IsCatchAll(Pattern):
|
||||||
|
# A pattern made of only "*" takes any file.
|
||||||
|
return Pattern != "" and set(Pattern) == {"*"}
|
||||||
|
|
||||||
|
|
||||||
def DecideNewPath(FilePath):
|
def DecideNewPath(FilePath):
|
||||||
UnsortedFile = os.path.basename(FilePath)
|
UnsortedFile = os.path.basename(FilePath)
|
||||||
|
Candidates = []
|
||||||
|
|
||||||
for Output in ConfDirs["OutputDir"]:
|
for Output in ConfDirs["OutputDir"]:
|
||||||
Output = Parse(String = Output, Path = ConfPath)
|
Output = ParsePath(Output)
|
||||||
|
|
||||||
ParentName = os.path.basename(Output)
|
ParentName = os.path.basename(Output)
|
||||||
|
|
||||||
OutDirConfName = Parse(String = ConfNames["OutDirConfName"], Path = ConfPath, Parent = ParentName)
|
OutDirConfPath = DirConfPath(Output, "Out")
|
||||||
OutDirConfPath = f"{Output}/{OutDirConfName}.toml"
|
|
||||||
|
|
||||||
ValidInputDirs = GetConf("ValidInputDirs", OutDirConfPath)
|
ValidInputDirs = GetConf("ValidInputDirs", OutDirConfPath) or []
|
||||||
|
InputDirValid = False
|
||||||
|
|
||||||
for ValidInputDir in ValidInputDirs:
|
for ValidInputDir in ValidInputDirs:
|
||||||
ValidInputDir = Parse(String = ValidInputDir, Parent = GetConf("ParentDir", OutDirConfPath))
|
ValidInputDir = ParsePath(ValidInputDir)
|
||||||
|
|
||||||
TextOutput = Parse(String = ConfLog["MatchPattern"], VarCall = f"{os.path.dirname(FilePath)} against {ValidInputDir}")
|
TextOutput = Parse(String = ConfLog["MatchPattern"], VarCall = f"{os.path.dirname(FilePath)} against {ValidInputDir}")
|
||||||
LogWrite(TextOutput)
|
LogWrite(TextOutput)
|
||||||
@@ -322,90 +464,94 @@ def DecideNewPath(FilePath):
|
|||||||
if not FileConf:
|
if not FileConf:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for File in FileConf:
|
Candidates.append((Output, ParentName, OutDirConfPath, FileConf))
|
||||||
Pattern = File["Pattern"]
|
|
||||||
|
|
||||||
TextOutput = Parse(String = ConfLog["MatchPattern"], VarCall = f"{UnsortedFile} against {Pattern}")
|
# Specific patterns first, in OutputDir order. Catch-alls ("*") are a fallback for files nothing else took,
|
||||||
LogWrite(TextOutput)
|
# so an output with the template's catch-all doesn't take every file just because it's listed first (since 7.3).
|
||||||
Speak(TextOutput)
|
for CatchAll in (False, True):
|
||||||
|
for Output, ParentName, OutDirConfPath, FileConf in Candidates:
|
||||||
if File["CaseSensitive"] == 1:
|
for FileIndex, File in enumerate(FileConf):
|
||||||
Match = fnmatch.fnmatchcase(UnsortedFile, Pattern)
|
if IsCatchAll(File["Pattern"]) != CatchAll or not FileInputValid(File, FilePath, ParentName):
|
||||||
|
|
||||||
else:
|
|
||||||
Match = fnmatch.fnmatch(UnsortedFile.lower(), Pattern.lower())
|
|
||||||
|
|
||||||
if Match:
|
|
||||||
NewFileName = File["NewFileName"]
|
|
||||||
|
|
||||||
NewFileName = Parse(String = NewFileName, Path = OutDirConfPath, OrgFile = UnsortedFile, NextNum = File["NextNum"], NextChar = File["NextChar"], Parent = ParentName)
|
|
||||||
|
|
||||||
if File["Overwrite"] == 0 and os.path.exists(os.path.join(Output, NewFileName)):
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if ConfVars["NextNum"] in File["NewFileName"]:
|
Pattern = File["Pattern"]
|
||||||
ConfData = toml.load(OutDirConfPath)
|
|
||||||
Width = len(File["NextNum"])
|
|
||||||
NewNum = str((int(File["NextNum"]) + 1)).rjust(Width, "0")
|
|
||||||
print(File["NextNum"],"=>",NewNum)
|
|
||||||
|
|
||||||
for Item in ConfData.get("Files", []):
|
TextOutput = Parse(String = ConfLog["MatchPattern"], VarCall = f"{UnsortedFile} against {Pattern}")
|
||||||
if Item.get("Pattern") == File["Pattern"]:
|
LogWrite(TextOutput)
|
||||||
Item["NextNum"] = NewNum
|
Speak(TextOutput)
|
||||||
break
|
|
||||||
|
|
||||||
with open(OutDirConfPath, "w", encoding="utf-8") as ConfFile:
|
if File["CaseSensitive"] == 1:
|
||||||
toml.dump(ConfData, ConfFile)
|
Match = fnmatch.fnmatchcase(UnsortedFile, Pattern)
|
||||||
|
|
||||||
TextOutput = Parse(String = ConfLog["ValueSet"], VarCall = f"NextNum to {NewNum}")
|
else:
|
||||||
LogWrite(TextOutput)
|
Match = fnmatch.fnmatch(UnsortedFile.lower(), Pattern.lower())
|
||||||
Speak(TextOutput)
|
|
||||||
|
|
||||||
if ConfVars["NextChar"] in File["NewFileName"]:
|
if Match:
|
||||||
CurrentChar = File["NextChar"]
|
NewFileName = File["NewFileName"]
|
||||||
CurrentChar = CurrentChar.upper()
|
|
||||||
Result = list(CurrentChar)
|
|
||||||
|
|
||||||
Count = len(Result) - 1
|
NewFileName = Parse(String = NewFileName, Path = OutDirConfPath, OrgFile = UnsortedFile, NextNum = File["NextNum"], NextChar = File["NextChar"], Parent = ParentName)
|
||||||
|
|
||||||
while Count >= 0:
|
if File["Overwrite"] == 0 and os.path.exists(os.path.join(Output, NewFileName)):
|
||||||
if Result[Count] == "Z":
|
continue
|
||||||
Result[Count] = "A"
|
|
||||||
Count -= 1
|
if ConfVars["NextNum"] in File["NewFileName"]:
|
||||||
|
ConfData = toml.load(OutDirConfPath)
|
||||||
|
Width = len(File["NextNum"])
|
||||||
|
NewNum = str((int(File["NextNum"]) + 1)).rjust(Width, "0")
|
||||||
|
print(File["NextNum"],"=>",NewNum)
|
||||||
|
|
||||||
|
# By position: two entries can share a Pattern (with different ValidInputDirs).
|
||||||
|
ConfData["Files"][FileIndex]["NextNum"] = NewNum
|
||||||
|
|
||||||
|
with open(OutDirConfPath, "w", encoding="utf-8") as ConfFile:
|
||||||
|
toml.dump(ConfData, ConfFile)
|
||||||
|
|
||||||
|
TextOutput = Parse(String = ConfLog["ValueSet"], VarCall = f"NextNum to {NewNum}")
|
||||||
|
LogWrite(TextOutput)
|
||||||
|
Speak(TextOutput)
|
||||||
|
|
||||||
|
if ConfVars["NextChar"] in File["NewFileName"]:
|
||||||
|
CurrentChar = File["NextChar"]
|
||||||
|
CurrentChar = CurrentChar.upper()
|
||||||
|
Result = list(CurrentChar)
|
||||||
|
|
||||||
|
Count = len(Result) - 1
|
||||||
|
|
||||||
|
while Count >= 0:
|
||||||
|
if Result[Count] == "Z":
|
||||||
|
Result[Count] = "A"
|
||||||
|
Count -= 1
|
||||||
|
|
||||||
|
else:
|
||||||
|
Result[Count] = chr(ord(Result[Count]) + 1)
|
||||||
|
break
|
||||||
|
|
||||||
else:
|
else:
|
||||||
Result[Count] = chr(ord(Result[Count]) + 1)
|
Result.insert(0, "A")
|
||||||
break
|
|
||||||
|
|
||||||
else:
|
NewChar = "".join(Result)
|
||||||
Result.insert(0, "A")
|
|
||||||
|
|
||||||
NewChar = "".join(Result)
|
ConfData = toml.load(OutDirConfPath)
|
||||||
|
|
||||||
ConfData = toml.load(OutDirConfPath)
|
ConfData["Files"][FileIndex]["NextChar"] = NewChar
|
||||||
|
|
||||||
for Item in ConfData.get("Files", []):
|
with open(OutDirConfPath, "w", encoding="utf-8") as ConfFile:
|
||||||
if Item.get("Pattern") == File["Pattern"]:
|
toml.dump(ConfData, ConfFile)
|
||||||
Item["NextChar"] = NewChar
|
|
||||||
break
|
|
||||||
|
|
||||||
with open(OutDirConfPath, "w", encoding="utf-8") as ConfFile:
|
TextOutput = Parse(String = ConfLog["ValueSet"], VarCall = f"NextChar to {NewChar}")
|
||||||
toml.dump(ConfData, ConfFile)
|
LogWrite(TextOutput)
|
||||||
|
Speak(TextOutput)
|
||||||
|
|
||||||
TextOutput = Parse(String = ConfLog["ValueSet"], VarCall = f"NextChar to {NewChar}")
|
return f"{Output}/{NewFileName}"
|
||||||
LogWrite(TextOutput)
|
|
||||||
Speak(TextOutput)
|
|
||||||
|
|
||||||
return f"{Output}/{NewFileName}"
|
|
||||||
|
|
||||||
|
|
||||||
def Sort(FilePath):
|
def Sort(FilePath):
|
||||||
NewPath = DecideNewPath(FilePath)
|
NewPath = DecideNewPath(FilePath)
|
||||||
|
|
||||||
if NewPath is None:
|
if NewPath is None:
|
||||||
Dir(Parse(String = ConfDirs["FailedDir"]), Output = False, CopyConf = False)
|
Dir(ParsePath(ConfDirs["FailedDir"]), Output = False, CopyConf = False)
|
||||||
NewName = os.path.basename(FilePath)
|
NewName = os.path.basename(FilePath)
|
||||||
Clone(FilePath, Parse(String = ConfDirs["FailedDir"]), NewName, True)
|
Clone(FilePath, ParsePath(ConfDirs["FailedDir"]), NewName, True)
|
||||||
|
|
||||||
TextOutput = Parse(String = ConfLog["NotSorted"], VarCall = FilePath)
|
TextOutput = Parse(String = ConfLog["NotSorted"], VarCall = FilePath)
|
||||||
LogWrite(TextOutput)
|
LogWrite(TextOutput)
|
||||||
@@ -415,7 +561,7 @@ def Sort(FilePath):
|
|||||||
NewDirPath = os.path.dirname(NewPath)
|
NewDirPath = os.path.dirname(NewPath)
|
||||||
NewName = os.path.basename(NewPath)
|
NewName = os.path.basename(NewPath)
|
||||||
|
|
||||||
if (GetConf("DeleteOrg", f"{NewDirPath}/{Parse(String = ConfNames['OutDirConfName'], Parent = (os.path.basename(NewDirPath)))}.toml") == 1):
|
if (GetConf("DeleteOrg", DirConfPath(NewDirPath, "Out")) == 1):
|
||||||
Delete = True
|
Delete = True
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -423,16 +569,16 @@ def Sort(FilePath):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
Clone(FilePath, NewDirPath, NewName, Delete)
|
Clone(FilePath, NewDirPath, NewName, Delete)
|
||||||
UpdateConf(f"{NewDirPath}/{Parse(String = ConfNames['OutDirConfName'], Parent = os.path.basename(NewDirPath))}.toml", "LastFile", NewName)
|
UpdateConf(DirConfPath(NewDirPath, "Out"), "LastFile", NewName)
|
||||||
|
|
||||||
TextOutput = Parse(String = ConfLog["Sorted"], VarCall = f"{FilePath} to {NewDirPath}/{NewName}")
|
TextOutput = Parse(String = ConfLog["Sorted"], VarCall = f"{FilePath} to {NewDirPath}/{NewName}")
|
||||||
LogWrite(TextOutput)
|
LogWrite(TextOutput)
|
||||||
Speak(TextOutput)
|
Speak(TextOutput)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
Dir(Parse(String = ConfDirs["FailedDir"]), Output = False, CopyConf = False)
|
Dir(ParsePath(ConfDirs["FailedDir"]), Output = False, CopyConf = False)
|
||||||
NewName = os.path.basename(NewPath)
|
NewName = os.path.basename(NewPath)
|
||||||
Clone(FilePath, Parse(String = ConfDirs["FailedDir"]), NewName, Delete)
|
Clone(FilePath, ParsePath(ConfDirs["FailedDir"]), NewName, Delete)
|
||||||
|
|
||||||
TextOutput = Parse(String = ConfLog["NotSorted"], VarCall = FilePath)
|
TextOutput = Parse(String = ConfLog["NotSorted"], VarCall = FilePath)
|
||||||
LogWrite(TextOutput)
|
LogWrite(TextOutput)
|
||||||
@@ -449,14 +595,14 @@ def Init():
|
|||||||
|
|
||||||
if Start:
|
if Start:
|
||||||
for InputDir in ConfDirs["InputDir"]:
|
for InputDir in ConfDirs["InputDir"]:
|
||||||
InputDir = Parse(String = InputDir, Path = ConfPath, Parent = os.path.basename(os.path.dirname(InputDir)))
|
InputDir = ParsePath(InputDir)
|
||||||
Dir(InputDir, Output = False, CopyConf = "InputDirConf.toml")
|
Dir(InputDir, Output = False, CopyConf = "InputDirConf.toml")
|
||||||
|
|
||||||
for Output in ConfDirs["OutputDir"]:
|
for Output in ConfDirs["OutputDir"]:
|
||||||
Output = Parse(String = Output, Parent = os.path.basename(os.path.dirname(Output)))
|
Output = ParsePath(Output)
|
||||||
Dir(Output)
|
Dir(Output)
|
||||||
|
|
||||||
OutDirConfPath = f"{Output}/{Parse(String = ConfNames["OutDirConfName"], Parent = os.path.basename(Output))}.toml"
|
OutDirConfPath = DirConfPath(Output, "Out")
|
||||||
ConfData = toml.load(OutDirConfPath)
|
ConfData = toml.load(OutDirConfPath)
|
||||||
|
|
||||||
for Item in ConfData.get("Files", []):
|
for Item in ConfData.get("Files", []):
|
||||||
@@ -490,20 +636,44 @@ def Main():
|
|||||||
for Input in ConfDirs["InputDir"]:
|
for Input in ConfDirs["InputDir"]:
|
||||||
RecurseDir = False
|
RecurseDir = False
|
||||||
|
|
||||||
InputPath = Parse(Input)
|
InputPath = ParsePath(Input)
|
||||||
|
|
||||||
if not os.path.exists(InputPath):
|
if not os.path.exists(InputPath):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
InputDirConfPath = f"{InputPath}/{Parse(ConfNames["InDirConfName"], Parent = os.path.basename(InputPath))}.toml"
|
InputDirConfPath = DirConfPath(InputPath, "In")
|
||||||
InputDirConf = toml.load(InputDirConfPath)
|
InputDirConf = toml.load(InputDirConfPath)
|
||||||
|
|
||||||
for Output in ConfDirs["OutputDir"]:
|
for Output in ConfDirs["OutputDir"]:
|
||||||
OutputPath = Parse(Output)
|
OutputPath = ParsePath(Output)
|
||||||
|
OutDirConfPath = DirConfPath(OutputPath, "Out")
|
||||||
|
|
||||||
|
if len(GetConf("Unzip", OutDirConfPath) or []) >= 1:
|
||||||
|
ZipTypes = GetConf("Unzip", OutDirConfPath)
|
||||||
|
for OutputFile in os.listdir(OutputPath):
|
||||||
|
if OutputFile.startswith(".") or IsPartialDownload(f"{OutputPath}/{OutputFile}"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
for ZipType in ZipTypes:
|
||||||
|
print(ZipType)
|
||||||
|
if (OutputFile.lower()).endswith((ZipType).lower()):
|
||||||
|
shutil.unpack_archive(f"{OutputPath}/{OutputFile}", OutputPath)
|
||||||
|
TextOutput = Parse(String = ConfLog["Unzipping"], VarCall = f"{OutputPath}/{OutputFile}")
|
||||||
|
LogWrite(TextOutput)
|
||||||
|
Speak(TextOutput)
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.remove(f"{OutputPath}/{OutputFile}")
|
||||||
|
|
||||||
|
except PermissionError:
|
||||||
|
TextOutput = Parse(String = ConfLog["NoPermission"], VarCall = f"remove {OutputPath}/{OutputFile}")
|
||||||
|
LogWrite(TextOutput)
|
||||||
|
Speak(TextOutput)
|
||||||
|
continue
|
||||||
|
|
||||||
if (OutputPath == InputPath):
|
if (OutputPath == InputPath):
|
||||||
RecurseDir = True
|
RecurseDir = True
|
||||||
FileHistoryPath = f"{InputPath}/{Parse(ConfNames["FileHistoryName"], Parent = os.path.basename(InputPath))}.toml"
|
FileHistoryPath = HistoryPath(InputPath)
|
||||||
|
|
||||||
if not os.path.exists(FileHistoryPath):
|
if not os.path.exists(FileHistoryPath):
|
||||||
with open(FileHistoryPath, "w", encoding = "utf-8") as HistoryFile:
|
with open(FileHistoryPath, "w", encoding = "utf-8") as HistoryFile:
|
||||||
@@ -511,7 +681,10 @@ def Main():
|
|||||||
|
|
||||||
for FileName in os.listdir(InputPath):
|
for FileName in os.listdir(InputPath):
|
||||||
FilePath = f"{InputPath}/{FileName}"
|
FilePath = f"{InputPath}/{FileName}"
|
||||||
if os.path.isdir(FilePath) and InputDirConf["SortDirs"] == 0:
|
if IsPartialDownload(FilePath):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if os.path.isdir(FilePath) and InputDirConf.get("SortDirs", 0) == 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
FileName = os.path.basename(FilePath)
|
FileName = os.path.basename(FilePath)
|
||||||
@@ -529,7 +702,7 @@ def Main():
|
|||||||
break
|
break
|
||||||
|
|
||||||
if Match:
|
if Match:
|
||||||
if os.path.isdir(FilePath) and InputDirConf["SortDirs"] == 1:
|
if os.path.isdir(FilePath) and InputDirConf.get("SortDirs", 0) == 1:
|
||||||
TextOutput = Parse(String = ConfLog["Zipping"], VarCall = FilePath)
|
TextOutput = Parse(String = ConfLog["Zipping"], VarCall = FilePath)
|
||||||
LogWrite(TextOutput)
|
LogWrite(TextOutput)
|
||||||
Speak(TextOutput)
|
Speak(TextOutput)
|
||||||
@@ -537,7 +710,7 @@ def Main():
|
|||||||
shutil.make_archive(FilePath, "zip", FilePath)
|
shutil.make_archive(FilePath, "zip", FilePath)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
os.rmdir(FilePath)
|
shutil.rmtree(FilePath, ignore_errors=True)
|
||||||
|
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
TextOutput = Parse(String = ConfLog["NoPermission"], VarCall = f"remove directory {InputPath}/{FileName}")
|
TextOutput = Parse(String = ConfLog["NoPermission"], VarCall = f"remove directory {InputPath}/{FileName}")
|
||||||
@@ -550,12 +723,12 @@ def Main():
|
|||||||
if RecurseDir:
|
if RecurseDir:
|
||||||
HistoryFile = toml.load(FileHistoryPath)
|
HistoryFile = toml.load(FileHistoryPath)
|
||||||
|
|
||||||
if (Parse(FileName, Parent = os.path.basename(InputPath)) in HistoryFile["History"]):
|
if (FileName in HistoryFile["History"]):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
Sort(FilePath)
|
Sort(FilePath)
|
||||||
|
|
||||||
time.sleep(Conf.get("CheckInput", 10))
|
time.sleep(Conf.get("SortTimer", 10))
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
TextOutput = GetConf("Stop", ConfPath)
|
TextOutput = GetConf("Stop", ConfPath)
|
||||||
|
|||||||