quran-go

Read Qur'an right in the terminal.
git clone http://git.hanabi.in/repos/quran-go.git
Log | Files | Refs | README | LICENSE

commit 2558e37fc5be4904a963cea119bb6c836217c27b
parent 400428b2328c12b4953c348522ebc07a26fa68cd
Author: Agastya Chandrakant <me@hanabi.in>
Date:   Mon,  2 May 2022 03:40:49 +0530

Refactored everything, added README.  Use semver.

Diffstat:
DPLAN | 20--------------------
AREADME | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ago.mod | 3+++
Asrc/cliinput/parse-flags.go | 31+++++++++++++++++++++++++++++++
Asrc/display/decorate/chapter-head.go | 22++++++++++++++++++++++
Asrc/display/decorate/end.go | 25+++++++++++++++++++++++++
Asrc/display/translations.go | 26++++++++++++++++++++++++++
Asrc/display/verses.go | 44++++++++++++++++++++++++++++++++++++++++++++
Asrc/driver/default-behaviour.go | 37+++++++++++++++++++++++++++++++++++++
Asrc/driver/run.go | 37+++++++++++++++++++++++++++++++++++++
Asrc/fetch/aayat.go | 27+++++++++++++++++++++++++++
Asrc/fetch/all-translations.go | 28++++++++++++++++++++++++++++
Asrc/fetch/chapter-details.go | 18++++++++++++++++++
Asrc/fetch/last-chap.go | 24++++++++++++++++++++++++
Asrc/fetch/translation-details.go | 46++++++++++++++++++++++++++++++++++++++++++++++
Msrc/main.go | 359+------------------------------------------------------------------------------
Asrc/print/about.go | 5+++++
Asrc/print/details.go | 5+++++
Asrc/print/err.go | 11+++++++++++
Asrc/print/fns.go | 19+++++++++++++++++++
Asrc/print/help.go | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/print/version.go | 5+++++
Asrc/quran-com/consts.go | 7+++++++
Asrc/quran-com/http-get.go | 36++++++++++++++++++++++++++++++++++++
Asrc/types/type.go | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/utils/check-verse-range.go | 35+++++++++++++++++++++++++++++++++++
Asrc/utils/format-aayat.go | 12++++++++++++
Asrc/utils/is-valid-chapter-range.go | 20++++++++++++++++++++
Asrc/utils/parse-input.go | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
29 files changed, 806 insertions(+), 375 deletions(-)

