phelpsify/main.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
}
}
}