diff --git a/.gitignore b/.gitignore index 2732c75..047a217 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ go.work build/bin node_modules frontend/dist + +# Configuration file +config.yaml \ No newline at end of file diff --git a/.vscode/.cspell/package-dictionary.txt b/.vscode/.cspell/package-dictionary.txt index 50b30ea..ec00196 100644 --- a/.vscode/.cspell/package-dictionary.txt +++ b/.vscode/.cspell/package-dictionary.txt @@ -8,6 +8,8 @@ Equalf Infof innerxml iofs +knadh +koanf Komga lazydb logrus diff --git a/README.md b/README.md index ff62081..25aa9ec 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,31 @@ Already contains a `.cbz` archive and `ComicInfo.xml` at `{selected-folder}/{com User can directly copy exported folder to `komga` comic directory. +## Configuration + +This program support some customizations by `.yaml` file configuration. + +Your configuration file should like: + +``` +your-folder/ +├─ ComicInfo-Parser.exe +├─ config.yaml +``` + +If no configuration is found, program will NOT create for yourself. Instead, it will use its default behavior. + +You may found a sample of configuration file in `config-example.yaml`. + +### Configuration Options + +You should use absolute paths as possible. If folder is missing, then program will try to create for all folders. + +| Field | Type | Usage | +| ---------------- | ------ | ------------------------------------------------------------------------------- | +| `default` | struct | storing default values for program | +| `default.export` | string | default export folder path, if empty string, then create inside input directory | + ## Data All data will be stored in sqlite3 database, which located at `{Home Directory}/comicInfo-parser/storage.db`. diff --git a/config-example.yaml b/config-example.yaml new file mode 100644 index 0000000..0174cac --- /dev/null +++ b/config-example.yaml @@ -0,0 +1,5 @@ +default: + export: ./my-export +export: + - ./folder1 + - ./folder2 \ No newline at end of file diff --git a/frontend/src/pages/exportPanel.tsx b/frontend/src/pages/exportPanel.tsx index 220535e..b395f93 100644 --- a/frontend/src/pages/exportPanel.tsx +++ b/frontend/src/pages/exportPanel.tsx @@ -10,7 +10,12 @@ import InputGroup from "react-bootstrap/InputGroup"; import { ModalControl } from "../controls/ModalControl"; // Wails -import { ExportCbz, GetDirectory, GetDirectoryWithDefault } from "../../wailsjs/go/application/App"; +import { + ExportCbz, + GetDefaultOutputDirectory, + GetDirectory, + GetDirectoryWithDefault, +} from "../../wailsjs/go/application/App"; import { comicinfo } from "../../wailsjs/go/models"; /** Props Interface for FolderSelect */ @@ -34,7 +39,10 @@ export default function ExportPanel({ comicInfo: info, originalDirectory, modalC // Set the export directory to input directory if it exists useEffect(() => { if (originalDirectory !== undefined) { - setExportDir(originalDirectory); + // Load config from file + GetDefaultOutputDirectory(originalDirectory).then((dir) => { + setExportDir(dir); + }); } }, []); diff --git a/frontend/wailsjs/go/application/App.d.ts b/frontend/wailsjs/go/application/App.d.ts index 97392c6..de74edd 100644 --- a/frontend/wailsjs/go/application/App.d.ts +++ b/frontend/wailsjs/go/application/App.d.ts @@ -15,6 +15,8 @@ export function GetAllTagInput():Promise; export function GetComicInfo(arg1:string):Promise; +export function GetDefaultOutputDirectory(arg1:string):Promise; + export function GetDirectory():Promise; export function GetDirectoryWithDefault(arg1:string):Promise; diff --git a/frontend/wailsjs/go/application/App.js b/frontend/wailsjs/go/application/App.js index ecd82ea..528a833 100644 --- a/frontend/wailsjs/go/application/App.js +++ b/frontend/wailsjs/go/application/App.js @@ -26,6 +26,10 @@ export function GetComicInfo(arg1) { return window['go']['application']['App']['GetComicInfo'](arg1); } +export function GetDefaultOutputDirectory(arg1) { + return window['go']['application']['App']['GetDefaultOutputDirectory'](arg1); +} + export function GetDirectory() { return window['go']['application']['App']['GetDirectory'](); } diff --git a/go.mod b/go.mod index 13192e5..2a2c530 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,9 @@ go 1.23.1 require ( github.com/dark-person/lazydb v0.1.6 + github.com/knadh/koanf/parsers/yaml v0.1.0 + github.com/knadh/koanf/providers/file v1.1.2 + github.com/knadh/koanf/v2 v2.1.2 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.10.0 github.com/wailsapp/wails/v2 v2.9.2 @@ -12,13 +15,16 @@ require ( require ( github.com/bep/debounce v1.2.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/golang-migrate/migrate/v4 v4.18.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect + github.com/knadh/koanf/maps v0.1.1 // indirect github.com/labstack/echo/v4 v4.11.4 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/leaanthony/go-ansi-parser v1.6.1 // indirect @@ -28,6 +34,8 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-sqlite3 v1.14.24 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/go.sum b/go.sum index 23d3b5c..3c220ba 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,12 @@ github.com/dark-person/lazydb v0.1.6/go.mod h1:5YakMfYNfg78SRqT9nW+QqpBvE7Q4VOHi github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang-migrate/migrate/v4 v4.18.1 h1:JML/k+t4tpHCpQTCAD62Nu43NUFzHY4CV3uAuvHGC+Y= @@ -20,6 +24,14 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= +github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= +github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w= +github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY= +github.com/knadh/koanf/providers/file v1.1.2 h1:aCC36YGOgV5lTtAFz2qkgtWdeQsgfxUkxDOe+2nQY3w= +github.com/knadh/koanf/providers/file v1.1.2/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI= +github.com/knadh/koanf/v2 v2.1.2 h1:I2rtLRqXRy1p01m/utEtpZSSA6dcJbgGVuE27kW2PzQ= +github.com/knadh/koanf/v2 v2.1.2/go.mod h1:Gphfaen0q1Fc1HTgJgSTC4oRX9R2R5ErYMZJy8fLJBo= github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8= github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= @@ -46,6 +58,10 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= diff --git a/internal/application/app.go b/internal/application/app.go index 70423d0..58548d7 100644 --- a/internal/application/app.go +++ b/internal/application/app.go @@ -5,6 +5,8 @@ import ( "fmt" "os" + "github.com/dark-person/comicinfo-parser/internal/assets" + "github.com/dark-person/comicinfo-parser/internal/config" "github.com/dark-person/lazydb" "github.com/wailsapp/wails/v2/pkg/runtime" ) @@ -13,11 +15,12 @@ import ( type App struct { DB *lazydb.LazyDB ctx context.Context + cfg *config.ProgramConfig } // NewApp creates a new App application struct func NewApp(db *lazydb.LazyDB) *App { - return &App{DB: db} + return &App{DB: db, cfg: assets.Config()} } // startup is called when the app starts. The context is saved diff --git a/internal/application/directory.go b/internal/application/directory.go index 3b5f5bc..f6a2e05 100644 --- a/internal/application/directory.go +++ b/internal/application/directory.go @@ -40,3 +40,13 @@ func (a *App) GetDirectoryWithDefault(defaultDirectory string) string { } return directory } + +// Attempt to load default output directory. +// If no default directory is set, then return input directory instead. +func (a *App) GetDefaultOutputDirectory(inputDir string) string { + if a.cfg.DefaultExport == "" { + return inputDir + } + + return a.cfg.DefaultExport +} diff --git a/internal/application/directory_test.go b/internal/application/directory_test.go new file mode 100644 index 0000000..5195e68 --- /dev/null +++ b/internal/application/directory_test.go @@ -0,0 +1,52 @@ +package application + +import ( + "path/filepath" + "testing" + + "github.com/dark-person/comicinfo-parser/internal/config" + "github.com/stretchr/testify/assert" +) + +func TestGetDefaultOutputDirectory(t *testing.T) { + type testCase struct { + configExportDir string // Default export directory for config files + inputDir string // Parameter for function + want string // Expected result + } + + absPath1, err := filepath.Abs("/absPath/") + if err != nil { + t.Errorf("Failed to get absolute path") + } + + absPath2, err := filepath.Abs("relatedPath") + if err != nil { + t.Errorf("Failed to get absolute path") + } + + // Create a dummy APP + a := &App{} + + // Start test + tests := []testCase{ + {"/absPath/", "inputDir", absPath1}, + {"relatedPath", "inputDir", absPath2}, + {"", "inputDir", "inputDir"}, + } + + for idx, tt := range tests { + // Reset config + a.cfg = config.Default() + + // Plug exported directory to dummy app + if tt.configExportDir != "" { + a.cfg.DefaultExport, _ = filepath.Abs(tt.configExportDir) + } + + // Run function + result := a.GetDefaultOutputDirectory(tt.inputDir) + + assert.EqualValuesf(t, tt.want, result, "Unexpected directory in case %d", idx) + } +} diff --git a/internal/assets/configs.go b/internal/assets/configs.go new file mode 100644 index 0000000..37b191b --- /dev/null +++ b/internal/assets/configs.go @@ -0,0 +1,28 @@ +package assets + +import ( + "fmt" + + "github.com/dark-person/comicinfo-parser/internal/config" +) + +var cfg *config.ProgramConfig + +// Load config from yaml file. +// If any error occur in loading, then a default config will be returned, and no error return. +func Config() *config.ProgramConfig { + // Return parsed config if any + if cfg != nil { + return cfg + } + + // Load Config from filesystem + c, err := config.LoadYaml("config.yaml") + if err != nil { + fmt.Println(err) + } + + // Set loaded config to package + cfg = c + return cfg +} diff --git a/internal/config/load.go b/internal/config/load.go new file mode 100644 index 0000000..d4898c9 --- /dev/null +++ b/internal/config/load.go @@ -0,0 +1,55 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/knadh/koanf/parsers/yaml" + "github.com/knadh/koanf/providers/file" + "github.com/knadh/koanf/v2" +) + +// Load yaml config from given path, +// while no koanf instance will preserved (i.e. every call overwrite previous call). +// +// If failed to load config, then a default config will be returned. +func LoadYaml(path string) (*ProgramConfig, error) { + var k = koanf.New(".") + + // Check if file exist + if _, err := os.Stat(path); err != nil { + return Default(), fmt.Errorf("path %s does not exist", path) + } + + // Start Load file + err := k.Load(file.Provider(path), yaml.Parser()) + if err != nil { + return Default(), err + } + + // Unmarshal to struct + var out ProgramConfig + err = k.UnmarshalWithConf("", &out, koanf.UnmarshalConf{Tag: "koanf", FlatPaths: true}) + if err != nil { + return Default(), err + } + + // Parse path due to relative path issue + out.DefaultExport, err = parsePath(out.DefaultExport) + if err != nil { + return Default(), err + } + + return &out, nil +} + +// Convert relative path to absolute path. +// If path passed is empty string, then it perform nothing. +func parsePath(relativePath string) (absPath string, err error) { + if relativePath == "" { + return "", nil + } + + return filepath.Abs(relativePath) +} diff --git a/internal/config/load_test.go b/internal/config/load_test.go new file mode 100644 index 0000000..3b6d0c5 --- /dev/null +++ b/internal/config/load_test.go @@ -0,0 +1,46 @@ +package config + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLoadYaml(t *testing.T) { + type testCase struct { + path string + want *ProgramConfig + wantErr bool + } + + exPath, err := os.Getwd() + if err != nil { + panic(err) + } + t.Log(exPath) + + tests := []testCase{ + {"mock/case-normal.yaml", &ProgramConfig{DefaultExport: filepath.Join(exPath, "./my-export")}, false}, + {"mock/case-typo1.yaml", &ProgramConfig{DefaultExport: ""}, false}, + {"mock/case-typo2.yaml", &ProgramConfig{DefaultExport: ""}, false}, + {"mock/not-exist.yaml", nil, true}, + } + + for idx, tt := range tests { + // Load YAML file and check result + c, err := LoadYaml(tt.path) + + if tt.wantErr { + assert.NotNilf(t, err, "Error should be returned in case %d, but return nil", idx) + + // Ensure default is returned + assert.EqualValuesf(t, Default(), c, "Incorrect values in case %d, should be default config", idx) + + } else { + assert.EqualValuesf(t, tt.want, c, "Incorrect values in case %d", idx) + assert.Nilf(t, err, "Unexpected error in case %d", idx) + } + } +} diff --git a/internal/config/mock/case-normal.yaml b/internal/config/mock/case-normal.yaml new file mode 100644 index 0000000..3f3bf9e --- /dev/null +++ b/internal/config/mock/case-normal.yaml @@ -0,0 +1,2 @@ +default: + export: ./my-export \ No newline at end of file diff --git a/internal/config/mock/case-typo1.yaml b/internal/config/mock/case-typo1.yaml new file mode 100644 index 0000000..109d5f7 --- /dev/null +++ b/internal/config/mock/case-typo1.yaml @@ -0,0 +1,2 @@ +defaults: + export: ./my-export \ No newline at end of file diff --git a/internal/config/mock/case-typo2.yaml b/internal/config/mock/case-typo2.yaml new file mode 100644 index 0000000..dabed2f --- /dev/null +++ b/internal/config/mock/case-typo2.yaml @@ -0,0 +1,2 @@ +default: + exports: ./my-export diff --git a/internal/config/struct.go b/internal/config/struct.go new file mode 100644 index 0000000..9b6f3de --- /dev/null +++ b/internal/config/struct.go @@ -0,0 +1,15 @@ +// Package for configuration, +// provide basic config struct and utility to load. +package config + +// Config for this program. +type ProgramConfig struct { + DefaultExport string `koanf:"default.export"` // Default export folder, apply to both quick & standard +} + +// Default config struct for this program. +func Default() *ProgramConfig { + return &ProgramConfig{ + DefaultExport: "", // Indicate input folder is used + } +}