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 {
File string `arg:"positional"`
Files []string `arg:"positional,required"`
}
type RenameCmd struct {
File string `arg:"positional"`
Files []string `arg:"positional,required"`
}
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 {
Scan *ScanCmd `arg:"subcommand:scan"`
Rename *RenameCmd `arg:"subcommand:rename"`
Art *ArtCmd `arg:"subcommand:art"`
Scan *ScanCmd `arg:"subcommand:scan"`
Rename *RenameCmd `arg:"subcommand:rename"`
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() {
var args args
arg.MustParse(&args)
p := arg.MustParse(&args)
config := oplcli.Config{
GameListLocation: args.GameListLocation,
}
switch {
case args.Scan != nil:
oplcli.Scan(args.Scan.File, oplcli.Config{})
oplcli.Scan(args.Scan.Files, config)
case args.Rename != nil:
oplcli.Rename(args.Rename.File, oplcli.Config{})
oplcli.Rename(args.Rename.Files, config)
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,51 +3,57 @@ package oplcli
import (
"fmt"
"io"
"log/slog"
"net/http"
"os"
"path"
)
type artUrls struct {
cover string
cover2 string
background string
disc string
spine string
logo string
screenshot string
cover string
cover2 string
background string
disc string
spine string
logo string
screenshot string
screenshot2 string
}
func getArtUrlForGameFromGithub(gameID string) artUrls {
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 {
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{
cover: genURL("COV"),
cover2: genURL("COV2"),
background: genURL("BG_00"),
disc: genURL("ICO"),
spine: genURL("LAB"),
logo: genURL("LGO"),
screenshot: genURL("SCR_00"),
cover: genURL("COV"),
cover2: genURL("COV2"),
background: genURL("BG_00"),
disc: genURL("ICO"),
spine: genURL("LAB"),
logo: genURL("LGO"),
screenshot: genURL("SCR_00"),
screenshot2: genURL("SCR_01"),
}
}
func downloadArtForGame(gameID string, targetDir string) error {
func downloadArtForGame(gameID string, targetDir string) {
urls := getArtUrlForGameFromGithub(gameID)
dl := func(url, kind string) (bool, error) {
return downloadFile(url, path.Join(targetDir, fmt.Sprintf("%s_%s.png", gameID, kind)))
totalDownloads := 0
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")
@@ -55,9 +61,14 @@ func downloadArtForGame(gameID string, targetDir string) error {
dl(urls.disc, "ICO")
dl(urls.background, "BG")
dl(urls.screenshot, "SCR")
dl(urls.screenshot2, "SRC2")
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) {

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 (
"fmt"
"log/slog"
"os"
"path/filepath"
"regexp"
@@ -11,7 +12,8 @@ import (
func scanGameFileForID(filename string) (string, error) {
file, err := os.Open(filename)
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()
@@ -20,11 +22,13 @@ func scanGameFileForID(filename string) (string, error) {
n, err := file.Read(buffer)
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])
if !found {
slog.Error("No PS2 ID found in the file", "file", file)
return "", fmt.Errorf("no PS2 game ID found in file")
}

View File

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

View File

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