Compare commits

...

3 commits

10 changed files with 897 additions and 74 deletions

3
go.mod
View file

@ -3,7 +3,8 @@ module git.gammaspectra.live/S.O.N.G/METANOIA
go 1.18
require (
git.gammaspectra.live/S.O.N.G/Hibiki v0.0.0-20220214135918-99bafdde7a4a
facette.io/natsort v0.0.0-20181210072756-2cd4dd1e2dcb
git.gammaspectra.live/S.O.N.G/Hibiki v0.0.0-20220216151616-63d8894466c0
git.gammaspectra.live/S.O.N.G/MakyuuIchaival v0.0.0-20220131114831-c08c7d9b4153
github.com/dgraph-io/badger/v3 v3.2103.2
github.com/dhowden/tag v0.0.0-20201120070457-d52dcb253c63

8
go.sum
View file

@ -6,11 +6,11 @@ dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
facette.io/natsort v0.0.0-20181210072756-2cd4dd1e2dcb h1:1pSweJFeR3Pqx7uoelppkzeegfUBXL6I2FFAbfXw570=
facette.io/natsort v0.0.0-20181210072756-2cd4dd1e2dcb/go.mod h1:npRYmtaITVom7rcSo+pRURltHSG2r4TQM1cdqJ2dUB0=
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
git.gammaspectra.live/S.O.N.G/Hibiki v0.0.0-20220206160547-e7023361fa2e h1:n1Fw+AcqqYhEiHXKBO1hhsggz/ND9d/VYKGvPx9KsL0=
git.gammaspectra.live/S.O.N.G/Hibiki v0.0.0-20220206160547-e7023361fa2e/go.mod h1:/NY+4FrfPnEXNCmF16085cSGWZ89YS+Glpg4cTJhamg=
git.gammaspectra.live/S.O.N.G/Hibiki v0.0.0-20220214135918-99bafdde7a4a h1:3LnPQmaEHjMTotiJaeg0t7L0JA/BR+ENlr5gTgwDK6c=
git.gammaspectra.live/S.O.N.G/Hibiki v0.0.0-20220214135918-99bafdde7a4a/go.mod h1:/NY+4FrfPnEXNCmF16085cSGWZ89YS+Glpg4cTJhamg=
git.gammaspectra.live/S.O.N.G/Hibiki v0.0.0-20220216151616-63d8894466c0 h1:MdhCDoFatXYEyweos0PnvWaOOPhw0xs6Y448lqBRa5s=
git.gammaspectra.live/S.O.N.G/Hibiki v0.0.0-20220216151616-63d8894466c0/go.mod h1:/NY+4FrfPnEXNCmF16085cSGWZ89YS+Glpg4cTJhamg=
git.gammaspectra.live/S.O.N.G/MakyuuIchaival v0.0.0-20220131114831-c08c7d9b4153 h1:RMDA05IEOytScNSiE2ms98x/CVMHSlA+eVBC0VCq4po=
git.gammaspectra.live/S.O.N.G/MakyuuIchaival v0.0.0-20220131114831-c08c7d9b4153/go.mod h1:z6KcP5RPhMxDJaVU48sBhiYRCJ6ZJBbx1iIhkUrrhfY=
git.gammaspectra.live/S.O.N.G/go-pus v0.0.0-20220130003320-c9b07c6bec7a h1:LxrTp9gf4w5KnFHRPFLXYfoxC58GCSEmZrHI6Ogtrm0=

View file

@ -99,7 +99,7 @@ type AccurateRipMetadata struct {
}
func (s *Source) Test() {
meta := s.FindMetadataByTOC(metadata.NewTOCFromString("267453 150 24647 71194 95579 139576 174573 199089 244305"))
meta := s.FindMetadataByTOC(metadata.NewTOCFromString("290543 150 27572 69794 109235 133506 177855 214680 252471"))
log.Print(meta)
}

View file

@ -259,7 +259,7 @@ func (s *Source) FindMetadataByTOC(toc metadata.TOC) *CueToolsMetadata {
}
func (s *Source) Test() {
album := s.FindByTocID(metadata.NewTOCFromString("267453 150 24647 71194 95579 139576 174573 199089 244305").GetTocID())
album := s.FindByTocID(metadata.NewTOCFromString("290543 150 27572 69794 109235 133506 177855 214680 252471").GetTocID())
//album := s.FindByTOC(metadata.NewTOCFromString("267453 150 24647 71194 95579 139576 174573 199089 244305"))
//album := s.GetRelease("9ca2748b-88fd-44a7-bc5c-036574148571")

407
metadata/dischandler.go Normal file
View file

@ -0,0 +1,407 @@
package metadata
import (
"encoding/binary"
"facette.io/natsort"
"fmt"
"git.gammaspectra.live/S.O.N.G/Hibiki/panako"
"git.gammaspectra.live/S.O.N.G/Hibiki/utilities/audio"
"git.gammaspectra.live/S.O.N.G/Hibiki/utilities/audio/format/flac"
"git.gammaspectra.live/S.O.N.G/Hibiki/utilities/audio/format/mp3"
"git.gammaspectra.live/S.O.N.G/Hibiki/utilities/audio/format/opus"
"git.gammaspectra.live/S.O.N.G/Hibiki/utilities/specializedstore"
"git.gammaspectra.live/S.O.N.G/METANOIA/utilities"
"github.com/dhowden/tag"
"golang.org/x/text/unicode/norm"
"io/ioutil"
"log"
"os"
"path"
"sort"
"strings"
"sync"
"sync/atomic"
"time"
"unicode"
)
type fileEntryList []fileEntry
var flacFormat = flac.NewFormat()
var mp3Format = mp3.NewFormat()
var opusFormat = opus.NewFormat()
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
}
fileMetadata tag.Metadata
panakoFingerprints []*panako.Fingerprint
hasherCrc32 *Hasher
hasherCueToolsCrc32 *Hasher
hasherAccurateRipV1 *Hasher
hasherAccurateRipV2 *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.RESAMPLER_QUALITY_LINEAR)
var joinedCTDBChannels []HasherChannel
var joinedChannels []HasherChannel
var preLastTotalSamplesWaitGroup sync.WaitGroup
preLastTotalSamples := uint32(0)
for trackIndex, e := range l {
f, err := os.Open(path.Join(directory, e.Name))
if err != nil {
//TODO
log.Print(err)
continue
}
var stream *audio.Stream
var analyzer HasherChannel
meta, err := tag.ReadFrom(f)
if err != nil {
log.Print(err)
err = nil
}
f.Seek(0, 0)
switch utilities.GetMimeTypeFromExtension(path.Ext(e.Name)) {
case "audio/flac":
stream, analyzer, err = flacFormat.OpenAnalyzer(f, panakoInstance.BlockSize)
case "audio/mpeg;codecs=mp3":
stream, err = mp3Format.Open(f, panakoInstance.BlockSize)
case "audio/ogg":
fallthrough
case "audio/opus":
stream, err = opusFormat.Open(f, panakoInstance.BlockSize)
}
if err != nil { //cannot decode
//TODO
log.Print(err)
f.Close()
continue
}
if stream == 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,
}
var panakoWaitGroup sync.WaitGroup
panakoWaitGroup.Add(1)
preLastTotalSamplesWaitGroup.Add(1)
go func(add bool) {
defer panakoWaitGroup.Done()
defer preLastTotalSamplesWaitGroup.Done()
entry.panakoFingerprints = printStrategy.StreamToFingerprints(stream)
entry.audioMetadata.sampleRate = int(stream.GetSampleRate())
entry.audioMetadata.channels = stream.GetChannels()
entry.audioMetadata.samples = stream.GetSamplesProcessed()
if add {
atomic.AddUint32(&preLastTotalSamples, uint32(entry.audioMetadata.samples/entry.audioMetadata.channels))
}
}(trackIndex < len(l)-1)
//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(Int16SamplesPerSector * 10).Split(2)
joinedCTDBChannels = append(joinedCTDBChannels, ctChannels[0])
entry.hasherCueToolsCrc32 = NewHasher(ctChannels[1], HashtypeCrc32)
arChannels := channels[2].SkipStartSamples(Int16SamplesPerSector*5 - 1).Split(2)
entry.hasherAccurateRipV1 = NewHasher(arChannels[0], HashtypeAccurateRipV1Start)
entry.hasherAccurateRipV2 = NewHasher(arChannels[1], HashtypeAccurateRipV2Start)
entry.hasherCrc32 = NewHasher(channels[3], HashtypeCrc32)
} else if trackIndex == len(l)-1 {
channels := analyzer.Split(4)
joinedChannels = append(joinedChannels, channels[0])
ctChannels := channels[1].SkipEndSamplesMultiple(&preLastTotalSamplesWaitGroup, &preLastTotalSamples, Int16SamplesPerSector*10).Split(2)
joinedCTDBChannels = append(joinedCTDBChannels, ctChannels[0])
entry.hasherCueToolsCrc32 = NewHasher(ctChannels[1], HashtypeCrc32)
arChannels := channels[2].SkipEndSamples(Int16SamplesPerSector * 5).Split(2)
entry.hasherAccurateRipV1 = NewHasher(arChannels[0], HashtypeAccurateRipV1)
entry.hasherAccurateRipV2 = NewHasher(arChannels[1], HashtypeAccurateRipV2)
entry.hasherCrc32 = NewHasher(channels[3], HashtypeCrc32)
} else {
channels := analyzer.Split(5)
joinedChannels = append(joinedChannels, channels[0])
joinedCTDBChannels = append(joinedCTDBChannels, channels[1])
entry.hasherCrc32 = NewHasher(channels[2], HashtypeCrc32)
entry.hasherAccurateRipV1 = NewHasher(channels[3], HashtypeAccurateRipV1)
entry.hasherAccurateRipV2 = NewHasher(channels[4], 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, &panakoWaitGroup)
entries = append(entries, entry)
}
fullHasher := NewHasher(MergeHasherChannels(joinedChannels...), HashtypeCrc32)
fullCTDBHasher := NewHasher(MergeHasherChannels(joinedCTDBChannels...), 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 := norm.NFC.String(f)
normalized = strings.ReplaceAll(normalized, "", "0")
normalized = strings.ReplaceAll(normalized, "", "1")
normalized = strings.ReplaceAll(normalized, "", "2")
normalized = strings.ReplaceAll(normalized, "", "3")
normalized = strings.ReplaceAll(normalized, "", "4")
normalized = strings.ReplaceAll(normalized, "", "5")
normalized = strings.ReplaceAll(normalized, "", "6")
normalized = strings.ReplaceAll(normalized, "", "7")
normalized = strings.ReplaceAll(normalized, "", "8")
normalized = strings.ReplaceAll(normalized, "", "9")
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 TOC
CRC32 uint32
CueToolsCRC32 uint32
Directory string
Tracks []DiscHandlerTrack
}
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
Album string
AlbumArtist string
Artist string
Composer string
Year int
TrackNumber int
Title string
EmbeddedPicture []byte
}
AudioMetadata struct {
SampleRate int
Channels int
NumberOfFullSamples int
Duration time.Duration
}
}
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{
TOC: TOC{TocPregap},
}
result, fullCRC32, fullCTDBCRC32 := sortedAudioEntries.analyze(pathEntry, panakoInstance)
defer func() {
for _, entry := range result {
entry.fileHandle.Close()
}
}()
disc.CRC32 = fullCRC32
disc.CueToolsCRC32 = fullCTDBCRC32
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 / entry.audioMetadata.channels
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/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()
track.FileMetadata.AlbumArtist = entry.fileMetadata.AlbumArtist()
track.FileMetadata.Artist = entry.fileMetadata.Artist()
track.FileMetadata.Composer = entry.fileMetadata.Composer()
track.FileMetadata.Title = entry.fileMetadata.Title()
if entry.fileMetadata.Picture() != nil {
track.FileMetadata.EmbeddedPicture = entry.fileMetadata.Picture().Data
}
disc.Tracks = append(disc.Tracks, track)
}
disc.TOC = append(TOC{disc.TOC[len(disc.TOC)-1]}, disc.TOC[0:len(disc.TOC)-1]...)
return disc
}

