mirror of
https://github.com/gabehf/music-importer.git
synced 2026-04-22 11:31:52 -07:00
182 lines
4.2 KiB
Go
182 lines
4.2 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// albumTargetDir returns the destination directory for an album without
|
|
// creating it. Use this to check for an existing import before moving files.
|
|
func albumTargetDir(libDir string, md *MusicMetadata) string {
|
|
date := md.Date
|
|
if date == "" {
|
|
date = md.Year
|
|
}
|
|
albumDir := fmt.Sprintf("[%s] %s", date, md.Album)
|
|
if md.Quality != "" {
|
|
albumDir += fmt.Sprintf(" [%s]", md.Quality)
|
|
}
|
|
return filepath.Join(libDir, sanitize(md.Artist), sanitize(albumDir))
|
|
}
|
|
|
|
// moveToLibrary moves a file to {libDir}/{artist}/[{date}] {album} [{quality}]/filename.
|
|
func moveToLibrary(libDir string, md *MusicMetadata, srcPath string) error {
|
|
targetDir := albumTargetDir(libDir, md)
|
|
if err := os.MkdirAll(targetDir, 0755); err != nil {
|
|
return err
|
|
}
|
|
|
|
dst := filepath.Join(targetDir, filepath.Base(srcPath))
|
|
fmt.Println("→ Moving:", srcPath, "→", dst)
|
|
if strings.ToLower(os.Getenv("COPYMODE")) == "true" {
|
|
return copy(srcPath, dst)
|
|
} else {
|
|
return os.Rename(srcPath, dst)
|
|
}
|
|
}
|
|
|
|
// cluster moves all top-level audio files in dir into subdirectories named
|
|
// after their embedded album tag.
|
|
func cluster(dir string) error {
|
|
files, err := getAudioFiles(dir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, f := range files {
|
|
tags, err := readTags(f)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
albumDir := path.Join(dir, sanitize(tags.Album))
|
|
if err = os.MkdirAll(albumDir, 0755); err != nil {
|
|
return err
|
|
}
|
|
if err = os.Rename(f, path.Join(albumDir, path.Base(f))); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getAudioFiles returns all .flac and .mp3 files directly inside dir.
|
|
func getAudioFiles(dir string) ([]string, error) {
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var tracks []string
|
|
for _, e := range entries {
|
|
if e.IsDir() {
|
|
continue
|
|
}
|
|
ext := strings.ToLower(filepath.Ext(e.Name()))
|
|
if ext == ".flac" || ext == ".mp3" {
|
|
tracks = append(tracks, filepath.Join(dir, e.Name()))
|
|
}
|
|
}
|
|
|
|
return tracks, nil
|
|
}
|
|
|
|
// getLyricFiles returns all .lrc files directly inside dir.
|
|
func getLyricFiles(dir string) ([]string, error) {
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var lyrics []string
|
|
for _, e := range entries {
|
|
if e.IsDir() {
|
|
continue
|
|
}
|
|
if strings.ToLower(filepath.Ext(e.Name())) == ".lrc" {
|
|
lyrics = append(lyrics, filepath.Join(dir, e.Name()))
|
|
}
|
|
}
|
|
|
|
return lyrics, nil
|
|
}
|
|
|
|
// sanitize removes or replaces characters that are unsafe in file system paths.
|
|
func sanitize(s string) string {
|
|
r := strings.NewReplacer(
|
|
"/", "_",
|
|
"\\", "_",
|
|
":", "-",
|
|
"?", "",
|
|
"*", "",
|
|
"\"", "",
|
|
"<", "",
|
|
">", "",
|
|
"|", "",
|
|
)
|
|
return r.Replace(s)
|
|
}
|
|
|
|
// CopyFile copies a file from src to dst. If src and dst files exist, and are
|
|
// the same, then return success. Otherise, attempt to create a hard link
|
|
// between the two files. If that fail, copy the file contents from src to dst.
|
|
func copy(src, dst string) (err error) {
|
|
sfi, err := os.Stat(src)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if !sfi.Mode().IsRegular() {
|
|
// cannot copy non-regular files (e.g., directories,
|
|
// symlinks, devices, etc.)
|
|
return fmt.Errorf("CopyFile: non-regular source file %s (%q)", sfi.Name(), sfi.Mode().String())
|
|
}
|
|
dfi, err := os.Stat(dst)
|
|
if err != nil {
|
|
if !os.IsNotExist(err) {
|
|
return
|
|
}
|
|
} else {
|
|
if !(dfi.Mode().IsRegular()) {
|
|
return fmt.Errorf("CopyFile: non-regular destination file %s (%q)", dfi.Name(), dfi.Mode().String())
|
|
}
|
|
if os.SameFile(sfi, dfi) {
|
|
return
|
|
}
|
|
}
|
|
if err = os.Link(src, dst); err == nil {
|
|
return
|
|
}
|
|
err = copyFileContents(src, dst)
|
|
return
|
|
}
|
|
|
|
// copyFileContents copies the contents of the file named src to the file named
|
|
// by dst. The file will be created if it does not already exist. If the
|
|
// destination file exists, all it's contents will be replaced by the contents
|
|
// of the source file.
|
|
func copyFileContents(src, dst string) (err error) {
|
|
in, err := os.Open(src)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer in.Close()
|
|
out, err := os.Create(dst)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer func() {
|
|
cerr := out.Close()
|
|
if err == nil {
|
|
err = cerr
|
|
}
|
|
}()
|
|
if _, err = io.Copy(out, in); err != nil {
|
|
return
|
|
}
|
|
err = out.Sync()
|
|
return
|
|
}
|