touhouwiki-mirror/server.go

1164 lines
36 KiB
Go
Raw Permalink Normal View History

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"
2022-02-20 00:15:21 +00:00
"math"
"net/http"
"os"
"path"
2022-02-19 19:48:02 +00:00
"sort"
"strconv"
"strings"
"sync"
"time"
"unicode"
)
2022-02-19 19:48:02 +00:00
var cdIndex = make(map[int]*albumEntry)
var cdIndexLock sync.Mutex
2022-02-19 19:48:02 +00:00
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
}
2022-02-19 19:48:02 +00:00
type JSONTime struct {
time.Time
}
2022-02-19 19:48:02 +00:00
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 {
2022-03-10 18:10:13 +00:00
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"`
2022-02-19 19:48:02 +00:00
}
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 {
2022-03-10 18:10:13 +00:00
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"`
2022-02-19 19:48:02 +00:00
}
type artistEntry struct {
Position string `json:"position"`
Names []string `json:"names"`
}
2022-02-20 21:12:48 +00:00
func getArtistEntries(kind string, entries []interface{}, opts *wikitext_parser.WikiStringValueOptions) (artists []artistEntry) {
2022-02-19 19:48:02 +00:00
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)
}
2022-02-19 19:48:02 +00:00
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)
}
2022-02-20 14:00:21 +00:00
} 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)
}
2022-02-20 14:00:21 +00:00
} 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)
}
}
2022-02-19 19:48:02 +00:00
}
} else if _, ok := value.(wikitext_parser.NewLineToken); ok {
2022-02-19 19:48:02 +00:00
recreateArtist(kind)
} else if tpl, ok := value.(*wikitext_parser.Template); ok {
2022-02-19 19:48:02 +00:00
if tpl.IsLink {
var result []string
for _, vv := range tpl.Parameters {
result = append(result, getStringValue(vv, opts)...)
2022-02-19 19:48:02 +00:00
}
if len(result) == 0 {
result = append(result, tpl.Name)
}
2022-02-19 19:48:02 +00:00
artist.Names = append(artist.Names, strings.Join(result, " "))
} else {
artist.Names = append(artist.Names, strings.Join(getStringValue([]interface{}{tpl}, opts), " "))
2022-02-19 19:48:02 +00:00
}
} else if link, ok := value.(*wikitext_parser.Link); ok {
2022-02-19 19:48:02 +00:00
if len(link.Name) > 0 {
artist.Names = append(artist.Names, strings.Join(getStringValue(link.Name, opts), " "))
2022-02-19 19:48:02 +00:00
} else if !link.IsExternal {
artist.Names = append(artist.Names, link.URL)
}
} else if unorderedList, ok := value.(*wikitext_parser.UnorderedList); ok {
2022-02-19 19:48:02 +00:00
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 {
2022-02-19 19:48:02 +00:00
if tpl.IsLink {
artist.Names = append(artist.Names, tpl.Name)
for _, val := range tpl.Parameters {
artist.Names = append(artist.Names, getStringValue(val, opts)...)
2022-02-19 19:48:02 +00:00
}
} else {
artist.Names = append(artist.Names, getStringValue([]interface{}{tpl}, opts)...)
2022-02-19 19:48:02 +00:00
}
}
}
recreateArtist(kind)
} else if descriptionList, ok := value.(*wikitext_parser.DescriptionList); ok {
2022-02-19 19:48:02 +00:00
for _, val := range descriptionList.Entries {
recreateArtist(kind, strings.Join(getStringValue(descriptionList.Name, opts), ", "))
2022-02-19 19:48:02 +00:00
if text, ok := val.(string); ok {
artist.Names = append(artist.Names, text)
} else if tpl, ok := val.(*wikitext_parser.Template); ok {
2022-02-19 19:48:02 +00:00
if tpl.IsLink {
artist.Names = append(artist.Names, tpl.Name)
for _, val := range tpl.Parameters {
artist.Names = append(artist.Names, getStringValue(val, opts)...)
2022-02-19 19:48:02 +00:00
}
} else {
artist.Names = append(artist.Names, getStringValue([]interface{}{tpl}, opts)...)
2022-02-19 19:48:02 +00:00
}
}
recreateArtist(kind)
}
}
}
recreateArtist(kind)
return
}
2022-02-20 21:12:48 +00:00
func getWikiStringOptions(title string, trim bool) *wikitext_parser.WikiStringValueOptions {
2022-02-19 19:48:02 +00:00
2022-02-20 21:12:48 +00:00
opts := &wikitext_parser.WikiStringValueOptions{}
opts.Default()
opts.PageName = title
2022-02-20 21:12:48 +00:00
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])
2022-02-19 19:48:02 +00:00
}
case "LANG":
if val, ok := template.Parameters["1"]; ok && len(val) > 0 {
result = append(result, wikitext_parser.GetWikiStringValue(val, opt)[0])
2022-02-19 19:48:02 +00:00
}
case "GENRE":
if val, ok := template.Parameters["0"]; ok && len(val) > 0 {
result = append(result, wikitext_parser.GetWikiStringValue(val, opt)[0])
2022-02-20 15:20:11 +00:00
}
case "MUSICARTICLE/DIVIDER":
case "PAGENAME", "SUBPAGENAME":
result = append(result, opt.PageName)
default:
result = append(result, template.Name)
2022-02-20 15:20:11 +00:00
}
return
2022-02-20 15:20:11 +00:00
}
opts.Trim = trim
return opts
}
2022-02-20 15:20:11 +00:00
2022-02-20 21:12:48 +00:00
func getStringValue(v []interface{}, opt *wikitext_parser.WikiStringValueOptions) []string {
return wikitext_parser.GetWikiStringValue(v, opt)
2022-02-19 19:48:02 +00:00
}
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" {
2022-02-19 19:48:02 +00:00
for _, v := range parseCategoryPageIndex(path.Join(indexPath, e.Name())) {
wg.Add(1)
2022-02-20 19:57:30 +00:00
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)
2022-02-20 00:33:37 +00:00
//var discLengths []int
//var discTrackNumbers []int
2022-02-19 19:48:02 +00:00
var val []interface{}
var stringVal []string
if val, ok = tpl.Parameters["titleen"]; ok {
if stringVal = getStringValue(val, opts); len(stringVal) > 0 {
2022-03-10 18:10:13 +00:00
entry.Titles["english"] = stringVal[0]
}
}
2022-02-19 19:48:02 +00:00
if val, ok = tpl.Parameters["titlejp"]; ok {
if stringVal = getStringValue(val, opts); len(stringVal) > 0 {
2022-03-10 18:10:13 +00:00
entry.Titles["japanese"] = stringVal[0]
}
}
2022-02-19 19:48:02 +00:00
if val, ok = tpl.Parameters["titlejprom"]; ok {
if stringVal = getStringValue(val, opts); len(stringVal) > 0 {
2022-03-10 18:10:13 +00:00
entry.Titles["romaji"] = stringVal[0]
}
}
2022-02-19 19:48:02 +00:00
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)
}
}
}
}
}
2022-02-19 19:48:02 +00:00
2022-02-20 00:15:34 +00:00
if val, ok = tpl.Parameters["genre"]; ok {
if stringVal = getStringValue(val, opts); len(stringVal) > 0 {
2022-02-20 00:15:34 +00:00
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), ""))
}
2022-02-19 19:48:02 +00:00
if val, ok = tpl.Parameters["group"]; ok {
entry.Artists = append(entry.Artists, getArtistEntries("group", val, opts)...)
2022-02-19 19:48:02 +00:00
}
if val, ok = tpl.Parameters["masterer"]; ok {
entry.Artists = append(entry.Artists, getArtistEntries("mastering", val, opts)...)
2022-02-19 19:48:02 +00:00
}
if val, ok = tpl.Parameters["illustrator"]; ok {
entry.Artists = append(entry.Artists, getArtistEntries("illustration", val, opts)...)
2022-02-19 19:48:02 +00:00
}
if val, ok = tpl.Parameters["arranger"]; ok {
entry.Artists = append(entry.Artists, getArtistEntries("arranger", val, opts)...)
2022-02-19 19:48:02 +00:00
}
if val, ok = tpl.Parameters["lyricist"]; ok {
entry.Artists = append(entry.Artists, getArtistEntries("lyrics", val, opts)...)
2022-02-19 19:48:02 +00:00
}
if val, ok = tpl.Parameters["vocalist"]; ok {
entry.Artists = append(entry.Artists, getArtistEntries("vocals", val, opts)...)
2022-02-19 19:48:02 +00:00
}
if val, ok = tpl.Parameters["producer"]; ok {
entry.Artists = append(entry.Artists, getArtistEntries("producer", val, opts)...)
2022-02-19 19:48:02 +00:00
}
if val, ok = tpl.Parameters["designer"]; ok {
entry.Artists = append(entry.Artists, getArtistEntries("design", val, opts)...)
2022-02-19 19:48:02 +00:00
}
if val, ok = tpl.Parameters["other"]; ok {
entry.Artists = append(entry.Artists, getArtistEntries("other", val, opts)...)
2022-02-19 19:48:02 +00:00
}
if val, ok = tpl.Parameters["convention"]; ok && len(val) > 0 {
value := getStringValue(val, opts)
if len(value) > 0 {
entry.ReleaseEvent = value[0]
}
}
2022-02-19 19:48:02 +00:00
if val, ok = tpl.Parameters["released"]; ok {
if stringVal = getStringValue(val, opts); len(stringVal) > 0 {
2022-02-19 19:48:02 +00:00
if releaseDate, err := time.ParseInLocation("2006-01-02", stringVal[0], time.UTC); err == nil {
entry.ReleaseDate = JSONTime{Time: releaseDate}
}
}
}
2022-02-19 19:48:02 +00:00
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)
}
}
}
2022-02-19 19:48:02 +00:00
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
2022-02-20 00:33:37 +00:00
entry.TrackCount += disc.TrackCount
2022-02-19 19:48:02 +00:00
entry.Discs = append(entry.Discs, disc)
}
2022-02-19 19:48:02 +00:00
}
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 {
2022-02-19 19:48:02 +00:00
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 {
2022-03-10 18:10:13 +00:00
track := trackEntry{
Titles: make(map[string]string),
}
2022-02-19 19:48:02 +00:00
if mainTitleValue := getStringValue(trackTpl.Parameters["1"], opts); len(mainTitleValue) > 0 {
track.MainTitle = strings.Join(mainTitleValue, "")
2022-03-10 18:10:13 +00:00
track.Titles["original"] = track.MainTitle
2022-02-19 19:48:02 +00:00
}
if durations := getStringValue(trackTpl.Parameters["2"], opts); len(durations) > 0 {
2022-02-19 19:48:02 +00:00
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:")))
2022-02-20 15:20:11 +00:00
}
}
2022-02-19 19:48:02 +00:00
if len(listVal.Entries) > 1 {
if extraListData, ok := listVal.Entries[1].(*wikitext_parser.UnorderedList); ok && len(extraListData.Entries) > 0 {
2022-02-19 19:48:02 +00:00
for i, entryValue := range extraListData.Entries {
if descVal, ok := entryValue.([]interface{}); ok && len(descVal) > 0 {
keyValue := strings.Split(strings.Join(getStringValue(descVal, opts), " "), ":")
2022-02-19 19:48:02 +00:00
if len(keyValue) > 0 {
if i == 0 && len(keyValue[0]) > 2 && keyValue[0][0:2] == "''" {
2022-03-10 18:10:13 +00:00
track.Titles["english"] = strings.Trim(strings.Join(keyValue, " "), "'' ")
2022-02-19 19:48:02 +00:00
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)...)
2022-02-19 19:48:02 +00:00
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)...)
2022-02-20 00:15:21 +00:00
case "arrangement":
track.Artists = append(track.Artists, getArtistEntries("arranger", values, opts)...)
2022-02-19 19:48:02 +00:00
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)
}
}
2022-02-19 19:48:02 +00:00
}
}
}
}
}
}
disc.Duration += track.Duration
disc.Tracks = append(disc.Tracks, track)
}
}
}
}
2022-02-19 19:48:02 +00:00
createDisc()
}
2022-02-19 19:48:02 +00:00
cdIndexLock.Lock()
cdIndex[entry.Id] = entry
for _, d := range entry.Discs {
if _, ok := discTracksLookup[d.TrackCount]; !ok {
discTracksLookup[d.TrackCount] = []*albumEntry{entry}
} else {
2022-02-19 19:48:02 +00:00
for _, val := range discTracksLookup[d.TrackCount] {
if val.Id == entry.Id {
goto exit2
}
}
discTracksLookup[d.TrackCount] = append(discTracksLookup[d.TrackCount], entry)
exit2:
}
}
2022-02-19 19:48:02 +00:00
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)
2022-02-19 19:48:02 +00:00
if _, ok := albumTitleLookup[normalized]; !ok {
albumTitleLookup[normalized] = []*albumEntry{entry}
} else {
2022-02-19 19:48:02 +00:00
for _, val := range albumTitleLookup[normalized] {
if val.Id == entry.Id {
goto exit
}
}
2022-02-19 19:48:02 +00:00
albumTitleLookup[normalized] = append(albumTitleLookup[normalized], entry)
exit:
}
}
2022-02-19 19:48:02 +00:00
defer cdIndexLock.Unlock()
}(&albumEntry{
Id: v.PageId,
MainTitle: v.Title,
2022-02-19 19:48:02 +00:00
Type: kind,
2022-03-10 18:10:13 +00:00
Titles: make(map[string]string),
})
}
}
}
2022-02-19 19:48:02 +00:00
}
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()
}
2022-02-20 15:20:11 +00:00
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 {
2022-02-21 09:49:17 +00:00
if contents, err = ioutil.ReadFile(path.Join(filePath, "pages", pageName+".wiki")); err != nil {
return
}
2022-02-20 15:20:11 +00:00
}
}
result := wikitext_parser.ParseWikiText(string(contents))
2022-02-20 15:20:11 +00:00
if len(result) == 0 {
return
}
tpl, ok := result[0].(*wikitext_parser.Template)
2022-02-20 15:20:11 +00:00
if !ok {
return
}
if tpl.Name != "Lyrics" {
return
}
lyrics = &Lyrics{
MainTitle: strings.ReplaceAll(pageName, "_", " "),
}
opts := getWikiStringOptions(lyrics.MainTitle, true)
2022-02-20 15:20:11 +00:00
//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 {
2022-02-20 15:20:11 +00:00
lyrics.Titles = append(lyrics.Titles, stringVal[0])
}
}
if val, ok = tpl.Parameters["titlejp"]; ok {
if stringVal = getStringValue(val, opts); len(stringVal) > 0 {
2022-02-20 15:20:11 +00:00
lyrics.Titles = append(lyrics.Titles, stringVal[0])
}
}
if val, ok = tpl.Parameters["titlerom"]; ok {
if stringVal = getStringValue(val, opts); len(stringVal) > 0 {
2022-02-20 15:20:11 +00:00
lyrics.Titles = append(lyrics.Titles, stringVal[0])
}
}
if val, ok = tpl.Parameters["group"]; ok {
lyrics.Artists = append(lyrics.Artists, getArtistEntries("group", val, opts)...)
2022-02-20 15:20:11 +00:00
}
if val, ok = tpl.Parameters["arranger"]; ok {
lyrics.Artists = append(lyrics.Artists, getArtistEntries("arranger", val, opts)...)
2022-02-20 15:20:11 +00:00
}
if val, ok = tpl.Parameters["lyricist"]; ok {
lyrics.Artists = append(lyrics.Artists, getArtistEntries("lyrics", val, opts)...)
2022-02-20 15:20:11 +00:00
}
if val, ok = tpl.Parameters["vocalist"]; ok {
lyrics.Artists = append(lyrics.Artists, getArtistEntries("vocals", val, opts)...)
2022-02-20 15:20:11 +00:00
}
if val, ok = tpl.Parameters["length"]; ok {
if stringVal = getStringValue(val, opts); len(stringVal) > 0 {
2022-02-20 15:20:11 +00:00
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)
2022-02-20 15:20:11 +00:00
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), "")))
2022-02-20 15:20:11 +00:00
}
if rom, okRom = tpl.Parameters[fmt.Sprintf("rom%d", i)]; okRom {
lyrics.Entries.Romaji = append(lyrics.Entries.Romaji, normalizeStringCharacters(strings.Join(getStringValue(rom, noTrimOpts), "")))
2022-02-20 15:20:11 +00:00
}
if eng, okEng = tpl.Parameters[fmt.Sprintf("eng%d", i)]; okEng {
lyrics.Entries.English = append(lyrics.Entries.English, normalizeStringCharacters(strings.Join(getStringValue(eng, noTrimOpts), "")))
2022-02-20 15:20:11 +00:00
}
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
}
2022-02-19 19:48:02 +00:00
type findByDurationResult struct {
albumEntry *albumEntry
2022-02-20 00:15:21 +00:00
cddb1 utilities.CDDB1
toc utilities.TOC
2022-02-19 19:48:02 +00:00
discIndex int
}
2022-02-20 00:15:21 +00:00
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)
}
2022-02-19 19:48:02 +00:00
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
}
2022-02-19 19:48:02 +00:00
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] {
2022-02-19 20:00:47 +00:00
case "hello":
writer.Write([]byte("200 hello and welcome\n.\n"))
return
case "query":
2022-02-19 19:48:02 +00:00
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)
2022-02-19 19:48:02 +00:00
} 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])
2022-02-19 20:00:47 +00:00
2022-02-20 00:15:21 +00:00
toc := utilities.NewTOCFromCDDBString(strings.Join(splits[3:], " "))
if cddb1 == 0 {
writer.Write([]byte("500 Command syntax error\n.\n"))
return
}
2022-02-20 00:15:21 +00:00
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 {
2022-02-19 19:48:02 +00:00
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 {
2022-02-19 19:48:02 +00:00
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
}
2022-02-19 19:48:02 +00:00
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
}
2022-02-19 19:48:02 +00:00
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
}
2022-02-19 19:48:02 +00:00
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"))
2022-02-19 19:48:02 +00:00
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())))
2022-02-19 19:48:02 +00:00
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 {
2022-02-19 19:48:02 +00:00
writer.Write([]byte(fmt.Sprintf("DTITLE=%s / %s\n", group, entry.MainTitle)))
}
2022-02-19 19:48:02 +00:00
if !entry.ReleaseDate.IsZero() {
writer.Write([]byte(fmt.Sprintf("DYEAR=%d\n", entry.ReleaseDate.Year())))
}
2022-02-19 19:48:02 +00:00
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, ", "))))
2022-02-19 19:48:02 +00:00
for i, t := range entry.Discs[discIndex].Tracks {
writer.Write([]byte(fmt.Sprintf("TTITLE%d=%s\n", i, t.MainTitle)))
}
2022-02-19 19:48:02 +00:00
writer.Write([]byte(fmt.Sprintf("EXTD=https://en.touhouwiki.net/index.php?curid=%d\n", entry.Id)))
2022-02-19 19:48:02 +00:00
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"))))
}
2022-02-19 19:48:02 +00:00
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") {
2022-02-20 03:06:25 +00:00
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)
}
2022-02-20 03:06:25 +00:00
case "loosealbum": //search by title or catalog number, loosely
2022-02-20 03:06:25 +00:00
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
})
2022-02-20 03:06:25 +00:00
jsonBytes, _ := json.Marshal(entries)
writer.Write(jsonBytes)
default:
writer.WriteHeader(http.StatusNotFound)
writer.Write([]byte("[]"))
}
2022-02-20 15:20:11 +00:00
} else if strings.Index(request.URL.Path, "/lyrics/") == 0 {
writer.Header().Set("Content-Type", "application/json")
2022-02-21 09:49:17 +00:00
lyrics := parseLyrics(*servePath, wikitext_parser.NormalizeWikiTitle(strings.TrimPrefix(request.URL.Path, "/lyrics/")))
2022-02-20 15:20:11 +00:00
if lyrics == nil {
writer.WriteHeader(http.StatusNotFound)
writer.Write([]byte("{}"))
return
}
jsonBytes, _ := json.Marshal(lyrics)
writer.Write(jsonBytes)
2022-02-21 09:49:17 +00:00
} 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, "/"))
2022-02-19 03:05:49 +00:00
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)
}
}