diff --git a/PLAN b/PLAN @@ -1,20 +0,0 @@ -Available commands -================== - -+ quran -+ quran 2 -+ quran 2:10 -+ quran 2:10-11 -+ quran [-list-trans] -+ quran [-decorate] [-delay <num>] [-trans <num>] chap[:verse1[-verse2]] - -Convention -========== - -In the source code (even in the comments), "verse" is used in place of "aayat" since the JSON response of quran.com API says verse. So staying consistent with it. - -Features -======== - -+ Should I support 2:14- to print all verses from 14 till the end of the chapter? -- I have never seen anyone use it like that. And definitely not me -- we put end verse for a reason, for staying withing context, so not doing it as of now. -+ Add `quran about|details|list-translation|help' -- as sub-commands, xor flags? I love sub-commands, but haven't used it much (it has more code, perhaps -- let's find out!) -- Come on! -decorate is a flag, list-translation is *not* a flag, it is a sub-command, segregate accordingly! diff --git a/README b/README @@ -0,0 +1,90 @@ + __ _ _ _ _ __ __ _ _ __ + / _` | | | | '__/ _` | '_ \ +| (_| | |_| | | | (_| | | | | + \__, |\__,_|_| \__,_|_| |_| + |_| +============================= + + +quran fetches various quran translations right in the terminal. +This is a successor of now dysfunct quran.py, but is not bug-compatible. + +Build and run +============= +Assuming you have the golang compiler, run the following command: + +$ git clone https://git.hanabi.in/reops/quran-go.git +$ cd quran-go +$ make # or `go build -o quran src/main.go` +$ ./quran help + +I have moved the binary to /usr/local/bin so I can play directly -- depending on +your $PATH, consider moving the binary there. +Alternatively, run `make install`. + +Usage +===== + +quran [-delay[=<millisecs>]] [-decorate] [-trans[=<translation-id>]] <command> [<args>] + +Flags +----- + -decorate: + Specify if the chapters and verses should be decorated xor not. Defaults to false. + -delay <delay-ms>: + Specify in ms, the delay between fetching aayahs. I advice using at least + 500. Defaults to 1000. + -trans <translation-id> + Select which translation to use. Defaults to 131. + +Sub-commands +------------ ++ about: Shows description of the binary. ++ details: Shows details about where the verses are fetchef from. ++ version: Prints version of the software. ++ ls-translations: List all available translations. + +Arguments +--------- +Argument is of the type <chapter[:verse1[-verse2]]> + ++ `quran 1' will print the first chapter. ++ `quran 1:2' will print the chapter 1, verse 2. ++ `quran 1:3-5' will print verses of chapter 1 from 3 to 5. + +Source code +=========== +The source code uses git for version control. + +Get the source code by running: +$ git clone http://git.hanabi.in/repos/quran-go.git + +The source code has three branches important dev, prod and master. ++ dev: new features are added committed to dev. ++ prod: branch refers to the code ready to be used for production. ++ master -- git.hanabi.in makes use of stagit + <https://codemadness.org/stagit.html>. Stagit requires a master branch to + generate web-front-end, and hence, the branch exists. + +Additionally, the source code also has few tags. ++ One tag is called quran.py -- in fond memory of the initial script, which + holds special place in my memory. ++ Other tags being semantic versioning (semver) of the software release. + +I don't plan to overly complicate the software, please mind UNIX philosophy +before requesting features. + +Please email patches to <me+git@hanabi.in>. + +Authors +======= ++ acagastya (Implementation of the stuff -- which anyone can do.) + +`git blame` should suffice. + +License +======= + +This source code is licened under the GNU AGPLv3 license. Read the LICENSE file +to see what you can do with it, or read +<https://www.gnu.org/licenses/agpl-3.0.en.html> diff --git a/go.mod b/go.mod @@ -0,0 +1,3 @@ +module git.hanabi.in/gitbox/quran-go + +go 1.18 diff --git a/src/cliinput/parse-flags.go b/src/cliinput/parse-flags.go @@ -0,0 +1,31 @@ +package cliinput + +import ( + "flag" + + q "git.hanabi.in/gitbox/quran-go/src/quran-com" + t "git.hanabi.in/gitbox/quran-go/src/types" +) + +// Supply runtime input. +func ParseFlags() (chapterVerseRange string, translationID t.Trans, delay t.Delay, shouldDecorate bool) { + + var transInt int + var delayUint float64 + + flag.IntVar(&transInt, "trans", q.DefaultTrans, "Specify translation ID of which translation to show. Defaults to 131, corresponding to Dr. Mustafa Khattab, the Clear Quran.") + + flag.Float64Var(&delayUint, "delay", q.DefaultDelay, "Minimum delay in milliseconds between fetching aayahs. Defaults to 1000.") + + flag.BoolVar(&shouldDecorate, "decorate", q.DefaultDecorate, "Specify if the chapters, or verses should be decorated. Defaults to false.") + + flag.Parse() + + chapterVerseRange = flag.Arg(0) + + translationID = t.Trans(transInt) + delay = t.Delay(delayUint) + + return chapterVerseRange, translationID, delay, shouldDecorate + +} diff --git a/src/display/decorate/chapter-head.go b/src/display/decorate/chapter-head.go @@ -0,0 +1,22 @@ +package decorate + +import ( + "fmt" + + f "git.hanabi.in/gitbox/quran-go/src/fetch" + t "git.hanabi.in/gitbox/quran-go/src/types" +) + +// Decorate Chapter Head like "Chapter 1 (Al-Fātiĥah)" +func ChapterHead(chapterID t.Chap) (err error) { + + chapDetails, err := f.ChapterDetails(chapterID) + if err != nil { + return err + } + + fmt.Printf("\n\tChapter %d (%s)\n\n", chapDetails.ID, chapDetails.NameComplex) + + return + +} diff --git a/src/display/decorate/end.go b/src/display/decorate/end.go @@ -0,0 +1,25 @@ +package decorate + +import ( + "fmt" + + t "git.hanabi.in/gitbox/quran-go/src/types" +) + +// Decorate end of printed verses like: +// "--Qur'an 1:1-7" +// "--Qur'an 1:2" +// "--Qur'an 1:2-5" +func End(transAuthor string, chapterID t.Chap, verse1, verse2 t.Verse) { + + res := fmt.Sprintf("\n\t--Qur'an %d:%d", chapterID, verse1) + + if verse1 < verse2 { // "1:3-3" + res += fmt.Sprintf("-%d", verse2) + } + + res += fmt.Sprintf(", %s translation.\n", transAuthor) + + fmt.Println(res) + +} diff --git a/src/display/translations.go b/src/display/translations.go @@ -0,0 +1,26 @@ +package display + +import ( + "fmt" + + f "git.hanabi.in/gitbox/quran-go/src/fetch" +) + +// Print a list of all available translations from quran.com, sorted ASC by their ID. +func Translations() (err error) { + + translationList, err := f.AllTranslations() + if err != nil { + return err + } + + fmt.Println("Available translations:\n") + fmt.Println("ID\t\ttLanguage\t\tTranslation name\n") + + for _, elem := range translationList { + fmt.Printf("%d\t\t(%s)\t\t%s\n", elem.ID, elem.LanguageName, elem.Name) + } + + return err + +} diff --git a/src/display/verses.go b/src/display/verses.go @@ -0,0 +1,44 @@ +package display + +import ( + "fmt" + + d "git.hanabi.in/gitbox/quran-go/src/display/decorate" + f "git.hanabi.in/gitbox/quran-go/src/fetch" + p "git.hanabi.in/gitbox/quran-go/src/print" + t "git.hanabi.in/gitbox/quran-go/src/types" + u "git.hanabi.in/gitbox/quran-go/src/utils" +) + +// Display verses from verse1 to verse2. +func Verses(chapter t.Chap, verse1, verse2 t.Verse, author string, translationID t.Trans, delay t.Delay, shouldDecorate bool) { + + if shouldDecorate { + d.ChapterHead(chapter) + } + + for verse := verse1; verse <= verse2; verse++ { + printVerse(translationID, chapter, verse, delay, shouldDecorate) + } + + if shouldDecorate { + d.End(author, chapter, verse1, verse2) + } + +} + +// Print a single verse. +func printVerse(transID t.Trans, chapID t.Chap, verse t.Verse, delay t.Delay, shouldDecorate bool) { + aayat, err := f.Aayat(transID, chapID, verse, delay) + if err != nil { + p.Err(err) + } + + formattedAayat := u.FormatAayat(aayat) + + if shouldDecorate { + fmt.Printf("(%d:%d) ", chapID, verse) + } + + fmt.Println(formattedAayat) +} diff --git a/src/driver/default-behaviour.go b/src/driver/default-behaviour.go @@ -0,0 +1,37 @@ +package driver + +import ( + c "git.hanabi.in/gitbox/quran-go/src/cliinput" + d "git.hanabi.in/gitbox/quran-go/src/display" + f "git.hanabi.in/gitbox/quran-go/src/fetch" + p "git.hanabi.in/gitbox/quran-go/src/print" + u "git.hanabi.in/gitbox/quran-go/src/utils" +) + +func defaultBehaviuor() { + + chapterVerseRange, translationID, delay, shouldDecorate := c.ParseFlags() + + chapter, verse1, verse2, err := u.ParseInput(chapterVerseRange) + if err != nil { + p.Err(err) + } + + translationDetails, err := f.TranslationDetails(translationID) + if err != nil { + p.Err(err) + } + author := translationDetails.AuthorName + + lastVerse, err := u.CheckVerseRange(chapter, verse1, verse2) + if err != nil { + p.Err(err) + } + + if verse1 == 0 { // Print whole chapter. + verse1, verse2 = 1, lastVerse + } + + d.Verses(chapter, verse1, verse2, author, translationID, delay, shouldDecorate) + +} diff --git a/src/driver/run.go b/src/driver/run.go @@ -0,0 +1,37 @@ +package driver + +import ( + "os" + + d "git.hanabi.in/gitbox/quran-go/src/display" + p "git.hanabi.in/gitbox/quran-go/src/print" +) + +// Driver code. +func Run() (err error) { + + option := "help" + if len(os.Args) > 1 { + option = os.Args[1] + } + + switch option { + case "about": + p.About() + case "details": + p.Details() + case "version": + p.Version() + case "ls-translations": + if err = d.Translations(); err != nil { + return err + } + case "help": + p.Help() + default: + defaultBehaviuor() + } + + return err + +} diff --git a/src/fetch/aayat.go b/src/fetch/aayat.go @@ -0,0 +1,27 @@ +package fetch + +import ( + "fmt" + "time" + + q "git.hanabi.in/gitbox/quran-go/src/quran-com" + t "git.hanabi.in/gitbox/quran-go/src/types" +) + +// Fetch an aayah based on translation ID, chapter ID, verse, with delay; returns aayat, and error if any. +func Aayat(transID t.Trans, chapterID t.Chap, verse t.Verse, delay t.Delay) (aayat string, err error) { + + var verseData t.VerseData + uri := fmt.Sprintf("%s/translations/%d/by_ayah/%d:%d", q.API, transID, chapterID, verse) + + time.Sleep(time.Duration(delay) * time.Millisecond) + + if err = q.HttpGet(uri, &verseData); err != nil { + return aayat, err + } + + aayat = verseData.Translations[0].Text + + return aayat, err + +} diff --git a/src/fetch/all-translations.go b/src/fetch/all-translations.go @@ -0,0 +1,28 @@ +package fetch + +import ( + "sort" + + q "git.hanabi.in/gitbox/quran-go/src/quran-com" + t "git.hanabi.in/gitbox/quran-go/src/types" +) + +// Fetch all available translations of Qur'an sorted based on the ID provided by quran.com. +func AllTranslations() (translationList []t.Translations, err error) { + + var resp t.TranslationList + uri := q.API + "/resources/translations" + + err = q.HttpGet(uri, &resp) + if err != nil { + return translationList, err + } + + translationList = resp.Translations + sort.Slice(translationList, func(a, b int) bool { + return translationList[a].ID < translationList[b].ID + }) + + return translationList, err + +} diff --git a/src/fetch/chapter-details.go b/src/fetch/chapter-details.go @@ -0,0 +1,18 @@ +package fetch + +import ( + "fmt" + + q "git.hanabi.in/gitbox/quran-go/src/quran-com" + t "git.hanabi.in/gitbox/quran-go/src/types" +) + +// Fetch chapter details based on chapter ID. +func ChapterDetails(chapterID t.Chap) (chapDetails t.ChapterDetails, err error) { + + uri := fmt.Sprintf("%s/chapters/%d", q.API, chapterID) + err = q.HttpGet(uri, &chapDetails) + + return chapDetails, err + +} diff --git a/src/fetch/last-chap.go b/src/fetch/last-chap.go @@ -0,0 +1,24 @@ +package fetch + +import ( + q "git.hanabi.in/gitbox/quran-go/src/quran-com" + t "git.hanabi.in/gitbox/quran-go/src/types" +) + +// Fetch the ID of the last chapter in Qur'an. +func LastChapter() (res t.Chap, err error) { + + var chapterList t.ChaptersList + uri := q.API + "/chapters" + + err = q.HttpGet(uri, &chapterList) + if err != nil { + return res, err + } + + chapters := chapterList.Chapters + res = t.Chap(chapters[len(chapters)-1].ID) + + return res, err + +} diff --git a/src/fetch/translation-details.go b/src/fetch/translation-details.go @@ -0,0 +1,46 @@ +package fetch + +import ( + "fmt" + + t "git.hanabi.in/gitbox/quran-go/src/types" +) + +// Fetch translation details based on translation ID. +func TranslationDetails(translationID t.Trans) (transDetails t.Translations, err error) { + + translationList, err := AllTranslations() + if err != nil { + return transDetails, err + } + + idx := searchTranslation(translationList, translationID) + if idx > -1 { + transDetails = translationList[idx] + } else { + err = fmt.Errorf("%d is not a valid translation, please use `quran ls-translations' to see available translations.", translationID) + } + + return transDetails, err + +} + +// Binary search on translation list against transID. Returns index of found element, else -1. +func searchTranslation(translationList []t.Translations, translationID t.Trans) (idx int) { + low, high := 0, len(translationList)-1 + for low <= high { + mid := (low + high) >> 1 + midElemID := t.Trans(translationList[mid].ID) + + if midElemID == translationID { + return mid + } else { + if midElemID > translationID { + high = mid - 1 + } else { + low = mid + 1 + } + } + } + return -1 +} diff --git a/src/main.go b/src/main.go @@ -1,365 +1,14 @@ package main import ( - "encoding/json" - "flag" - "fmt" - "io/ioutil" - "net/http" - "os" - "regexp" - "sort" - "strconv" - "time" + dr "git.hanabi.in/gitbox/quran-go/src/driver" + p "git.hanabi.in/gitbox/quran-go/src/print" ) -const DEBUG = true -const API = "https://api.quran.com/api/v4" - func main() { - input, trans, timeDelay, shouldListTrans, shouldDecorate := handleInputFlags() - - if shouldListTrans { - if err := printTranslations(); err != nil { - printerr(err) - } - } else { - - if err, chap, ver1, ver2 := parseInput(input); err != nil { - printerr(err) - } else if err, transAuthor := checkTrans(trans); err != nil { - printerr(err) - } else { - if err, maxVerse := checkVerseRange(chap, ver1, ver2); err != nil { - printerr(err) - } else { - if ver1 == 0 { - // Print whole chapter, ie from 1-maxVerse - decorateChapterHead(shouldDecorate, chap) - for verse := t_verse(1); verse <= maxVerse; verse++ { - printVerse(trans, chap, verse, timeDelay) - } - } else if ver1 < ver2 { - // Print the range of verses, if from ver1 to ver2. - // @TODO check if < or <= ? - for verse := ver1; verse <= ver2; verse++ { - printVerse(trans, chap, verse, timeDelay) - } - } else if ver1 == ver2 { - // Print just one verse, if just ver 1. - printVerse(trans, chap, ver1, timeDelay) - } - decorateEnd(shouldDecorate, transAuthor, chap, ver1, ver2, maxVerse) - } - } - } -} - -func decorateChapterHead(shouldDecorate bool, chap t_chap) (err error) { - if shouldDecorate { - if chapDetails, err := getChapDetails(chap); err != nil { - return err - } else { - fmt.Printf("\tChapter %d (%s)\n\n", chapDetails.ID, chapDetails.NameSimple) - } - } - return err -} - -func decorateEnd(shouldDecorate bool, transAuthor string, chap t_chap, ver1, ver2, maxVerse t_verse) { - if shouldDecorate { - printRange(transAuthor, chap, ver1, ver2, maxVerse) - } -} - -func printRange(transAuthor string, chap t_chap, ver1, ver2, maxVerse t_verse) { - res := fmt.Sprintf("\t--Qur'an %d:", chap) - // if "1:0-0" || "1:3-3" || "1:3-7" - if ver1 == 0 { // "1:0-0" - res += fmt.Sprintf("%d-%d", 1, maxVerse) - } else if ver1 == ver2 { // "1:3-3" - res += fmt.Sprintf("%d", ver1) - } else { // "1:3-7" - res += fmt.Sprintf("%d:%d", ver1, ver2) - } - - res += fmt.Sprintf(", %s translation.\n", transAuthor) - - fmt.Println(res) -} - -func checkTrans(trans t_trans) (err error, transAuthor string) { - var transList TranslationList - if err = quranHttpGet(API+"/resources/translations", &transList); err != nil { - return - } - for _, elem := range transList.Translations { - if trans == t_trans(elem.ID) { - transAuthor = elem.AuthorName - return - } - } - err = fmt.Errorf("%d is not a valid translation, please use `quran list-trans' to see available translations.", trans) - return -} - -type TranslationList struct { - Translations []Translations `json:"translations"` -} -type TranslatedName struct { - Name string `json:"name"` - LanguageName string `json:"language_name"` -} -type Translations struct { - ID int `json:"id"` - Name string `json:"name"` - AuthorName string `json:"author_name"` - Slug string `json:"slug"` - LanguageName string `json:"language_name"` - TranslatedName TranslatedName `json:"translated_name"` -} - -func printVerse(trans t_trans, chap t_chap, verse t_verse, timeDelay float64) { - uri := fmt.Sprintf("%s/translations/%d/by_ayah/%d:%d", API, trans, chap, verse) - var verseData Verse - time.Sleep(time.Duration(timeDelay) * time.Second) // Rate-limit to minimise server load. - if err := quranHttpGet(uri, &verseData); err != nil { - printerr(err) - } else { - formattedAayat := formatAayat(verseData.Translations[0].Text) - fmt.Println(formattedAayat) - } -} - -func formatAayat(aayat string) (formattedAayat string) { - reg := regexp.MustCompile(`<sup foot_note=\d+>\d+<\/sup>`) - formattedAayat = reg.ReplaceAllString(aayat, "") - return -} - -type Verse struct { - Translations []struct { - ID int `json:"id"` - ResourceID int `json:"resource_id"` - Text string `json:"text"` - } `json:"translations"` - Pagination struct { - PerPage int `json:"per_page"` - CurrentPage int `json:"current_page"` - NextPage interface{} `json:"next_page"` - TotalPages int `json:"total_pages"` - TotalRecords int `json:"total_records"` - } `json:"pagination"` -} - -type t_trans int - -func checkVerseRange(chap t_chap, ver1, ver2 t_verse) (err error, maxVerse t_verse) { - if err = checkChapRange(chap); err != nil { - return - } - chapDetails, err := getChapDetails(chap) - maxVerse = t_verse(chapDetails.VersesCount) - if err != nil { - return - } - if ver2 > maxVerse { - err = fmt.Errorf("Verse for the chapter %d is out of range. That chapter has verses only till %d.\n", chap, maxVerse) - return - } - return -} - -func getChapDetails(chap t_chap) (chapDetails ChapterDetails, err error) { - - uri := fmt.Sprintf("%s/chapters/%d", API, chap) - err = quranHttpGet(uri, &chapDetails) - return -} - -type ChapterDetails struct { - Chapter `json:"chapter"` -} - -type Chapter struct { - ID int `json:"id"` - RevelationPlace string `json:"revelation_place"` - RevelationOrder int `json:"revelation_order"` - BismillahPre bool `json:"bismillah_pre"` - NameSimple string `json:"name_simple"` - NameComplex string `json:"name_complex"` - NameArabic string `json:"name_arabic"` - VersesCount int `json:"verses_count"` - Pages []int `json:"pages"` - TranslatedName TranslatedName `json:"translated_name"` -} - -type t_chap uint -type t_verse uint - -func checkChapRange(chap t_chap) (err error) { - maxChap, err := getMaxChap() - if err != nil { - return err - } - - if chap < 1 || chap > maxChap { - err = fmt.Errorf("Chapter is out of range.\n") - } - return -} - -func quranHttpGet(endpoint string, st interface{}) (err error) { - - client := &http.Client{} - req, err := http.NewRequest(http.MethodGet, endpoint, nil) - if err != nil { - return - } - - req.Header.Set("User-Agent", "git.hanabi.in/repos/quran-go.git v2022-04-26") - - resp, err := client.Do(req) - if err != nil { - return + if err := dr.Run(); err != nil { + p.Err(err) } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return - } - - err = json.Unmarshal(body, &st) - if err != nil { - return - } - - return -} - -func getMaxChap() (res t_chap, err error) { - var chapList ChaptersList - if err = quranHttpGet(API+"/chapters", &chapList); err != nil { - return - } - chaps := chapList.Chapters - res = t_chap(chaps[len(chaps)-1].ID) - return -} - -type ChaptersList struct { - Chapters []Chapter `json:"chapters"` -} - -func handleInputFlags() (input string, trans t_trans, timeDelay float64, shouldListTrans, shouldDecorate bool) { - var transInt int - - flag.IntVar(&transInt, "trans", 131, "Which translation to use.") - flag.Float64Var(&timeDelay, "delay", 0.2, "Minimum delay in seconds between fetching of two aayah.") - flag.BoolVar(&shouldListTrans, "list-trans", false, "Print list of available translations.") - flag.BoolVar(&shouldDecorate, "decorate", false, "Print list of available translations.") - - flag.Parse() - - input = flag.Arg(0) - trans = t_trans(transInt) - return -} - -func printHelp() { - // @TODO - debug("printing help") -} - -func printTranslations() (err error) { - - var transList TranslationList - if err := quranHttpGet(API+"/resources/translations", &transList); err != nil { - return err - } - - trans := transList.Translations - sort.Slice(trans, func(a, b int) bool { - return trans[a].ID < trans[b].ID - }) - - fmt.Println("Available translations:\n") - - for _, elem := range transList.Translations { - fmt.Printf("%d\t(%s)\t\t%s\n", elem.ID, elem.LanguageName, elem.Name) - } - - return err - -} - -func parseInput(input string) (err error, chapter t_chap, verse1, verse2 t_verse) { - - if len(os.Args) == 1 { - printHelp() - os.Exit(0) - } - - // Group 1, 3, 5 = Chapter:Verse1-Verse2 - re := regexp.MustCompile(`^(\d)+(:(\d+)(-(\d+))?)?$`) - isValid := re.Match([]byte(input)) - if !isValid { - err = fmt.Errorf("Expected in the form `quran [-lang=code] chapter[:verse1[-verse2]]'\n") - return - } - - res := re.FindAllStringSubmatch(input, -1) - - ch, err := strconv.ParseUint(res[0][1], 10, 0) - if err != nil { - return - } - chapter = t_chap(ch) - - if res[0][3] == res[0][5] && len(res[0][3]) > 0 { - err = fmt.Errorf("Verse 2 should not be same as Verse 1.\n") - return - } - - if res[0][3] == "" { - res[0][3] = "0" - } - ve1, err := strconv.ParseUint(res[0][3], 10, 0) - verse1 = t_verse(ve1) - - if err != nil { - return - } - - if res[0][5] == "" { - res[0][5] = res[0][3] - } - - ve2, err := strconv.ParseUint(res[0][5], 10, 0) - verse2 = t_verse(ve2) - - if err != nil { - return - } - - if verse2 < verse1 { - err = fmt.Errorf("Verse 2 must be bigger than Verse 1.\n") - } - - return -} - -func printerr(err error) { - fmt.Fprint(os.Stderr, err.Error()+"\n") - os.Exit(1) -} - -func debug(s interface{}) { - if DEBUG { - fmt.Print("[DEBUG]: ") - fmt.Println(s) - } } diff --git a/src/print/about.go b/src/print/about.go @@ -0,0 +1,5 @@ +package print + +func About() { + p("`quran' prints Qur'an chapters and verses right in the terminal.") +} diff --git a/src/print/details.go b/src/print/details.go @@ -0,0 +1,5 @@ +package print + +func Details() { + p("`quran' fetches verses and translation information from <quran.com>'s v4 API.") +} diff --git a/src/print/err.go b/src/print/err.go @@ -0,0 +1,11 @@ +package print + +import ( + "fmt" + "os" +) + +func Err(err error) { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(1) +} diff --git a/src/print/fns.go b/src/print/fns.go @@ -0,0 +1,19 @@ +package print + +import "fmt" + +func p(a string) (n int, err error) { + return fmt.Println(a) +} +func pt(a string) (n int, err error) { + return fmt.Println("\t", a) +} +func ptt(a string) (n int, err error) { + return fmt.Println("\t\t", a) +} +func ptn(a string) (n int, err error) { + return fmt.Println("\t", a, "\n\n") +} +func pttn(a string) (n int, err error) { + return fmt.Println("\t\t", a, "\n") +} diff --git a/src/print/help.go b/src/print/help.go @@ -0,0 +1,64 @@ +package print + +func Help() { + + p("QURAN\n\n") + + p("NAME") + ptn("quran - fetch various quran translations right in the terminal.") + + p("SYNOPSIS") + ptn("quran [-delay[=<millisecs>]] [-decorate] [-trans[=<translation-id>]] <command> [<args>]") + + p("DESCRIPTION") + pt("`quran' fetches Qur'an translations right in the terminal.") + ptn("`quran' is not 'bug compatible' with `quran.py', as this software handles parsing input differently.") + + p("OPTIONS") + pt("-decorate") + ptt("Specify if the chapters and verses should be decorated xor not.") + ptt("Defaults to false.") + + pt("-delay <delay-ms>") + ptt("Specify <delay-ms> in milliseconds, the delay between fetching aayahs.") + ptt("I advice using at least 500.") + ptt("Default is 1000.") // Should the delay be *only* between verses? Or *any* API call? + + pt("-trans <translation-id>") + ptt("Select which translation to use, for fetching the verses.") + pttn("Default id is 131, corresponding to Dr. Mustafa Khattab, the Clear Quran.") + pttn("Use `quran ls-translations' to see available translations.") + + p("COMMANDS") + pt("about") + ptt("Prints basic description about `quran'.") + + pt("details") + ptt("Prints information about the source of the data.") + + pt("version") + ptt("Prints version of `quran'.") + + pt("ls-translations") + pttn("Prints available translations, which can be specified with the `-trans' flag.") + + p("ARGUMENTS") + ptn("Without specifying any commands, arguemnt is of the type <chapter:[verse1[-verse2]]>") + + p("LICENSE") + pt("This software is licensed under GNU Affero General Public License v3.") + pt("Please see <https://www.gnu.org/licenses/agpl-3.0.txt>") + ptn("for what you are allowed to do with this software's source code.") + + p("AUTHORS") + pt("quran was written by acagastya.") + ptn("Running `git blame' on the source code of this software can show the authors of project.") + + p("REPORTING ERRORS") + pt("Please do NOT call errors as bugs. Any incorrect behaviour of the software is an ERROR.") + pt("Report issues to <me+git@hanabi.in>.") + ptn("Additionally, if you wish to submit changes/error fixes, please send a git-patch to the same email.") + + p("Quran v1.0.0\t\tMay 01, 2022\t\tacagastya") + +} diff --git a/src/print/version.go b/src/print/version.go @@ -0,0 +1,5 @@ +package print + +func Version() { + p("quran v1.0.0") +} diff --git a/src/quran-com/consts.go b/src/quran-com/consts.go @@ -0,0 +1,7 @@ +package qurancom + +const API = "https://api.quran.com/api/v4" + +const DefaultDecorate = false +const DefaultDelay = 1000 +const DefaultTrans = 131 diff --git a/src/quran-com/http-get.go b/src/quran-com/http-get.go @@ -0,0 +1,36 @@ +package qurancom + +import ( + "encoding/json" + "io/ioutil" + "net/http" +) + +// Custom HTTP client for GET method. +func HttpGet(endpoint string, st interface{}) (err error) { + + client := &http.Client{} + + req, err := http.NewRequest(http.MethodGet, endpoint, nil) + if err != nil { + return err + } + + req.Header.Set("User-Agent", "git.hanabi.in/quran-go v1.0.0") + resp, err := client.Do(req) + + if err != nil { + return err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + err = json.Unmarshal(body, &st) + + return err + +} diff --git a/src/types/type.go b/src/types/type.go @@ -0,0 +1,60 @@ +package types + +type TranslationList struct { + Translations []Translations `json:"translations"` +} + +type TranslatedName struct { + Name string `json:"name"` + LanguageName string `json:"language_name"` +} + +type Translations struct { + ID int `json:"id"` + Name string `json:"name"` + AuthorName string `json:"author_name"` + Slug string `json:"slug"` + LanguageName string `json:"language_name"` + TranslatedName TranslatedName `json:"translated_name"` +} + +type VerseData struct { + Translations []struct { + ID int `json:"id"` + ResourceID int `json:"resource_id"` + Text string `json:"text"` + } `json:"translations"` + Pagination struct { + PerPage int `json:"per_page"` + CurrentPage int `json:"current_page"` + NextPage interface{} `json:"next_page"` + TotalPages int `json:"total_pages"` + TotalRecords int `json:"total_records"` + } `json:"pagination"` +} + +type ChapterDetails struct { + Chapter `json:"chapter"` +} + +type Chapter struct { + ID int `json:"id"` + RevelationPlace string `json:"revelation_place"` + RevelationOrder int `json:"revelation_order"` + BismillahPre bool `json:"bismillah_pre"` + NameSimple string `json:"name_simple"` + NameComplex string `json:"name_complex"` + NameArabic string `json:"name_arabic"` + VersesCount int `json:"verses_count"` + Pages []int `json:"pages"` + TranslatedName TranslatedName `json:"translated_name"` +} + +type ChaptersList struct { + Chapters []Chapter `json:"chapters"` +} + +type Chap uint64 +type Delay float64 +type Trans int // As of now, I have only seen integers, could be negative in future, for all I know. +type Verse uint64 diff --git a/src/utils/check-verse-range.go b/src/utils/check-verse-range.go @@ -0,0 +1,35 @@ +package utils + +import ( + "fmt" + + f "git.hanabi.in/gitbox/quran-go/src/fetch" + t "git.hanabi.in/gitbox/quran-go/src/types" +) + +// Check if chapter verse are within range, returns last verse of the chapter, and error if any, +func CheckVerseRange(chapterID t.Chap, verse1, verse2 t.Verse) (lastVerse t.Verse, err error) { + + res, err := isValidChapRange(chapterID) + if err != nil { + return lastVerse, err + } + + if res == false { + err = fmt.Errorf("Chapter %d is out of range.\n", chapterID) + return lastVerse, err + } + + chapDetails, err := f.ChapterDetails(chapterID) + if err != nil { + return lastVerse, err + } + + if lastVerse = t.Verse(chapDetails.VersesCount); verse2 > lastVerse { + err = fmt.Errorf("Verse for the chapter %d is out of range. That chapter has verses only till %d.\n", chapterID, lastVerse) + return lastVerse, err + } + + return lastVerse, err + +} diff --git a/src/utils/format-aayat.go b/src/utils/format-aayat.go @@ -0,0 +1,12 @@ +package utils + +import "regexp" + +// Regex fix an Aayah to remove things like <sup foot_note=76373>1</sup> +func FormatAayat(aayat string) (formattedAayat string) { + + reg := regexp.MustCompile(`<sup foot_note=\d+>\d+<\/sup>`) + formattedAayat = reg.ReplaceAllString(aayat, "") + return formattedAayat + +} diff --git a/src/utils/is-valid-chapter-range.go b/src/utils/is-valid-chapter-range.go @@ -0,0 +1,20 @@ +package utils + +import ( + f "git.hanabi.in/gitbox/quran-go/src/fetch" + t "git.hanabi.in/gitbox/quran-go/src/types" +) + +// Check if the chapter number is valid. +func isValidChapRange(chapter t.Chap) (res bool, err error) { + + if lastChapter, err := f.LastChapter(); err != nil { + return res, err + } else { + if 1 <= chapter && chapter <= lastChapter { + res = true + } + return res, err + } + +} diff --git a/src/utils/parse-input.go b/src/utils/parse-input.go @@ -0,0 +1,65 @@ +package utils + +import ( + "fmt" + "regexp" + "strconv" + + t "git.hanabi.in/gitbox/quran-go/src/types" +) + +// Takes "1" or "1:2" or "1:2-5" and returns chapter, verse1, verse2 and error, if any. +func ParseInput(input string) (chapter t.Chap, verse1, verse2 t.Verse, err error) { + + // Group 1, 3, 5 = Chapter:Verse1-Verse2 + re := regexp.MustCompile(`^(\d+)(:(\d+)(-(\d+))?)?$`) + + isValid := re.Match([]byte(input)) + if !isValid { + err = fmt.Errorf("Expected in the form `quran [-lang=code] chapter[:verse1[-verse2]]'\n") + return + } + + res := re.FindAllStringSubmatch(input, -1) + + ch, err := strconv.ParseUint(res[0][1], 10, 64) + if err != nil { + return + } + + chapter = t.Chap(ch) + + if res[0][3] == res[0][5] && len(res[0][3]) > 0 { + err = fmt.Errorf("Verse 2 should not be same as Verse 1.\n") + return + } + + if res[0][3] == "" { + res[0][3] = "0" + } + + ve1, err := strconv.ParseUint(res[0][3], 10, 64) + if err != nil { + return + } + + verse1 = t.Verse(ve1) + + if res[0][5] == "" { + res[0][5] = res[0][3] + } + + ve2, err := strconv.ParseUint(res[0][5], 10, 64) + if err != nil { + return + } + + verse2 = t.Verse(ve2) + + if verse2 < verse1 { + err = fmt.Errorf("Verse 2 must be bigger than Verse 1.\n") + } + + return + +}