82
metadata/hash.go Normal file
View file

@ -0,0 +1,82 @@
package metadata
import (
"hash"
"unsafe"
)
type accurateRipDigestV1 struct {
crc uint32
pos uint32
offset uint32
}
func NewAccurateRipV1(offset uint32) hash.Hash32 {
return &accurateRipDigestV1{0, offset + 1, offset}
}
func (d *accurateRipDigestV1) Size() int { return 4 }
func (d *accurateRipDigestV1) BlockSize() int { return 1 }
func (d *accurateRipDigestV1) Reset() { d.crc = 0; d.pos = d.offset + 1 }
func (d *accurateRipDigestV1) Sum32() uint32 { return d.crc }
func (d *accurateRipDigestV1) Sum(in []byte) []byte {
s := d.Sum32()
return append(in, byte(s>>24), byte(s>>16), byte(s>>8), byte(s))
}
func (d *accurateRipDigestV1) Write(p []byte) (n int, err error) {
numWords := uintptr(len(p)) * unsafe.Sizeof(p[0]) / unsafe.Sizeof(uint32(0))
words := unsafe.Slice((*uint32)(unsafe.Pointer(&p[0])), numWords)
for _, w := range words {
//this can wrap
d.crc += d.pos * w
d.pos++
}
return len(p), nil
}
type accurateRipDigestV2 struct {
crc uint32
multiplier uint32
offset uint32
}
func NewAccurateRipV2(offset uint32) hash.Hash32 {
return &accurateRipDigestV2{0, offset + 1, offset}
}
func (d *accurateRipDigestV2) Size() int { return 4 }
func (d *accurateRipDigestV2) BlockSize() int { return 1 }
func (d *accurateRipDigestV2) Reset() { d.crc = 0; d.multiplier = d.offset + 1 }
func (d *accurateRipDigestV2) Sum32() uint32 { return d.crc }
func (d *accurateRipDigestV2) Sum(in []byte) []byte {
s := d.Sum32()
return append(in, byte(s>>24), byte(s>>16), byte(s>>8), byte(s))
}
func (d *accurateRipDigestV2) Write(p []byte) (n int, err error) {
numWords := uintptr(len(p)) * unsafe.Sizeof(p[0]) / unsafe.Sizeof(uint32(0))
words := unsafe.Slice((*uint32)(unsafe.Pointer(&p[0])), numWords)
for _, w := range words {
crcNew := uint64(w) * uint64(d.multiplier)
LO := crcNew & 0xFFFFFFFF
HI := crcNew / 0x100000000
//this can wrap
d.crc += uint32(HI)
d.crc += uint32(LO)
d.multiplier++
}
return len(p), nil
}

