initial commit
This commit is contained in:
commit
32a57cc050
12 changed files with 1169 additions and 0 deletions
193
musicindex/index.go
Normal file
193
musicindex/index.go
Normal 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
118
musicindex/musicindex.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue