905 lines
24 KiB
Go
905 lines
24 KiB
Go
package handler
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"facette.io/natsort"
|
|
"fmt"
|
|
"git.gammaspectra.live/S.O.N.G/Hibiki/panako"
|
|
"git.gammaspectra.live/S.O.N.G/Hibiki/utilities/specializedstore"
|
|
"git.gammaspectra.live/S.O.N.G/Kirika/audio"
|
|
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format"
|
|
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/guess"
|
|
"git.gammaspectra.live/S.O.N.G/Kirika/audio/replaygain"
|
|
"git.gammaspectra.live/S.O.N.G/Kirika/hasher"
|
|
"git.gammaspectra.live/S.O.N.G/METANOIA/metadata"
|
|
"git.gammaspectra.live/S.O.N.G/METANOIA/utilities"
|
|
"github.com/dhowden/tag"
|
|
"github.com/oriser/regroup"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"path"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
"unicode"
|
|
)
|
|
|
|
type fileEntryList []fileEntry
|
|
|
|
const separatorTrimSet = ",.-_()[]{}"
|
|
|
|
func isSeparator(b byte) bool {
|
|
if b == ' ' {
|
|
return true
|
|
}
|
|
for i := 0; i < len(separatorTrimSet); i++ {
|
|
if separatorTrimSet[i] == b {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
type analyzeEntry struct {
|
|
fileEntry
|
|
fileHandle *os.File
|
|
audioMetadata struct {
|
|
sampleRate int
|
|
channels int
|
|
samples int
|
|
}
|
|
replayGain struct {
|
|
albumGain float64
|
|
albumPeak float64
|
|
trackGain float64
|
|
trackPeak float64
|
|
}
|
|
fileMetadata tag.Metadata
|
|
panakoFingerprints []*panako.Fingerprint
|
|
hasherCrc32 *hasher.Hasher
|
|
hasherCueToolsCrc32 *hasher.Hasher
|
|
hasherAccurateRipV1 *hasher.Hasher
|
|
hasherAccurateRipV2 *hasher.Hasher
|
|
}
|
|
|
|
func (l fileEntryList) analyze(directory string, panakoInstance *panako.Instance) (entries []*analyzeEntry, fullCRC32 uint32, fullCTDBCRC32 uint32) {
|
|
var waitGroups []*sync.WaitGroup
|
|
|
|
printStrategy := panakoInstance.GetStrategy(specializedstore.NewMemoryStore(), audio.Linear)
|
|
|
|
var joinedCTDBChannels []format.AnalyzerChannel
|
|
var joinedChannels []format.AnalyzerChannel
|
|
|
|
var preLastTotalSamplesWaitGroup sync.WaitGroup
|
|
preLastTotalSamples := uint32(0)
|
|
|
|
var replayGainSources []audio.Source
|
|
|
|
for trackIndex, e := range l {
|
|
f, err := os.Open(path.Join(directory, e.Name))
|
|
if err != nil {
|
|
//TODO
|
|
log.Print(err)
|
|
continue
|
|
}
|
|
|
|
var source audio.Source
|
|
var analyzer format.AnalyzerChannel
|
|
|
|
meta, err := tag.ReadFrom(f)
|
|
if err != nil {
|
|
log.Print(err)
|
|
err = nil
|
|
}
|
|
f.Seek(0, io.SeekStart)
|
|
|
|
decoders, err := guess.GetDecoders(f, f.Name())
|
|
if err != nil { //cannot decode
|
|
//TODO
|
|
log.Print(err)
|
|
f.Close()
|
|
continue
|
|
}
|
|
|
|
if source, analyzer, err = guess.OpenAnalyzer(f, decoders); err != nil || source.Blocks == nil {
|
|
analyzer = nil
|
|
source, err = guess.Open(f, decoders)
|
|
}
|
|
|
|
if err != nil { //cannot decode
|
|
//TODO
|
|
log.Print(err)
|
|
f.Close()
|
|
continue
|
|
}
|
|
|
|
if source.Blocks == nil { //no known decoder
|
|
//TODO
|
|
log.Print(fmt.Errorf("no known decoder for %s", f.Name()))
|
|
f.Close()
|
|
continue
|
|
}
|
|
|
|
entry := &analyzeEntry{
|
|
fileEntry: e,
|
|
fileHandle: f,
|
|
fileMetadata: meta,
|
|
}
|
|
|
|
sources := source.Split(3)
|
|
|
|
var sinkWaitGroup sync.WaitGroup
|
|
|
|
sinkWaitGroup.Add(1)
|
|
preLastTotalSamplesWaitGroup.Add(1)
|
|
go func(source audio.Source, add bool) {
|
|
defer sinkWaitGroup.Done()
|
|
defer preLastTotalSamplesWaitGroup.Done()
|
|
|
|
entry.audioMetadata.sampleRate = source.SampleRate
|
|
entry.audioMetadata.channels = source.Channels
|
|
var samples int
|
|
for block := range source.Blocks {
|
|
samples += len(block) / source.Channels
|
|
}
|
|
entry.audioMetadata.samples = samples
|
|
if add {
|
|
atomic.AddUint32(&preLastTotalSamples, uint32(samples))
|
|
}
|
|
}(sources[0], trackIndex < len(l)-1)
|
|
|
|
sinkWaitGroup.Add(1)
|
|
go func(add bool) {
|
|
defer sinkWaitGroup.Done()
|
|
entry.panakoFingerprints = printStrategy.BlockChannelToFingerprints(sources[1].Blocks)
|
|
}(trackIndex < len(l)-1)
|
|
|
|
replayGainSources = append(replayGainSources, sources[2])
|
|
|
|
//TODO: handle extra appended/prepended silence
|
|
|
|
if analyzer != nil {
|
|
if trackIndex == 0 {
|
|
channels := analyzer.Split(4)
|
|
joinedChannels = append(joinedChannels, channels[0])
|
|
ctChannels := channels[1].SkipStartSamples(metadata.Int16SamplesPerSector * 10).Split(2)
|
|
joinedCTDBChannels = append(joinedCTDBChannels, ctChannels[0])
|
|
entry.hasherCueToolsCrc32 = hasher.NewHasher(ctChannels[1], hasher.HashtypeCrc32)
|
|
arChannels := channels[2].SkipStartSamples(metadata.Int16SamplesPerSector*5 - 1).Split(2)
|
|
entry.hasherAccurateRipV1 = hasher.NewHasher(arChannels[0], hasher.HashtypeAccurateRipV1Start)
|
|
entry.hasherAccurateRipV2 = hasher.NewHasher(arChannels[1], hasher.HashtypeAccurateRipV2Start)
|
|
entry.hasherCrc32 = hasher.NewHasher(channels[3], hasher.HashtypeCrc32)
|
|
} else if trackIndex == len(l)-1 {
|
|
|
|
channels := analyzer.Split(4)
|
|
joinedChannels = append(joinedChannels, channels[0])
|
|
ctChannels := channels[1].SkipEndSamplesMultiple(&preLastTotalSamplesWaitGroup, &preLastTotalSamples, metadata.Int16SamplesPerSector*10).Split(2)
|
|
joinedCTDBChannels = append(joinedCTDBChannels, ctChannels[0])
|
|
entry.hasherCueToolsCrc32 = hasher.NewHasher(ctChannels[1], hasher.HashtypeCrc32)
|
|
arChannels := channels[2].SkipEndSamples(metadata.Int16SamplesPerSector * 5).Split(2)
|
|
entry.hasherAccurateRipV1 = hasher.NewHasher(arChannels[0], hasher.HashtypeAccurateRipV1)
|
|
entry.hasherAccurateRipV2 = hasher.NewHasher(arChannels[1], hasher.HashtypeAccurateRipV2)
|
|
entry.hasherCrc32 = hasher.NewHasher(channels[3], hasher.HashtypeCrc32)
|
|
} else {
|
|
channels := analyzer.Split(5)
|
|
joinedChannels = append(joinedChannels, channels[0])
|
|
joinedCTDBChannels = append(joinedCTDBChannels, channels[1])
|
|
entry.hasherCrc32 = hasher.NewHasher(channels[2], hasher.HashtypeCrc32)
|
|
entry.hasherAccurateRipV1 = hasher.NewHasher(channels[3], hasher.HashtypeAccurateRipV1)
|
|
entry.hasherAccurateRipV2 = hasher.NewHasher(channels[4], hasher.HashtypeAccurateRipV2)
|
|
}
|
|
|
|
waitGroups = append(waitGroups, entry.hasherCrc32.GetWaitGroup(), entry.hasherAccurateRipV1.GetWaitGroup(), entry.hasherAccurateRipV2.GetWaitGroup())
|
|
if entry.hasherCueToolsCrc32 != nil {
|
|
waitGroups = append(waitGroups, entry.hasherCueToolsCrc32.GetWaitGroup())
|
|
}
|
|
}
|
|
|
|
waitGroups = append(waitGroups, &sinkWaitGroup)
|
|
entries = append(entries, entry)
|
|
|
|
}
|
|
|
|
var rgwg sync.WaitGroup
|
|
rgwg.Add(1)
|
|
go func() {
|
|
defer rgwg.Done()
|
|
albumGain, albumPeak, trackGains, trackPeaks, err := replaygain.GetAlbumReplayGain(replayGainSources)
|
|
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
for i, e := range entries {
|
|
e.replayGain.albumGain = albumGain
|
|
e.replayGain.albumPeak = albumPeak
|
|
e.replayGain.trackGain = trackGains[i]
|
|
e.replayGain.trackPeak = trackPeaks[i]
|
|
}
|
|
}()
|
|
waitGroups = append(waitGroups, &rgwg)
|
|
|
|
fullHasher := hasher.NewHasher(format.MergeHasherChannels(joinedChannels...), hasher.HashtypeCrc32)
|
|
fullCTDBHasher := hasher.NewHasher(format.MergeHasherChannels(joinedCTDBChannels...), hasher.HashtypeCrc32)
|
|
|
|
fullHasher.Wait()
|
|
fullCTDBHasher.Wait()
|
|
|
|
fullCRC32 = binary.BigEndian.Uint32(fullHasher.GetResult())
|
|
fullCTDBCRC32 = binary.BigEndian.Uint32(fullCTDBHasher.GetResult())
|
|
|
|
//Wait for all tasks
|
|
for _, wg := range waitGroups {
|
|
wg.Wait()
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
type fileEntry struct {
|
|
Name string
|
|
NormalizedSortName string
|
|
NormalizedName string
|
|
}
|
|
|
|
func processAudioFiles(files []string) (result fileEntryList) {
|
|
|
|
result = make(fileEntryList, 0, len(files))
|
|
|
|
for _, f := range files {
|
|
normalized := utilities.NormalizeUnicode(f)
|
|
|
|
ext := strings.LastIndex(normalized, ".")
|
|
for k := 0; k < ext; k++ {
|
|
index := strings.IndexFunc(normalized[k:], unicode.IsNumber)
|
|
if index == -1 {
|
|
//wtf, no numbers?
|
|
result = append(result, fileEntry{
|
|
Name: f,
|
|
NormalizedSortName: strings.TrimSpace(strings.TrimLeft(strings.TrimSpace(normalized[:ext]), separatorTrimSet)),
|
|
NormalizedName: strings.TrimSpace(strings.TrimLeft(strings.TrimSpace(normalized[:ext]), separatorTrimSet)),
|
|
})
|
|
break
|
|
}
|
|
|
|
index += k
|
|
|
|
if index == 0 || isSeparator(normalized[index-1]) { //If it's start of string or prefixed by a space
|
|
normalized = normalized[index:ext]
|
|
|
|
firstNotNumber := strings.IndexFunc(normalized, func(r rune) bool {
|
|
return !unicode.IsNumber(r)
|
|
})
|
|
|
|
r := fileEntry{
|
|
Name: f,
|
|
NormalizedSortName: strings.TrimSpace(strings.TrimLeft(strings.TrimSpace(normalized), separatorTrimSet)),
|
|
NormalizedName: strings.TrimSpace(strings.TrimLeft(strings.TrimSpace(normalized[firstNotNumber:]), separatorTrimSet)),
|
|
}
|
|
result = append(result, r)
|
|
break
|
|
}
|
|
|
|
k = index
|
|
}
|
|
|
|
}
|
|
|
|
//Sort files naturally
|
|
sort.SliceStable(result, func(i, j int) bool {
|
|
return natsort.Compare(result[i].NormalizedSortName, result[j].NormalizedSortName)
|
|
})
|
|
|
|
return
|
|
}
|
|
|
|
type DiscHandlerResult struct {
|
|
TOC metadata.TOC
|
|
CRC32 uint32
|
|
CueToolsCRC32 uint32
|
|
Directory string
|
|
Tracks []DiscHandlerTrack
|
|
CommonMetadata map[string]string
|
|
Identifiers metadata.NameSlice
|
|
//DiscNumber 1-indexed disc number
|
|
DiscNumber int
|
|
DiscTotal int
|
|
Album string
|
|
|
|
ReplayGain struct {
|
|
Gain float64
|
|
Peak float64
|
|
}
|
|
}
|
|
|
|
func (d *DiscHandlerResult) ToMetadataAlbum() (album *metadata.Album) {
|
|
album = &metadata.Album{
|
|
SourceUniqueIdentifier: d.Directory,
|
|
License: metadata.License{
|
|
Code: metadata.Unknown,
|
|
},
|
|
Name: metadata.NameSlice{
|
|
{Kind: "file", Name: d.Album},
|
|
},
|
|
Identifiers: metadata.NameSlice{},
|
|
}
|
|
|
|
diskIds := make(metadata.NameSlice, 0)
|
|
|
|
diskIds = append(diskIds, metadata.Name{
|
|
Kind: "toc",
|
|
Name: d.TOC.String(),
|
|
})
|
|
diskIds = append(diskIds, metadata.Name{
|
|
Kind: "crc32",
|
|
Name: fmt.Sprintf("%08X", d.CRC32),
|
|
})
|
|
diskIds = append(diskIds, metadata.Name{
|
|
Kind: "ctdb_crc32",
|
|
Name: fmt.Sprintf("%08X", d.CueToolsCRC32),
|
|
})
|
|
|
|
for _, id := range d.Identifiers {
|
|
if id.Kind == "album" {
|
|
album.Name = append(album.Name, metadata.Name{
|
|
Kind: "file",
|
|
Name: id.Name,
|
|
})
|
|
} else if id.Kind == "catalog" {
|
|
album.Identifiers = append(album.Identifiers, id)
|
|
} else if id.Kind == "toc" || id.Kind == "cddb1" || id.Kind == "discid" {
|
|
diskIds = append(diskIds, id)
|
|
}
|
|
}
|
|
|
|
for k, v := range d.CommonMetadata {
|
|
if k == "date" {
|
|
if album.ReleaseDate, _ = time.ParseInLocation("2006.01.02", v, time.UTC); album.ReleaseDate.IsZero() {
|
|
if album.ReleaseDate, _ = time.ParseInLocation("2006-01-02", v, time.UTC); album.ReleaseDate.IsZero() {
|
|
album.ReleaseDate, _ = time.ParseInLocation("2006", v, time.UTC)
|
|
}
|
|
}
|
|
} else if k == "event" {
|
|
album.Links = append(album.Links, metadata.Link{
|
|
Kind: "release",
|
|
Name: metadata.NameSlice{
|
|
{Kind: "name", Name: v},
|
|
},
|
|
})
|
|
} else if k == "albumartist" || k == "artist" || k == "composer" || k == "mastering" || k == "performer" {
|
|
album.Roles = append(album.Roles, metadata.Role{
|
|
Kind: k,
|
|
Name: metadata.NameSlice{
|
|
{Kind: "file", Name: v},
|
|
},
|
|
})
|
|
} else if k == "lyricist" {
|
|
album.Roles = append(album.Roles, metadata.Role{
|
|
Kind: "lyrics",
|
|
Name: metadata.NameSlice{
|
|
{Kind: "file", Name: v},
|
|
},
|
|
})
|
|
} else if k == "guitar" {
|
|
album.Roles = append(album.Roles, metadata.Role{
|
|
Kind: "performer, guitar",
|
|
Name: metadata.NameSlice{
|
|
{Kind: "file", Name: v},
|
|
},
|
|
})
|
|
} else if k == "arrange" {
|
|
album.Roles = append(album.Roles, metadata.Role{
|
|
Kind: "arranger",
|
|
Name: metadata.NameSlice{
|
|
{Kind: "file", Name: v},
|
|
},
|
|
})
|
|
} else if k == "vocal" || k == "chorus" {
|
|
album.Roles = append(album.Roles, metadata.Role{
|
|
Kind: "vocals",
|
|
Name: metadata.NameSlice{
|
|
{Kind: "file", Name: v},
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
var arts []pictureEntry
|
|
|
|
discNumber := d.DiscNumber
|
|
discTotal := d.DiscTotal
|
|
|
|
if discTotal <= 0 {
|
|
discTotal = 1
|
|
}
|
|
if discNumber <= 0 || discNumber > discTotal {
|
|
discNumber = 1
|
|
}
|
|
|
|
album.Discs = make([]metadata.Disc, discTotal)
|
|
|
|
disc := metadata.Disc{
|
|
Identifiers: diskIds,
|
|
}
|
|
|
|
for _, track := range d.Tracks {
|
|
t := metadata.Track{
|
|
Name: metadata.NameSlice{
|
|
{Kind: "file", Name: track.TrackName},
|
|
},
|
|
Roles: make(metadata.RoleSlice, 0),
|
|
Duration: track.AudioMetadata.Duration,
|
|
}
|
|
|
|
if track.Fingerprints.CRC32 != 0 || track.Fingerprints.CueToolsCRC32 != 0 {
|
|
t.Identifiers = metadata.NameSlice{
|
|
{Kind: "crc32", Name: fmt.Sprintf("%08X", track.Fingerprints.CRC32)},
|
|
{Kind: "ctdb_crc32", Name: fmt.Sprintf("%08X", track.Fingerprints.CueToolsCRC32)},
|
|
{Kind: "accurateripv1", Name: fmt.Sprintf("%08X", track.Fingerprints.AccurateRipV1)},
|
|
{Kind: "accurateripv2", Name: fmt.Sprintf("%08X", track.Fingerprints.AccurateRipV2)},
|
|
}
|
|
}
|
|
|
|
if len(track.FileMetadata.Title) > 0 {
|
|
t.Name = append(t.Name, metadata.Name{
|
|
Kind: "file",
|
|
Name: track.FileMetadata.Title,
|
|
})
|
|
}
|
|
|
|
if len(track.FileMetadata.OriginalTitle) > 0 {
|
|
t.Links = append(t.Links, metadata.Link{
|
|
Kind: "original release title",
|
|
Name: metadata.NameSlice{
|
|
{Kind: "file", Name: track.FileMetadata.OriginalTitle},
|
|
},
|
|
})
|
|
}
|
|
|
|
for _, artist := range track.FileMetadata.Artists {
|
|
role := metadata.Role{
|
|
Kind: artist.Kind,
|
|
Name: metadata.NameSlice{
|
|
{Kind: "file", Name: artist.Name},
|
|
},
|
|
}
|
|
|
|
//only add when not shared
|
|
if album.Roles.Exists(role, false) == -1 {
|
|
t.Roles = append(t.Roles, role)
|
|
}
|
|
}
|
|
|
|
//TODO: check track number again?
|
|
|
|
if track.FileMetadata.Lyrics != "" {
|
|
//TODO: guess format?
|
|
lyrics := []metadata.Lyrics{
|
|
&metadata.TextLyrics{
|
|
Entries: strings.Split(strings.ReplaceAll(track.FileMetadata.Lyrics, "\r", ""), "\n\n"),
|
|
},
|
|
}
|
|
t.Lyrics = func() []metadata.Lyrics {
|
|
return lyrics
|
|
}
|
|
}
|
|
|
|
if len(track.FileMetadata.EmbeddedPicture.Data) > 0 {
|
|
for _, e := range arts {
|
|
if bytes.Compare(e.Data, track.FileMetadata.EmbeddedPicture.Data) == 0 {
|
|
goto skipAdd
|
|
}
|
|
}
|
|
|
|
arts = append(arts, track.FileMetadata.EmbeddedPicture)
|
|
|
|
skipAdd:
|
|
}
|
|
|
|
disc.Tracks = append(disc.Tracks, t)
|
|
}
|
|
|
|
//TODO: load files in current directory, find album art, then look for scans
|
|
for _, a := range arts {
|
|
mime := "application/octet-stream"
|
|
if a.Mime != "" {
|
|
mime = a.Mime
|
|
}
|
|
|
|
pictureType := "other"
|
|
if a.Kind == "" || strings.Index(strings.ToLower(a.Kind), "front") != -1 {
|
|
pictureType = "front"
|
|
} else if strings.Index(strings.ToLower(a.Kind), "back") != -1 {
|
|
pictureType = "back"
|
|
}
|
|
|
|
album.Art = append(album.Art, metadata.Name{
|
|
Kind: pictureType,
|
|
Name: fmt.Sprintf("data:%s;base64,%s", mime, base64.StdEncoding.EncodeToString(a.Data)),
|
|
})
|
|
}
|
|
|
|
album.Discs[discNumber-1] = disc
|
|
|
|
return album
|
|
}
|
|
|
|
type DiscHandlerTrack struct {
|
|
FileName string
|
|
TrackName string
|
|
SortName string
|
|
Fingerprints struct {
|
|
Panako []*panako.Fingerprint
|
|
CRC32 uint32
|
|
CueToolsCRC32 uint32
|
|
AccurateRipV1 uint32
|
|
AccurateRipV2 uint32
|
|
}
|
|
FileMetadata struct {
|
|
DiscNumber int
|
|
Artists metadata.NameSlice
|
|
Album string
|
|
Year int
|
|
TrackNumber int
|
|
Title string
|
|
OriginalTitle string
|
|
Lyrics string
|
|
EmbeddedPicture pictureEntry
|
|
}
|
|
AudioMetadata struct {
|
|
SampleRate int
|
|
Channels int
|
|
NumberOfFullSamples int
|
|
Duration time.Duration
|
|
ReplayGain struct {
|
|
Gain float64
|
|
Peak float64
|
|
}
|
|
}
|
|
}
|
|
|
|
type pictureEntry struct {
|
|
Kind string
|
|
Data []byte
|
|
Mime string
|
|
}
|
|
|
|
func HandleDiscEntry(panakoInstance *panako.Instance, pathEntry string) *DiscHandlerResult {
|
|
log.Printf("Handling %q", pathEntry)
|
|
entries, err := ioutil.ReadDir(pathEntry)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
var audioFiles []string
|
|
var imageFiles []string
|
|
var metadataFiles []string
|
|
var folders []string
|
|
|
|
for _, entry := range entries {
|
|
if !entry.IsDir() {
|
|
ext := path.Ext(entry.Name())
|
|
mime := utilities.GetMimeTypeFromExtension(ext)
|
|
isAudio := mime[0:6] == "audio/"
|
|
isAudioMetadata := mime == "text/x-log" || mime == "text/x-accurip" || mime == "text/x-cue" || mime == "text/x-toc"
|
|
isImage := mime[0:6] == "image/"
|
|
|
|
if isAudio {
|
|
audioFiles = append(audioFiles, entry.Name())
|
|
} else if isImage {
|
|
imageFiles = append(imageFiles, entry.Name())
|
|
} else if isAudioMetadata {
|
|
metadataFiles = append(metadataFiles, entry.Name())
|
|
}
|
|
} else {
|
|
folders = append(folders, entry.Name())
|
|
}
|
|
}
|
|
|
|
if len(audioFiles) == 0 {
|
|
return nil
|
|
}
|
|
|
|
sortedAudioEntries := processAudioFiles(audioFiles)
|
|
|
|
disc := &DiscHandlerResult{
|
|
Directory: pathEntry,
|
|
TOC: metadata.TOC{metadata.TocPregap},
|
|
CommonMetadata: make(map[string]string),
|
|
}
|
|
|
|
result, fullCRC32, fullCTDBCRC32 := sortedAudioEntries.analyze(pathEntry, panakoInstance)
|
|
defer func() {
|
|
for _, entry := range result {
|
|
entry.fileHandle.Close()
|
|
}
|
|
}()
|
|
|
|
disc.CRC32 = fullCRC32
|
|
disc.CueToolsCRC32 = fullCTDBCRC32
|
|
|
|
var discNumber int
|
|
var discTotal int
|
|
|
|
for _, entry := range result {
|
|
|
|
track := DiscHandlerTrack{
|
|
FileName: entry.Name,
|
|
TrackName: entry.NormalizedName,
|
|
SortName: entry.NormalizedSortName,
|
|
}
|
|
track.AudioMetadata.SampleRate = entry.audioMetadata.sampleRate
|
|
track.AudioMetadata.Channels = entry.audioMetadata.channels
|
|
track.AudioMetadata.NumberOfFullSamples = entry.audioMetadata.samples
|
|
|
|
track.AudioMetadata.ReplayGain.Gain = entry.replayGain.trackGain
|
|
track.AudioMetadata.ReplayGain.Peak = entry.replayGain.trackPeak
|
|
disc.ReplayGain.Gain = entry.replayGain.albumGain
|
|
disc.ReplayGain.Peak = entry.replayGain.albumPeak
|
|
|
|
track.AudioMetadata.Duration = time.Duration(float64(time.Second) * float64(track.AudioMetadata.NumberOfFullSamples) / float64(track.AudioMetadata.SampleRate))
|
|
track.Fingerprints.Panako = entry.panakoFingerprints
|
|
|
|
disc.TOC = append(disc.TOC, disc.TOC[len(disc.TOC)-1]+track.AudioMetadata.NumberOfFullSamples/metadata.Int16SamplesPerSector)
|
|
|
|
if entry.hasherCrc32 != nil {
|
|
track.Fingerprints.CRC32 = binary.BigEndian.Uint32(entry.hasherCrc32.GetResult())
|
|
track.Fingerprints.CueToolsCRC32 = track.Fingerprints.CRC32
|
|
}
|
|
if entry.hasherCueToolsCrc32 != nil {
|
|
track.Fingerprints.CueToolsCRC32 = binary.BigEndian.Uint32(entry.hasherCueToolsCrc32.GetResult())
|
|
}
|
|
if entry.hasherAccurateRipV1 != nil {
|
|
track.Fingerprints.AccurateRipV1 = binary.BigEndian.Uint32(entry.hasherAccurateRipV1.GetResult())
|
|
}
|
|
if entry.hasherAccurateRipV2 != nil {
|
|
track.Fingerprints.AccurateRipV2 = binary.BigEndian.Uint32(entry.hasherAccurateRipV2.GetResult())
|
|
}
|
|
|
|
track.FileMetadata.DiscNumber, _ = entry.fileMetadata.Disc()
|
|
track.FileMetadata.TrackNumber, _ = entry.fileMetadata.Track()
|
|
track.FileMetadata.Year = entry.fileMetadata.Year()
|
|
if entry.fileMetadata.Artist() != "" {
|
|
track.FileMetadata.Artists = append(track.FileMetadata.Artists, metadata.Name{
|
|
Kind: "artist",
|
|
Name: entry.fileMetadata.Artist(),
|
|
})
|
|
}
|
|
if entry.fileMetadata.AlbumArtist() != "" {
|
|
track.FileMetadata.Artists = append(track.FileMetadata.Artists, metadata.Name{
|
|
Kind: "albumartist",
|
|
Name: entry.fileMetadata.AlbumArtist(),
|
|
})
|
|
}
|
|
if entry.fileMetadata.Composer() != "" {
|
|
track.FileMetadata.Artists = append(track.FileMetadata.Artists, metadata.Name{
|
|
Kind: "composer",
|
|
Name: entry.fileMetadata.Composer(),
|
|
})
|
|
}
|
|
|
|
n, t := entry.fileMetadata.Disc()
|
|
|
|
if n != 0 && discNumber != -1 {
|
|
if discNumber == 0 {
|
|
discNumber = n
|
|
} else if discNumber != n {
|
|
discNumber = -1
|
|
}
|
|
}
|
|
|
|
if t != 0 && discTotal != -1 {
|
|
if discTotal == 0 {
|
|
discTotal = t
|
|
} else if discTotal != t {
|
|
discTotal = -1
|
|
}
|
|
}
|
|
|
|
track.FileMetadata.Lyrics = entry.fileMetadata.Lyrics()
|
|
|
|
rawValues := entry.fileMetadata.Raw()
|
|
for k, v := range rawValues {
|
|
str, ok := v.(string)
|
|
if !ok {
|
|
var number int
|
|
number, ok = v.(int)
|
|
if ok {
|
|
str = fmt.Sprint(number)
|
|
}
|
|
}
|
|
if ok && len(str) > 0 {
|
|
if k == "mastering" {
|
|
track.FileMetadata.Artists = append(track.FileMetadata.Artists, metadata.Name{
|
|
Kind: "mastering",
|
|
Name: str,
|
|
})
|
|
}
|
|
if k == "lyricist" {
|
|
track.FileMetadata.Artists = append(track.FileMetadata.Artists, metadata.Name{
|
|
Kind: "lyrics",
|
|
Name: str,
|
|
})
|
|
}
|
|
if k == "guitar" {
|
|
track.FileMetadata.Artists = append(track.FileMetadata.Artists, metadata.Name{
|
|
Kind: "performer, guitar",
|
|
Name: str,
|
|
})
|
|
}
|
|
if k == "arrange" {
|
|
track.FileMetadata.Artists = append(track.FileMetadata.Artists, metadata.Name{
|
|
Kind: "arranger",
|
|
Name: str,
|
|
})
|
|
}
|
|
if k == "vocal" {
|
|
track.FileMetadata.Artists = append(track.FileMetadata.Artists, metadata.Name{
|
|
Kind: "vocals",
|
|
Name: str,
|
|
})
|
|
}
|
|
if k == "chorus" {
|
|
track.FileMetadata.Artists = append(track.FileMetadata.Artists, metadata.Name{
|
|
Kind: "vocals",
|
|
Name: str,
|
|
})
|
|
}
|
|
if k == "performer" {
|
|
track.FileMetadata.Artists = append(track.FileMetadata.Artists, metadata.Name{
|
|
Kind: "performer",
|
|
Name: str,
|
|
})
|
|
}
|
|
if k == "originaltitle" {
|
|
track.FileMetadata.OriginalTitle = str
|
|
}
|
|
if k == "unsyncedlyrics" && track.FileMetadata.Lyrics == "" {
|
|
track.FileMetadata.Lyrics = str
|
|
}
|
|
|
|
value, exists := disc.CommonMetadata[k]
|
|
if !exists {
|
|
disc.CommonMetadata[k] = str
|
|
} else if len(value) > 0 && value != str {
|
|
disc.CommonMetadata[k] = ""
|
|
}
|
|
}
|
|
}
|
|
|
|
for k := range disc.CommonMetadata {
|
|
_, exists := rawValues[k]
|
|
if !exists {
|
|
disc.CommonMetadata[k] = ""
|
|
}
|
|
}
|
|
|
|
track.FileMetadata.Album = entry.fileMetadata.Album()
|
|
track.FileMetadata.Title = entry.fileMetadata.Title()
|
|
|
|
if p := entry.fileMetadata.Picture(); p != nil {
|
|
track.FileMetadata.EmbeddedPicture.Kind = p.Type
|
|
track.FileMetadata.EmbeddedPicture.Mime = p.MIMEType
|
|
track.FileMetadata.EmbeddedPicture.Data = p.Data
|
|
}
|
|
|
|
disc.Tracks = append(disc.Tracks, track)
|
|
}
|
|
|
|
for k, v := range disc.CommonMetadata {
|
|
if v == "" {
|
|
delete(disc.CommonMetadata, k)
|
|
}
|
|
}
|
|
|
|
if discNumber > 0 {
|
|
disc.DiscNumber = discNumber
|
|
}
|
|
|
|
if discNumber <= discTotal && discTotal > 0 {
|
|
disc.DiscTotal = discTotal
|
|
}
|
|
|
|
if catno, ok := disc.CommonMetadata["catalogid"]; ok {
|
|
for _, n := range strings.Split(catno, ";") {
|
|
disc.Identifiers = append(disc.Identifiers, metadata.Name{
|
|
Kind: "catalog",
|
|
Name: n,
|
|
})
|
|
}
|
|
}
|
|
|
|
if catno, ok := disc.CommonMetadata["catalognumber"]; ok {
|
|
for _, n := range strings.Split(catno, ";") {
|
|
disc.Identifiers = append(disc.Identifiers, metadata.Name{
|
|
Kind: "catalog",
|
|
Name: n,
|
|
})
|
|
}
|
|
}
|
|
|
|
if catno, ok := disc.CommonMetadata["labelno"]; ok {
|
|
for _, n := range strings.Split(catno, ";") {
|
|
disc.Identifiers = append(disc.Identifiers, metadata.Name{
|
|
Kind: "catalog",
|
|
Name: n,
|
|
})
|
|
}
|
|
}
|
|
|
|
if album, ok := disc.CommonMetadata["TALB"]; ok { //ID3v2;
|
|
disc.Identifiers = append(disc.Identifiers, metadata.Name{
|
|
Kind: "album",
|
|
Name: album,
|
|
})
|
|
disc.Album = album
|
|
}
|
|
|
|
if album, ok := disc.CommonMetadata["album"]; ok {
|
|
disc.Identifiers = append(disc.Identifiers, metadata.Name{
|
|
Kind: "album",
|
|
Name: album,
|
|
})
|
|
disc.Album = album
|
|
}
|
|
|
|
if discid, ok := disc.CommonMetadata["discid"]; ok {
|
|
disc.Identifiers = append(disc.Identifiers, metadata.Name{
|
|
Kind: "cddb1",
|
|
Name: discid,
|
|
})
|
|
}
|
|
|
|
if mbDiscId, ok := disc.CommonMetadata["musicbrainz_discid"]; ok {
|
|
disc.Identifiers = append(disc.Identifiers, metadata.Name{
|
|
Kind: "discid",
|
|
Name: mbDiscId,
|
|
})
|
|
}
|
|
|
|
if cdtoc, ok := disc.CommonMetadata["cdtoc"]; ok {
|
|
toc := metadata.TOC{}
|
|
for _, v := range strings.Split(cdtoc, "+")[1:] {
|
|
if number, err := strconv.ParseInt(v, 16, 0); err == nil {
|
|
toc = append(toc, int(number))
|
|
} else {
|
|
toc = metadata.TOC{}
|
|
break
|
|
}
|
|
}
|
|
if len(toc) > 0 {
|
|
toc = append(metadata.TOC{toc[len(toc)-1]}, toc[0:len(toc)-1]...)
|
|
disc.Identifiers = append(disc.Identifiers, metadata.Name{
|
|
Kind: "toc",
|
|
Name: toc.String(),
|
|
})
|
|
}
|
|
}
|
|
|
|
catalogRE := regroup.MustCompile(`(?i)[\[\(\{](?P<catno>(?:[a-z]{2,}-?[0-9][a-z0-9\-~]*)|(?:[0-9 ]{5,}))[\}\)\]]`)
|
|
m := &struct {
|
|
CatalogNumber string `regroup:"catno"`
|
|
}{}
|
|
if err = catalogRE.MatchToTarget(utilities.NormalizeUnicode(disc.Directory), m); err == nil {
|
|
//todo handle catalog number ranges
|
|
disc.Identifiers = append(disc.Identifiers, metadata.Name{
|
|
Kind: "catalog",
|
|
Name: m.CatalogNumber,
|
|
})
|
|
}
|
|
//TODO: get disc number via path
|
|
|
|
disc.TOC = append(metadata.TOC{disc.TOC[len(disc.TOC)-1]}, disc.TOC[0:len(disc.TOC)-1]...)
|
|
|
|
return disc
|
|
}
|