Compare commits

..

9 Commits

7 changed files with 253 additions and 85 deletions

12
README.md Normal file
View File

@@ -0,0 +1,12 @@
# OPL-CLI
Manage your [OPL](https://github.com/ps2homebrew/Open-PS2-Loader) library from the command line.
# Usage
There are 4 subcommands provided:
- `scan` Scan PS2 ISOs and determine the game ID and name.
- `rename` Rename the ISO based on the scanned name.
- `art` Download game art like cover and screenshots for the game.
- `cfg` Download base configuration for the game.

View File

@@ -6,33 +6,65 @@ import (
) )
type ScanCmd struct { type ScanCmd struct {
File string `arg:"positional"` Files []string `arg:"positional,required"`
} }
type RenameCmd struct { type RenameCmd struct {
File string `arg:"positional"` Files []string `arg:"positional,required"`
} }
type ArtCmd struct { type ArtCmd struct {
File string `arg:"positional"` Files []string `arg:"positional,required"`
OutputDirectory string `arg:"--output,-o" help:"where to download art files" default:"."`
}
type CfgCmd struct {
Files []string `arg:"positional,required"`
OutputDirectory string `arg:"--output,-o" help:"where to download cfg files" default:"."`
Override bool `arg:"-f,--force" help:"Override existing cfg files"`
Append bool `arg:"-a,--append" help:"Append to existing cfg files"`
} }
type args struct { type args struct {
Scan *ScanCmd `arg:"subcommand:scan"` Scan *ScanCmd `arg:"subcommand:scan"`
Rename *RenameCmd `arg:"subcommand:rename"` Rename *RenameCmd `arg:"subcommand:rename"`
Art *ArtCmd `arg:"subcommand:art"` Art *ArtCmd `arg:"subcommand:art"`
Cfg *CfgCmd `arg:"subcommand:cfg"`
GameListLocation string `args:"--gamelist" default:"./ps2-gameslist.txt" help:"Location of game list"`
} }
func Run() { func Run() {
var args args var args args
arg.MustParse(&args) p := arg.MustParse(&args)
config := oplcli.Config{
GameListLocation: args.GameListLocation,
}
switch { switch {
case args.Scan != nil: case args.Scan != nil:
oplcli.Scan(args.Scan.File, oplcli.Config{}) oplcli.Scan(args.Scan.Files, config)
case args.Rename != nil: case args.Rename != nil:
oplcli.Rename(args.Rename.File, oplcli.Config{}) oplcli.Rename(args.Rename.Files, config)
case args.Art != nil: case args.Art != nil:
oplcli.DownloadGameArt(args.Art.File, oplcli.Config{}) oplcli.DownloadGameArt(args.Art.Files, args.Art.OutputDirectory, config)
case args.Cfg != nil:
if args.Cfg.Override && args.Cfg.Append {
p.FailSubcommand("Can only append or override cfg files", "cfg")
}
var mode oplcli.FileMode = oplcli.ModeOnlyCreate
if args.Cfg.Append {
mode = oplcli.ModeAppend
}
if args.Cfg.Override {
mode = oplcli.ModeOverride
}
oplcli.DownloadCfg(args.Cfg.Files, args.Cfg.OutputDirectory, mode, config)
default:
p.Fail("Must specify command")
} }
} }

View File

@@ -3,6 +3,7 @@ package oplcli
import ( import (
"fmt" "fmt"
"io" "io"
"log/slog"
"net/http" "net/http"
"os" "os"
"path" "path"
@@ -16,19 +17,14 @@ type artUrls struct {
spine string spine string
logo string logo string
screenshot string screenshot string
screenshot2 string
} }
func getArtUrlForGameFromGithub(gameID string) artUrls { func getArtUrlForGameFromGithub(gameID string) artUrls {
const baseURL = "https://raw.githubusercontent.com/Luden02/psx-ps2-opl-art-database/refs/heads/main/PS2" const baseURL = "https://raw.githubusercontent.com/Luden02/psx-ps2-opl-art-database/refs/heads/main/PS2"
prefix := gameID[0:4]
num1 := gameID[5:8]
num2 := gameID[8:10]
newGameID := fmt.Sprintf("%s_%s.%s", prefix, num1, num2)
genURL := func(kind string) string { genURL := func(kind string) string {
return fmt.Sprintf("%s/%s/%s_%s.png", baseURL, newGameID, newGameID, kind) return fmt.Sprintf("%s/%s/%s_%s.png", baseURL, gameID, gameID, kind)
} }
return artUrls{ return artUrls{
@@ -39,15 +35,25 @@ func getArtUrlForGameFromGithub(gameID string) artUrls {
spine: genURL("LAB"), spine: genURL("LAB"),
logo: genURL("LGO"), logo: genURL("LGO"),
screenshot: genURL("SCR_00"), screenshot: genURL("SCR_00"),
screenshot2: genURL("SCR_01"),
} }
} }
func downloadArtForGame(gameID string, targetDir string) error { func downloadArtForGame(gameID string, targetDir string) {
urls := getArtUrlForGameFromGithub(gameID) urls := getArtUrlForGameFromGithub(gameID)
dl := func(url, kind string) (bool, error) { totalDownloads := 0
return downloadFile(url, path.Join(targetDir, fmt.Sprintf("%s_%s.png", gameID, kind)))
dl := func(url, kind string) {
file := path.Join(targetDir, fmt.Sprintf("%s_%s.png", gameID, kind))
found, err := downloadFile(url, file)
if err != nil {
slog.Warn("Failed to download art file", "gameID", gameID, "url", url)
}
if found {
slog.Debug("Downloaded art file", "gameID", gameID, "kind", kind, "target", file)
totalDownloads++
}
} }
dl(urls.cover, "COV") dl(urls.cover, "COV")
@@ -55,9 +61,14 @@ func downloadArtForGame(gameID string, targetDir string) error {
dl(urls.disc, "ICO") dl(urls.disc, "ICO")
dl(urls.background, "BG") dl(urls.background, "BG")
dl(urls.screenshot, "SCR") dl(urls.screenshot, "SCR")
dl(urls.screenshot2, "SRC2")
dl(urls.spine, "LAB") dl(urls.spine, "LAB")
return nil if totalDownloads == 0 {
slog.Error("No art files found", "gameID", gameID)
} else {
slog.Info("Finished downloading art files", "gameID", gameID, "total", totalDownloads)
}
} }
func downloadFile(url, target string) (bool, error) { func downloadFile(url, target string) (bool, error) {

85
internal/cfg.go Normal file
View File

@@ -0,0 +1,85 @@
package oplcli
import (
"fmt"
"io"
"log/slog"
"net/http"
"os"
"path"
)
type FileMode int
const (
ModeOverride FileMode = iota
ModeAppend
ModeOnlyCreate
)
func downloadCfgForGame(gameID string, output string, mode FileMode) error {
url := getDownloadURLFromGithub(gameID)
filePath := path.Join(output, fmt.Sprintf("%s.cfg", gameID))
file, err := openFileBasedOnMode(filePath, mode)
if err != nil {
slog.Error("Failed to open cfg file", "file", filePath, "gameID", gameID, "err", err)
return err
}
defer file.Close()
resp, err := http.Get(url)
if err != nil {
slog.Error("Failed to download cfg file", "file", filePath, "gameID", gameID, "err", err)
return err
}
defer resp.Body.Close()
_, err = io.Copy(file, resp.Body)
if err != nil {
slog.Error("Failed to write cfg file", "file", filePath, "gameID", gameID, "err", err)
return err
}
return nil
}
func openFileBasedOnMode(filePath string, mode FileMode) (*os.File, error) {
_, err := os.Stat(filePath)
if err != nil {
if err != os.ErrNotExist {
file, err := os.Create(filePath)
if err != nil {
return nil, err
}
return file, nil
}
}
if mode == ModeOverride {
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
return nil, err
}
return file, nil
}
if mode == ModeAppend {
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
return nil, err
}
return file, nil
}
return nil, fmt.Errorf("File already exists")
}
func getDownloadURLFromGithub(gameID string) string {
// const baseURL = "https://raw.githubusercontent.com/VTSTech/PS2-OPL-CFG/refs/heads/master/CFG/" // This one is more basic
const baseURL = "https://raw.githubusercontent.com/Tom-Bruise/PS2-OPL-CFG-Database/refs/heads/master/CFG_en/" // This one contains descriptions and more
return fmt.Sprintf("%s/%s.cfg", baseURL, gameID)
}

View File

@@ -2,6 +2,7 @@ package oplcli
import ( import (
"fmt" "fmt"
"log/slog"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
@@ -11,7 +12,8 @@ import (
func scanGameFileForID(filename string) (string, error) { func scanGameFileForID(filename string) (string, error) {
file, err := os.Open(filename) file, err := os.Open(filename)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to open file: %w", err) slog.Error("Failed to open file", "file", filename, "err", err)
return "", err
} }
defer file.Close() defer file.Close()
@@ -20,11 +22,13 @@ func scanGameFileForID(filename string) (string, error) {
n, err := file.Read(buffer) n, err := file.Read(buffer)
if err != nil && n == 0 { if err != nil && n == 0 {
return "", fmt.Errorf("failed to read file: %w", err) slog.Error("Failed to read file", "file", file, "err", err)
return "", err
} }
id, found := scanBufferForID(buffer[:n]) id, found := scanBufferForID(buffer[:n])
if !found { if !found {
slog.Error("No PS2 ID found in the file", "file", file)
return "", fmt.Errorf("no PS2 game ID found in file") return "", fmt.Errorf("no PS2 game ID found in file")
} }

View File

@@ -3,6 +3,7 @@ package oplcli
import ( import (
"bufio" "bufio"
"fmt" "fmt"
"log/slog"
"os" "os"
"strings" "strings"
) )
@@ -16,15 +17,16 @@ func loadGamelist(filepath string) (map[string]string, error) {
gameList := make(map[string]string) gameList := make(map[string]string)
lineNum := 0
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() line := scanner.Text()
lineNum++
substrings := strings.SplitN(line, " ", 2) substrings := strings.SplitN(line, " ", 2)
if len(substrings) != 2 { if len(substrings) != 2 {
// TODO: something in the gamelist file is not formatted right slog.Warn("Line in gamelist is malformed", "line", lineNum)
fmt.Printf("Malformed: %s\n", line)
continue continue
} }

View File

@@ -1,73 +1,95 @@
package oplcli package oplcli
import ( import (
"fmt" "log/slog"
"os" "os"
) )
type Config struct { type Config struct {
GameListLocation string
} }
func Scan(file string, config Config) { func Scan(files []string, config Config) {
fmt.Printf("Scanning file: %s\n", file) gameList, err := loadGamelist(config.GameListLocation)
if err != nil {
slog.Error("Failed to load game list", "file", config.GameListLocation)
os.Exit(1)
}
for _, file := range files {
slog.Info("Scanning file", "file", file)
gameID, err := scanGameFileForID(file)
if err != nil {
slog.Error("Failed to scan game file", "file", file)
os.Exit(1)
}
slog.Info("Found game ID", "id", gameID)
gameName := gameList[gameID]
slog.Info("Found game name", "name", gameName)
}
}
func Rename(files []string, config Config) {
gameList, err := loadGamelist(config.GameListLocation)
if err != nil {
slog.Error("Failed to load game list", "file", config.GameListLocation)
os.Exit(1)
}
for _, file := range files {
slog.Info("Scanning file", "file", file)
gameID, err := scanGameFileForID(file) gameID, err := scanGameFileForID(file)
if err != nil { if err != nil {
fmt.Printf("Failed to scan game file: %s", err) slog.Error("Failed to scan game file", "file", file)
os.Exit(1) os.Exit(1)
} }
fmt.Printf("Found game ID: %s\n", gameID) slog.Info("Found game ID", "id", gameID)
gameList, err := loadGamelist("./ps2-gameslist.txt")
if err != nil {
fmt.Printf("Failed to load game list: %v", err)
os.Exit(1)
}
gameName := gameList[gameID] gameName := gameList[gameID]
fmt.Printf("Found game name: %s\n", gameName) slog.Info("Found game name", "name", gameName)
}
func Rename(file string, config Config) {
fmt.Printf("Scanning file: %s\n", file)
gameID, err := scanGameFileForID(file)
if err != nil {
fmt.Printf("Failed to scan game file: %s", err)
os.Exit(1)
}
fmt.Printf("Found game ID: %s\n", gameID)
gameList, err := loadGamelist("./ps2-gameslist.txt")
if err != nil {
fmt.Printf("Failed to load game list: %v", err)
os.Exit(1)
}
gameName := gameList[gameID]
fmt.Printf("Found game name: %s\n", gameName)
newPath, err := renameGameFile(file, gameName) newPath, err := renameGameFile(file, gameName)
if err != nil { if err != nil {
fmt.Printf("Failed to rename file: %v", err) slog.Error("Failed to reanme file", "file", file, "gameName", gameName, "err", err)
os.Exit(1) os.Exit(1)
} }
fmt.Printf("Renamed %s to %s", file, newPath) slog.Info("Renamed file", "file", file, "to", newPath)
}
} }
func DownloadGameArt(file string, config Config) { func DownloadGameArt(files []string, output string, config Config) {
fmt.Printf("Scanning file: %s\n", file) for _, file := range files {
slog.Info("Scanning file", "file", file)
gameID, err := scanGameFileForID(file) gameID, err := scanGameFileForID(file)
if err != nil { if err != nil {
fmt.Printf("Failed to scan game file: %s", err) slog.Error("Failed to scan game file", "file", file)
os.Exit(1) os.Exit(1)
} }
fmt.Printf("Found game ID: %s\n", gameID) slog.Info("Found game ID", "id", gameID)
downloadArtForGame(gameID, "./art") downloadArtForGame(gameID, output)
}
}
func DownloadCfg(files []string, output string, mode FileMode, config Config) {
for _, file := range files {
slog.Info("Scanning file", "file", file)
gameID, err := scanGameFileForID(file)
if err != nil {
slog.Error("Failed to scan game file", "file", file)
os.Exit(1)
}
slog.Info("Found game ID", "id", gameID)
downloadCfgForGame(gameID, output, mode)
}
} }