912 lines
28 KiB
Go
912 lines
28 KiB
Go
package main
|
||
|
||
import (
|
||
"encoding/json"
|
||
"flag"
|
||
"fmt"
|
||
"git.gammaspectra.live/S.O.N.G/touhouwiki-mirror/utilities"
|
||
"git.gammaspectra.live/S.O.N.G/touhouwiki-mirror/wikiparser"
|
||
"golang.org/x/text/cases"
|
||
"golang.org/x/text/language"
|
||
"golang.org/x/text/runes"
|
||
"golang.org/x/text/transform"
|
||
"golang.org/x/text/unicode/norm"
|
||
"io/fs"
|
||
"io/ioutil"
|
||
"log"
|
||
"math"
|
||
"net/http"
|
||
"os"
|
||
"path"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
"unicode"
|
||
)
|
||
|
||
var cdIndex = make(map[int]*albumEntry)
|
||
var cdIndexLock sync.Mutex
|
||
|
||
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"`
|
||
Namespace int `json:"ns"`
|
||
Title string `json:"title"`
|
||
Timestamp string `json:"timestamp"`
|
||
}
|
||
|
||
func parseCategoryPageIndex(filePath string) []categoryPageIndex {
|
||
s := &struct {
|
||
Query struct {
|
||
Members []categoryPageIndex `json:"categorymembers"`
|
||
} `json:"query"`
|
||
}{}
|
||
|
||
fileData, err := ioutil.ReadFile(filePath)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
|
||
err = json.Unmarshal(fileData, s)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
return s.Query.Members
|
||
}
|
||
|
||
type JSONTime struct {
|
||
time.Time
|
||
}
|
||
|
||
func (t JSONTime) MarshalJSON() ([]byte, error) {
|
||
if t.IsZero() {
|
||
return nil, nil
|
||
}
|
||
return []byte(fmt.Sprintf("\"%s\"", t.Format("2006-01-02"))), nil
|
||
}
|
||
|
||
type albumEntry struct {
|
||
Id int `json:"pageid"`
|
||
Type string `json:"type"`
|
||
MainTitle string `json:"pagetitle"`
|
||
Titles []string `json:"titles"`
|
||
CatalogNumber string `json:"catalognumber,omitempty"`
|
||
Genre []string `json:"genre,omitempty"`
|
||
TrackCount int `json:"trackcount,omitempty"`
|
||
Duration int `json:"duration,omitempty"`
|
||
ReleaseDate JSONTime `json:"date,omitempty"`
|
||
Artists []artistEntry `json:"artists,omitempty"`
|
||
Discs []discEntry `json:"discs,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"`
|
||
Original string `json:"original,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
|
||
for _, vv := range tpl.Parameters {
|
||
result = append(result, getStringValue("", vv)...)
|
||
}
|
||
if len(result) == 0 {
|
||
result = append(result, tpl.Name)
|
||
}
|
||
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 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 {
|
||
for _, vv := range template.Parameters {
|
||
result = append(result, getStringValue(pageName, vv)...)
|
||
}
|
||
if len(result) == 0 {
|
||
result = append(result, template.Name)
|
||
}
|
||
} 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
|
||
}
|
||
|
||
for _, e := range entries {
|
||
if path.Ext(e.Name()) == ".json" {
|
||
for _, v := range parseCategoryPageIndex(path.Join(indexPath, e.Name())) {
|
||
wg.Add(1)
|
||
go func(entry *albumEntry) {
|
||
defer wg.Done()
|
||
|
||
contents, err := ioutil.ReadFile(path.Join(filePath, "pages", fmt.Sprintf("%d.wiki", entry.Id)))
|
||
if err != nil {
|
||
return
|
||
}
|
||
result := wikiparser.ParseWikiText(string(contents))
|
||
|
||
if len(result) == 0 {
|
||
return
|
||
}
|
||
|
||
tpl, ok := result[0].(*wikiparser.Template)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
if tpl.Name != "MusicArticle" {
|
||
return
|
||
}
|
||
|
||
var discLengths []int
|
||
var discTrackNumbers []int
|
||
|
||
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["genre"]; ok {
|
||
if stringVal = getStringValue(entry.MainTitle, val); len(stringVal) > 0 {
|
||
for _, genre := range strings.Split(strings.Join(stringVal, " "), ",") {
|
||
entry.Genre = append(entry.Genre, strings.ToLower(strings.TrimSpace(genre)))
|
||
}
|
||
}
|
||
}
|
||
|
||
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}
|
||
}
|
||
}
|
||
}
|
||
|
||
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.Duration += disc.Duration
|
||
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.Original = strings.TrimSpace(strings.Join(getStringValue(entry.MainTitle, values), " "))
|
||
case "lyrics", "vocals", "arranger", "composer", "producer", "remix":
|
||
track.Artists = append(track.Artists, getArtistEntries(keyEntry, values)...)
|
||
case "arrangement":
|
||
track.Artists = append(track.Artists, getArtistEntries("arranger", values)...)
|
||
case "original arrangement":
|
||
track.Artists = append(track.Artists, getArtistEntries("original 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 {
|
||
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 := normalizeSearchTitle(entry.CatalogNumber)
|
||
if _, ok := albumTitleLookup[normalized]; !ok {
|
||
albumTitleLookup[normalized] = []*albumEntry{entry}
|
||
} else {
|
||
albumTitleLookup[normalized] = append(albumTitleLookup[normalized], entry)
|
||
}
|
||
}
|
||
for _, title := range entry.Titles {
|
||
normalized := normalizeSearchTitle(title)
|
||
if _, ok := albumTitleLookup[normalized]; !ok {
|
||
albumTitleLookup[normalized] = []*albumEntry{entry}
|
||
} else {
|
||
for _, val := range albumTitleLookup[normalized] {
|
||
if val.Id == entry.Id {
|
||
goto exit
|
||
}
|
||
}
|
||
albumTitleLookup[normalized] = append(albumTitleLookup[normalized], entry)
|
||
|
||
exit:
|
||
}
|
||
}
|
||
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()
|
||
}
|
||
|
||
var normalizeSearchTitleTransformer = transform.Chain(
|
||
norm.NFKD,
|
||
//width.Narrow,
|
||
runes.Remove(runes.In(unicode.Cc)),
|
||
runes.Remove(runes.In(unicode.Cf)),
|
||
runes.Remove(runes.In(unicode.Mn)),
|
||
runes.Remove(runes.In(unicode.Me)),
|
||
runes.Remove(runes.In(unicode.Mc)),
|
||
runes.Remove(runes.In(unicode.Po)),
|
||
runes.Remove(runes.In(unicode.Pe)),
|
||
runes.Remove(runes.In(unicode.Ps)),
|
||
runes.Remove(runes.In(unicode.Pf)),
|
||
runes.Remove(runes.In(unicode.Pi)),
|
||
runes.Remove(runes.In(unicode.Pd)),
|
||
runes.Remove(runes.In(unicode.Sc)),
|
||
runes.Remove(runes.In(unicode.Sk)),
|
||
runes.Remove(runes.In(unicode.Sm)),
|
||
runes.Remove(runes.In(unicode.So)),
|
||
runes.Remove(runes.In(unicode.Space)),
|
||
cases.Lower(language.Und),
|
||
norm.NFC,
|
||
)
|
||
|
||
func normalizeSearchTitle(title string) (normalized string) {
|
||
normalized, _, _ = transform.String(normalizeSearchTitleTransformer, title)
|
||
return
|
||
}
|
||
|
||
func normalizeStringCharacters(text string) (normalized string) {
|
||
normalized = norm.NFKC.String(text)
|
||
|
||
return
|
||
}
|
||
|
||
type findByDurationResult struct {
|
||
albumEntry *albumEntry
|
||
cddb1 utilities.CDDB1
|
||
toc utilities.TOC
|
||
discIndex int
|
||
}
|
||
|
||
func findByTOC(toc utilities.TOC, trackThreshold, discThreshold int) (results []findByDurationResult) {
|
||
for _, result := range findByCDDB1(toc.GetCDDB1(), discThreshold) {
|
||
func() {
|
||
for i, track := range result.albumEntry.Discs[result.discIndex].Tracks {
|
||
diff := math.Abs(float64(track.Duration) - toc.GetTrackDuration(i).Seconds())
|
||
if diff > float64(trackThreshold) { //too much variation
|
||
return
|
||
}
|
||
}
|
||
results = append(results, result)
|
||
}()
|
||
}
|
||
|
||
return
|
||
}
|
||
|
||
func findByCDDB1(cddb1 utilities.CDDB1, discThreshold int) []findByDurationResult {
|
||
return findByTracksAndDuration(cddb1.GetTrackNumber(), int(cddb1.GetDuration().Seconds()), discThreshold)
|
||
}
|
||
|
||
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,
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
return
|
||
}
|
||
|
||
func main() {
|
||
wd, _ := os.Getwd()
|
||
servePath := flag.String("path", wd, "Path that will be served by default")
|
||
flag.Parse()
|
||
|
||
processIndex(*servePath)
|
||
|
||
server := &http.Server{
|
||
Addr: ":8777",
|
||
Handler: http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||
if request.URL.Path == "/cddb" {
|
||
//cddb emulator
|
||
writer.Header().Set("Content-Type", "text/plain; charset=UTF-8")
|
||
writer.WriteHeader(http.StatusOK)
|
||
|
||
cmd := request.URL.Query().Get("cmd")
|
||
if len(cmd) > 4 && cmd[0:4] == "cddb" {
|
||
splits := strings.Split(cmd, " ")
|
||
if len(splits) < 3 {
|
||
writer.Write([]byte("500 Unrecognized command\n.\n"))
|
||
return
|
||
}
|
||
switch splits[1] {
|
||
case "hello":
|
||
writer.Write([]byte("200 hello and welcome\n.\n"))
|
||
return
|
||
|
||
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])
|
||
|
||
toc := utilities.NewTOCFromCDDBString(strings.Join(splits[3:], " "))
|
||
if cddb1 == 0 {
|
||
writer.Write([]byte("500 Command syntax error\n.\n"))
|
||
return
|
||
}
|
||
|
||
var entries []findByDurationResult
|
||
if toc != nil {
|
||
cddb1 = toc.GetCDDB1()
|
||
entries = findByTOC(toc, 3, 10)
|
||
} else {
|
||
entries = findByCDDB1(cddb1, 10)
|
||
}
|
||
|
||
if len(entries) == 0 {
|
||
writer.Write([]byte("202 No match found\n.\n"))
|
||
return
|
||
} else if len(entries) == 1 {
|
||
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 {
|
||
writer.Write([]byte(fmt.Sprintf("%s\n", formatEntry(cddb1, e))))
|
||
}
|
||
writer.Write([]byte(".\n"))
|
||
return
|
||
}
|
||
case "read":
|
||
if len(splits) < 4 {
|
||
writer.Write([]byte("500 Command syntax error\n.\n"))
|
||
return
|
||
}
|
||
cddb1 := utilities.NewCDDB1FromString(splits[3])
|
||
if cddb1 == 0 {
|
||
writer.Write([]byte("500 Command syntax error\n.\n"))
|
||
return
|
||
}
|
||
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
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
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.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", group, entry.CatalogNumber, entry.MainTitle)))
|
||
} else {
|
||
writer.Write([]byte(fmt.Sprintf("DTITLE=%s / %s\n", group, entry.MainTitle)))
|
||
}
|
||
if !entry.ReleaseDate.IsZero() {
|
||
writer.Write([]byte(fmt.Sprintf("DYEAR=%d\n", entry.ReleaseDate.Year())))
|
||
}
|
||
|
||
var genres []string
|
||
if entry.Type == "arrangement" {
|
||
genres = append(genres, entry.Type)
|
||
}
|
||
genres = append(genres, entry.Genre...)
|
||
if len(genres) == 0 {
|
||
genres = append(genres, "soundtrack")
|
||
}
|
||
writer.Write([]byte(fmt.Sprintf("DGENRE=%s\n", strings.Join(genres, ", "))))
|
||
|
||
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, 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
|
||
}
|
||
}
|
||
writer.Write([]byte("500 Unrecognized command\n.\n"))
|
||
} else if request.URL.Path == "/search" {
|
||
writer.Header().Set("Content-Type", "application/json")
|
||
|
||
switch request.URL.Query().Get("type") {
|
||
case "album": //search by title or catalog number
|
||
entries, ok := albumTitleLookup[normalizeSearchTitle(request.URL.Query().Get("query"))]
|
||
|
||
if !ok {
|
||
writer.Write([]byte("[]"))
|
||
} else {
|
||
jsonBytes, _ := json.Marshal(entries)
|
||
|
||
writer.Write(jsonBytes)
|
||
}
|
||
|
||
default:
|
||
writer.WriteHeader(http.StatusNotFound)
|
||
writer.Write([]byte("[]"))
|
||
}
|
||
|
||
} else {
|
||
filePath := path.Join(*servePath, strings.TrimLeft(request.URL.Path, "/"))
|
||
if request.URL.Path == "/" {
|
||
filePath = path.Join(*servePath, "README.md")
|
||
}
|
||
|
||
stat, err := os.Stat(filePath)
|
||
if err != nil {
|
||
http.NotFound(writer, request)
|
||
return
|
||
}
|
||
|
||
if stat.IsDir() {
|
||
writer.Header().Set("Content-Type", "application/json")
|
||
type dirEntry struct {
|
||
Name string `json:"name"`
|
||
Size int64 `json:"size"`
|
||
Mtime int64 `json:"mtime"`
|
||
Link string `json:"link,omitempty"`
|
||
Type string `json:"type"`
|
||
}
|
||
dirEntries := make([]dirEntry, 0, 1024)
|
||
|
||
entries, _ := ioutil.ReadDir(filePath)
|
||
for _, e := range entries {
|
||
entry := dirEntry{
|
||
Name: e.Name(),
|
||
Size: e.Size(),
|
||
Mtime: e.ModTime().UTC().Unix(),
|
||
}
|
||
if e.Mode()&fs.ModeSymlink > 0 {
|
||
entry.Link, _ = os.Readlink(path.Join(filePath, entry.Name))
|
||
entry.Type = "l"
|
||
} else if e.IsDir() {
|
||
entry.Type = "d"
|
||
} else {
|
||
entry.Type = "f"
|
||
}
|
||
dirEntries = append(dirEntries, entry)
|
||
}
|
||
|
||
writer.WriteHeader(http.StatusOK)
|
||
byteEntries, _ := json.Marshal(dirEntries)
|
||
writer.Write(byteEntries)
|
||
} else {
|
||
writer.Header().Set("Content-Type", "text/plain")
|
||
http.ServeFile(writer, request, filePath)
|
||
}
|
||
}
|
||
}),
|
||
}
|
||
err := server.ListenAndServe()
|
||
if err != nil {
|
||
log.Panic(err)
|
||
}
|
||
}
|