432 lines
11 KiB
Go
432 lines
11 KiB
Go
// Phelpsify is a tool to help assign prayer codes to prayers.
|
|
// It requests prayers from bahaiprayers.net via the API url.
|
|
// It reads already assigned prayer codes from rel/code.list.
|
|
// It reads the conversion from number to language code from rel/lang.csv.
|
|
// It writes the new prayer codes to rel/code.list.
|
|
// rel/code.list is structured as prayer code, comma, prayer ids from bahaiprayers.net all separated by commas per line.
|
|
// rel/lang.csv is a csv file with header id,iso,iso_type,name,english,flag_link,rtl.
|
|
// The tool is a command line tool that first asks which languages you want to complete.
|
|
// It then presents you a random prayer from those languages that doesn't have
|
|
// a prayer code yet. It will then help you find the prayer among the prayers that already have a prayer code using keyword based search.
|
|
// When a match is found, the id of the prayer will be added to the list after the prayer code.
|
|
// The tool then asks you if you want to add another prayer and repeat the process.
|
|
package main
|
|
|
|
import (
|
|
"encoding/csv"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math/rand"
|
|
"net/http"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.kiefte.eu/lapingvino/prompt"
|
|
)
|
|
|
|
//BPNAPI is the API link of bahaiprayers.net
|
|
//It is used to get the list of prayers per language by numerical id from lang.csv
|
|
var BPNAPI = "https://bahaiprayers.net/api/prayer/prayersystembylanguage?languageid="
|
|
|
|
type BPNKind string
|
|
|
|
type Language struct {
|
|
Id int
|
|
Iso string
|
|
IsoType string
|
|
Name string
|
|
English string
|
|
FlagLink string
|
|
Rtl bool
|
|
}
|
|
|
|
//BPNAPIOutput is the JSON structure of the API output
|
|
type BPNAPIOutput struct {
|
|
ErrorMessage string
|
|
IsInError bool
|
|
Version int
|
|
Prayers []Prayer
|
|
Tags []struct {
|
|
Id int
|
|
LanguageId int
|
|
Name string
|
|
Kind BPNKind
|
|
PrayerCount int
|
|
}
|
|
TagRelations []struct {
|
|
Id int
|
|
PrayerId int
|
|
PrayerTagId int
|
|
LanguageId int
|
|
}
|
|
Urls []interface{}
|
|
Languages []struct {
|
|
Id int
|
|
Name string
|
|
English string
|
|
IsLeftToRight bool
|
|
FlagLink string
|
|
}
|
|
}
|
|
|
|
type Prayer struct {
|
|
Id int
|
|
AuthorId int
|
|
LanguageId int
|
|
Text string
|
|
Tags []struct {
|
|
Id int
|
|
Name string
|
|
Kind BPNKind
|
|
}
|
|
Tagkind struct {
|
|
Kind BPNKind
|
|
}
|
|
Urls []interface{}
|
|
}
|
|
|
|
func (p Prayer) Author() string {
|
|
if p.AuthorId > 0 && p.AuthorId < 4 {
|
|
return []string{"Báb", "Bahá'u'lláh", "Abdu'l-Bahá"}[p.AuthorId-1]
|
|
}
|
|
return "Unknown Author"
|
|
}
|
|
|
|
type PrayerCode struct {
|
|
Code string
|
|
Language string
|
|
}
|
|
|
|
var Languages map[int]Language
|
|
var CodeList map[int]string
|
|
var PrayersWithCode map[PrayerCode]Prayer
|
|
|
|
// ReadLangCSV reads rel/lang.csv and puts it in Languages
|
|
// It does so by matching each CSV field to a struct field
|
|
func ReadLangCSV() error {
|
|
file, err := os.Open("rel/lang.csv")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
reader := csv.NewReader(file)
|
|
langCSV, err := reader.ReadAll()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
Languages = make(map[int]Language)
|
|
for _, lang := range langCSV[1:] {
|
|
var language Language
|
|
language.Id, _ = strconv.Atoi(lang[0])
|
|
language.Iso = lang[1]
|
|
language.IsoType = lang[2]
|
|
language.Name = lang[3]
|
|
language.English = lang[4]
|
|
language.FlagLink = lang[5]
|
|
language.Rtl, _ = strconv.ParseBool(lang[6])
|
|
Languages[language.Id] = language
|
|
}
|
|
fmt.Print("Available languages: ")
|
|
var langs []string
|
|
for _, lang := range Languages {
|
|
langs = append(langs, lang.English+" ("+lang.Iso+")")
|
|
}
|
|
sort.Strings(langs)
|
|
fmt.Println(strings.Join(langs, ", "))
|
|
return nil
|
|
}
|
|
|
|
func ReadCodeList(filename string) error {
|
|
file, err := os.Open(filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
CodeList = make(map[int]string)
|
|
reader := csv.NewReader(file)
|
|
reader.FieldsPerRecord = -1
|
|
for {
|
|
line, err := reader.Read()
|
|
if err != nil {
|
|
break
|
|
}
|
|
for _, prayerIDstr := range line[1:] {
|
|
prayerID, err := strconv.Atoi(prayerIDstr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
CodeList[prayerID] = line[0]
|
|
}
|
|
}
|
|
fmt.Println("Number of prayers done:", len(CodeList))
|
|
return nil
|
|
}
|
|
|
|
func WriteCodeList(codeList map[int]string) error {
|
|
file, err := os.Create("rel/code.list")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
writer := csv.NewWriter(file)
|
|
invertedCodeList := make(map[string][]int)
|
|
for prayerID, code := range codeList {
|
|
invertedCodeList[code] = append(invertedCodeList[code], prayerID)
|
|
}
|
|
|
|
var codes []string
|
|
for code := range invertedCodeList {
|
|
codes = append(codes, code)
|
|
}
|
|
|
|
sort.Strings(codes)
|
|
|
|
for _, code := range codes {
|
|
var line []string
|
|
for _, prayerID := range invertedCodeList[code] {
|
|
line = append(line, strconv.Itoa(prayerID))
|
|
}
|
|
sort.Strings(line)
|
|
line = append([]string{code}, line...)
|
|
err := writer.Write(line)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
writer.Flush()
|
|
return nil
|
|
}
|
|
|
|
func AskLanguages(pr string) []Language {
|
|
fmt.Print(pr)
|
|
var languages []string
|
|
for {
|
|
fmt.Print("Language name or code, leave blank to continue: ")
|
|
s := prompt.MustRead[string]()
|
|
if s == "" {
|
|
break
|
|
}
|
|
languages = append(languages, s)
|
|
}
|
|
var outLangs []Language
|
|
for _, language := range languages {
|
|
for _, lang := range Languages {
|
|
if lang.Iso == language || lang.English == language {
|
|
outLangs = append(outLangs, lang)
|
|
}
|
|
}
|
|
}
|
|
return outLangs
|
|
}
|
|
|
|
func ReadPrayers(lang []Language, codep bool) []Prayer {
|
|
var prayers []Prayer
|
|
for _, language := range lang {
|
|
response, err := http.Get(BPNAPI + strconv.Itoa(language.Id))
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
panic("Could not get prayers, abort")
|
|
}
|
|
defer response.Body.Close()
|
|
var output BPNAPIOutput
|
|
err = json.NewDecoder(response.Body).Decode(&output)
|
|
if err != nil {
|
|
fmt.Println("Issue when reading " + language.English + " prayers: " + err.Error())
|
|
continue
|
|
}
|
|
fmt.Print(language.Iso + "..")
|
|
for _, prayer := range output.Prayers {
|
|
if (CodeList[prayer.Id] != "") == codep {
|
|
prayers = append(prayers, prayer)
|
|
}
|
|
}
|
|
fmt.Println("done")
|
|
}
|
|
return prayers
|
|
}
|
|
|
|
// ByPrayerLength tries to return prayers closest in length to
|
|
// the reference prayer r (in characters)
|
|
func ByPrayerLength(p []Prayer, r Prayer) []Prayer {
|
|
out := NewPrayerLengthSort(p, r)
|
|
sort.Sort(out)
|
|
return out.Prayers()
|
|
}
|
|
|
|
type PrayerLengthSort []struct {
|
|
P Prayer
|
|
Diff int
|
|
}
|
|
|
|
func (p PrayerLengthSort) Len() int {
|
|
return len(p)
|
|
}
|
|
|
|
func (p PrayerLengthSort) Less(i, j int) bool {
|
|
return p[i].Diff < p[j].Diff
|
|
}
|
|
|
|
func (p PrayerLengthSort) Swap(i, j int) {
|
|
p[i], p[j] = p[j], p[i]
|
|
}
|
|
|
|
func (p PrayerLengthSort) Sort() {
|
|
sort.Sort(p)
|
|
}
|
|
|
|
func (p PrayerLengthSort) Prayers() []Prayer {
|
|
var out []Prayer
|
|
for _, prayer := range p {
|
|
out = append(out, prayer.P)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func NewPrayerLengthSort(p []Prayer, r Prayer) PrayerLengthSort {
|
|
var out PrayerLengthSort
|
|
for _, prayer := range p {
|
|
diff := len(prayer.Text) - len(r.Text)
|
|
if diff < 0 {
|
|
diff = -diff
|
|
}
|
|
// Add a penalty if the author is not the same
|
|
if prayer.AuthorId != r.AuthorId {
|
|
diff += 100
|
|
}
|
|
out = append(out, struct {
|
|
P Prayer
|
|
Diff int
|
|
}{prayer, diff})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func main() {
|
|
err := ReadLangCSV()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
err = ReadCodeList("rel/code.list")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
fmt.Print("Do you want to merge in someone else's work? (y/n): ")
|
|
s := prompt.MustRead[string]()
|
|
if s == "y" {
|
|
fmt.Print("Enter the name of the file: ")
|
|
s = prompt.MustRead[string]()
|
|
err = ReadCodeList(s)
|
|
if err != nil {
|
|
fmt.Println("Could not read " + s + ": " + err.Error())
|
|
}
|
|
}
|
|
// Ask which languages to use as a reference and read in all prayers
|
|
// with a language code to PrayersWithCode
|
|
refLanguages := AskLanguages("Which languages do you want to reference to? ")
|
|
prayers := ReadPrayers(refLanguages, true)
|
|
PrayersWithCode = make(map[PrayerCode]Prayer)
|
|
for _, prayer := range prayers {
|
|
code := CodeList[prayer.Id]
|
|
if code != "" {
|
|
PrayersWithCode[PrayerCode{code, Languages[prayer.LanguageId].Iso}] = prayer
|
|
}
|
|
}
|
|
fmt.Println("Number of prayers with code loaded: " + strconv.Itoa(len(PrayersWithCode)))
|
|
// Ask which language to complete
|
|
languages := AskLanguages("Which languages do you want to complete? ")
|
|
prayers = ReadPrayers(languages, false)
|
|
// randomize the order of the prayers
|
|
for i := len(prayers) - 1; i > 0; i-- {
|
|
j := rand.Intn(i + 1)
|
|
prayers[i], prayers[j] = prayers[j], prayers[i]
|
|
}
|
|
for i, prayer := range prayers {
|
|
var code string
|
|
for code == "" {
|
|
// Clear the screen
|
|
fmt.Print("\033[H\033[2J")
|
|
// Present the text, id and author of the prayer
|
|
fmt.Println("Prayer " + strconv.Itoa(i+1) + " of " + strconv.Itoa(len(prayers)))
|
|
fmt.Println(prayer.Text)
|
|
fmt.Println("ID:", prayer.Id)
|
|
fmt.Println("Author:", prayer.Author())
|
|
// Ask for a keyword
|
|
fmt.Print("Input a keyword for this prayer, skip to pick another one or code to enter the code manually, or quit to just save and quit: ")
|
|
keyword := prompt.MustRead[string]()
|
|
if keyword == "skip" {
|
|
break
|
|
}
|
|
if keyword == "code" {
|
|
fmt.Print("Enter the code: ")
|
|
code = prompt.MustRead[string]()
|
|
break
|
|
}
|
|
if keyword == "quit" {
|
|
fmt.Println("Saving and quitting")
|
|
err := WriteCodeList(CodeList)
|
|
if err != nil {
|
|
fmt.Println("Could not write code list: " + err.Error())
|
|
continue
|
|
}
|
|
return
|
|
}
|
|
var Matches []Prayer
|
|
// Check for the prayer text of each prayer in
|
|
// PrayersWithCode if there is a match with the keyword
|
|
// and add it to Matches
|
|
for _, pr := range PrayersWithCode {
|
|
if strings.Contains(pr.Text, keyword) {
|
|
Matches = append(Matches, pr)
|
|
}
|
|
}
|
|
// If there are no matches, ask again
|
|
if len(Matches) == 0 {
|
|
fmt.Println("No matches found.")
|
|
continue
|
|
}
|
|
// Ask which of the matches to use
|
|
fmt.Println("Found " + strconv.Itoa(len(Matches)) + " matches.")
|
|
fmt.Println("Which of the following matches?")
|
|
sortedMatches := NewPrayerLengthSort(Matches, prayer)
|
|
sortedMatches.Sort()
|
|
for i, match := range sortedMatches.Prayers() {
|
|
fmt.Println(i+1, ":", match.Text)
|
|
fmt.Print("Does this match? (y/n/skip) ")
|
|
answer := prompt.MustRead[string]()
|
|
if answer == "y" {
|
|
fmt.Println(CodeList[match.Id])
|
|
code = CodeList[match.Id]
|
|
break
|
|
}
|
|
if answer == "skip" {
|
|
break
|
|
}
|
|
// If the answer is not y or skip, clear the screen
|
|
fmt.Print("\033[H\033[2J")
|
|
// Present the text, id and author of the prayer again
|
|
fmt.Println(prayer.Text)
|
|
fmt.Println("ID:", prayer.Id)
|
|
fmt.Println("Author:", prayer.Author())
|
|
}
|
|
}
|
|
if code != "" {
|
|
// If the code is not empty, add it to the code list
|
|
// and write the code list to CodeList.csv
|
|
CodeList[prayer.Id] = code
|
|
err = WriteCodeList(CodeList)
|
|
if err != nil {
|
|
fmt.Println("Could not write code list: " + err.Error())
|
|
}
|
|
}
|
|
// Ask if the user wants to identify another prayer
|
|
// or if they want to quit
|
|
fmt.Print("Identify another prayer? (y/n) ")
|
|
answer := prompt.MustRead[string]()
|
|
if answer == "n" {
|
|
break
|
|
}
|
|
}
|
|
}
|