Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
72ace9fc62 | |||
074e4f874f | |||
2bfb5ffddf | |||
2c9de7c56c | |||
b374c11c86 | |||
6493b2c5fc | |||
f753b9c67d | |||
8829a3b3ba |
@ -1,6 +1,7 @@
|
||||
package ncm
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
"strings"
|
||||
|
||||
"unlock-music.dev/cli/algo/common"
|
||||
@ -17,33 +18,49 @@ type ncmMeta interface {
|
||||
}
|
||||
|
||||
type ncmMetaMusic struct {
|
||||
Format string `json:"format"`
|
||||
MusicName string `json:"musicName"`
|
||||
Artist [][]interface{} `json:"artist"`
|
||||
Album string `json:"album"`
|
||||
AlbumPicDocID interface{} `json:"albumPicDocId"`
|
||||
AlbumPic string `json:"albumPic"`
|
||||
Flag int `json:"flag"`
|
||||
Bitrate int `json:"bitrate"`
|
||||
Duration int `json:"duration"`
|
||||
Alias []interface{} `json:"alias"`
|
||||
TransNames []interface{} `json:"transNames"`
|
||||
logger *zap.Logger
|
||||
|
||||
Format string `json:"format"`
|
||||
MusicName string `json:"musicName"`
|
||||
Artist interface{} `json:"artist"`
|
||||
Album string `json:"album"`
|
||||
AlbumPicDocID interface{} `json:"albumPicDocId"`
|
||||
AlbumPic string `json:"albumPic"`
|
||||
Flag int `json:"flag"`
|
||||
Bitrate int `json:"bitrate"`
|
||||
Duration int `json:"duration"`
|
||||
Alias []interface{} `json:"alias"`
|
||||
TransNames []interface{} `json:"transNames"`
|
||||
}
|
||||
|
||||
func newNcmMetaMusic(logger *zap.Logger) *ncmMetaMusic {
|
||||
ncm := new(ncmMetaMusic)
|
||||
ncm.logger = logger.With(zap.String("module", "ncmMetaMusic"))
|
||||
return ncm
|
||||
}
|
||||
|
||||
func (m *ncmMetaMusic) GetAlbumImageURL() string {
|
||||
return m.AlbumPic
|
||||
}
|
||||
|
||||
func (m *ncmMetaMusic) GetArtists() (artists []string) {
|
||||
for _, artist := range m.Artist {
|
||||
for _, item := range artist {
|
||||
name, ok := item.(string)
|
||||
if ok {
|
||||
func (m *ncmMetaMusic) GetArtists() []string {
|
||||
m.logger.Debug("ncm artists", zap.Any("artists", m.Artist))
|
||||
|
||||
var artists []string = nil
|
||||
if jsonArtists, ok := m.Artist.([][]string); ok {
|
||||
for _, artist := range jsonArtists {
|
||||
for _, name := range artist {
|
||||
artists = append(artists, name)
|
||||
}
|
||||
}
|
||||
} else if artist, ok := m.Artist.(string); ok {
|
||||
// #78: artist is a string type.
|
||||
// https://git.unlock-music.dev/um/cli/issues/78
|
||||
artists = []string{artist}
|
||||
} else {
|
||||
m.logger.Warn("unexpected artist type", zap.Any("artists", m.Artist))
|
||||
}
|
||||
return
|
||||
return artists
|
||||
}
|
||||
|
||||
func (m *ncmMetaMusic) GetTitle() string {
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go.uber.org/zap"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
@ -30,11 +31,12 @@ var (
|
||||
)
|
||||
|
||||
func NewDecoder(p *common.DecoderParams) common.Decoder {
|
||||
return &Decoder{rd: p.Reader}
|
||||
return &Decoder{rd: p.Reader, logger: p.Logger.With(zap.String("module", "ncm"))}
|
||||
}
|
||||
|
||||
type Decoder struct {
|
||||
rd io.ReadSeeker // rd is the original file reader
|
||||
logger *zap.Logger
|
||||
rd io.ReadSeeker // rd is the original file reader
|
||||
|
||||
offset int
|
||||
cipher common.StreamDecoder
|
||||
@ -74,7 +76,7 @@ func (d *Decoder) Validate() error {
|
||||
}
|
||||
|
||||
if err := d.parseMeta(); err != nil {
|
||||
return fmt.Errorf("parse meta failed: %w", err)
|
||||
return fmt.Errorf("parse meta failed: %w (raw json=%s)", err, string(d.metaRaw))
|
||||
}
|
||||
|
||||
d.cipher = newNcmCipher(keyData)
|
||||
@ -181,7 +183,7 @@ func (d *Decoder) readCoverData() error {
|
||||
func (d *Decoder) parseMeta() error {
|
||||
switch d.metaType {
|
||||
case "music":
|
||||
d.meta = new(ncmMetaMusic)
|
||||
d.meta = newNcmMetaMusic(d.logger)
|
||||
return json.Unmarshal(d.metaRaw, d.meta)
|
||||
case "dj":
|
||||
d.meta = new(ncmMetaDJ)
|
||||
|
@ -5,21 +5,19 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/urfave/cli/v2"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/urfave/cli/v2"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"unlock-music.dev/cli/algo/common"
|
||||
_ "unlock-music.dev/cli/algo/kgm"
|
||||
_ "unlock-music.dev/cli/algo/kwm"
|
||||
@ -29,14 +27,13 @@ import (
|
||||
_ "unlock-music.dev/cli/algo/xiami"
|
||||
_ "unlock-music.dev/cli/algo/ximalaya"
|
||||
"unlock-music.dev/cli/internal/ffmpeg"
|
||||
"unlock-music.dev/cli/internal/logging"
|
||||
"unlock-music.dev/cli/internal/sniff"
|
||||
"unlock-music.dev/cli/internal/utils"
|
||||
)
|
||||
|
||||
var AppVersion = "v0.2.8"
|
||||
var AppVersion = "v0.2.11"
|
||||
|
||||
var logger, _ = logging.NewZapLogger() // TODO: inject logger to application, instead of using global logger
|
||||
var logger = setupLogger(false) // TODO: inject logger to application, instead of using global logger
|
||||
|
||||
func main() {
|
||||
module, ok := debug.ReadBuildInfo()
|
||||
@ -55,6 +52,7 @@ func main() {
|
||||
&cli.StringFlag{Name: "qmc-mmkv-key", Aliases: []string{"key"}, Usage: "mmkv password (16 ascii chars)", Required: false},
|
||||
&cli.BoolFlag{Name: "remove-source", Aliases: []string{"rs"}, Usage: "remove source file", Required: false, Value: false},
|
||||
&cli.BoolFlag{Name: "skip-noop", Aliases: []string{"n"}, Usage: "skip noop decoder", Required: false, Value: true},
|
||||
&cli.BoolFlag{Name: "verbose", Aliases: []string{"V"}, Usage: "verbose logging", Required: false, Value: false},
|
||||
&cli.BoolFlag{Name: "update-metadata", Usage: "update metadata & album art from network", Required: false, Value: false},
|
||||
&cli.BoolFlag{Name: "overwrite", Usage: "overwrite output file without asking", Required: false, Value: false},
|
||||
&cli.BoolFlag{Name: "watch", Usage: "watch the input dir and process new files", Required: false, Value: false},
|
||||
@ -67,6 +65,7 @@ func main() {
|
||||
HideHelpCommand: true,
|
||||
UsageText: "um [-o /path/to/output/dir] [--extra-flags] [-i] /path/to/input",
|
||||
}
|
||||
|
||||
err := app.Run(os.Args)
|
||||
if err != nil {
|
||||
logger.Fatal("run app failed", zap.Error(err))
|
||||
@ -93,7 +92,27 @@ func printSupportedExtensions() {
|
||||
}
|
||||
}
|
||||
|
||||
func setupLogger(verbose bool) *zap.Logger {
|
||||
logConfig := zap.NewProductionEncoderConfig()
|
||||
logConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
|
||||
logConfig.EncodeTime = zapcore.RFC3339TimeEncoder
|
||||
enabler := zap.LevelEnablerFunc(func(level zapcore.Level) bool {
|
||||
if verbose {
|
||||
return true
|
||||
}
|
||||
return level >= zapcore.InfoLevel
|
||||
})
|
||||
|
||||
return zap.New(zapcore.NewCore(
|
||||
zapcore.NewConsoleEncoder(logConfig),
|
||||
os.Stdout,
|
||||
enabler,
|
||||
))
|
||||
}
|
||||
|
||||
func appMain(c *cli.Context) (err error) {
|
||||
logger = setupLogger(c.Bool("verbose"))
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
@ -115,6 +134,11 @@ func appMain(c *cli.Context) (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
input, absErr := filepath.Abs(input)
|
||||
if absErr != nil {
|
||||
return fmt.Errorf("get abs path failed: %w", absErr)
|
||||
}
|
||||
|
||||
output := c.String("output")
|
||||
inputStat, err := os.Stat(input)
|
||||
if err != nil {
|
||||
@ -125,13 +149,18 @@ func appMain(c *cli.Context) (err error) {
|
||||
if inputStat.IsDir() {
|
||||
inputDir = input
|
||||
} else {
|
||||
inputDir = path.Dir(input)
|
||||
inputDir = filepath.Dir(input)
|
||||
}
|
||||
inputDir, absErr = filepath.Abs(inputDir)
|
||||
if absErr != nil {
|
||||
return fmt.Errorf("get abs path (inputDir) failed: %w", absErr)
|
||||
}
|
||||
|
||||
if output == "" {
|
||||
// Default to where the input dir is
|
||||
output = inputDir
|
||||
}
|
||||
logger.Debug("resolve input/output path", zap.String("inputDir", inputDir), zap.String("input", input), zap.String("output", output))
|
||||
|
||||
outputStat, err := os.Stat(output)
|
||||
if err != nil {
|
||||
@ -155,6 +184,7 @@ func appMain(c *cli.Context) (err error) {
|
||||
}
|
||||
|
||||
proc := &processor{
|
||||
logger: logger,
|
||||
inputDir: inputDir,
|
||||
outputDir: output,
|
||||
skipNoopDecoder: c.Bool("skip-noop"),
|
||||
@ -177,6 +207,7 @@ func appMain(c *cli.Context) (err error) {
|
||||
}
|
||||
|
||||
type processor struct {
|
||||
logger *zap.Logger
|
||||
inputDir string
|
||||
outputDir string
|
||||
|
||||
@ -268,6 +299,8 @@ func (p *processor) processDir(inputDir string) error {
|
||||
}
|
||||
|
||||
func (p *processor) processFile(filePath string) error {
|
||||
p.logger.Debug("processFile", zap.String("file", filePath), zap.String("inputDir", p.inputDir))
|
||||
|
||||
allDec := common.GetDecoder(filePath, p.skipNoopDecoder)
|
||||
if len(allDec) == 0 {
|
||||
return errors.New("skipping while no suitable decoder")
|
||||
@ -406,7 +439,7 @@ func (p *processor) process(inputFile string, allDec []common.DecoderFactory) er
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
if err := ffmpeg.UpdateMeta(ctx, outPath, params); err != nil {
|
||||
if err := ffmpeg.UpdateMeta(ctx, outPath, params, logger); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"go.uber.org/zap"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
@ -43,9 +44,9 @@ type UpdateMetadataParams struct {
|
||||
AlbumArtExt string // required if AlbumArt is not nil
|
||||
}
|
||||
|
||||
func UpdateMeta(ctx context.Context, outPath string, params *UpdateMetadataParams) error {
|
||||
func UpdateMeta(ctx context.Context, outPath string, params *UpdateMetadataParams, logger *zap.Logger) error {
|
||||
if params.AudioExt == ".flac" {
|
||||
return updateMetaFlac(ctx, outPath, params)
|
||||
return updateMetaFlac(ctx, outPath, params, logger.With(zap.String("module", "updateMetaFlac")))
|
||||
} else {
|
||||
return updateMetaFFmpeg(ctx, outPath, params)
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package ffmpeg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"go.uber.org/zap"
|
||||
"mime"
|
||||
"strings"
|
||||
|
||||
@ -11,7 +12,7 @@ import (
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
func updateMetaFlac(_ context.Context, outPath string, m *UpdateMetadataParams) error {
|
||||
func updateMetaFlac(_ context.Context, outPath string, m *UpdateMetadataParams, logger *zap.Logger) error {
|
||||
f, err := flac.ParseFile(m.Audio)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -62,27 +63,30 @@ func updateMetaFlac(_ context.Context, outPath string, m *UpdateMetadataParams)
|
||||
}
|
||||
|
||||
if m.AlbumArt != nil {
|
||||
|
||||
coverMime := mime.TypeByExtension(m.AlbumArtExt)
|
||||
logger.Debug("cover image mime detect", zap.String("mime", coverMime))
|
||||
cover, err := flacpicture.NewFromImageData(
|
||||
flacpicture.PictureTypeFrontCover,
|
||||
"Front cover",
|
||||
m.AlbumArt,
|
||||
mime.TypeByExtension(m.AlbumArtExt),
|
||||
coverMime,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
coverBlock := cover.Marshal()
|
||||
f.Meta = append(f.Meta, &coverBlock)
|
||||
|
||||
// add / replace flac cover
|
||||
coverIdx := slices.IndexFunc(f.Meta, func(b *flac.MetaDataBlock) bool {
|
||||
return b.Type == flac.Picture
|
||||
})
|
||||
if coverIdx < 0 {
|
||||
f.Meta = append(f.Meta, &coverBlock)
|
||||
if err != nil {
|
||||
logger.Warn("failed to create flac cover", zap.Error(err))
|
||||
} else {
|
||||
f.Meta[coverIdx] = &coverBlock
|
||||
coverBlock := cover.Marshal()
|
||||
f.Meta = append(f.Meta, &coverBlock)
|
||||
|
||||
// add / replace flac cover
|
||||
coverIdx := slices.IndexFunc(f.Meta, func(b *flac.MetaDataBlock) bool {
|
||||
return b.Type == flac.Picture
|
||||
})
|
||||
if coverIdx < 0 {
|
||||
f.Meta = append(f.Meta, &coverBlock)
|
||||
} else {
|
||||
f.Meta[coverIdx] = &coverBlock
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user