Added track parsing, artist parsing

This commit is contained in:
DataHoarder 2022-02-19 20:48:02 +01:00
parent 8321adf007
commit 9948b9cab0
5 changed files with 900 additions and 147 deletions

View file

@ -17,8 +17,8 @@ Small Golang server that loads all albums in an index at startup and is used to
* Access page by name ex. `/pages_by_name/RETRO_FUTURE_GIRLS.wiki`
* List files in a directory (ex. `/pages/` or `/pageindex/Arrangement_CDs/`)
* Query database by either any album title or album catalog number
* Title ex. `/search?type=title&query=マジコカタストロフィ`
* Catalog ex. `/search?type=title&query=STAL-1302`
* Album Title ex. `/search?type=album&query=マジコカタストロフィ`
* Album Catalog ex. `/search?type=album&query=STAL-1302`
* Values here are normalized so special characters or uppercase are changed. It deals with Unicode too.
* Bare CDDB emulator for `query` and `read` commands. Used to query by track count + length of album.
* query cmd ex. `/cddb?cmd=cddb+query+730dec08` (no need to provide TOC, but can be provided)

685
server.go
View file

@ -17,6 +17,7 @@ import (
"net/http"
"os"
"path"
"sort"
"strconv"
"strings"
"sync"
@ -24,11 +25,13 @@ import (
"unicode"
)
var arrangeCDIndex = make(map[int]*arrangeCdEntry)
var arrangeCDIndexLock sync.Mutex
var cdIndex = make(map[int]*albumEntry)
var cdIndexLock sync.Mutex
var titleLookup = make(map[string][]*arrangeCdEntry)
var tracksLookup = make(map[int][]*arrangeCdEntry)
var albumTitleLookup = make(map[string][]*albumEntry)
//TODO: make this work with discs
var discTracksLookup = make(map[int][]*albumEntry)
type categoryPageIndex struct {
PageId int `json:"pageid"`
@ -56,31 +59,237 @@ func parseCategoryPageIndex(filePath string) []categoryPageIndex {
return s.Query.Members
}
type arrangeCdEntry struct {
Id int `json:"pageid"`
MainTitle string `json:"pagetitle"`
Group string `json:"pagetitle,omitempty"`
Titles []string `json:"titles"`
CatalogNumber string `json:"catalognumber,omitempty"`
TrackCount int `json:"trackcount,omitempty"`
Duration int `json:"duration,omitempty"`
Year int `json:"year,omitempty"`
type JSONTime struct {
time.Time
}
func processIndex(filePath string) {
func (t JSONTime) MarshalJSON() ([]byte, error) {
if t.IsZero() {
return nil, nil
}
return []byte(fmt.Sprintf("\"%s\"", t.Format("2006-01-02"))), nil
}
arrangeIndexPath := path.Join(filePath, "pageindex", "Arrangement_CDs")
entries, err := ioutil.ReadDir(arrangeIndexPath)
type albumEntry struct {
Id int `json:"pageid"`
Type string `json:"type"`
MainTitle string `json:"pagetitle"`
Artists []artistEntry `json:"artists,omitempty"`
Titles []string `json:"titles"`
CatalogNumber string `json:"catalognumber,omitempty"`
Discs []discEntry `json:"discs,omitempty"`
TrackCount int `json:"trackcount,omitempty"`
Duration int `json:"duration,omitempty"`
ReleaseDate JSONTime `json:"date,omitempty"`
}
type discEntry struct {
Name string `json:"name,omitempty"`
Duration int `json:"duration,omitempty"`
TrackCount int `json:"trackcount,omitempty"`
Tracks []trackEntry `json:"tracks"`
}
type trackEntry struct {
Duration int `json:"duration,omitempty"`
MainTitle string `json:"title,omitempty"`
OriginalTitle string `json:"originaltitle,omitempty"`
Titles []string `json:"titles,omitempty"`
Artists []artistEntry `json:"artists,omitempty"`
}
type artistEntry struct {
Position string `json:"position"`
Names []string `json:"names"`
}
func getArtistEntries(kind string, entries []interface{}) (artists []artistEntry) {
artist := artistEntry{
Position: kind,
}
recreateArtist := func(kinds ...string) {
var names []string
for _, n := range artist.Names {
//TODO
//n = strings.Trim(n, " ()[]")
n = strings.TrimSpace(n)
if len(n) > 0 {
var curName string
if len(names) > 0 {
curName = names[len(names)-1]
}
if len(curName) > 0 && curName[len(curName)-1] == '(' {
names[len(names)-1] += n
} else if len(curName) > 0 && len(n) > 1 && n[len(n)-1] == ')' && strings.Index(n, "(") <= 0 {
names[len(names)-1] += " " + n
} else if len(curName) > 0 && n[0] == '(' && (strings.Index(n, ")") == -1 || n[len(n)-1] == ')') {
names[len(names)-1] += " " + n
} else if len(curName) > 0 && n == ")" {
names[len(names)-1] += n
} else if len(curName) > 0 && curName[len(curName)-1] == '/' {
names[len(names)-1] += n
} else if len(curName) > 0 && n == "/" {
names[len(names)-1] += n
} else {
names = append(names, n)
}
}
}
artist.Names = names
if len(artist.Names) > 0 {
artists = append(artists, artist)
artist = artistEntry{
Position: strings.Join(kinds, ", "),
}
}
}
for _, value := range entries {
if text, ok := value.(string); ok {
switch strings.ToLower(text) {
case "&", "and":
recreateArtist(kind)
}
artist.Names = append(artist.Names, normalizeStringCharacters(text))
} else if _, ok := value.(wikiparser.NewLineToken); ok {
recreateArtist(kind)
} else if tpl, ok := value.(*wikiparser.Template); ok {
if tpl.IsLink {
var result []string
result = append(result, tpl.Name)
for _, vv := range tpl.Parameters {
result = append(result, getStringValue("", vv)...)
}
artist.Names = append(artist.Names, strings.Join(result, " "))
} else {
artist.Names = append(artist.Names, strings.Join(getStringValue("", []interface{}{tpl}), " "))
}
} else if link, ok := value.(*wikiparser.Link); ok {
if len(link.Name) > 0 {
artist.Names = append(artist.Names, strings.Join(getStringValue(" ", link.Name), " "))
} else if !link.IsExternal {
artist.Names = append(artist.Names, link.URL)
}
} else if unorderedList, ok := value.(*wikiparser.UnorderedList); ok {
for _, val := range unorderedList.Entries {
recreateArtist(kind)
if text, ok := val.(string); ok {
artist.Names = append(artist.Names, text)
} else if tpl, ok := val.(*wikiparser.Template); ok {
if tpl.IsLink {
artist.Names = append(artist.Names, tpl.Name)
for _, val := range tpl.Parameters {
artist.Names = append(artist.Names, getStringValue("", val)...)
}
} else {
artist.Names = append(artist.Names, getStringValue("", []interface{}{tpl})...)
}
}
}
recreateArtist(kind)
} else if descriptionList, ok := value.(*wikiparser.DescriptionList); ok {
for _, val := range descriptionList.Entries {
recreateArtist(kind, strings.Join(getStringValue("", descriptionList.Name), ", "))
if text, ok := val.(string); ok {
artist.Names = append(artist.Names, text)
} else if tpl, ok := val.(*wikiparser.Template); ok {
if tpl.IsLink {
artist.Names = append(artist.Names, tpl.Name)
for _, val := range tpl.Parameters {
artist.Names = append(artist.Names, getStringValue("", val)...)
}
} else {
artist.Names = append(artist.Names, getStringValue("", []interface{}{tpl})...)
}
}
recreateArtist(kind)
}
}
}
recreateArtist(kind)
return
}
func normalizeStringCharacters(text string) string {
//TODO: use transform chain
for _, v := range [][2]string{
{"", ", "},
{"", "("},
{"", ")"},
} {
text = strings.Replace(text, v[0], v[1], -1)
}
return text
}
func getStringValue(pageName string, v []interface{}) (result []string) {
for _, value := range v {
if text, ok := value.(string); ok {
result = append(result, normalizeStringCharacters(text))
} else if template, ok := value.(*wikiparser.Template); ok {
if template.IsLink {
result = append(result, template.Name)
for _, vv := range template.Parameters {
result = append(result, getStringValue(pageName, vv)...)
}
} else {
switch strings.ToUpper(template.Name) {
case "H:TITLE":
if val, ok := template.Parameters["0"]; ok && len(val) > 0 {
result = append(result, getStringValue(pageName, val)[0])
}
case "LANG":
if val, ok := template.Parameters["1"]; ok && len(val) > 0 {
result = append(result, getStringValue(pageName, val)[0])
}
case "GENRE":
if val, ok := template.Parameters["0"]; ok && len(val) > 0 {
result = append(result, getStringValue(pageName, val)[0])
}
case "PAGENAME":
fallthrough
case "SUBPAGENAME":
result = append(result, pageName)
default:
result = append(result, template.Name)
}
}
} else if link, ok := value.(*wikiparser.Link); ok {
if len(link.Name) > 0 {
result = append(result, getStringValue(pageName, link.Name)...)
} else {
result = append(result, link.URL)
}
result = append(result, getStringValue(pageName, link.Name)...)
} else if unorderedList, ok := value.(*wikiparser.UnorderedList); ok {
result = append(result, getStringValue(pageName, unorderedList.Entries)...)
} else if descriptionList, ok := value.(*wikiparser.DescriptionList); ok {
result = append(result, strings.Join(getStringValue("", descriptionList.Name), ", ")+": "+strings.Join(getStringValue(pageName, descriptionList.Entries), ", "))
}
}
return
}
func processIndexDirectory(filePath, indexPath, kind string, wg *sync.WaitGroup) {
entries, err := ioutil.ReadDir(indexPath)
if err != nil {
return
}
var wg sync.WaitGroup
for _, e := range entries {
if path.Ext(e.Name()) == ".json" {
for _, v := range parseCategoryPageIndex(path.Join(arrangeIndexPath, e.Name())) {
for _, v := range parseCategoryPageIndex(path.Join(indexPath, e.Name())) {
wg.Add(1)
go func(entry *arrangeCdEntry) {
go func(entry *albumEntry) {
defer wg.Done()
contents, err := ioutil.ReadFile(path.Join(filePath, "pages", fmt.Sprintf("%d.wiki", entry.Id)))
@ -102,120 +311,259 @@ func processIndex(filePath string) {
return
}
var stringName string
title, ok := tpl.Parameters["titleen"]
if ok && len(title) > 0 {
stringName, ok = title[0].(string)
if ok && len(stringName) > 0 {
entry.Titles = append(entry.Titles, stringName)
}
}
title, ok = tpl.Parameters["titlejp"]
if ok && len(title) > 0 {
stringName, ok = title[0].(string)
if ok && len(stringName) > 0 {
entry.Titles = append(entry.Titles, stringName)
}
}
title, ok = tpl.Parameters["titlejprom"]
if ok && len(title) > 0 {
stringName, ok = title[0].(string)
if ok && len(stringName) > 0 {
entry.Titles = append(entry.Titles, stringName)
}
}
catalogNo, ok := tpl.Parameters["catalogno"]
if ok && len(catalogNo) > 0 {
stringName, ok = catalogNo[0].(string)
if ok && len(stringName) > 0 {
entry.CatalogNumber = stringName
}
}
groupCat, ok := tpl.Parameters["groupCat"]
if ok && len(groupCat) > 0 {
stringName, ok = groupCat[0].(string)
if ok && len(stringName) > 0 {
entry.Group = stringName
}
}
released, ok := tpl.Parameters["released"]
if ok && len(released) > 0 {
stringName, ok = released[0].(string)
if ok && len(released) > 0 {
releaseDate, err := time.ParseInLocation("2006-01-02", stringName, time.UTC)
if err == nil {
entry.Year = releaseDate.Year()
}
}
}
tracks, ok := tpl.Parameters["tracks"]
if ok && len(tracks) > 0 {
stringName, ok = tracks[0].(string)
if ok && len(stringName) > 0 {
entry.TrackCount, _ = strconv.Atoi(stringName)
}
}
length, ok := tpl.Parameters["length"]
if ok && len(length) > 0 {
stringName, ok = length[0].(string)
if ok && len(stringName) > 0 {
split := strings.Split(stringName, ":")
numbers := make([]int, len(split))
for i, val := range split {
numbers[i], _ = strconv.Atoi(val)
}
var discLengths []int
var discTrackNumbers []int
if len(numbers) == 3 {
entry.Duration = 3600*numbers[0] + 60*numbers[1] + numbers[2]
} else if len(numbers) == 2 {
entry.Duration = 60*numbers[0] + numbers[1]
} else if len(numbers) == 1 {
entry.Duration = numbers[0]
var val []interface{}
var stringVal []string
if val, ok = tpl.Parameters["titleen"]; ok {
if stringVal = getStringValue(entry.MainTitle, val); len(stringVal) > 0 {
entry.Titles = append(entry.Titles, stringVal[0])
}
}
if val, ok = tpl.Parameters["titlejp"]; ok {
if stringVal = getStringValue(entry.MainTitle, val); len(stringVal) > 0 {
entry.Titles = append(entry.Titles, stringVal[0])
}
}
if val, ok = tpl.Parameters["titlejprom"]; ok {
if stringVal = getStringValue(entry.MainTitle, val); len(stringVal) > 0 {
entry.Titles = append(entry.Titles, stringVal[0])
}
}
if val, ok = tpl.Parameters["catalogno"]; ok {
if stringVal = getStringValue(entry.MainTitle, val); len(stringVal) > 0 {
entry.CatalogNumber = stringVal[0]
}
}
if val, ok = tpl.Parameters["group"]; ok {
entry.Artists = append(entry.Artists, getArtistEntries("group", val)...)
}
if val, ok = tpl.Parameters["masterer"]; ok {
entry.Artists = append(entry.Artists, getArtistEntries("mastering", val)...)
}
if val, ok = tpl.Parameters["illustrator"]; ok {
entry.Artists = append(entry.Artists, getArtistEntries("illustration", val)...)
}
if val, ok = tpl.Parameters["arranger"]; ok {
entry.Artists = append(entry.Artists, getArtistEntries("arranger", val)...)
}
if val, ok = tpl.Parameters["lyricist"]; ok {
entry.Artists = append(entry.Artists, getArtistEntries("lyrics", val)...)
}
if val, ok = tpl.Parameters["vocalist"]; ok {
entry.Artists = append(entry.Artists, getArtistEntries("vocals", val)...)
}
if val, ok = tpl.Parameters["producer"]; ok {
entry.Artists = append(entry.Artists, getArtistEntries("producer", val)...)
}
if val, ok = tpl.Parameters["designer"]; ok {
entry.Artists = append(entry.Artists, getArtistEntries("design", val)...)
}
if val, ok = tpl.Parameters["other"]; ok {
entry.Artists = append(entry.Artists, getArtistEntries("other", val)...)
}
if val, ok = tpl.Parameters["released"]; ok {
if stringVal = getStringValue(entry.MainTitle, val); len(stringVal) > 0 {
if releaseDate, err := time.ParseInLocation("2006-01-02", stringVal[0], time.UTC); err == nil {
entry.ReleaseDate = JSONTime{Time: releaseDate}
}
}
}
arrangeCDIndexLock.Lock()
arrangeCDIndex[entry.Id] = entry
if entry.TrackCount > 0 {
if _, ok := tracksLookup[entry.TrackCount]; !ok {
tracksLookup[entry.TrackCount] = []*arrangeCdEntry{entry}
if val, ok = tpl.Parameters["length"]; ok {
if stringVal = getStringValue(entry.MainTitle, val); len(stringVal) > 0 {
for _, item := range strings.Split(strings.Split(stringVal[0], "=")[0], "+") {
split := strings.Split(item, ":")
numbers := make([]int, len(split))
for i, numVal := range split {
numbers[i], _ = strconv.Atoi(numVal)
}
var duration int
if len(numbers) == 3 {
duration = 3600*numbers[0] + 60*numbers[1] + numbers[2]
} else if len(numbers) == 2 {
duration = 60*numbers[0] + numbers[1]
} else if len(numbers) == 1 {
duration = numbers[0]
}
discLengths = append(discLengths, duration)
entry.Duration += duration
}
}
}
if val, ok = tpl.Parameters["tracks"]; ok {
if stringVal = getStringValue(entry.MainTitle, val); len(stringVal) > 0 {
for _, item := range strings.Split(strings.Split(stringVal[0], "=")[0], "+") {
trackCount, _ := strconv.Atoi(item)
discTrackNumbers = append(discTrackNumbers, trackCount)
entry.TrackCount += trackCount
}
}
}
if val, ok = tpl.Parameters["tracklist"]; ok {
var disc discEntry
createDisc := func() {
if len(disc.Tracks) > 0 {
disc.TrackCount = len(disc.Tracks)
entry.Discs = append(entry.Discs, disc)
}
}
for _, listEntry := range val {
if strVal, ok := listEntry.(string); ok && len(strVal) > 3 && strVal[0:3] == "===" {
//title header
if disc.Name != "" {
createDisc()
}
disc.Name = strings.Trim(strVal, "= ")
} else if listVal, ok := listEntry.(*wikiparser.UnorderedList); ok && len(listVal.Entries) > 0 && len(listVal.Entries) > 0 {
if sliceVal, ok := listVal.Entries[0].([]interface{}); ok && len(sliceVal) > 0 {
if trackTpl, ok := sliceVal[0].(*wikiparser.Template); ok && strings.ToUpper(trackTpl.Name) == "TRACK" && len(trackTpl.Parameters) >= 3 {
track := trackEntry{}
if mainTitleValue := getStringValue(entry.MainTitle, trackTpl.Parameters["1"]); len(mainTitleValue) > 0 {
track.MainTitle = mainTitleValue[0]
track.Titles = append(track.Titles, mainTitleValue...)
}
if durations := getStringValue(entry.MainTitle, trackTpl.Parameters["2"]); len(durations) > 0 {
split := strings.Split(durations[0], ":")
numbers := make([]int, len(split))
for i, numVal := range split {
numbers[i], _ = strconv.Atoi(numVal)
}
var duration int
if len(numbers) == 3 {
duration = 3600*numbers[0] + 60*numbers[1] + numbers[2]
} else if len(numbers) == 2 {
duration = 60*numbers[0] + numbers[1]
} else if len(numbers) == 1 {
duration = numbers[0]
}
track.Duration = duration
}
if len(listVal.Entries) > 1 {
if extraListData, ok := listVal.Entries[1].(*wikiparser.UnorderedList); ok && len(extraListData.Entries) > 0 {
for i, entryValue := range extraListData.Entries {
if descVal, ok := entryValue.([]interface{}); ok && len(descVal) > 0 {
keyValue := strings.Split(strings.Join(getStringValue(entry.MainTitle, descVal), " "), ":")
if len(keyValue) > 0 {
if i == 0 && len(keyValue[0]) > 2 && keyValue[0][0:2] == "''" {
track.Titles = append(track.Titles, strings.Trim(strings.Join(keyValue, " "), "'' "))
continue
}
keyEntry := strings.ToLower(keyValue[0])
var values []interface{}
for j, fullValue := range descVal {
if strVal, ok = fullValue.(string); j == 0 && ok {
values = append(values, strings.Join(strings.Split(strVal, ":")[1:], ":"))
} else {
values = append(values, fullValue)
}
}
switch keyEntry {
case "original title":
track.OriginalTitle = strings.TrimSpace(strings.Join(getStringValue(entry.MainTitle, values), " "))
case "lyrics", "vocals", "arranger", "composer", "producer":
track.Artists = append(track.Artists, getArtistEntries(keyEntry, values)...)
case "arrangement":
track.Artists = append(track.Artists, getArtistEntries("arranger", values)...)
case "composition":
track.Artists = append(track.Artists, getArtistEntries("composer", values)...)
}
}
}
}
}
}
disc.Duration += track.Duration
disc.Tracks = append(disc.Tracks, track)
}
}
}
}
createDisc()
}
cdIndexLock.Lock()
cdIndex[entry.Id] = entry
for _, d := range entry.Discs {
if _, ok := discTracksLookup[d.TrackCount]; !ok {
discTracksLookup[d.TrackCount] = []*albumEntry{entry}
} else {
tracksLookup[entry.TrackCount] = append(tracksLookup[entry.TrackCount], entry)
for _, val := range discTracksLookup[d.TrackCount] {
if val.Id == entry.Id {
goto exit2
}
}
discTracksLookup[d.TrackCount] = append(discTracksLookup[d.TrackCount], entry)
exit2:
}
}
if len(entry.CatalogNumber) > 0 {
normalized := normalizeTitle(entry.CatalogNumber)
if _, ok := titleLookup[normalized]; !ok {
titleLookup[normalized] = []*arrangeCdEntry{entry}
if _, ok := albumTitleLookup[normalized]; !ok {
albumTitleLookup[normalized] = []*albumEntry{entry}
} else {
titleLookup[normalized] = append(titleLookup[normalized], entry)
albumTitleLookup[normalized] = append(albumTitleLookup[normalized], entry)
}
}
for _, title := range entry.Titles {
normalized := normalizeTitle(title)
if _, ok := titleLookup[normalized]; !ok {
titleLookup[normalized] = []*arrangeCdEntry{entry}
if _, ok := albumTitleLookup[normalized]; !ok {
albumTitleLookup[normalized] = []*albumEntry{entry}
} else {
for _, val := range titleLookup[normalized] {
for _, val := range albumTitleLookup[normalized] {
if val.Id == entry.Id {
goto exit
}
}
titleLookup[normalized] = append(titleLookup[normalized], entry)
albumTitleLookup[normalized] = append(albumTitleLookup[normalized], entry)
exit:
}
}
defer arrangeCDIndexLock.Unlock()
}(&arrangeCdEntry{
defer cdIndexLock.Unlock()
}(&albumEntry{
Id: v.PageId,
MainTitle: v.Title,
Type: kind,
})
}
}
}
}
func processIndex(filePath string) {
var wg sync.WaitGroup
processIndexDirectory(filePath, path.Join(filePath, "pageindex", "Official_CDs"), "official", &wg)
processIndexDirectory(filePath, path.Join(filePath, "pageindex", "Arrangement_CDs"), "arrangement", &wg)
wg.Wait()
}
@ -247,15 +595,26 @@ func normalizeTitle(title string) (normalized string) {
return
}
func findByTracksAndDuration(tracks, duration, threshold int) (results []*arrangeCdEntry) {
for _, r := range tracksLookup[tracks] {
diff := r.Duration - duration
if diff < 0 {
diff = -diff
}
type findByDurationResult struct {
albumEntry *albumEntry
discIndex int
}
if diff <= threshold {
results = append(results, r)
func findByTracksAndDuration(tracks, duration, threshold int) (results []findByDurationResult) {
for _, r := range discTracksLookup[tracks] {
for i, d := range r.Discs {
diff := d.Duration - duration
if diff < 0 {
diff = -diff
}
if diff <= threshold {
results = append(results, findByDurationResult{
albumEntry: r,
discIndex: i,
})
}
}
}
@ -286,6 +645,26 @@ func main() {
}
switch splits[1] {
case "query":
formatEntry := func(cddb1 utilities.CDDB1, result findByDurationResult) (out string) {
var group string
for _, a := range result.albumEntry.Artists {
if a.Position == "group" {
group = a.Names[0]
break
}
}
if len(result.albumEntry.CatalogNumber) > 0 {
out = fmt.Sprintf("%sSoundtrack%d_%d %s [%s] %s / %s", strings.ToUpper(result.albumEntry.Type[0:1])+result.albumEntry.Type[1:], result.albumEntry.Id, result.discIndex, cddb1.String(), result.albumEntry.CatalogNumber, group, result.albumEntry.MainTitle)
} else {
out = fmt.Sprintf("%sSoundtrack%d_%d %s %s / %s", strings.ToUpper(result.albumEntry.Type[0:1])+result.albumEntry.Type[1:], result.albumEntry.Id, result.discIndex, cddb1.String(), group, result.albumEntry.MainTitle)
}
if len(result.albumEntry.Discs) > 1 {
out += fmt.Sprintf(" (Disc %d)", result.discIndex+1)
}
return
}
cddb1 := utilities.NewCDDB1FromString(splits[2])
if cddb1 == 0 {
writer.Write([]byte("500 Command syntax error\n.\n"))
@ -297,20 +676,12 @@ func main() {
writer.Write([]byte("202 No match found\n.\n"))
return
} else if len(entries) == 1 {
if len(entries[0].CatalogNumber) > 0 {
writer.Write([]byte(fmt.Sprintf("200 Soundtrack%d %s [%s] %s / %s\n", entries[0].Id, cddb1.String(), entries[0].CatalogNumber, entries[0].Group, entries[0].MainTitle)))
} else {
writer.Write([]byte(fmt.Sprintf("200 Soundtrack%d %s %s / %s\n", entries[0].Id, cddb1.String(), entries[0].Group, entries[0].MainTitle)))
}
writer.Write([]byte(fmt.Sprintf("200 %s\n", formatEntry(cddb1, entries[0]))))
return
} else {
writer.Write([]byte("211 Found inexact matches list follows (until terminating marker `.')\n"))
for _, e := range entries {
if len(e.CatalogNumber) > 0 {
writer.Write([]byte(fmt.Sprintf("Soundtrack%d %s [%s] %s / %s\n", e.Id, cddb1.String(), e.CatalogNumber, e.Group, e.MainTitle)))
} else {
writer.Write([]byte(fmt.Sprintf("Soundtrack%d %s %s / %s\n", e.Id, cddb1.String(), e.Group, e.MainTitle)))
}
writer.Write([]byte(fmt.Sprintf("%s\n", formatEntry(cddb1, e))))
}
writer.Write([]byte(".\n"))
return
@ -325,39 +696,83 @@ func main() {
writer.Write([]byte("500 Command syntax error\n.\n"))
return
}
pageid, err := strconv.Atoi(strings.ReplaceAll(splits[2], "Soundtrack", ""))
firstDigit := strings.IndexFunc(splits[2], unicode.IsNumber)
if firstDigit == -1 {
writer.Write([]byte("500 Command syntax error\n.\n"))
return
}
params := strings.Split(splits[2][firstDigit:], "_")
pageid, err := strconv.Atoi(params[0])
var discIndex int
if err != nil {
writer.Write([]byte("500 Command syntax error\n.\n"))
return
}
entry, ok := arrangeCDIndex[pageid]
if !ok {
if len(params) > 1 {
discIndex, err = strconv.Atoi(params[1])
if err != nil {
writer.Write([]byte("500 Command syntax error\n.\n"))
return
}
}
entry, ok := cdIndex[pageid]
if !ok || len(entry.Discs) <= discIndex {
writer.Write([]byte("401 Entry not found\n.\n"))
return
}
writer.Write([]byte(fmt.Sprintf("210 Soundtrack %s\n", cddb1.String())))
var group string
for _, a := range entry.Artists {
if a.Position == "group" {
group = a.Names[0]
break
}
}
writer.Write([]byte(fmt.Sprintf("210 %sSoundtrack %s\n", strings.ToUpper(entry.Type[0:1])+entry.Type[1:], cddb1.String())))
writer.Write([]byte("# xmcd\n# Track frame offsets:\n"))
for i := 0; i < entry.TrackCount; i++ {
for i := 0; i < entry.Discs[discIndex].TrackCount; i++ {
writer.Write([]byte("#\t0\n"))
}
writer.Write([]byte(fmt.Sprintf("# Disc length: %d seconds\n#\n", entry.Duration)))
writer.Write([]byte(fmt.Sprintf("DISCID=%s\n", cddb1.String())))
writer.Write([]byte(fmt.Sprintf("DNUM=%d\n", len(entry.Discs))))
writer.Write([]byte(fmt.Sprintf("DINDEX=%d\n", discIndex+1)))
if len(entry.CatalogNumber) > 0 {
writer.Write([]byte(fmt.Sprintf("DTITLE=%s / [%s] %s\n", entry.Group, entry.CatalogNumber, entry.MainTitle)))
writer.Write([]byte(fmt.Sprintf("DTITLE=%s / [%s] %s\n", group, entry.CatalogNumber, entry.MainTitle)))
} else {
writer.Write([]byte(fmt.Sprintf("DTITLE=%s / %s\n", entry.Group, entry.MainTitle)))
writer.Write([]byte(fmt.Sprintf("DTITLE=%s / %s\n", group, entry.MainTitle)))
}
if entry.Year > 0 {
writer.Write([]byte(fmt.Sprintf("DYEAR=%d\nDGENRE=Soundtrack\n", entry.Year)))
if !entry.ReleaseDate.IsZero() {
writer.Write([]byte(fmt.Sprintf("DYEAR=%d\nDGENRE=Soundtrack\n", entry.ReleaseDate.Year())))
}
for i := 0; i < entry.TrackCount; i++ {
writer.Write([]byte(fmt.Sprintf("TTITLE%d=\n", i)))
for i, t := range entry.Discs[discIndex].Tracks {
writer.Write([]byte(fmt.Sprintf("TTITLE%d=%s\n", i, t.MainTitle)))
}
writer.Write([]byte(fmt.Sprintf("EXTD=https://en.touhouwiki.net/index.php?curid=%d\n", entry.Id)))
for i := 0; i < entry.TrackCount; i++ {
writer.Write([]byte(fmt.Sprintf("EXTT%d=\n", i)))
for i, t := range entry.Discs[discIndex].Tracks {
result := make(map[string][]string)
for _, a := range t.Artists {
if _, ok := result[a.Position]; !ok {
result[a.Position] = []string{}
}
result[a.Position] = append(result[a.Position], strings.Join(a.Names, "/"))
}
var resultLines []string
for k, v := range result {
resultLines = append(resultLines, k+": "+strings.Join(v, ", "))
}
sort.SliceStable(resultLines, func(i, j int) bool {
return resultLines[i] < resultLines[j]
})
writer.Write([]byte(fmt.Sprintf("EXTT%d=%s\n", i, strings.Join(resultLines, "\\n"))))
}
writer.Write([]byte("PLAYORDER=\n.\n"))
return
}
@ -367,8 +782,8 @@ func main() {
writer.Header().Set("Content-Type", "application/json")
switch request.URL.Query().Get("type") {
case "title": //search by title or catalog number
entries, ok := titleLookup[normalizeTitle(request.URL.Query().Get("query"))]
case "album": //search by title or catalog number
entries, ok := albumTitleLookup[normalizeTitle(request.URL.Query().Get("query"))]
if !ok {
writer.Write([]byte("[]"))

10
wikiparser/list.go Normal file
View file

@ -0,0 +1,10 @@
package wikiparser
type DescriptionList struct {
Name []interface{}
Entries []interface{}
}
type UnorderedList struct {
Entries []interface{}
}

View file

@ -6,25 +6,43 @@ func NormalizeWikiTitle(title string) string {
return strings.Replace(title, " ", "_", -1)
}
type Link struct {
URL string
IsExternal bool
Name []interface{}
}
type NewLineToken struct {
}
//ParseWikiText small WikiText parser that extracts text, Templates, and its arguments/parameters
func ParseWikiText(text string) (result []interface{}) {
index := 0
for index < len(text) {
templateIndex := strings.Index(text[index:], "{{")
if templateIndex == -1 {
linkIndex := strings.Index(text[index:], "[[")
if templateIndex == -1 && linkIndex == -1 {
t := strings.TrimSpace(text[index:])
if len(t) > 0 {
result = append(result, t)
}
break
} else {
t := strings.TrimSpace(text[index : index+templateIndex])
bestIndex := templateIndex
if templateIndex == -1 {
bestIndex = linkIndex
} else {
if linkIndex != -1 && linkIndex < bestIndex {
bestIndex = linkIndex
}
}
t := strings.TrimSpace(text[index : index+bestIndex])
if len(t) > 0 {
result = append(result, t)
}
var tpl *Template
index, tpl = ParseTemplate(text, index+templateIndex+2, 0)
index, tpl = ParseTemplate(text, index+bestIndex+2, 0, text[index+bestIndex])
if tpl != nil {
result = append(result, tpl)
}
@ -34,7 +52,64 @@ func ParseWikiText(text string) (result []interface{}) {
return
}
func ParseTemplate(text string, index int, depth int) (i int, template *Template) {
func ParseLink(text string, index int, depth int, startCharacter byte) (i int, link *Link) {
var c byte
lastToken := index
addValue := func() int {
if lastToken < len(text) && i-lastToken > 0 {
t := strings.TrimSpace(text[lastToken:i])
if len(t) > 0 {
if link == nil {
link = &Link{URL: t, IsExternal: startCharacter == '{'}
} else {
link.Name = append(link.Name, t)
}
}
return len(t)
}
return 0
}
for i = index; i < len(text); i++ {
c = text[i]
if c == ' ' || c == '\t' && link == nil {
addValue()
lastToken = i + 1
} else if startCharacter == '{' && c == '}' {
addValue()
i += 1
break
} else if startCharacter == '[' && c == ']' { //end of link
addValue()
i += 1
break
//template or light might have parameters
} else if (c == '{' && i < len(text)-1 && text[i+1] == '{') || (c == '[' && i < len(text)-1 && text[i+1] == '[') {
addValue()
var tpl *Template
var scanIndex int
scanIndex, tpl = ParseTemplate(text, i+2, depth+1, c)
if tpl != nil {
if link == nil {
link = &Link{}
}
link.Name = append(link.Name, tpl)
}
lastToken = scanIndex
i = scanIndex - 1
}
}
return
}
func ParseTemplate(text string, index int, depth int, startCharacter byte) (i int, template *Template) {
var c byte
lastToken := index
@ -46,7 +121,7 @@ func ParseTemplate(text string, index int, depth int) (i int, template *Template
t := strings.TrimSpace(text[lastToken:i])
if len(t) > 0 {
if template == nil {
template = NewTemplate(t)
template = NewTemplate(t, startCharacter == '[')
} else {
if key == "" {
template.AddParameterUnkeyed(t)
@ -69,17 +144,26 @@ func ParseTemplate(text string, index int, depth int) (i int, template *Template
}
}
}
afterNewLine := false
for i = index; i < len(text); i++ {
c = text[i]
if c == '}' && i < len(text)-1 && text[i+1] == '}' {
if startCharacter == '{' && c == '}' && i < len(text)-1 && text[i+1] == '}' { //end of template
addValue()
i += 2
break
} else if c == '{' && i < len(text)-1 && text[i+1] == '{' {
} else if startCharacter == '[' && c == ']' && i < len(text)-1 && text[i+1] == ']' { //end of link
addValue()
i += 2
break
//template or light might have parameters
} else if (c == '{' && i < len(text)-1 && text[i+1] == '{') || (c == '[' && i < len(text)-1 && text[i+1] == '[') {
addValue()
var tpl *Template
i, tpl = ParseTemplate(text, i+2, depth+1)
var scanIndex int
scanIndex, tpl = ParseTemplate(text, i+2, depth+1, c)
if tpl != nil {
if key == "" {
template.AddParameterUnkeyed(tpl)
@ -87,7 +171,22 @@ func ParseTemplate(text string, index int, depth int) (i int, template *Template
template.AddParameter(key, tpl)
}
}
lastToken = i
lastToken = scanIndex
i = scanIndex - 1
} else if (c == '{' && i < len(text)-1 && text[i+1] != '{' && text[i+1] != '[') || (c == '[' && i < len(text)-1 && text[i+1] != '[' && text[i+1] != '{') {
addValue()
var link *Link
var scanIndex int
scanIndex, link = ParseLink(text, i+1, depth+1, c)
if link != nil && template != nil {
if key == "" {
template.AddParameterUnkeyed(link)
} else {
template.AddParameter(key, link)
}
}
lastToken = scanIndex
i = scanIndex - 1
} else if c == '|' {
addValue()
lastToken = i + 1
@ -95,12 +194,239 @@ func ParseTemplate(text string, index int, depth int) (i int, template *Template
} else if c == '\n' {
addValue()
lastToken = i + 1
afterNewLine = true
if template != nil {
if key == "" {
template.AddParameterUnkeyed(NewLineToken{})
} else {
template.AddParameter(key, NewLineToken{})
}
}
} else if afterNewLine && (c == '*' || c == '#') {
addValue()
var list *UnorderedList
var scanIndex int
scanIndex, list = ParseUnorderedList(text, i, depth+1, 1, c)
if list != nil {
if key == "" {
template.AddParameterUnkeyed(list)
} else {
template.AddParameter(key, list)
}
}
lastToken = scanIndex
i = scanIndex - 1
} else if c == ';' {
addValue()
var list *DescriptionList
var scanIndex int
scanIndex, list = ParseDescriptionList(text, i+1, depth+1)
if list != nil {
if key == "" {
template.AddParameterUnkeyed(list)
} else {
template.AddParameter(key, list)
}
}
lastToken = scanIndex
i = scanIndex - 1
} else if afterNewLine && c == ':' {
addValue()
lastToken = i + 1
} else if c == '=' {
if key == "" {
addKey()
lastToken = i + 1
}
}
if afterNewLine && c != '\n' && c != ' ' && c != '\t' {
afterNewLine = false
}
}
return
}
func ParseUnorderedList(text string, index int, depth int, indent int, startCharacter byte) (i int, list *UnorderedList) {
list = &UnorderedList{}
var c byte
lastToken := index
var currentValue []interface{}
addValue := func() int {
if lastToken < len(text) && i-lastToken > 0 {
t := strings.TrimSpace(text[lastToken:i])
if len(t) > 0 {
currentValue = append(currentValue, t)
}
return len(t)
}
return 0
}
afterNewLine := true
processIndent := true
indentation := 0
for i = index; i < len(text); i++ {
c = text[i]
if c == ' ' || c == '\t' {
//keep the check for new line
if !afterNewLine {
processIndent = false
}
} else if processIndent && c == startCharacter {
indentation++
lastToken = i + 1
afterNewLine = false
} else if afterNewLine { //no new list values
if len(currentValue) > 0 {
list.Entries = append(list.Entries, currentValue)
currentValue = []interface{}{}
}
return lastToken, list
} else if indentation > indent {
if len(currentValue) > 0 {
list.Entries = append(list.Entries, currentValue)
currentValue = []interface{}{}
}
var level *UnorderedList
var scanIndex int
scanIndex, level = ParseUnorderedList(text, lastToken-indentation, depth+1, indentation, startCharacter)
if level != nil {
list.Entries = append(list.Entries, level)
}
lastToken = scanIndex
i = scanIndex - 1
indentation = 0
afterNewLine = true
processIndent = true
} else if indentation < indent {
if len(currentValue) > 0 {
list.Entries = append(list.Entries, currentValue)
currentValue = []interface{}{}
}
return lastToken - indentation, list
} else if c == '\n' {
addValue()
if len(currentValue) > 0 {
list.Entries = append(list.Entries, currentValue)
currentValue = []interface{}{}
}
indentation = 0
lastToken = i + 1
afterNewLine = true
processIndent = true
} else if (c == '{' && i < len(text)-1 && text[i+1] == '{') || (c == '[' && i < len(text)-1 && text[i+1] == '[') {
addValue()
var tpl *Template
var scanIndex int
scanIndex, tpl = ParseTemplate(text, i+2, depth+1, c)
if tpl != nil {
currentValue = append(currentValue, tpl)
}
lastToken = scanIndex
i = scanIndex - 1
} else if (c == '{' && i < len(text)-1 && text[i+1] != '{' && text[i+1] != '[') || (c == '[' && i < len(text)-1 && text[i+1] != '[' && text[i+1] != '{') {
addValue()
var link *Link
var scanIndex int
scanIndex, link = ParseLink(text, i+1, depth+1, c)
if link != nil {
currentValue = append(currentValue, link)
}
lastToken = scanIndex
i = scanIndex - 1
} else {
processIndent = false
}
}
return
}
func ParseDescriptionList(text string, index int, depth int) (i int, list *DescriptionList) {
var c byte
lastToken := index
list = &DescriptionList{}
hasKey := false
addValue := func() int {
if lastToken < len(text) && i-lastToken > 0 {
t := strings.TrimSpace(text[lastToken:i])
if len(t) > 0 {
if !hasKey {
list.Name = append(list.Name, t)
} else {
list.Entries = append(list.Entries, t)
}
}
return len(t)
}
return 0
}
afterNewLine := false
for i = index; i < len(text); i++ {
c = text[i]
if c == ' ' || c == '\t' {
//keep the check for new line
} else if c == ':' {
addValue()
lastToken = i + 1
afterNewLine = false
hasKey = true
} else if afterNewLine { //no new list values
return lastToken, list
} else if c == '\n' {
addValue()
lastToken = i + 1
afterNewLine = true
hasKey = true
} else if (c == '{' && i < len(text)-1 && text[i+1] == '{') || (c == '[' && i < len(text)-1 && text[i+1] == '[') {
addValue()
var tpl *Template
var scanIndex int
scanIndex, tpl = ParseTemplate(text, i+2, depth+1, c)
if tpl != nil {
if !hasKey {
list.Name = append(list.Name, tpl)
} else {
list.Entries = append(list.Entries, tpl)
}
}
lastToken = scanIndex
i = scanIndex - 1
} else if (c == '{' && i < len(text)-1 && text[i+1] != '{' && text[i+1] != '[') || (c == '[' && i < len(text)-1 && text[i+1] != '[' && text[i+1] != '{') {
addValue()
var link *Link
var scanIndex int
scanIndex, link = ParseLink(text, i+1, depth+1, c)
if link != nil {
if !hasKey {
list.Name = append(list.Name, link)
} else {
list.Entries = append(list.Entries, link)
}
}
lastToken = scanIndex
i = scanIndex - 1
}
}
return

View file

@ -4,12 +4,14 @@ import "fmt"
type Template struct {
Name string
IsLink bool
Parameters map[string][]interface{}
}
func NewTemplate(name string) *Template {
func NewTemplate(name string, isLink bool) *Template {
return &Template{
Name: name,
IsLink: isLink,
Parameters: make(map[string][]interface{}),
}
}