cli/cmd/um/main.go

217 lines
5.6 KiB
Go
Raw Normal View History

2020-12-26 15:22:39 +00:00
package main
import (
"bytes"
2020-12-26 15:22:39 +00:00
"errors"
2021-11-11 16:23:37 +00:00
"fmt"
"io"
2021-12-13 20:01:04 +00:00
"os"
"path/filepath"
"runtime"
2022-11-18 23:25:39 +00:00
"runtime/debug"
2021-12-13 20:01:04 +00:00
"sort"
"strings"
2022-11-18 23:25:39 +00:00
"time"
2021-12-13 20:01:04 +00:00
"github.com/urfave/cli/v2"
"go.uber.org/zap"
2022-11-18 23:44:44 +00:00
"unlock-music.dev/cli/algo/common"
_ "unlock-music.dev/cli/algo/kgm"
_ "unlock-music.dev/cli/algo/kwm"
_ "unlock-music.dev/cli/algo/ncm"
_ "unlock-music.dev/cli/algo/qmc"
_ "unlock-music.dev/cli/algo/tm"
_ "unlock-music.dev/cli/algo/xm"
"unlock-music.dev/cli/internal/logging"
2020-12-26 15:22:39 +00:00
)
2021-11-11 16:23:37 +00:00
var AppVersion = "v0.0.6"
2021-05-16 04:30:48 +00:00
2022-11-18 23:25:41 +00:00
var logger, _ = logging.NewZapLogger() // TODO: inject logger to application, instead of using global logger
2020-12-26 15:22:39 +00:00
func main() {
2022-11-18 23:25:39 +00:00
module, ok := debug.ReadBuildInfo()
if ok && module.Main.Version != "(devel)" {
AppVersion = module.Main.Version
}
2020-12-26 15:22:39 +00:00
app := cli.App{
Name: "Unlock Music CLI",
HelpName: "um",
2022-11-18 23:25:39 +00:00
Usage: "Unlock your encrypted music file https://git.unlock-music.dev/um/cli",
2021-11-11 16:23:37 +00:00
Version: fmt.Sprintf("%s (%s,%s/%s)", AppVersion, runtime.Version(), runtime.GOOS, runtime.GOARCH),
2020-12-26 15:22:39 +00:00
Flags: []cli.Flag{
2021-05-16 08:59:49 +00:00
&cli.StringFlag{Name: "input", Aliases: []string{"i"}, Usage: "path to input file or dir", Required: false},
&cli.StringFlag{Name: "output", Aliases: []string{"o"}, Usage: "path to output dir", Required: false},
2022-02-24 15:32:17 +00:00
&cli.BoolFlag{Name: "remove-source", Aliases: []string{"rs"}, Usage: "remove source file", Required: false, Value: false},
2021-11-11 15:43:20 +00:00
&cli.BoolFlag{Name: "skip-noop", Aliases: []string{"n"}, Usage: "skip noop decoder", Required: false, Value: true},
2021-12-13 20:01:04 +00:00
&cli.BoolFlag{Name: "supported-ext", Usage: "Show supported file extensions and exit", Required: false, Value: false},
2020-12-26 15:22:39 +00:00
},
2021-05-16 08:59:49 +00:00
2020-12-26 15:22:39 +00:00
Action: appMain,
2022-11-18 23:25:39 +00:00
Copyright: fmt.Sprintf("Copyright (c) 2020 - %d Unlock Music https://git.unlock-music.dev/um/cli/src/branch/master/LICENSE", time.Now().Year()),
2020-12-26 15:22:39 +00:00
HideHelpCommand: true,
2021-11-11 15:43:20 +00:00
UsageText: "um [-o /path/to/output/dir] [--extra-flags] [-i] /path/to/input",
2020-12-26 15:22:39 +00:00
}
err := app.Run(os.Args)
if err != nil {
2022-11-18 23:25:41 +00:00
logger.Fatal("run app failed", zap.Error(err))
2020-12-26 15:22:39 +00:00
}
}
2021-12-13 20:01:04 +00:00
func printSupportedExtensions() {
2022-11-18 23:25:41 +00:00
var exts []string
2021-12-13 20:01:04 +00:00
for ext := range common.DecoderRegistry {
exts = append(exts, ext)
}
sort.Strings(exts)
for _, ext := range exts {
fmt.Printf("%s: %d\n", ext, len(common.DecoderRegistry[ext]))
}
}
2021-11-11 16:23:37 +00:00
func appMain(c *cli.Context) (err error) {
2021-12-13 20:01:04 +00:00
if c.Bool("supported-ext") {
printSupportedExtensions()
return nil
}
2020-12-26 15:22:39 +00:00
input := c.String("input")
2021-11-11 16:23:37 +00:00
if input == "" {
switch c.Args().Len() {
case 0:
input, err = os.Getwd()
if err != nil {
return err
}
case 1:
input = c.Args().Get(0)
default:
return errors.New("please specify input file (or directory)")
}
2021-05-16 08:59:49 +00:00
}
2020-12-26 15:22:39 +00:00
output := c.String("output")
2021-05-16 08:59:49 +00:00
if output == "" {
var err error
output, err = os.Getwd()
if err != nil {
return err
}
2021-11-11 16:23:37 +00:00
if input == output {
return errors.New("input and output path are same")
}
2021-05-16 08:59:49 +00:00
}
2021-11-11 15:43:20 +00:00
skipNoop := c.Bool("skip-noop")
2022-02-24 15:32:17 +00:00
removeSource := c.Bool("remove-source")
2021-11-11 15:43:20 +00:00
2020-12-26 15:22:39 +00:00
inputStat, err := os.Stat(input)
if err != nil {
return err
}
outputStat, err := os.Stat(output)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
err = os.MkdirAll(output, 0755)
}
if err != nil {
return err
}
} else if !outputStat.IsDir() {
return errors.New("output should be a writable directory")
}
if inputStat.IsDir() {
2022-02-24 15:32:17 +00:00
return dealDirectory(input, output, skipNoop, removeSource)
2020-12-26 15:22:39 +00:00
} else {
2021-11-11 15:43:20 +00:00
allDec := common.GetDecoder(inputStat.Name(), skipNoop)
2020-12-26 15:22:39 +00:00
if len(allDec) == 0 {
2022-11-18 23:25:41 +00:00
logger.Fatal("skipping while no suitable decoder")
2020-12-26 15:22:39 +00:00
}
2022-02-24 15:32:17 +00:00
return tryDecFile(input, output, allDec, removeSource)
2020-12-26 15:22:39 +00:00
}
}
2022-02-24 15:32:17 +00:00
func dealDirectory(inputDir string, outputDir string, skipNoop bool, removeSource bool) error {
2020-12-26 15:22:39 +00:00
items, err := os.ReadDir(inputDir)
if err != nil {
return err
}
for _, item := range items {
if item.IsDir() {
continue
}
2021-11-11 15:43:20 +00:00
allDec := common.GetDecoder(item.Name(), skipNoop)
2020-12-26 15:22:39 +00:00
if len(allDec) == 0 {
2022-11-18 23:25:41 +00:00
logger.Info("skipping while no suitable decoder", zap.String("file", item.Name()))
2020-12-26 15:22:39 +00:00
continue
}
2022-02-24 15:32:17 +00:00
err := tryDecFile(filepath.Join(inputDir, item.Name()), outputDir, allDec, removeSource)
2020-12-26 15:22:39 +00:00
if err != nil {
2022-11-18 23:25:41 +00:00
logger.Error("conversion failed", zap.String("source", item.Name()), zap.Error(err))
2020-12-26 15:22:39 +00:00
}
}
return nil
}
2022-02-24 15:32:17 +00:00
func tryDecFile(inputFile string, outputDir string, allDec []common.NewDecoderFunc, removeSource bool) error {
file, err := os.Open(inputFile)
2020-12-26 15:22:39 +00:00
if err != nil {
return err
}
defer file.Close()
2020-12-26 15:22:39 +00:00
var dec common.Decoder
for _, decFunc := range allDec {
dec = decFunc(file)
if err := dec.Validate(); err == nil {
break
2022-11-18 23:25:39 +00:00
} else {
2022-11-18 23:25:41 +00:00
logger.Warn("try decode failed", zap.Error(err))
2022-11-18 23:25:39 +00:00
dec = nil
2020-12-26 15:22:39 +00:00
}
}
if dec == nil {
return errors.New("no any decoder can resolve the file")
}
header := bytes.NewBuffer(nil)
_, err = io.CopyN(header, dec, 16)
if err != nil {
return fmt.Errorf("read header failed: %w", err)
2020-12-26 15:22:39 +00:00
}
outExt := ".mp3"
if ext, ok := common.SniffAll(header.Bytes()); ok {
outExt = ext
2020-12-26 15:22:39 +00:00
}
filenameOnly := strings.TrimSuffix(filepath.Base(inputFile), filepath.Ext(inputFile))
2021-05-16 04:18:19 +00:00
outPath := filepath.Join(outputDir, filenameOnly+outExt)
outFile, err := os.OpenFile(outPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
2020-12-26 15:22:39 +00:00
if err != nil {
return err
}
defer outFile.Close()
if _, err := io.Copy(outFile, header); err != nil {
return err
}
if _, err := io.Copy(outFile, dec); err != nil {
return err
}
2022-02-24 15:32:17 +00:00
// if source file need to be removed
if removeSource {
err := os.RemoveAll(inputFile)
if err != nil {
return err
}
2022-11-18 23:25:41 +00:00
logger.Info("successfully converted, and source file is removed", zap.String("source", inputFile), zap.String("destination", outPath))
2022-02-24 15:32:17 +00:00
} else {
2022-11-18 23:25:41 +00:00
logger.Info("successfully converted", zap.String("source", inputFile), zap.String("destination", outPath))
2022-02-24 15:32:17 +00:00
}
2020-12-26 15:22:39 +00:00
return nil
}