initial commit
This commit is contained in:
65
internal/metadata.go
Normal file
65
internal/metadata.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package ripsort
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/dhowden/tag"
|
||||
)
|
||||
|
||||
type metadata struct {
|
||||
Format tag.Format
|
||||
FileType tag.FileType
|
||||
Title string
|
||||
Artist []string
|
||||
Album string
|
||||
AlbumArtist []string
|
||||
Composer []string
|
||||
Genre string
|
||||
Year int
|
||||
Track int
|
||||
TotalTracks int
|
||||
Disc int
|
||||
TotalDiscs int
|
||||
Lyrics string
|
||||
Comment string
|
||||
HasPicture bool
|
||||
}
|
||||
|
||||
func ReadAudioTags(filePath string) (*metadata, error) {
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open file: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
m, err := tag.ReadFrom(f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read tags: %w", err)
|
||||
}
|
||||
|
||||
track, totalTracks := m.Track()
|
||||
disc, totalDiscs := m.Disc()
|
||||
|
||||
info := &metadata{
|
||||
Format: m.Format(),
|
||||
FileType: m.FileType(),
|
||||
Title: m.Title(),
|
||||
Artist: strings.Split(m.Artist(), ";"),
|
||||
Album: m.Album(),
|
||||
AlbumArtist: strings.Split(m.AlbumArtist(), ";"),
|
||||
Composer: strings.Split(m.Composer(), ";"),
|
||||
Genre: m.Genre(),
|
||||
Year: m.Year(),
|
||||
Track: track,
|
||||
TotalTracks: totalTracks,
|
||||
Disc: disc,
|
||||
TotalDiscs: totalDiscs,
|
||||
Lyrics: m.Lyrics(),
|
||||
Comment: m.Comment(),
|
||||
HasPicture: m.Picture() != nil,
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
72
internal/ripsort.go
Normal file
72
internal/ripsort.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package ripsort
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func Scan(filePath string) {
|
||||
info, err := ReadAudioTags(filePath)
|
||||
|
||||
if err != nil {
|
||||
slog.Error("Reading metadata", "file", filePath, "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Format: %v\n", info.Format)
|
||||
fmt.Printf("File Type: %v\n", info.FileType)
|
||||
fmt.Printf("Title: %s\n", info.Title)
|
||||
fmt.Printf("Artist: %s\n", info.Artist)
|
||||
fmt.Printf("Album: %s\n", info.Album)
|
||||
fmt.Printf("Album Artist: %s\n", info.AlbumArtist)
|
||||
fmt.Printf("Composer: %s\n", info.Composer)
|
||||
fmt.Printf("Genre: %s\n", info.Genre)
|
||||
fmt.Printf("Year: %d\n", info.Year)
|
||||
fmt.Printf("Track: %d/%d\n", info.Track, info.TotalTracks)
|
||||
fmt.Printf("Disc: %d/%d\n", info.Disc, info.TotalDiscs)
|
||||
fmt.Printf("Lyrics: %s\n", info.Lyrics)
|
||||
fmt.Printf("Comment: %s\n", info.Comment)
|
||||
fmt.Printf("Has Picture: %v\n", info.HasPicture)
|
||||
|
||||
sortPath := pathForFile(filePath, *info)
|
||||
fmt.Printf("Sort path: %s\n", sortPath)
|
||||
}
|
||||
|
||||
func Sort(dst, path string) {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
slog.Error("Failed to stat path", "file", path, "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
if err := sortSong(path, dst); err != nil {
|
||||
slog.Error("Failed to sort single file", "file", path, "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
err = filepath.WalkDir(path, func(p string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
slog.Error("Failed to walk path", "path", path, "file", p, "err", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if !d.IsDir() {
|
||||
if err := sortSong(p, dst); err != nil {
|
||||
slog.Error("Failed to sort file", "file", p, "err", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
125
internal/sorter.go
Normal file
125
internal/sorter.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package ripsort
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func sortSong(src, dst string) error {
|
||||
m, err := ReadAudioTags(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dstPath := pathForFile(src, *m)
|
||||
finalPath := filepath.Join(dst, dstPath)
|
||||
|
||||
err = copyFile(src, finalPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func sanitizeName(name string) *string {
|
||||
if name == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
re := regexp.MustCompile(`[<>:"/\\|?*\x00-\x1F]`)
|
||||
name = re.ReplaceAllString(name, "_")
|
||||
|
||||
name = strings.Trim(name, " .")
|
||||
|
||||
if name == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &name
|
||||
}
|
||||
|
||||
func getArtistName(m metadata) string {
|
||||
var artist *string
|
||||
|
||||
if len(m.Artist) > 0 {
|
||||
artist = sanitizeName(m.Artist[0])
|
||||
}
|
||||
|
||||
if artist == nil && len(m.AlbumArtist) > 0 {
|
||||
if aa := m.AlbumArtist[0]; aa != "" {
|
||||
artist = sanitizeName(aa)
|
||||
}
|
||||
}
|
||||
|
||||
if artist == nil {
|
||||
return "Unknown Artist"
|
||||
}
|
||||
|
||||
return *artist
|
||||
}
|
||||
|
||||
func getAlbumName(m metadata) string {
|
||||
album := sanitizeName(m.Album)
|
||||
if album == nil {
|
||||
return "Unknown Album"
|
||||
}
|
||||
return *album
|
||||
}
|
||||
|
||||
func getTitle(src string, m metadata) string {
|
||||
title := sanitizeName(m.Title)
|
||||
if title == nil {
|
||||
return *sanitizeName(strings.TrimSuffix(filepath.Base(src), filepath.Ext(src)))
|
||||
}
|
||||
return *title
|
||||
}
|
||||
|
||||
func pathForFile(src string, m metadata) string {
|
||||
ext := strings.ToLower(filepath.Ext(src))
|
||||
|
||||
artist := getArtistName(m)
|
||||
album := getAlbumName(m)
|
||||
title := getTitle(src, m)
|
||||
|
||||
filename := title + ext
|
||||
|
||||
return filepath.Join(artist, album, filename)
|
||||
}
|
||||
|
||||
func copyFile(src, dst string) error {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open source file: %w", err)
|
||||
}
|
||||
defer in.Close()
|
||||
|
||||
dir := filepath.Dir(dst)
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return fmt.Errorf("create destination directory: %w", err)
|
||||
}
|
||||
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create destination file: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if cerr := out.Close(); err == nil && cerr != nil {
|
||||
err = cerr
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err = io.Copy(out, in); err != nil {
|
||||
return fmt.Errorf("copy file contents: %w", err)
|
||||
}
|
||||
|
||||
if err = out.Sync(); err != nil {
|
||||
return fmt.Errorf("sync destination file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user