Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rework console parameters and add export to csv option #26

Merged
merged 10 commits into from
Apr 9, 2024
61 changes: 41 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Switch library manager

Fork of [Switch Library Manager](https://github.com/giwty/switch-library-manager) created by giwty with continued improvements and changes

# Switch library manager
Easily manage your switch game backups

![Image description](https://raw.githubusercontent.com/trembon/switch-library-manager/master/.github/readme/updates_ui.png)
Expand All @@ -9,7 +10,8 @@ Easily manage your switch game backups

![Image description](https://raw.githubusercontent.com/trembon/switch-library-manager/master/.github/readme/cmd.png)

#### Features:
## Features

- Cross platform, works on Windows / Mac / Linux
- GUI and command line interfaces
- Scan your local switch backup library (NSP/NSZ/XCI)
Expand All @@ -21,21 +23,23 @@ Easily manage your switch game backups
- Rename files based on metadata read from NSP
- Delete old update files (in case you have multiple update files for the same game, only the latest will remain)
- Delete empty folders
- Zero dependencies, all crypto operations implemented in Go.
- Zero dependencies, all crypto operations implemented in Go.

## Keys (optional)

Having a prod.keys file will allow you to ensure the files you have a correctly classified.
The app will look for the "prod.keys" file in the app folder or under ${HOME}/.switch/
You can also specify a custom location in the settings.json (see below)

Note: Only the header_key, and the key_area_key_application_XX keys are required.

## Settings

During the App first launch a "settings.json" file will be created, that allows for granular control over the Apps execution.

You can customize the folder/file re-naming, as well as turn on/off features.

```
```json
{
"versions_etag": "W/\"c3f5ecb3392d61:0\"",
"titles_etag": "W/\"4a4fcc163a92d61:0\"",
Expand All @@ -62,7 +66,9 @@ You can customize the folder/file re-naming, as well as turn on/off features.
```

## Naming template

The following template elements are supported:

- {TITLE_NAME} - game name
- {TITLE_ID} - title id
- {VERSION} - version id (only applicable to files)
Expand All @@ -72,30 +78,44 @@ The following template elements are supported:
- {DLC_NAME} - DLC name (only applicable to DLCs)

## Usage
##### Windows

### Windows

- Extract the zip file
- Double click the Exe file
- If you want to use command line mode, update the settings.json with `'GUI':false`
- Open `cmd`
- Run `switch-library-manager.exe`
- Optionally -f `X:\folder\containing\nsp\files"`
- Optionally add `-r` to recursively scan for nested folders
- Edit the settings.json file for additional options
- Open `cmd`
- Run `switch-library-manager.exe`
- Optionally -f `X:\folder\containing\nsp\files"`
- Optionally add `-r` to recursively scan for nested folders
- Edit the settings.json file for additional options

### macOS or Linux


##### macOS or Linux
- Extract the zip file
- Double click the App file
- If you want to use command line mode, update the settings.json with `'GUI':false`
- Open your Terminal
- `cd` to the folder containing `switch-library-manager`
- `chmod +x switch-library-manager` to make it executable
- Run `./switch-library-manager'
- Optionally -f `X:\folder\containing\nsp\files"`
- Optionally add `-r` to recursively scan for nested folders
- Edit the settings.json file for additional options
- Open your Terminal
- `cd` to the folder containing `switch-library-manager`
- `chmod +x switch-library-manager` to make it executable
- Run `./switch-library-manager'
- Optionally -f `X:\folder\containing\nsp\files"`
- Optionally add `-r` to recursively scan for nested folders
- Edit the settings.json file for additional options

### Console parameters

NOTE: parameters are only usable in console mode, exept the parameter -m (mode) which will override the gui setting.

|Name|Flag|Value|Description|
|---|---|---|---|
|Mode|-m|console/gui|Which mode to start the application in, overrides **gui** in settings.json|
|NSP Folder|-|*path*|Path to the NSP folder, overrides **folder** in settings.json|
|Recursive scan|-r|true/false|If recursive scan should be used for the NSP folder, overrides **scan_recursively** in settings.json|
|Export CSV|-e|*path*|Which folder to output missing_updates, missing_dlcs and issues in CSV format|

## Building

- Install and setup Go
- Clone the repo: `git clone https://github.com/trembon/switch-library-manager.git`
- Move into the src folder `cd src`
Expand All @@ -105,5 +125,6 @@ The following template elements are supported:
- Execute `./astilectron-bundler`
- Binaries will be available under output

#### Thanks
## Thanks

This program relies on [blawar's titledb](https://github.com/blawar/titledb), to get the latest titles and versions.
126 changes: 102 additions & 24 deletions src/console.go
Original file line number Diff line number Diff line change
@@ -1,48 +1,56 @@
package main

import (
"flag"
"encoding/csv"
"fmt"
"os"
"path"
"path/filepath"
"strconv"
"strings"

"github.com/jedib0t/go-pretty/table"
"github.com/schollz/progressbar/v3"
"github.com/trembon/switch-library-manager/console"
"github.com/trembon/switch-library-manager/db"
"github.com/trembon/switch-library-manager/process"
"github.com/trembon/switch-library-manager/settings"
"go.uber.org/zap"
)

var (
nspFolder = flag.String("f", "", "path to NSP folder")
recursive = flag.Bool("r", true, "recursively scan sub folders")
mode = flag.String("m", "", "**deprecated**")
progressBar *progressbar.ProgressBar
)

type Console struct {
baseFolder string
sugarLogger *zap.SugaredLogger
baseFolder string
sugarLogger *zap.SugaredLogger
consoleFlags *console.ConsoleFlags
}

func CreateConsole(baseFolder string, sugarLogger *zap.SugaredLogger) *Console {
return &Console{baseFolder: baseFolder, sugarLogger: sugarLogger}
func CreateConsole(baseFolder string, sugarLogger *zap.SugaredLogger, consoleFlags *console.ConsoleFlags) *Console {
return &Console{baseFolder: baseFolder, sugarLogger: sugarLogger, consoleFlags: consoleFlags}
}

func (c *Console) Start() {
flag.Parse()
settingsObj := settings.ReadSettings(c.baseFolder)

if mode != nil && *mode != "" {
fmt.Println("note : the mode option ('-m') is deprecated, please use the settings.json to control options.")
// 0. prepare csv export folder
csvOutput := ""
if c.consoleFlags.ExportCsv.IsSet() {
csvOutput = c.consoleFlags.ExportCsv.String()

if _, err := os.Stat(csvOutput); os.IsNotExist(err) {
err = os.Mkdir(csvOutput, os.ModePerm)
if err != nil {
fmt.Printf("Failed to create folder for csv export %v - %v\n", csvOutput, err)
zap.S().Errorf("Failed to create folder for csv export %v - %v\n", csvOutput, err)
}
}
}

settingsObj := settings.ReadSettings(c.baseFolder)

//1. load the titles JSON object
fmt.Printf("Downlading latest switch titles json file")
fmt.Println("Downlading latest switch titles json file")
progressBar = progressbar.New(2)

filename := filepath.Join(c.baseFolder, settings.TITLE_JSON_FILENAME)
Expand Down Expand Up @@ -77,8 +85,8 @@ func (c *Console) Start() {

//5. read local files
folderToScan := settingsObj.Folder
if nspFolder != nil && *nspFolder != "" {
folderToScan = *nspFolder
if c.consoleFlags.NspFolder.IsSet() && c.consoleFlags.NspFolder.String() != "" {
folderToScan = c.consoleFlags.NspFolder.String()
}

if folderToScan == "" {
Expand All @@ -93,8 +101,8 @@ func (c *Console) Start() {
}

recursiveMode := settingsObj.ScanRecursively
if recursive != nil && *recursive != true {
recursiveMode = *recursive
if c.consoleFlags.Recursive.IsSet() {
recursiveMode = c.consoleFlags.Recursive.Bool()
}

localDbManager, err := db.NewLocalSwitchDBManager(c.baseFolder)
Expand All @@ -118,7 +126,11 @@ func (c *Console) Start() {

fmt.Printf("Local library completion status: %.2f%% (have %d titles, out of %d titles)\n", p, len(localDB.TitlesMap), len(titlesDB.TitlesMap))

c.processIssues(localDB)
issuesCsvFile := ""
if csvOutput != "" {
issuesCsvFile = filepath.Join(csvOutput, "issues.csv")
}
c.processIssues(localDB, issuesCsvFile)

if settingsObj.OrganizeOptions.DeleteOldUpdateFiles {
progressBar = progressbar.New(2000)
Expand All @@ -136,58 +148,84 @@ func (c *Console) Start() {

if settingsObj.CheckForMissingUpdates {
fmt.Printf("\nChecking for missing updates\n")
c.processMissingUpdates(localDB, titlesDB, settingsObj)

missingUpdatesCsvFile := ""
if csvOutput != "" {
missingUpdatesCsvFile = filepath.Join(csvOutput, "missing_updates.csv")
}

c.processMissingUpdates(localDB, titlesDB, settingsObj, missingUpdatesCsvFile)
}

if settingsObj.CheckForMissingDLC {
fmt.Printf("\nChecking for missing DLC\n")
c.processMissingDLC(localDB, titlesDB)

missingDlcCsvFile := ""
if csvOutput != "" {
missingDlcCsvFile = filepath.Join(csvOutput, "missing_dlc.csv")
}

c.processMissingDLC(localDB, titlesDB, missingDlcCsvFile)
}

fmt.Printf("Completed")
}

func (c *Console) processIssues(localDB *db.LocalSwitchFilesDB) {
func (c *Console) processIssues(localDB *db.LocalSwitchFilesDB, csvOutput string) {
if len(localDB.Skipped) != 0 {
fmt.Print("\nSkipped files:\n\n")
} else {
return
}

csv := CreateCsvFile(csvOutput, []string{"Skipped file", "Reason", "Reason_Code"})

t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.SetStyle(table.StyleColoredBright)
t.AppendHeader(table.Row{"#", "Skipped file", "Reason"})
i := 0
for k, v := range localDB.Skipped {
csv.Write([]string{path.Join(k.BaseFolder, k.FileName), v.ReasonText, strconv.Itoa(v.ReasonCode)})

t.AppendRow([]interface{}{i, path.Join(k.BaseFolder, k.FileName), v})
i++
}
t.AppendFooter(table.Row{"", "", "", "", "Total", len(localDB.Skipped)})
t.Render()

csv.Close()
}

func (c *Console) processMissingUpdates(localDB *db.LocalSwitchFilesDB, titlesDB *db.SwitchTitlesDB, settingsObj *settings.AppSettings) {
func (c *Console) processMissingUpdates(localDB *db.LocalSwitchFilesDB, titlesDB *db.SwitchTitlesDB, settingsObj *settings.AppSettings, csvOutput string) {
incompleteTitles := process.ScanForMissingUpdates(localDB.TitlesMap, titlesDB.TitlesMap, settingsObj.IgnoreDLCUpdates)
if len(incompleteTitles) != 0 {
fmt.Print("\nFound available updates:\n\n")
} else {
fmt.Print("\nAll NSP's are up to date!\n\n")
return
}

csv := CreateCsvFile(csvOutput, []string{"Title", "TitleId", "Local version", "Latest Version", "Update Date"})

t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.SetStyle(table.StyleColoredBright)
t.AppendHeader(table.Row{"#", "Title", "TitleId", "Local version", "Latest Version", "Update Date"})
i := 0
for _, v := range incompleteTitles {
csv.Write([]string{v.Attributes.Name, v.Attributes.Id, strconv.Itoa(v.LocalUpdate), strconv.Itoa(v.LatestUpdate), v.LatestUpdateDate})

t.AppendRow([]interface{}{i, v.Attributes.Name, v.Attributes.Id, v.LocalUpdate, v.LatestUpdate, v.LatestUpdateDate})
i++
}
t.AppendFooter(table.Row{"", "", "", "", "Total", len(incompleteTitles)})
t.Render()

csv.Close()
}

func (c *Console) processMissingDLC(localDB *db.LocalSwitchFilesDB, titlesDB *db.SwitchTitlesDB) {
func (c *Console) processMissingDLC(localDB *db.LocalSwitchFilesDB, titlesDB *db.SwitchTitlesDB, csvOutput string) {
settingsObj := settings.ReadSettings(c.baseFolder)
ignoreIds := map[string]struct{}{}
for _, id := range settingsObj.IgnoreDLCTitleIds {
Expand All @@ -200,21 +238,61 @@ func (c *Console) processMissingDLC(localDB *db.LocalSwitchFilesDB, titlesDB *db
fmt.Print("\nYou have all the DLCS!\n\n")
return
}

csv := CreateCsvFile(csvOutput, []string{"Title", "TitleId", "Dlc (titleId - Name)"})

t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.SetStyle(table.StyleColoredBright)
t.AppendHeader(table.Row{"#", "Title", "TitleId", "Missing DLCs (titleId - Name)"})
i := 0
for _, v := range incompleteTitles {
for _, dlc := range v.MissingDLC {
csv.Write([]string{v.Attributes.Name, v.Attributes.Id, dlc})
}

t.AppendRow([]interface{}{i, v.Attributes.Name, v.Attributes.Id, strings.Join(v.MissingDLC, "\n")})
i++
}
t.AppendFooter(table.Row{"", "", "", "", "Total", len(incompleteTitles)})
t.Render()

csv.Close()
}

func (c *Console) UpdateProgress(curr int, total int, message string) {
progressBar.ChangeMax(total)
progressBar.Set(curr)
}

type CsvFile struct {
Writer *csv.Writer
File *os.File
}

func CreateCsvFile(output string, header []string) *CsvFile {
if output != "" {
file, _ := os.Create(output)
writer := csv.NewWriter(file)

_ = writer.Write(header)

instance := &CsvFile{Writer: writer, File: file}
return instance
} else {
return nil
}
}

func (csv *CsvFile) Close() {
if csv != nil {
csv.Writer.Flush()
csv.File.Close()
}
}

func (csv *CsvFile) Write(row []string) {
if csv != nil {
_ = csv.Writer.Write(row)
}
}
Loading
Loading