Files

277 lines
7.3 KiB
Go

package music
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"jsuse.com/dev/dec-music/decrypt"
"jsuse.com/dev/dec-music/internal/fileutil"
"jsuse.com/dev/dec-music/internal/sniff"
"jsuse.com/dev/dec-music/log"
_ "jsuse.com/dev/dec-music/decrypt/kgm"
_ "jsuse.com/dev/dec-music/decrypt/kwm"
_ "jsuse.com/dev/dec-music/decrypt/ncm"
_ "jsuse.com/dev/dec-music/decrypt/qmc"
_ "jsuse.com/dev/dec-music/decrypt/tm"
_ "jsuse.com/dev/dec-music/decrypt/xiami"
_ "jsuse.com/dev/dec-music/decrypt/ximalaya"
)
const (
// ErrNoDecoder is returned when no decoder can handle the file.
ErrNoDecoder = "no suitable decoder"
)
var defaultLogger = log.New()
// OnProcess is called after each file is processed (err empty on success).
type OnProcess func(src, dst, err string)
// DecryptFile decrypts a single file into outputDir.
func DecryptFile(inputFile, outputDir string, noSubOutput bool, onProcess OnProcess) error {
inputDir, err := filepath.Abs(filepath.Dir(inputFile))
if err != nil {
return err
}
outDir, err := filepath.Abs(outputDir)
if err != nil {
return err
}
return decryptProcessFile(inputFile, outDir, inputDir, noSubOutput, false, false, onProcess)
}
func decryptProcessFile(inputFile, outputDir, inputDir string, noSubOutput, skipNoop, overwrite bool, onProcess OnProcess) error {
proc := &processor{
onProcess: onProcess,
logger: defaultLogger,
inputDir: inputDir,
outputDir: outputDir,
singleOutput: noSubOutput,
skipNoopDecoder: skipNoop,
overwriteOutput: overwrite,
}
return proc.processFile(inputFile)
}
// DecryptProcess decrypts encrypted media under inputDir into outputDir.
// When noSubOutput is false, relative subdirectory structure is preserved.
func DecryptProcess(inputDir, outputDir string, noSubOutput bool, onProcess OnProcess) error {
return decryptProcessEx(inputDir, outputDir, noSubOutput, false, false, onProcess)
}
// DecryptProcessCLI is like DecryptProcess but accepts skip-noop and overwrite (CLI helper).
func DecryptProcessCLI(inputDir, outputDir string, noSubOutput, skipNoop, overwrite bool) error {
return decryptProcessEx(inputDir, outputDir, noSubOutput, skipNoop, overwrite, nil)
}
// DecryptFileCLI decrypts one file (CLI helper).
func DecryptFileCLI(inputFile, outputDir string, noSubOutput, skipNoop, overwrite bool) error {
inputDir, err := filepath.Abs(filepath.Dir(inputFile))
if err != nil {
return err
}
outDir, err := filepath.Abs(outputDir)
if err != nil {
return err
}
return decryptProcessFile(inputFile, outDir, inputDir, noSubOutput, skipNoop, overwrite, nil)
}
func decryptProcessEx(inputDir, outputDir string, noSubOutput, skipNoop, overwrite bool, onProcess OnProcess) error {
proc := &processor{
onProcess: onProcess,
logger: defaultLogger,
inputDir: inputDir,
outputDir: outputDir,
singleOutput: noSubOutput,
skipNoopDecoder: skipNoop,
overwriteOutput: overwrite,
}
return proc.processDir(inputDir)
}
type processor struct {
onProcess OnProcess
logger *log.Logger
inputDir string
outputDir string
skipNoopDecoder bool
overwriteOutput bool
singleOutput bool
}
func (p *processor) processDir(inputDir string) error {
items, err := os.ReadDir(inputDir)
if err != nil {
return err
}
var lastError error
for _, item := range items {
if strings.EqualFold(item.Name(), "temp") {
continue
}
filePath := filepath.Join(inputDir, item.Name())
if item.IsDir() {
if err = p.processDir(filePath); err != nil {
lastError = err
}
continue
}
if !fileutil.IsMediaFile(item.Name()) {
continue
}
if err = p.processFile(filePath); err != nil {
lastError = err
if p.logger != nil {
p.logger.Error("conversion failed:", item.Name(), err)
}
}
}
if lastError != nil {
return fmt.Errorf("last error: %w", lastError)
}
return nil
}
func (p *processor) processFile(filePath string) error {
factories := decrypt.GetDecoder(filePath, p.skipNoopDecoder)
if len(factories) == 0 {
return errors.New("skipping while no suitable decoder")
}
if err := p.process(filePath, factories); err != nil {
if err.Error() == ErrNoDecoder {
// Fallback: copy as-is when no decoder matches (keeps original extension).
fallbackDst := filepath.Join(p.outputDir, filepath.Base(filePath))
copyErr := p.copyFile(filePath, fallbackDst)
if copyErr == nil {
p.logResult(filePath, fallbackDst)
}
return copyErr
}
return err
}
return nil
}
func (p *processor) logResult(inputFile, outPath string) {
inputRelDir, err := filepath.Rel(p.inputDir, filepath.Dir(inputFile))
dstName := filepath.Base(outPath)
if !p.singleOutput {
dstName = filepath.Join(inputRelDir, filepath.Base(outPath))
}
srcName := filepath.Join(inputRelDir, filepath.Base(inputFile))
if err != nil {
if p.onProcess != nil {
p.onProcess(srcName, dstName, err.Error())
}
return
}
if p.onProcess != nil {
p.onProcess(srcName, dstName, "")
} else {
log.PrintSuccess(srcName, dstName)
}
}
func (p *processor) copyFile(src, dst string) error {
sourceFile, err := os.Open(src)
if err != nil {
return err
}
defer sourceFile.Close()
destFile, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return err
}
defer destFile.Close()
_, err = io.Copy(destFile, sourceFile)
return err
}
func (p *processor) findDecoder(factories []decrypt.Factory, params *decrypt.DecoderParams) (decrypt.Decoder, *decrypt.Factory, error) {
for i := range factories {
factory := &factories[i]
dec := factory.Create(params)
if err := dec.Validate(); err == nil {
return dec, factory, nil
}
}
return nil, nil, errors.New("no any decoder can resolve the file")
}
func (p *processor) process(inputFile string, factories []decrypt.Factory) error {
file, err := os.Open(inputFile)
if err != nil {
return err
}
defer file.Close()
params := &decrypt.DecoderParams{
Reader: file,
Extension: filepath.Ext(inputFile),
FilePath: inputFile,
Logger: p.logger,
}
dec, factory, err := p.findDecoder(factories, params)
if err != nil {
return errors.New(ErrNoDecoder)
}
header := bytes.NewBuffer(nil)
if _, err = io.CopyN(header, dec, 64); err != nil {
return fmt.Errorf("read header failed: %w", err)
}
audio := io.MultiReader(header, dec)
audioExt := sniff.AudioExtensionWithFallback(header.Bytes(), ".mp3")
inputRelDir, err := filepath.Rel(p.inputDir, filepath.Dir(inputFile))
if err != nil {
return fmt.Errorf("get relative dir failed: %w", err)
}
inFilename := strings.TrimSuffix(filepath.Base(inputFile), factory.Suffix)
outFolder := p.outputDir
if !p.singleOutput {
outFolder = filepath.Join(p.outputDir, inputRelDir)
}
if err = os.MkdirAll(outFolder, os.ModePerm); err != nil {
return err
}
outPath := filepath.Join(outFolder, inFilename+audioExt)
if !p.overwriteOutput {
if _, err := os.Stat(outPath); err == nil {
if p.logger != nil {
p.logger.Info("output already exists, skipped:", outPath)
}
return nil
} else if !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("stat output file failed: %w", err)
}
}
outFile, err := os.OpenFile(outPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return err
}
defer outFile.Close()
if _, err = io.Copy(outFile, audio); err != nil {
return err
}
p.logResult(inputFile, outPath)
return nil
}