Files
music-rename/main.go

454 lines
8.9 KiB
Go

package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"os"
"path"
"regexp"
"strings"
"text/template"
"time"
"slices"
"github.com/dhowden/tag"
"github.com/joho/godotenv"
)
type DownloadDirectoryComplete struct {
Version int `json:"version"`
LocalDirectoryName string `json:"localDirectoryName"`
RemoteDirectoryName string `json:"remoteDirectoryName"`
Username string `json:"username"`
}
var printLogs = true
func (d *DownloadDirectoryComplete) WarnVersion() {
supportedVersions := []int{ 0 }
if !slices.Contains(supportedVersions, d.Version) {
supportedVersionsStr := strings.Trim(strings.Join(strings.Fields(fmt.Sprint(supportedVersions)), ","), "[]")
log.Printf("WARNING: Given data version not supported. Supported versions: %s. Given version: %d. Continuing, but there may be demons\n", supportedVersionsStr, d.Version)
}
}
func main() {
err := mainErr()
if err != nil {
log.Fatal("ERROR: ", err)
}
}
func mainErr() error {
if _, err := os.Stat(".env"); err == nil {
if err := godotenv.Load(".env"); err != nil {
return err
}
}
execPath, _ := path.Split(os.Args[0])
localConfPath := path.Join(execPath, "music-rename.env")
if _, err := os.Stat(localConfPath); err == nil {
if err := godotenv.Load(localConfPath); err != nil {
return err
}
}
directory, err := getDir()
if err != nil {
return err
}
return mainPath(directory)
}
func getDir() (string, error) {
if len(os.Args) > 1 {
return os.Args[1], nil
} else {
dataArg, found := os.LookupEnv("SLSKD_SCRIPT_DATA")
if !found {
return "", errors.New("missing any reference to a path")
}
var data DownloadDirectoryComplete
if err := json.Unmarshal([]byte(dataArg), &data); err != nil {
return "", err
}
data.WarnVersion()
printLogs = false
return data.LocalDirectoryName, nil
}
}
func mainPath(directory string) error {
if err := testEnv(); err != nil {
return err
}
// parse $DATA
files, err := os.ReadDir(directory)
if err != nil {
return err
}
if len(files) == 0 {
return errors.New("no files found")
}
// read tags
tags, _ := getTagsInDir(directory)
// move files
if tags != nil {
if printLogs {
log.Println("found tags, moving based on tags")
}
err := doMoveTags(directory, tags)
if err != nil {
return err
}
} else {
if printLogs {
log.Println("no tags found, moving to notags folder")
}
err := doMoveNotags(directory)
if err != nil {
return err
}
}
return nil
}
func testEnv() error {
_, ok := os.LookupEnv("LIBRARY_ROOT")
if !ok {
return errors.New("missing LIBRARY_ROOT")
}
_, ok = os.LookupEnv("LIBRARY_FILETEMPLATE_PATH")
if !ok {
return errors.New("missing LIBRARY_FILETEMPLATE_PATH")
}
_, ok = os.LookupEnv("LIBRARY_FILETEMPLATE_FILE")
if !ok {
return errors.New("missing LIBRARY_FILETEMPLATE_FILE")
}
_, ok = os.LookupEnv("LIBRARY_NOTAGS_ROOT")
if !ok {
return errors.New("missing LIBRARY_NOTAGS_ROOT")
}
return nil
}
func getTagsInDir(directory string) (tag.Metadata, error) {
files, err := os.ReadDir(directory)
if err != nil {
return nil, err
}
missingTags := false
for _, file := range files {
fname := path.Join(directory, file.Name())
if file.IsDir() {
continue
}
file, err := os.Open(fname)
if err != nil {
continue
}
defer file.Close()
tags, err := tag.ReadFrom(file)
if err != nil {
continue
}
// if we found tags but they're incomplete, switch the flag
if !checkTags(tags) {
missingTags = true
continue
}
// found tag data
return tags, nil
}
// no tag data in any file
if missingTags {
return nil, errors.New("files have incomplete tag data")
} else {
return nil, errors.New("unable to read tag data")
}
}
func checkTags(tags tag.Metadata) bool {
// most important tags
if tags.Album() == "" || tags.AlbumArtist() == "" || tags.Artist() == "" || tags.Title() == "" {
return false
}
return true
}
func doMoveNotags(directory string) error {
newRoot := os.Getenv("LIBRARY_NOTAGS_ROOT")
_, folderName := path.Split(directory)
toDirectory := path.Join(newRoot, folderName)
return move(directory, toDirectory)
}
func doMoveTags(directory string, albumTags tag.Metadata) error {
// move all tagged files
files, err := os.ReadDir(directory)
if err != nil {
return err
}
for _, file := range files {
if file.IsDir() {
continue
}
if file.Name()[0] == '.' {
continue
}
fname := path.Join(directory, file.Name())
file, err := os.Open(fname)
if err != nil {
continue
}
defer file.Close()
tags, err := tag.ReadFrom(file)
if err != nil {
continue
}
err = doMoveTagged(fname, tags)
if err != nil {
return err
}
}
// move the rest
files, err = os.ReadDir(directory)
if err != nil {
return err
}
for _, file := range files {
if file.Name()[0] == '.' {
continue
}
fname := path.Join(directory, file.Name())
err = doMoveTaggedPathonly(fname, albumTags)
if err != nil {
return err
}
}
// delete folder
files, err = os.ReadDir(directory)
if err != nil {
return err
}
if len(files) == 0 {
return os.Remove(directory)
}
// some files remain, wait a second to see if they go away
time.Sleep(1 * time.Second)
files, err = os.ReadDir(directory)
if err != nil {
return err
}
if len(files) == 0 {
return os.Remove(directory)
}
return nil
}
type LibTemplatePath struct {
Album string
AlbumArtist string
Genre string
Year int
}
type LibTemplateFile struct {
Title string
Album string
Artist string
AlbumArtist string
Composer string
Genre string
Year int
Track int
TrackCount int
Disc int
DiscCount int
}
func doMoveTagged(filePath string, fileTags tag.Metadata) error {
libRoot := os.Getenv("LIBRARY_ROOT")
libTemplatePath := os.Getenv("LIBRARY_FILETEMPLATE_PATH")
libTemplateFile := os.Getenv("LIBRARY_FILETEMPLATE_FILE")
fileExt := path.Ext(filePath)
track, trackCount := fileTags.Track()
disc, discCount := fileTags.Disc()
tmplPath := template.Must(template.New("libPath").Parse(libTemplatePath))
tmplFile := template.Must(template.New("libFile").Parse(libTemplateFile))
tmplDataFile := LibTemplateFile{
Title: fileTags.Title(),
Album: fileTags.Album(),
Artist: fileTags.Artist(),
AlbumArtist: fileTags.AlbumArtist(),
Composer: fileTags.Composer(),
Genre: fileTags.Genre(),
Year: fileTags.Year(),
Track: track,
TrackCount: trackCount,
Disc: disc,
DiscCount: discCount,
}
tmplDataPath := LibTemplatePath{
Album: fileTags.Album(),
AlbumArtist: fileTags.AlbumArtist(),
Genre: fileTags.Genre(),
Year: fileTags.Year(),
}
bufPath := bytes.NewBufferString("")
err := tmplPath.Execute(bufPath, tmplDataPath)
if err != nil {
return err
}
bufFile := bytes.NewBufferString("")
err = tmplFile.Execute(bufFile, tmplDataFile)
if err != nil {
return err
}
libFile := path.Join(libRoot, bufPath.String(), bufFile.String() + fileExt)
return move(filePath, libFile)
}
func doMoveTaggedPathonly(filePath string, albumTags tag.Metadata) error {
libRoot := os.Getenv("LIBRARY_ROOT")
libTemplatePath := os.Getenv("LIBRARY_FILETEMPLATE_PATH")
_, fileName := path.Split(filePath)
tmplPath := template.Must(template.New("libPath").Parse(libTemplatePath))
tmplData := LibTemplatePath{
Album: albumTags.Album(),
AlbumArtist: albumTags.AlbumArtist(),
Genre: albumTags.Genre(),
Year: albumTags.Year(),
}
bufPath := bytes.NewBufferString("")
err := tmplPath.Execute(bufPath, tmplData)
if err != nil {
return err
}
libFile := path.Join(libRoot, bufPath.String(), fileName)
return move(filePath, libFile)
}
func ensureDirectoryPath(filePath string) error {
directoryPath, _ := path.Split(filePath)
return os.MkdirAll(directoryPath, 0777)
}
func move(from, to string) error {
to = cleanPath(to)
if _, err := os.Stat(to); err == nil {
return fmt.Errorf("file already exists: %s", to)
}
if err := ensureDirectoryPath(to); err != nil {
return err
}
if printLogs {
log.Printf("moving \"%s\" => \"%s\"\n", from, to)
}
// os.Rename gives error "invalid cross-device link"
if err := os.Rename(from, to); err != nil {
return moveFile(from, to)
}
return nil
}
func moveFile(source, destination string) (err error) {
src, err := os.Open(source)
if err != nil {
return err
}
defer src.Close()
fi, err := src.Stat()
if err != nil {
return err
}
flag := os.O_WRONLY | os.O_CREATE | os.O_TRUNC
perm := fi.Mode() & os.ModePerm
dst, err := os.OpenFile(destination, flag, perm)
if err != nil {
return err
}
defer dst.Close()
_, err = io.Copy(dst, src)
if err != nil {
dst.Close()
os.Remove(destination)
return err
}
err = dst.Close()
if err != nil {
return err
}
err = src.Close()
if err != nil {
return err
}
err = os.Remove(source)
if err != nil {
return err
}
return nil
}
func cleanPath(str string) string {
re := regexp.MustCompile(`[<>:"|?*]`)
return re.ReplaceAllString(str, "")
}