You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
431 lines
11 KiB
431 lines
11 KiB
// 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 |
|
} |
|
} |
|
}
|
|
|