// 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 } } }