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(?:[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 }