initial commit

This commit is contained in:
Jeremy Baxter 2026-01-04 23:33:46 +13:00
commit 32a57cc050
12 changed files with 1169 additions and 0 deletions

193
musicindex/index.go Normal file
View file

@ -0,0 +1,193 @@
package musicindex
import (
"errors"
"fmt"
"io/fs"
"log"
"os"
"os/user"
"slices"
"strconv"
"strings"
"syscall"
"golang.org/x/sys/unix"
"github.com/walle/targz"
"git.baxters.nz/jeremy/records/util"
)
var TempDir = "/tmp/records"
func assertAccessTo(path string) {
f, err := os.Stat(path)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
util.Die("no such file or directory: %s", path)
}
util.Die("cannot stat %s: %s", path, err.Error())
}
if !f.IsDir() {
util.Die("not a directory: %s", path)
}
if unix.Access(path, unix.R_OK) == nil {
return
}
procUser, err := user.Current()
if err != nil {
util.Die("cannot access directory: %s", path)
return
}
groupObject, err := user.LookupGroupId(strconv.Itoa(syscall.Getgid()))
if err != nil {
util.Die("cannot access directory: %s", path)
return
}
util.Die("cannot access directory %s\nrepair all directories with: chown -R %s:%s %s",
path, procUser.Username, groupObject.Name, mediaDir)
}
func indexArtists() {
entries, err := util.Dirents(mediaDir)
if err != nil {
util.Die("%s", err.Error())
}
err = os.RemoveAll(TempDir)
if err != nil {
util.Die("cannot remove temporary directory %s: %s", TempDir, err.Error())
}
err = os.Mkdir(TempDir, 0711)
if err != nil {
util.Die("cannot make temporary directory %s: %s", TempDir, err.Error())
}
artists = make(map[string]Artist)
for _, artist := range entries {
stat, err := os.Stat(mediaDir + "/" + artist)
if err != nil {
continue
}
if !stat.IsDir() {
continue
}
var a Artist
a.Name = artist
a.Albums = indexAlbums(&a)
a.Songs = 0
a.Valid = true
for _, album := range a.Albums {
a.Songs += len(album.TrackFiles)
}
artists[a.Name] = a
}
}
func indexAlbums(artist *Artist) (albums map[string]Album) {
artistDir := mediaDir + "/" + artist.Name
assertAccessTo(artistDir)
entries, err := util.Dirents(artistDir)
if err != nil {
util.Die("%s", err.Error())
}
albums = make(map[string]Album)
for _, albumName := range entries {
util.Iprint("* index %s - %s\r", artist.Name, albumName)
albumDir := artistDir + "/" + albumName
album, err := indexAlbum(artist, albumName, albumDir)
if err != nil {
log.Printf("warn: skipping inaccessible album %s: %s", albumName, err.Error())
} else {
albums[albumName] = album
}
}
return
}
func indexAlbum(artist *Artist, albumName, albumDir string) (a Album, err error) {
stat, err := os.Stat(albumDir)
if err != nil {
return
}
if !stat.IsDir() {
err = errors.New("not a directory")
}
a.Artist = artist
a.Name = albumName
a.HasCover = false
a.CoverFile = ""
err = unix.Access(albumDir + "/" + defaultCoverFile, unix.R_OK)
if err == nil {
a.HasCover = true
a.CoverFile = defaultCoverFile
}
var tracks []string
albumDirEntries, err := util.Dirents(albumDir)
if err != nil {
return
}
for _, track := range albumDirEntries {
if strings.HasSuffix(track, trackFileExtension) {
tracks = append(tracks, track)
}
}
slices.Sort(tracks)
a.TrackFiles = tracks
hash := util.HashOf(a.Artist.Name + "::" + a.Name)
path := TempDir + "/" + hash
if _, err = os.Stat(path); errors.Is(err, os.ErrNotExist) {
a.Tarball = path
} else if err == nil {
a.Tarball = ""
for i := range 9 {
if _, err = os.Stat(fmt.Sprintf("%s.%d", path, i)); err == nil {
continue
} else if errors.Is(err, os.ErrNotExist) {
a.Tarball = path
} else {
err = errors.New("cannot create tarball in filesystem: " +
err.Error())
return
}
}
if len(a.Tarball) == 0 {
err = errors.New("cannot create tarball in filesystem; " +
"please clean out " + TempDir)
return
}
} else {
err = errors.New("cannot create tarball in filesystem: " + err.Error())
return
}
a.Tarball = path
err = targz.Compress(a.Directory(), a.Tarball)
if err != nil {
return
}
tarball, err := os.Stat(a.Tarball)
if err != nil {
return
}
a.TarballSize = tarball.Size()
err = nil
return
}

118
musicindex/musicindex.go Normal file
View file

@ -0,0 +1,118 @@
// musicindex.go
// Copyright (c) 2025 Jeremy Baxter.
// Ephemeral music (artist, album, cover & track) index
package musicindex
import (
"cmp"
"slices"
"git.baxters.nz/jeremy/records/util"
)
type Artist struct {
Name string
Albums map[string]Album
Songs int
Valid bool
}
type Album struct {
Artist *Artist
Name string
HasCover bool
CoverFile string
TrackFiles []string
Tarball string
TarballSize int64
}
var initialised = false
var artists map[string]Artist
var coverFileExtension string
var defaultCoverFile string
var mediaDir string
var trackFileExtension string
func Init(path string) {
InitWith(path, ".flac", "cover.jpg", ".jpg")
}
func InitWith(path, mediaExtension, coverFile, coverExtension string) {
if initialised {
return
}
mediaDir = path
trackFileExtension = mediaExtension
defaultCoverFile = coverFile
coverFileExtension = coverExtension
assertAccessTo(mediaDir)
indexArtists()
initialised = true
}
func MediaDirectory() string {
return mediaDir
}
func Artists() (l []string) {
for k, v := range artists {
util.Assert(k == v.Name)
l = append(l, k)
}
slices.Sort(l)
return
}
func ArtistExists(name string) bool {
_, ok := artists[name]
return ok
}
func FindArtist(name string) (a Artist) {
if ArtistExists(name) {
return artists[name]
}
a.Name = "Invalid artist"
a.Valid = false
return
}
func ArtistDirectory(name string) string {
return mediaDir + "/" + name
}
func (a Album) RelativeDirectory() string {
return a.Artist.Name + "/" + a.Name
}
func (a Album) Directory() string {
return mediaDir + "/" + a.RelativeDirectory()
}
func (a Album) RelativeCoverPath() string {
return a.RelativeDirectory() + "/" + a.CoverFile
}
func (a Album) CoverPath() string {
return mediaDir + "/" + a.RelativeCoverPath()
}
func Albums() (albums []Album) {
for _, artist := range Artists() {
for _, album := range FindArtist(artist).Albums {
albums = append(albums, album)
}
}
slices.SortFunc(albums, func(a, b Album) int {
return cmp.Compare(a.Name, b.Name)
})
return
}