View file

@ -1,14 +1,226 @@
package metadata
import (
"encoding/binary"
"git.gammaspectra.live/S.O.N.G/Hibiki/utilities/audio/format"
"github.com/minio/sha256-simd"
"hash"
"hash/crc32"
"sync"
"sync/atomic"
"time"
)
const chanBuf = 16
type HasherChannel chan *format.AnalyzerPacket
func (c HasherChannel) Split(n int) (channels []HasherChannel) {
channels = make([]HasherChannel, n)
for i := range channels {
channels[i] = make(HasherChannel, chanBuf)
}
go func() {
defer func() {
for _, channel := range channels {
close(channel)
}
}()
for packet := range c {
for _, channel := range channels {
channel <- packet
}
}
}()
return
}
func (c HasherChannel) PrependGap(samples, sampleRate, channels, bitDepth int) (channel HasherChannel) {
return MergeHasherChannels(NewHasherAudioGap(samples, sampleRate, channels, bitDepth), c)
}
func (c HasherChannel) AppendGap(samples, sampleRate, channels, bitDepth int) (channel HasherChannel) {
return MergeHasherChannels(c, NewHasherAudioGap(samples, sampleRate, channels, bitDepth))
}
func (c HasherChannel) SkipStartSamples(samples int) (channel HasherChannel) {
channel = make(HasherChannel, chanBuf)
go func() {
defer close(channel)
for samples > 0 {
packet, ok := <-c
if !ok {
return
}
if len(packet.Samples)/packet.Channels > samples {
startIndex := samples * packet.Channels
channel <- &format.AnalyzerPacket{
Samples: packet.Samples[startIndex:],
Channels: packet.Channels,
SampleRate: packet.SampleRate,
BitDepth: packet.BitDepth,
}
samples = 0
break
} else {
samples -= len(packet.Samples) / packet.Channels
continue
}
}
for packet := range c {
channel <- packet
}
}()
return
}
func (c HasherChannel) SkipEndSamples(samples int) (channel HasherChannel) {
channel = make(HasherChannel, chanBuf)
go func() {
defer close(channel)
var buffer []*format.AnalyzerPacket
bufferSamples := 0
for packet := range c {
for len(buffer) > 0 && (bufferSamples-len(buffer[0].Samples)/buffer[0].Channels) > samples {
channel <- buffer[0]
bufferSamples -= len(buffer[0].Samples) / buffer[0].Channels
buffer = buffer[1:]
}
bufferSamples += len(packet.Samples) / packet.Channels
buffer = append(buffer, packet)
}
for _, packet := range buffer {
//TODO: check this
leftSamples := bufferSamples - len(packet.Samples)/packet.Channels
if leftSamples <= samples {
endIndex := len(packet.Samples) - (samples-leftSamples)*packet.Channels
channel <- &format.AnalyzerPacket{
Samples: packet.Samples[:endIndex],
Channels: packet.Channels,
SampleRate: packet.SampleRate,
BitDepth: packet.BitDepth,
}
samples = 0
break
} else {
channel <- packet
bufferSamples -= len(packet.Samples) / packet.Channels
}
}
}()
return
}
func (c HasherChannel) SkipEndSamplesMultiple(wg *sync.WaitGroup, offset *uint32, samples int) (channel HasherChannel) {
channel = make(HasherChannel, chanBuf)
go func() {
defer close(channel)
var buffer []*format.AnalyzerPacket
bufferSamples := 0
maxSamples := samples * 2
samplesRead := 0
for packet := range c {
for len(buffer) > 0 && (bufferSamples-len(buffer[0].Samples)/buffer[0].Channels) > maxSamples {
channel <- buffer[0]
samplesRead += len(buffer[0].Samples) / buffer[0].Channels
bufferSamples -= len(buffer[0].Samples) / buffer[0].Channels
buffer = buffer[1:]
}
bufferSamples += len(packet.Samples) / packet.Channels
buffer = append(buffer, packet)
}
wg.Wait()
totalSampleOffset := samplesRead + int(atomic.LoadUint32(offset))
if len(buffer) > 0 {
p := &format.AnalyzerPacket{
Channels: buffer[0].Channels,
SampleRate: buffer[0].SampleRate,
BitDepth: buffer[0].BitDepth,
}
for _, packet := range buffer {
p.Samples = append(p.Samples, packet.Samples...)
}
nsamples := samples + (((len(p.Samples) / p.Channels) + totalSampleOffset) % samples)
if len(p.Samples)/p.Channels > nsamples {
endIndex := len(p.Samples) - nsamples*p.Channels
channel <- &format.AnalyzerPacket{
Samples: p.Samples[:endIndex],
Channels: p.Channels,
SampleRate: p.SampleRate,
BitDepth: p.BitDepth,
}
}
}
}()
return
}
func NewHasherAudioGap(samples, sampleRate, channels, bitDepth int) (channel HasherChannel) {
channel = make(HasherChannel, 1)
channel <- &format.AnalyzerPacket{
Samples: make([]int32, samples*channels),
Channels: channels,
SampleRate: sampleRate,
BitDepth: bitDepth,
}
close(channel)
return
}
func MergeHasherChannels(channels ...HasherChannel) (channel HasherChannel) {
channel = make(HasherChannel, chanBuf)
go func() {
defer close(channel)
for _, c := range channels {
for packet := range c {
channel <- packet
}
}
}()
return
}
type HashType int
const (
HashtypeCrc32 = HashType(iota)
HashtypeSha256
HashtypeAccurateRipV1
HashtypeAccurateRipV1Start
HashtypeAccurateRipV2
HashtypeAccurateRipV2Start
)
type Hasher struct {
options HasherOptions
channel chan *format.AnalyzerPacket
hash HashType
hasher hash.Hash
result []byte
channel HasherChannel
wg sync.WaitGroup
samples int
duration float64
@ -18,41 +230,26 @@ type Hasher struct {
buffer [][]int32
}
type HasherOptions struct {
PrefixSilenceSamples int
AppendSilenceSamples int
SkipStartSamples int
SkipEndSamples int
}
func NewMuxedHasher(channel chan *format.AnalyzerPacket, options ...HasherOptions) (h []*Hasher) {
channels := make([]chan *format.AnalyzerPacket, len(options))
for i := range channels {
channels[i] = make(chan *format.AnalyzerPacket, 16)
h = append(h, NewHasher(channels[i], options[i]))
func NewHasher(channel HasherChannel, hashType HashType) (h *Hasher) {
h = &Hasher{
hash: hashType,
channel: channel,
}
go func() {
defer func() {
for _, c := range channels {
close(c)
}
}()
switch hashType {
case HashtypeCrc32:
h.hasher = crc32.NewIEEE()
case HashtypeSha256:
h.hasher = sha256.New()
case HashtypeAccurateRipV1:
h.hasher = NewAccurateRipV1(0)
case HashtypeAccurateRipV1Start:
h.hasher = NewAccurateRipV1(Int16SamplesPerSector*5 - 1)
case HashtypeAccurateRipV2:
h.hasher = NewAccurateRipV2(0)
case HashtypeAccurateRipV2Start:
h.hasher = NewAccurateRipV2(Int16SamplesPerSector*5 - 1)
for packet := range channel {
for _, c := range channels {
c <- packet
}
}
}()
return
}
func NewHasher(channel chan *format.AnalyzerPacket, options HasherOptions) (h *Hasher) {
h = &Hasher{
options: options,
channel: channel,
}
h.startRoutine()
@ -66,29 +263,11 @@ func (h *Hasher) startRoutine() {
defer h.wg.Done()
for packet := range h.channel {
if h.samples == 0 {
h.buffer = make([][]int32, h.options.SkipStartSamples, h.options.SkipEndSamples)
if h.options.PrefixSilenceSamples > 0 {
h.handlePacket(&format.AnalyzerPacket{
Samples: make([]int32, packet.Channels*h.options.PrefixSilenceSamples),
Channels: packet.Channels,
SampleRate: packet.SampleRate,
BitDepth: packet.BitDepth,
})
}
}
h.handlePacket(packet)
}
if h.options.AppendSilenceSamples > 0 {
h.handlePacket(&format.AnalyzerPacket{
Samples: make([]int32, h.channels*h.options.AppendSilenceSamples),
Channels: h.channels,
SampleRate: h.sampleRate,
BitDepth: h.bitDepth,
})
}
h.result = h.hasher.Sum([]byte{})
}()
}
@ -114,6 +293,35 @@ func (h *Hasher) handlePacket(packet *format.AnalyzerPacket) {
}
h.duration += float64(samples) / float64(packet.SampleRate)
var buf []byte
switch packet.BitDepth {
case 8:
buf = make([]byte, len(packet.Samples))
for i := range packet.Samples {
buf[i] = byte(packet.Samples[i])
}
case 16:
buf = make([]byte, len(packet.Samples)*2)
for i := range packet.Samples {
binary.LittleEndian.PutUint16(buf[i*2:], uint16(int16(packet.Samples[i])))
}
case 24:
buf = make([]byte, len(packet.Samples)*3)
for i := range packet.Samples {
buf[i*3] = byte((packet.Samples[i] >> 16) & 0xFF)
buf[i*3+1] = byte((packet.Samples[i] >> 8) & 0xFF)
buf[i*3+2] = byte(packet.Samples[i] & 0xFF)
}
default:
buf = make([]byte, len(packet.Samples)*4)
for i := range packet.Samples {
binary.LittleEndian.PutUint32(buf[i*4:], uint32(packet.Samples[i]))
}
}
h.hasher.Write(buf)
}
func (h *Hasher) GetSampleCount() int {
@ -128,8 +336,12 @@ func (h *Hasher) GetSampleRate() int {
return h.sampleRate
}
func (h *Hasher) GetOptions() HasherOptions {
return h.options
func (h *Hasher) GetHashType() HashType {
return h.hash
}
func (h *Hasher) GetResult() []byte {
return h.result
}
func (h *Hasher) GetDuration() time.Duration {
@ -141,6 +353,10 @@ func (h *Hasher) GetDuration() time.Duration {
return time.Duration(float64(time.Second) * h.duration)
}
func (h *Hasher) GetWaitGroup() *sync.WaitGroup {
return &h.wg
}
func (h *Hasher) Wait() {
h.wg.Wait()
}

View file

@ -161,16 +161,13 @@ type eventEntry struct {
Disambiguation string `json:"disambiguation"`
}
type artistEntry struct {
Id string `json:"id"`
Name string `json:"name"`
SortName string `json:"sort-name"`
Disambiguation string `json:"disambiguation"`
Type string `json:"type"`
Rating ratingEntry `json:"rating"`
Aliases []struct {
Name string `json:"name"`
SortName string `json:"sort-name"`
} `json:"aliases"`
Id string `json:"id"`
Name string `json:"name"`
SortName string `json:"sort-name"`
Disambiguation string `json:"disambiguation"`
Type string `json:"type"`
Rating ratingEntry `json:"rating"`
Aliases []aliasEntry `json:"aliases"`
}
type artistCreditEntry struct {
Name string `json:"name"`
@ -202,6 +199,7 @@ type recordingEntry struct {
Relations []relationEntry `json:"relations"`
ISRCS []string `json:"isrcs"`
ArtistCredit []artistCreditEntry `json:"artist-credit"`
//TODO Aliases
}
type trackEntry struct {
@ -230,6 +228,12 @@ type mediaEntry struct {
Title string `json:"title"`
}
type aliasEntry struct {
Type string `json:"type"`
Name string `json:"name"`
SortName string `json:"sort-name"`
}
type labelEntry struct {
Id string `json:"id"`
Tags []tagEntry `json:"tags"`
@ -382,6 +386,7 @@ func (s *Source) GetRelease(releaseId string) *metadata.Album {
Date string `json:"date"`
Media []mediaEntry `json:"media"`
Title string `json:"title"`
Aliases []aliasEntry `json:"aliases"`
Status string `json:"status"`
Id string `json:"id"`
Barcode string `json:"barcode"`
@ -407,6 +412,15 @@ func (s *Source) GetRelease(releaseId string) *metadata.Album {
},
}
for _, n := range release.Aliases {
if n.Type == "Release name" {
album.Name = append(album.Name, metadata.Name{Kind: "original", Name: n.Name})
if n.SortName != n.Name {
album.Name = append(album.Name, metadata.Name{Kind: "sort", Name: n.SortName})
}
}
}
if release.CoverArtArchive.Count > 0 {
album.Art = s.GetReleaseCoverArt(release.Id)
}

View file

@ -12,6 +12,10 @@ import (
const TocPregap = 150
const SectorsPerSecond = 75
const DataTrackGap = 11400
const BytesPerSector = 2352
const CDChannels = 2
const Int16SamplesPerSector = BytesPerSector / (2 * CDChannels)
const CDSampleRate = Int16SamplesPerSector * SectorsPerSecond
//TOC includes a list, index 0 being total sectors/end, then start times follow, with TocPregap added
type TOC []int

View file

@ -1,3 +1,102 @@
package utilities
import "strings"
const Version = "1.0"
func GetMimeTypeFromExtension(ext string) string {
if len(ext) > 0 {
switch strings.ToLower(ext[1:]) {
//Audio types
case "flac":
return "audio/flac"
case "mp3":
return "audio/mpeg;codecs=mp3"
case "m4a":
return "audio/mp4"
case "mka":
return "audio/x-matroska"
case "ogg":
return "audio/ogg"
case "opus":
return "audio/opus"
case "tta":
return "audio/tta"
case "aac":
return "audio/aac"
case "alac":
return "audio/alac"
case "wav":
return "audio/wav"
case "ape":
return "audio/ape"
//Image types
case "png":
return "image/png"
case "jfif":
fallthrough
case "jpeg":
fallthrough
case "jpg":
return "image/jpeg"
case "gif":
return "image/gif"
case "svg":
return "image/svg+xml"
case "tiff":
fallthrough
case "tif":
return "image/tiff"
case "webp":
return "image/webp"
case "bmp":
return "image/bmp"
//Text types
case "txt":
return "text/plain"
case "log":
return "text/x-log"
case "accurip":
return "text/x-accurip"
case "cue":
return "text/x-cue"
case "toc":
return "text/x-toc"
//Text subtitles
case "lrc":
return "text/x-subtitle-lrc"
case "ssa":
return "text/x-subtitle-ssa"
case "ass":
return "text/x-subtitle-ass"
case "srt":
return "text/x-subtitle-subrip"
//Web types
case "js":
return "text/javascript"
case "wasm":
return "application/wasm"
case "html":
return "text/html"
case "css":
return "text/css"
case "ttf":
return "font/ttf"
case "otf":
return "font/otf"
case "woff":
return "font/woff"
case "woff2":
return "font/woff2"
}
}
return "application/octet-stream"
}