touhouwiki-mirror/server.go
2022-03-10 19:10:13 +01:00

1164 lines
36 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"encoding/json"
"flag"
"fmt"
"git.gammaspectra.live/S.O.N.G/touhouwiki-mirror/utilities"
wikitext_parser "git.gammaspectra.live/S.O.N.G/wikitext-parser"
"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 map[string]string `json:"titles"`
Image string `json:"image,omitempty"`
CatalogNumbers []string `json:"catalognumbers,omitempty"`
Genre []string `json:"genre,omitempty"`
TrackCount int `json:"trackcount,omitempty"`
Duration int `json:"duration,omitempty"`
ReleaseDate JSONTime `json:"releasedate,omitempty"`
ReleaseEvent string `json:"releaseevent,omitempty"`
Links []string `json:"links,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"`
Titles map[string]string `json:"titles,omitempty"`
Artists []artistEntry `json:"artists,omitempty"`
Lyrics string `json:"lyrics,omitempty"`
Links []string `json:"links,omitempty"`
Original []string `json:"original,omitempty"`
}
type artistEntry struct {
Position string `json:"position"`
Names []string `json:"names"`
}
func getArtistEntries(kind string, entries []interface{}, opts *wikitext_parser.WikiStringValueOptions) (artists []artistEntry) {
artist := artistEntry{
Position: kind,
}
recreateArtist := func(kinds ...string) {
var names []string
for _, n := range artist.Names {
//TODO
//n = strings.Trim(n, " ()[]")
if opts.Trim {
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 {
if i := strings.Index(text, " & "); i != -1 {
for _, vv := range strings.Split(text, " & ") {
vv = normalizeStringCharacters(vv)
if len(vv) > 0 {
artist.Names = append(artist.Names, vv)
}
recreateArtist(kind)
}
} else if i := strings.Index(text, "& "); i == 0 {
for _, vv := range strings.Split(text, "& ") {
vv = normalizeStringCharacters(vv)
if len(vv) > 0 {
artist.Names = append(artist.Names, vv)
}
recreateArtist(kind)
}
} else if i := strings.Index(text, " &"); i != -1 && i == len(text)-2 {
for _, vv := range strings.Split(text, " &") {
vv = normalizeStringCharacters(vv)
if len(vv) > 0 {
artist.Names = append(artist.Names, vv)
}
recreateArtist(kind)
}
} else if i := strings.Index(text, " and "); i != -1 {
for _, vv := range strings.Split(text, " and ") {
vv = normalizeStringCharacters(vv)
if len(vv) > 0 {
artist.Names = append(artist.Names, vv)
}
recreateArtist(kind)
}
} else if i := strings.Index(text, "and "); i == 0 {
for _, vv := range strings.Split(text, "and ") {
vv = normalizeStringCharacters(vv)
if len(vv) > 0 {
artist.Names = append(artist.Names, vv)
}
recreateArtist(kind)
}
} else if i := strings.Index(text, " and"); i != -1 && i == len(text)-4 {
for _, vv := range strings.Split(text, " and") {
vv = normalizeStringCharacters(vv)
if len(vv) > 0 {
artist.Names = append(artist.Names, vv)
}
recreateArtist(kind)
}
} else {
for _, vv := range strings.Split(text, " and") {
vv = normalizeStringCharacters(vv)
if len(vv) > 0 {
artist.Names = append(artist.Names, vv)
}
}
}
} else if _, ok := value.(wikitext_parser.NewLineToken); ok {
recreateArtist(kind)
} else if tpl, ok := value.(*wikitext_parser.Template); ok {
if tpl.IsLink {
var result []string
for _, vv := range tpl.Parameters {
result = append(result, getStringValue(vv, opts)...)
}
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}, opts), " "))
}
} else if link, ok := value.(*wikitext_parser.Link); ok {
if len(link.Name) > 0 {
artist.Names = append(artist.Names, strings.Join(getStringValue(link.Name, opts), " "))
} else if !link.IsExternal {
artist.Names = append(artist.Names, link.URL)
}
} else if unorderedList, ok := value.(*wikitext_parser.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.(*wikitext_parser.Template); ok {
if tpl.IsLink {
artist.Names = append(artist.Names, tpl.Name)
for _, val := range tpl.Parameters {
artist.Names = append(artist.Names, getStringValue(val, opts)...)
}
} else {
artist.Names = append(artist.Names, getStringValue([]interface{}{tpl}, opts)...)
}
}
}
recreateArtist(kind)
} else if descriptionList, ok := value.(*wikitext_parser.DescriptionList); ok {
for _, val := range descriptionList.Entries {
recreateArtist(kind, strings.Join(getStringValue(descriptionList.Name, opts), ", "))
if text, ok := val.(string); ok {
artist.Names = append(artist.Names, text)
} else if tpl, ok := val.(*wikitext_parser.Template); ok {
if tpl.IsLink {
artist.Names = append(artist.Names, tpl.Name)
for _, val := range tpl.Parameters {
artist.Names = append(artist.Names, getStringValue(val, opts)...)
}
} else {
artist.Names = append(artist.Names, getStringValue([]interface{}{tpl}, opts)...)
}
}
recreateArtist(kind)
}
}
}
recreateArtist(kind)
return
}
func getWikiStringOptions(title string, trim bool) *wikitext_parser.WikiStringValueOptions {
opts := &wikitext_parser.WikiStringValueOptions{}
opts.Default()
opts.PageName = title
opts.TemplateHandler = func(template *wikitext_parser.Template, opt *wikitext_parser.WikiStringValueOptions) (result []string) {
switch strings.ToUpper(template.Name) {
case "H:TITLE":
if val, ok := template.Parameters["0"]; ok && len(val) > 0 {
result = append(result, wikitext_parser.GetWikiStringValue(val, opt)[0])
}
case "LANG":
if val, ok := template.Parameters["1"]; ok && len(val) > 0 {
result = append(result, wikitext_parser.GetWikiStringValue(val, opt)[0])
}
case "GENRE":
if val, ok := template.Parameters["0"]; ok && len(val) > 0 {
result = append(result, wikitext_parser.GetWikiStringValue(val, opt)[0])
}
case "MUSICARTICLE/DIVIDER":
case "PAGENAME", "SUBPAGENAME":
result = append(result, opt.PageName)
default:
result = append(result, template.Name)
}
return
}
opts.Trim = trim
return opts
}
func getStringValue(v []interface{}, opt *wikitext_parser.WikiStringValueOptions) []string {
return wikitext_parser.GetWikiStringValue(v, opt)
}
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 := wikitext_parser.ParseWikiText(string(contents))
if len(result) == 0 {
return
}
tpl, ok := result[0].(*wikitext_parser.Template)
if !ok {
return
}
if tpl.Name != "MusicArticle" {
return
}
opts := getWikiStringOptions(entry.MainTitle, true)
//var discLengths []int
//var discTrackNumbers []int
var val []interface{}
var stringVal []string
if val, ok = tpl.Parameters["titleen"]; ok {
if stringVal = getStringValue(val, opts); len(stringVal) > 0 {
entry.Titles["english"] = stringVal[0]
}
}
if val, ok = tpl.Parameters["titlejp"]; ok {
if stringVal = getStringValue(val, opts); len(stringVal) > 0 {
entry.Titles["japanese"] = stringVal[0]
}
}
if val, ok = tpl.Parameters["titlejprom"]; ok {
if stringVal = getStringValue(val, opts); len(stringVal) > 0 {
entry.Titles["romaji"] = stringVal[0]
}
}
if val, ok = tpl.Parameters["catalogno"]; ok {
if stringVal = getStringValue(val, opts); len(stringVal) > 0 {
for _, catnos := range stringVal {
for _, catno := range strings.Split(catnos, " ") {
if strings.ToLower(catno) != "n/a" {
catno = strings.ReplaceAll(catno, "(", "")
catno = strings.ReplaceAll(catno, ")", "")
entry.CatalogNumbers = append(entry.CatalogNumbers, catno)
}
}
}
}
}
if val, ok = tpl.Parameters["genre"]; ok {
if stringVal = getStringValue(val, opts); 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["image"]; ok && len(val) > 0 {
entry.Image = strings.TrimSpace(strings.Join(getStringValue(val, opts), ""))
}
if val, ok = tpl.Parameters["group"]; ok {
entry.Artists = append(entry.Artists, getArtistEntries("group", val, opts)...)
}
if val, ok = tpl.Parameters["masterer"]; ok {
entry.Artists = append(entry.Artists, getArtistEntries("mastering", val, opts)...)
}
if val, ok = tpl.Parameters["illustrator"]; ok {
entry.Artists = append(entry.Artists, getArtistEntries("illustration", val, opts)...)
}
if val, ok = tpl.Parameters["arranger"]; ok {
entry.Artists = append(entry.Artists, getArtistEntries("arranger", val, opts)...)
}
if val, ok = tpl.Parameters["lyricist"]; ok {
entry.Artists = append(entry.Artists, getArtistEntries("lyrics", val, opts)...)
}
if val, ok = tpl.Parameters["vocalist"]; ok {
entry.Artists = append(entry.Artists, getArtistEntries("vocals", val, opts)...)
}
if val, ok = tpl.Parameters["producer"]; ok {
entry.Artists = append(entry.Artists, getArtistEntries("producer", val, opts)...)
}
if val, ok = tpl.Parameters["designer"]; ok {
entry.Artists = append(entry.Artists, getArtistEntries("design", val, opts)...)
}
if val, ok = tpl.Parameters["other"]; ok {
entry.Artists = append(entry.Artists, getArtistEntries("other", val, opts)...)
}
if val, ok = tpl.Parameters["convention"]; ok && len(val) > 0 {
value := getStringValue(val, opts)
if len(value) > 0 {
entry.ReleaseEvent = value[0]
}
}
if val, ok = tpl.Parameters["released"]; ok {
if stringVal = getStringValue(val, opts); 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["website"]; ok {
for _, value := range val {
if linkVal, ok := value.(*wikitext_parser.Link); ok && linkVal.IsExternal {
entry.Links = append(entry.Links, linkVal.URL)
}
}
}
if val, ok = tpl.Parameters["digital"]; ok {
for _, value := range val {
if linkVal, ok := value.(*wikitext_parser.Link); ok && linkVal.IsExternal {
entry.Links = append(entry.Links, linkVal.URL)
}
}
}
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.TrackCount += disc.TrackCount
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.(*wikitext_parser.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].(*wikitext_parser.Template); ok && strings.ToUpper(trackTpl.Name) == "TRACK" && len(trackTpl.Parameters) >= 3 {
track := trackEntry{
Titles: make(map[string]string),
}
if mainTitleValue := getStringValue(trackTpl.Parameters["1"], opts); len(mainTitleValue) > 0 {
track.MainTitle = strings.Join(mainTitleValue, "")
track.Titles["original"] = track.MainTitle
}
if durations := getStringValue(trackTpl.Parameters["2"], opts); 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 lyrics, ok := trackTpl.Parameters["lyrics"]; ok {
if stringVal = getStringValue(lyrics, opts); ok && len(stringVal) > 0 {
track.Lyrics = wikitext_parser.NormalizeWikiTitle(strings.TrimSpace(strings.TrimPrefix(stringVal[0], "Lyrics:")))
}
}
if len(listVal.Entries) > 1 {
if extraListData, ok := listVal.Entries[1].(*wikitext_parser.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(descVal, opts), " "), ":")
if len(keyValue) > 0 {
if i == 0 && len(keyValue[0]) > 2 && keyValue[0][0:2] == "''" {
track.Titles["english"] = 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 arrangement":
track.Artists = append(track.Artists, getArtistEntries("original arranger", values, opts)...)
case "original title":
track.Original = append(track.Original, strings.TrimSpace(strings.Join(getStringValue(values, opts), " ")))
case "original album":
var clean []string
for _, albumValue := range getStringValue(values, opts) {
albumValue = strings.Trim(albumValue, " \t'\"")
if len(albumValue) > 0 {
clean = append(clean, albumValue)
}
}
track.Original = append(track.Original, "Album: "+strings.TrimSpace(strings.Join(clean, " ")))
case "lyrics", "vocals", "chorus", "arranger", "composer", "producer", "remix":
track.Artists = append(track.Artists, getArtistEntries(keyEntry, values, opts)...)
case "arrangement":
track.Artists = append(track.Artists, getArtistEntries("arranger", values, opts)...)
case "composition":
track.Artists = append(track.Artists, getArtistEntries("composer", values, opts)...)
case "promotional video":
for _, value := range values {
if linkVal, ok := value.(*wikitext_parser.Link); ok && linkVal.IsExternal {
track.Links = append(track.Links, linkVal.URL)
}
}
}
}
}
}
}
}
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.CatalogNumbers) > 0 {
for _, catno := range entry.CatalogNumbers {
normalized := normalizeSearchTitle(catno)
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,
Titles: make(map[string]string),
})
}
}
}
}
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()
}
type Lyrics struct {
MainTitle string `json:"pagetitle"`
Titles []string `json:"titles"`
Links []string `json:"links,omitempty"`
Duration int `json:"duration,omitempty"`
Artists []artistEntry `json:"artists"`
Entries struct {
Kanji []string `json:"kanji,omitempty"`
Romaji []string `json:"romaji,omitempty"`
English []string `json:"english,omitempty"`
} `json:"entries"`
}
func parseLyrics(filePath, pageName string) (lyrics *Lyrics) {
if strings.Index(pageName, "/") != -1 {
return
}
var err error
var contents []byte
if contents, err = ioutil.ReadFile(path.Join(filePath, "pages_by_name", "Lyrics:_"+pageName+".wiki")); err != nil {
if contents, err = ioutil.ReadFile(path.Join(filePath, "pages_by_name", "Lyrics:"+pageName+".wiki")); err != nil {
if contents, err = ioutil.ReadFile(path.Join(filePath, "pages", pageName+".wiki")); err != nil {
return
}
}
}
result := wikitext_parser.ParseWikiText(string(contents))
if len(result) == 0 {
return
}
tpl, ok := result[0].(*wikitext_parser.Template)
if !ok {
return
}
if tpl.Name != "Lyrics" {
return
}
lyrics = &Lyrics{
MainTitle: strings.ReplaceAll(pageName, "_", " "),
}
opts := getWikiStringOptions(lyrics.MainTitle, true)
//var discLengths []int
//var discTrackNumbers []int
var val []interface{}
var stringVal []string
if val, ok = tpl.Parameters["titleen"]; ok {
if stringVal = getStringValue(val, opts); len(stringVal) > 0 {
lyrics.Titles = append(lyrics.Titles, stringVal[0])
}
}
if val, ok = tpl.Parameters["titlejp"]; ok {
if stringVal = getStringValue(val, opts); len(stringVal) > 0 {
lyrics.Titles = append(lyrics.Titles, stringVal[0])
}
}
if val, ok = tpl.Parameters["titlerom"]; ok {
if stringVal = getStringValue(val, opts); len(stringVal) > 0 {
lyrics.Titles = append(lyrics.Titles, stringVal[0])
}
}
if val, ok = tpl.Parameters["group"]; ok {
lyrics.Artists = append(lyrics.Artists, getArtistEntries("group", val, opts)...)
}
if val, ok = tpl.Parameters["arranger"]; ok {
lyrics.Artists = append(lyrics.Artists, getArtistEntries("arranger", val, opts)...)
}
if val, ok = tpl.Parameters["lyricist"]; ok {
lyrics.Artists = append(lyrics.Artists, getArtistEntries("lyrics", val, opts)...)
}
if val, ok = tpl.Parameters["vocalist"]; ok {
lyrics.Artists = append(lyrics.Artists, getArtistEntries("vocals", val, opts)...)
}
if val, ok = tpl.Parameters["length"]; ok {
if stringVal = getStringValue(val, opts); len(stringVal) > 0 {
split := strings.Split(stringVal[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]
}
lyrics.Duration = duration
}
}
noTrimOpts := getWikiStringOptions(lyrics.MainTitle, false)
var kan []interface{}
var okKan bool
var rom []interface{}
var okRom bool
var eng []interface{}
var okEng bool
for i := 1; ; i++ {
if kan, okKan = tpl.Parameters[fmt.Sprintf("kan%d", i)]; okKan {
lyrics.Entries.Kanji = append(lyrics.Entries.Kanji, normalizeStringCharacters(strings.Join(getStringValue(kan, noTrimOpts), "")))
}
if rom, okRom = tpl.Parameters[fmt.Sprintf("rom%d", i)]; okRom {
lyrics.Entries.Romaji = append(lyrics.Entries.Romaji, normalizeStringCharacters(strings.Join(getStringValue(rom, noTrimOpts), "")))
}
if eng, okEng = tpl.Parameters[fmt.Sprintf("eng%d", i)]; okEng {
lyrics.Entries.English = append(lyrics.Entries.English, normalizeStringCharacters(strings.Join(getStringValue(eng, noTrimOpts), "")))
}
if !okKan && !okRom && !okEng {
break
}
}
return
}
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.Pc)),
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 = strings.TrimSpace(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.CatalogNumbers) > result.discIndex {
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.CatalogNumbers[result.discIndex], 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.CatalogNumbers) > discIndex {
writer.Write([]byte(fmt.Sprintf("DTITLE=%s / [%s] %s\n", group, entry.CatalogNumbers[discIndex], 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, "exact" after normalization
entries, ok := albumTitleLookup[normalizeSearchTitle(request.URL.Query().Get("query"))]
if !ok {
writer.Write([]byte("[]"))
} else {
jsonBytes, _ := json.Marshal(entries)
writer.Write(jsonBytes)
}
case "loosealbum": //search by title or catalog number, loosely
var entries []*albumEntry
normalized := normalizeSearchTitle(request.URL.Query().Get("query"))
for k, v := range albumTitleLookup {
if strings.Index(k, normalized) != -1 {
entries = append(entries, v...)
}
}
sort.SliceStable(entries, func(i, j int) bool {
return entries[i].Id < entries[j].Id
})
jsonBytes, _ := json.Marshal(entries)
writer.Write(jsonBytes)
default:
writer.WriteHeader(http.StatusNotFound)
writer.Write([]byte("[]"))
}
} else if strings.Index(request.URL.Path, "/lyrics/") == 0 {
writer.Header().Set("Content-Type", "application/json")
lyrics := parseLyrics(*servePath, wikitext_parser.NormalizeWikiTitle(strings.TrimPrefix(request.URL.Path, "/lyrics/")))
if lyrics == nil {
writer.WriteHeader(http.StatusNotFound)
writer.Write([]byte("{}"))
return
}
jsonBytes, _ := json.Marshal(lyrics)
writer.Write(jsonBytes)
} else if strings.Index(request.URL.Path, "/album/") == 0 {
writer.Header().Set("Content-Type", "application/json")
if pageid, err := strconv.Atoi(strings.TrimPrefix(request.URL.Path, "/album/")); err == nil {
if album, ok := cdIndex[pageid]; ok {
jsonBytes, _ := json.Marshal(album)
writer.Write(jsonBytes)
return
}
}
writer.WriteHeader(http.StatusNotFound)
writer.Write([]byte("{}"))
return
} 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 {
if path.Ext(filePath) == ".wiki" {
writer.Header().Set("Content-Type", "text/plain")
} else if path.Ext(filePath) == ".md" {
writer.Header().Set("Content-Type", "text/markdown")
}
http.ServeFile(writer, request, filePath)
}
}
}),
}
err := server.ListenAndServe()
if err != nil {
log.Panic(err)
}
}