feat: Add location search
This commit is contained in:
@@ -3,15 +3,18 @@ module prayertimes
|
|||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/doug-martin/goqu/v9 v9.19.0
|
||||||
github.com/gofiber/fiber/v3 v3.0.0
|
github.com/gofiber/fiber/v3 v3.0.0
|
||||||
github.com/imroc/req/v3 v3.57.0
|
github.com/imroc/req/v3 v3.57.0
|
||||||
github.com/rs/zerolog v1.29.0
|
github.com/rs/zerolog v1.29.0
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
|
modernc.org/sqlite v1.46.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/gofiber/schema v1.6.0 // indirect
|
github.com/gofiber/schema v1.6.0 // indirect
|
||||||
github.com/gofiber/utils/v2 v2.0.0 // indirect
|
github.com/gofiber/utils/v2 v2.0.0 // indirect
|
||||||
github.com/google/go-querystring v1.2.0 // indirect
|
github.com/google/go-querystring v1.2.0 // indirect
|
||||||
@@ -21,17 +24,23 @@ require (
|
|||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
github.com/philhofer/fwd v1.2.0 // indirect
|
github.com/philhofer/fwd v1.2.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/quic-go/qpack v0.6.0 // indirect
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
github.com/quic-go/quic-go v0.57.1 // indirect
|
github.com/quic-go/quic-go v0.57.1 // indirect
|
||||||
github.com/refraction-networking/utls v1.8.2 // indirect
|
github.com/refraction-networking/utls v1.8.2 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/tinylib/msgp v1.6.3 // indirect
|
github.com/tinylib/msgp v1.6.3 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasthttp v1.69.0 // indirect
|
github.com/valyala/fasthttp v1.69.0 // indirect
|
||||||
golang.org/x/crypto v0.48.0 // indirect
|
golang.org/x/crypto v0.48.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||||
golang.org/x/net v0.50.0 // indirect
|
golang.org/x/net v0.50.0 // indirect
|
||||||
golang.org/x/sys v0.41.0 // indirect
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
golang.org/x/text v0.34.0 // indirect
|
golang.org/x/text v0.34.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
modernc.org/libc v1.67.6 // indirect
|
||||||
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
|
modernc.org/memory v1.11.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
|
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||||
|
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||||
|
github.com/doug-martin/goqu/v9 v9.19.0 h1:PD7t1X3tRcUiSdc5TEyOFKujZA5gs3VSA7wxSvBx7qo=
|
||||||
|
github.com/doug-martin/goqu/v9 v9.19.0/go.mod h1:nf0Wc2/hV3gYK9LiyqIrzBEVGlI8qW3GuDCEobC4wBQ=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||||
|
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/gofiber/fiber/v3 v3.0.0 h1:GPeCG8X60L42wLKrzgeewDHBr6pE6veAvwaXsqD3Xjk=
|
github.com/gofiber/fiber/v3 v3.0.0 h1:GPeCG8X60L42wLKrzgeewDHBr6pE6veAvwaXsqD3Xjk=
|
||||||
github.com/gofiber/fiber/v3 v3.0.0/go.mod h1:kVZiO/AwyT5Pq6PgC8qRCJ+j/BHrMy5jNw1O9yH38aY=
|
github.com/gofiber/fiber/v3 v3.0.0/go.mod h1:kVZiO/AwyT5Pq6PgC8qRCJ+j/BHrMy5jNw1O9yH38aY=
|
||||||
@@ -13,13 +22,18 @@ github.com/gofiber/schema v1.6.0 h1:rAgVDFwhndtC+hgV7Vu5ItQCn7eC2mBA4Eu1/ZTiEYY=
|
|||||||
github.com/gofiber/schema v1.6.0/go.mod h1:WNZWpQx8LlPSK7ZaX0OqOh+nQo/eW2OevsXs1VZfs/s=
|
github.com/gofiber/schema v1.6.0/go.mod h1:WNZWpQx8LlPSK7ZaX0OqOh+nQo/eW2OevsXs1VZfs/s=
|
||||||
github.com/gofiber/utils/v2 v2.0.0 h1:SCC3rpsEDWupFSHtc0RKxg/BKgV0s1qKfZg9Jv6D0sM=
|
github.com/gofiber/utils/v2 v2.0.0 h1:SCC3rpsEDWupFSHtc0RKxg/BKgV0s1qKfZg9Jv6D0sM=
|
||||||
github.com/gofiber/utils/v2 v2.0.0/go.mod h1:xF9v89FfmbrYqI/bQUGN7gR8ZtXot2jxnZvmAUtiavE=
|
github.com/gofiber/utils/v2 v2.0.0/go.mod h1:xF9v89FfmbrYqI/bQUGN7gR8ZtXot2jxnZvmAUtiavE=
|
||||||
|
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
|
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
|
||||||
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
|
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
|
||||||
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4=
|
github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4=
|
||||||
github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
|
github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
|
||||||
github.com/imroc/req/v3 v3.57.0 h1:LMTUjNRUybUkTPn8oJDq8Kg3JRBOBTcnDhKu7mzupKI=
|
github.com/imroc/req/v3 v3.57.0 h1:LMTUjNRUybUkTPn8oJDq8Kg3JRBOBTcnDhKu7mzupKI=
|
||||||
@@ -30,12 +44,17 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
|||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/lib/pq v1.10.1 h1:6VXZrLU0jHBYyAqrSPa+MgPfnSvTPuMgK+k0o5kVFWo=
|
||||||
|
github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||||
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
@@ -47,6 +66,8 @@ github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI1
|
|||||||
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
|
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
|
||||||
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
|
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
|
||||||
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
@@ -54,6 +75,10 @@ github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
|
|||||||
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
|
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
|
||||||
github.com/shamaton/msgpack/v3 v3.0.0 h1:xl40uxWkSpwBCSTvS5wyXvJRsC6AcVcYeox9PspKiZg=
|
github.com/shamaton/msgpack/v3 v3.0.0 h1:xl40uxWkSpwBCSTvS5wyXvJRsC6AcVcYeox9PspKiZg=
|
||||||
github.com/shamaton/msgpack/v3 v3.0.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc=
|
github.com/shamaton/msgpack/v3 v3.0.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=
|
github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=
|
||||||
@@ -68,23 +93,67 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
|
|||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||||
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||||
|
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||||
|
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||||
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||||
|
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||||
|
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
|
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
|
||||||
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
||||||
|
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||||
|
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||||
|
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
|
||||||
|
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
|
||||||
|
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||||
|
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||||
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
|
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
|
||||||
|
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||||
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
|
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
|
||||||
|
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
|
||||||
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
|
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
|
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||||
|
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
|
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
|
||||||
|
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
||||||
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
|
|||||||
+10
-6
@@ -13,7 +13,7 @@ import (
|
|||||||
"github.com/gofiber/fiber/v3/middleware/cors"
|
"github.com/gofiber/fiber/v3/middleware/cors"
|
||||||
"github.com/gofiber/fiber/v3/middleware/favicon"
|
"github.com/gofiber/fiber/v3/middleware/favicon"
|
||||||
"github.com/gofiber/fiber/v3/middleware/logger"
|
"github.com/gofiber/fiber/v3/middleware/logger"
|
||||||
recover "github.com/gofiber/fiber/v3/middleware/recover"
|
"github.com/gofiber/fiber/v3/middleware/recover"
|
||||||
"github.com/gofiber/fiber/v3/middleware/static"
|
"github.com/gofiber/fiber/v3/middleware/static"
|
||||||
|
|
||||||
"prayertimes/pkg/prayer"
|
"prayertimes/pkg/prayer"
|
||||||
@@ -25,12 +25,16 @@ type httpError struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Services struct {
|
type Services struct {
|
||||||
Provider DiyanetProvider
|
PrayerProvider PrayerProvider
|
||||||
|
LocationProvider LocationProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
type DiyanetProvider interface {
|
type PrayerProvider interface {
|
||||||
Get(ctx context.Context, locationID string) ([]prayer.Times, error)
|
Get(ctx context.Context, locationID string) ([]prayer.Times, error)
|
||||||
GetByCoords(ctx context.Context, coords prayer.Coordinates) ([]prayer.Times, error)
|
GetByCoords(ctx context.Context, coords prayer.Coordinates) ([]prayer.Times, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type LocationProvider interface {
|
||||||
SearchLocations(ctx context.Context, query string) ([]prayer.Location, error)
|
SearchLocations(ctx context.Context, query string) ([]prayer.Location, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +86,7 @@ func New(services Services) *fiber.App {
|
|||||||
|
|
||||||
switch {
|
switch {
|
||||||
case locationID != "":
|
case locationID != "":
|
||||||
times, err = services.Provider.Get(ctx.Context(), locationID)
|
times, err = services.PrayerProvider.Get(ctx.Context(), locationID)
|
||||||
case latitude != "" && longitude != "":
|
case latitude != "" && longitude != "":
|
||||||
lat, latErr := strconv.ParseFloat(latitude, 64)
|
lat, latErr := strconv.ParseFloat(latitude, 64)
|
||||||
if latErr != nil {
|
if latErr != nil {
|
||||||
@@ -93,7 +97,7 @@ func New(services Services) *fiber.App {
|
|||||||
return fmt.Errorf("failed to parse longitude query parameter: %w", errors.Join(fiber.ErrBadRequest, lngErr))
|
return fmt.Errorf("failed to parse longitude query parameter: %w", errors.Join(fiber.ErrBadRequest, lngErr))
|
||||||
}
|
}
|
||||||
|
|
||||||
times, err = services.Provider.GetByCoords(ctx.Context(), prayer.Coordinates{
|
times, err = services.PrayerProvider.GetByCoords(ctx.Context(), prayer.Coordinates{
|
||||||
Latitude: lat,
|
Latitude: lat,
|
||||||
Longitude: lng,
|
Longitude: lng,
|
||||||
})
|
})
|
||||||
@@ -124,7 +128,7 @@ func New(services Services) *fiber.App {
|
|||||||
return fmt.Errorf("failed to validate location query parameters: %w", fiber.ErrBadRequest)
|
return fmt.Errorf("failed to validate location query parameters: %w", fiber.ErrBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
locations, err := services.Provider.SearchLocations(ctx.Context(), query.Text)
|
locations, err := services.LocationProvider.SearchLocations(ctx.Context(), query.Text)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to search locations: %w", err)
|
return fmt.Errorf("failed to search locations: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,54 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
"prayertimes/internal/api"
|
"prayertimes/internal/api"
|
||||||
"prayertimes/internal/net"
|
"prayertimes/internal/net"
|
||||||
|
"prayertimes/pkg/citydb"
|
||||||
"prayertimes/pkg/diyanetapi"
|
"prayertimes/pkg/diyanetapi"
|
||||||
"prayertimes/pkg/diyanetcalc"
|
"prayertimes/pkg/diyanetcalc"
|
||||||
"prayertimes/pkg/diyanetprovider"
|
"prayertimes/pkg/diyanetprovider"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
if err := run(); err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("failed to run app")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() error {
|
||||||
port := getDefaultEnv("PORT", "8000")
|
port := getDefaultEnv("PORT", "8000")
|
||||||
|
dbPath := strings.TrimSpace(getDefaultEnv("DB_PATH", "./prayertimes.sqlite3"))
|
||||||
|
if dbPath == "" {
|
||||||
|
return fmt.Errorf("DB_PATH is not set")
|
||||||
|
}
|
||||||
|
|
||||||
services, err := newServices()
|
locationProvider, err := citydb.Open(dbPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("failed to init services")
|
return fmt.Errorf("failed to open city database: %w", err)
|
||||||
}
|
}
|
||||||
app := api.New(services)
|
defer locationProvider.Close()
|
||||||
|
|
||||||
err = app.Listen(":" + port)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("exit with an error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newServices() (api.Services, error) {
|
|
||||||
diyanetAPIProvider := diyanetapi.New(net.ReqClient)
|
diyanetAPIProvider := diyanetapi.New(net.ReqClient)
|
||||||
diyanetCalcProvider := diyanetcalc.New()
|
diyanetCalcProvider := diyanetcalc.New()
|
||||||
provider := diyanetprovider.New(diyanetAPIProvider, diyanetCalcProvider)
|
prayerProvider := diyanetprovider.New(diyanetAPIProvider, diyanetCalcProvider)
|
||||||
|
|
||||||
return api.Services{
|
services := api.Services{
|
||||||
Provider: provider,
|
PrayerProvider: prayerProvider,
|
||||||
}, nil
|
LocationProvider: locationProvider,
|
||||||
|
}
|
||||||
|
|
||||||
|
app := api.New(services)
|
||||||
|
if err := app.Listen(":" + port); err != nil {
|
||||||
|
return fmt.Errorf("failed to listen on http port: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDefaultEnv(name string, defaultValue string) string {
|
func getDefaultEnv(name string, defaultValue string) string {
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package citydb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/doug-martin/goqu/v9"
|
||||||
|
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
|
||||||
|
"prayertimes/pkg/prayer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Provider struct {
|
||||||
|
db *goqu.Database
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(db *goqu.Database) Provider {
|
||||||
|
return Provider{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Open(path string) (Provider, error) {
|
||||||
|
conn, err := sql.Open("sqlite", path)
|
||||||
|
if err != nil {
|
||||||
|
return Provider{}, fmt.Errorf("failed to open cities database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := conn.Ping(); err != nil {
|
||||||
|
return Provider{}, fmt.Errorf("failed to connect to cities database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return New(goqu.New("sqlite3", conn)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Provider) Close() error {
|
||||||
|
return p.db.Db.(*sql.DB).Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
type locationRow struct {
|
||||||
|
ID int `db:"geoname_id"`
|
||||||
|
Name string `db:"name"`
|
||||||
|
ASCIIName string `db:"ascii_name"`
|
||||||
|
AlternateNames string `db:"alternate_names"`
|
||||||
|
CountryCode string `db:"country_code"`
|
||||||
|
Latitude float64 `db:"latitude"`
|
||||||
|
Longitude float64 `db:"longitude"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Provider) SearchLocations(ctx context.Context, query string) ([]prayer.Location, error) {
|
||||||
|
query = strings.TrimSpace(query)
|
||||||
|
if query == "" {
|
||||||
|
return []prayer.Location{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pattern := "%" + query + "%"
|
||||||
|
|
||||||
|
q := p.db.
|
||||||
|
From("cities").
|
||||||
|
Select(
|
||||||
|
goqu.I("geoname_id"),
|
||||||
|
goqu.I("name"),
|
||||||
|
goqu.I("ascii_name"),
|
||||||
|
goqu.COALESCE(goqu.I("alternate_names"), "").As("alternate_names"),
|
||||||
|
goqu.I("country_code"),
|
||||||
|
goqu.I("latitude"),
|
||||||
|
goqu.I("longitude"),
|
||||||
|
).
|
||||||
|
Where(
|
||||||
|
goqu.Or(
|
||||||
|
goqu.L("name LIKE ? COLLATE NOCASE", pattern),
|
||||||
|
goqu.L("ascii_name LIKE ? COLLATE NOCASE", pattern),
|
||||||
|
goqu.L("alternate_names LIKE ? COLLATE NOCASE", pattern),
|
||||||
|
),
|
||||||
|
).
|
||||||
|
Order(goqu.I("population").Desc(), goqu.I("name").Asc()).
|
||||||
|
Limit(50)
|
||||||
|
|
||||||
|
var rows []locationRow
|
||||||
|
if err := q.ScanStructsContext(ctx, &rows); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to query locations from cities database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
locations := make([]prayer.Location, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
locations = append(locations, prayer.Location{
|
||||||
|
ID: row.ID,
|
||||||
|
Name: row.Name,
|
||||||
|
ASCIIName: row.ASCIIName,
|
||||||
|
AlternateNames: row.AlternateNames,
|
||||||
|
CountryCode: row.CountryCode,
|
||||||
|
Latitude: row.Latitude,
|
||||||
|
Longitude: row.Longitude,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return locations, nil
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/imroc/req/v3"
|
"github.com/imroc/req/v3"
|
||||||
@@ -67,61 +66,6 @@ func (d Provider) getLocationIDByCoords(ctx context.Context, coords prayer.Coord
|
|||||||
return fmt.Sprintf("%d", response.ResultObject[0].ID), nil
|
return fmt.Sprintf("%d", response.ResultObject[0].ID), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d Provider) SearchLocations(ctx context.Context, query string) ([]prayer.Location, error) {
|
|
||||||
query = strings.TrimSpace(query)
|
|
||||||
if query == "" {
|
|
||||||
return []prayer.Location{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := d.http.NewRequest().
|
|
||||||
SetContext(ctx).
|
|
||||||
SetQueryParam("searchText", query).
|
|
||||||
Get("https://namazvakti.diyanet.gov.tr/api/Search/GetByName")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to search locations: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var response struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
ResultObject struct {
|
|
||||||
Results []struct {
|
|
||||||
ID int `json:"cityID"`
|
|
||||||
CityNameTR string `json:"cityNameTR"`
|
|
||||||
StateNameTR string `json:"stateNameTR"`
|
|
||||||
CountryNameTR string `json:"countryNameTR"`
|
|
||||||
CityNameEN string `json:"cityNameEN"`
|
|
||||||
StateNameEN string `json:"stateNameEN"`
|
|
||||||
CountryNameEN string `json:"countryNameEN"`
|
|
||||||
Latitude float64 `json:"latitude"`
|
|
||||||
Longitude float64 `json:"longitude"`
|
|
||||||
} `json:"results"`
|
|
||||||
} `json:"resultObject"`
|
|
||||||
}
|
|
||||||
if err := res.Unmarshal(&response); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to unmarshal search response: %w", err)
|
|
||||||
}
|
|
||||||
if !response.Success {
|
|
||||||
return nil, fmt.Errorf("failed to search locations in upstream: %w", errors.New(res.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
locations := make([]prayer.Location, 0, len(response.ResultObject.Results))
|
|
||||||
for _, it := range response.ResultObject.Results {
|
|
||||||
locations = append(locations, prayer.Location{
|
|
||||||
ID: it.ID,
|
|
||||||
NameTR: formatLocationName(it.CountryNameTR, it.StateNameTR, it.CityNameTR),
|
|
||||||
NameEN: formatLocationName(it.CountryNameEN, it.StateNameEN, it.CityNameEN),
|
|
||||||
Latitude: it.Latitude,
|
|
||||||
Longitude: it.Longitude,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return locations, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatLocationName(country, state, city string) string {
|
|
||||||
return fmt.Sprintf("%s / %s / %s", strings.TrimSpace(country), strings.TrimSpace(state), strings.TrimSpace(city))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d Provider) Get(ctx context.Context, locationID string) ([]prayer.Times, error) {
|
func (d Provider) Get(ctx context.Context, locationID string) ([]prayer.Times, error) {
|
||||||
res, err := d.http.NewRequest().
|
res, err := d.http.NewRequest().
|
||||||
SetContext(ctx).
|
SetContext(ctx).
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
type APIProvider interface {
|
type APIProvider interface {
|
||||||
Get(ctx context.Context, locationID string) ([]prayer.Times, error)
|
Get(ctx context.Context, locationID string) ([]prayer.Times, error)
|
||||||
GetByCoords(ctx context.Context, coords prayer.Coordinates) ([]prayer.Times, error)
|
GetByCoords(ctx context.Context, coords prayer.Coordinates) ([]prayer.Times, error)
|
||||||
SearchLocations(ctx context.Context, query string) ([]prayer.Location, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type FallbackProvider interface {
|
type FallbackProvider interface {
|
||||||
@@ -29,18 +28,11 @@ func New(api APIProvider, fallback FallbackProvider) Provider {
|
|||||||
return Provider{
|
return Provider{
|
||||||
api: api,
|
api: api,
|
||||||
fallback: fallback,
|
fallback: fallback,
|
||||||
timeout: time.Second,
|
timeout: 2 * time.Second,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p Provider) SearchLocations(ctx context.Context, query string) ([]prayer.Location, error) {
|
var ErrEmptyTimes = errors.New("diyanet did not return any prayer times")
|
||||||
locations, err := p.api.SearchLocations(ctx, query)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to search locations from api provider: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return locations, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p Provider) Get(ctx context.Context, locationID string) ([]prayer.Times, error) {
|
func (p Provider) Get(ctx context.Context, locationID string) ([]prayer.Times, error) {
|
||||||
ctxWithTimeout, cancel := context.WithTimeout(ctx, p.timeout)
|
ctxWithTimeout, cancel := context.WithTimeout(ctx, p.timeout)
|
||||||
@@ -51,7 +43,7 @@ func (p Provider) Get(ctx context.Context, locationID string) ([]prayer.Times, e
|
|||||||
return nil, fmt.Errorf("failed to get prayer times from api provider: %w", err)
|
return nil, fmt.Errorf("failed to get prayer times from api provider: %w", err)
|
||||||
}
|
}
|
||||||
if len(times) == 0 {
|
if len(times) == 0 {
|
||||||
return nil, fmt.Errorf("failed to get prayer times from api provider: %w", errors.New("empty prayer times result"))
|
return nil, ErrEmptyTimes
|
||||||
}
|
}
|
||||||
|
|
||||||
return times, nil
|
return times, nil
|
||||||
@@ -66,16 +58,14 @@ func (p Provider) GetByCoords(ctx context.Context, coords prayer.Coordinates) ([
|
|||||||
return times, nil
|
return times, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fallbackTimes, fallbackErr := p.fallback.GetByCoords(ctx, coords)
|
times, err = p.fallback.GetByCoords(ctx, coords)
|
||||||
if fallbackErr != nil {
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get prayer times from fallback provider: %w", errors.Join(err, fallbackErr))
|
return nil, fmt.Errorf("failed to get prayer times from fallback provider: %w", err)
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("failed to get prayer times from fallback provider: %w", fallbackErr)
|
|
||||||
}
|
|
||||||
if len(fallbackTimes) == 0 {
|
|
||||||
return nil, fmt.Errorf("failed to get prayer times from fallback provider: %w", errors.New("empty prayer times result"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return fallbackTimes, nil
|
if len(times) == 0 {
|
||||||
|
return nil, fmt.Errorf("fallback provider did not return any prayer times")
|
||||||
|
}
|
||||||
|
|
||||||
|
return times, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -13,8 +13,10 @@ type Coordinates struct {
|
|||||||
|
|
||||||
type Location struct {
|
type Location struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
NameTR string `json:"name_tr"`
|
Name string `json:"name"`
|
||||||
NameEN string `json:"name_en"`
|
ASCIIName string `json:"ascii_name"`
|
||||||
|
AlternateNames string `json:"alternate_names"`
|
||||||
|
CountryCode string `json:"country_code"`
|
||||||
Latitude float64 `json:"latitude"`
|
Latitude float64 `json:"latitude"`
|
||||||
Longitude float64 `json:"longitude"`
|
Longitude float64 `json:"longitude"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Download GeoNames cities5000 from https://download.geonames.org/export/dump/ and seed a SQLite database."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import io
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
import urllib.request
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SOURCE_URL = "https://download.geonames.org/export/dump/cities5000.zip"
|
||||||
|
ARCHIVE_ENTRY = "cities5000.txt"
|
||||||
|
|
||||||
|
CREATE_TABLE_SQL = """
|
||||||
|
CREATE TABLE IF NOT EXISTS cities (
|
||||||
|
geoname_id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
ascii_name TEXT NOT NULL,
|
||||||
|
alternate_names TEXT,
|
||||||
|
latitude REAL NOT NULL,
|
||||||
|
longitude REAL NOT NULL,
|
||||||
|
feature_class TEXT,
|
||||||
|
feature_code TEXT,
|
||||||
|
country_code TEXT,
|
||||||
|
cc2 TEXT,
|
||||||
|
admin1_code TEXT,
|
||||||
|
admin2_code TEXT,
|
||||||
|
admin3_code TEXT,
|
||||||
|
admin4_code TEXT,
|
||||||
|
population INTEGER,
|
||||||
|
elevation INTEGER,
|
||||||
|
dem INTEGER,
|
||||||
|
timezone TEXT,
|
||||||
|
modification_date TEXT
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
CREATE_INDEXES_SQL = (
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_cities_name ON cities(name)",
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_cities_ascii_name ON cities(ascii_name)",
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_cities_alternate_names ON cities(alternate_names)",
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_cities_country_code ON cities(country_code)",
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_cities_population ON cities(population DESC)",
|
||||||
|
)
|
||||||
|
|
||||||
|
INSERT_SQL = """
|
||||||
|
INSERT OR REPLACE INTO cities (
|
||||||
|
geoname_id,
|
||||||
|
name,
|
||||||
|
ascii_name,
|
||||||
|
alternate_names,
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
feature_class,
|
||||||
|
feature_code,
|
||||||
|
country_code,
|
||||||
|
cc2,
|
||||||
|
admin1_code,
|
||||||
|
admin2_code,
|
||||||
|
admin3_code,
|
||||||
|
admin4_code,
|
||||||
|
population,
|
||||||
|
elevation,
|
||||||
|
dem,
|
||||||
|
timezone,
|
||||||
|
modification_date
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(description="Seed GeoNames cities5000 into SQLite")
|
||||||
|
parser.add_argument("--db", required=True, help="Target SQLite database path")
|
||||||
|
parser.add_argument(
|
||||||
|
"--batch-size", type=int, default=1000, help="Insert batch size"
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def to_int(value: str) -> int | None:
|
||||||
|
value = value.strip()
|
||||||
|
if value == "":
|
||||||
|
return None
|
||||||
|
return int(value)
|
||||||
|
|
||||||
|
|
||||||
|
def to_float(value: str) -> float:
|
||||||
|
return float(value.strip())
|
||||||
|
|
||||||
|
|
||||||
|
def row_from_columns(columns: list[str]) -> tuple:
|
||||||
|
return (
|
||||||
|
int(columns[0]),
|
||||||
|
columns[1],
|
||||||
|
columns[2],
|
||||||
|
columns[3],
|
||||||
|
to_float(columns[4]),
|
||||||
|
to_float(columns[5]),
|
||||||
|
columns[6],
|
||||||
|
columns[7],
|
||||||
|
columns[8],
|
||||||
|
columns[9],
|
||||||
|
columns[10],
|
||||||
|
columns[11],
|
||||||
|
columns[12],
|
||||||
|
columns[13],
|
||||||
|
to_int(columns[14]),
|
||||||
|
to_int(columns[15]),
|
||||||
|
to_int(columns[16]),
|
||||||
|
columns[17],
|
||||||
|
columns[18],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def download_archive() -> bytes:
|
||||||
|
with urllib.request.urlopen(SOURCE_URL) as response:
|
||||||
|
return response.read()
|
||||||
|
|
||||||
|
|
||||||
|
def stream_city_rows(archive_bytes: bytes):
|
||||||
|
with zipfile.ZipFile(io.BytesIO(archive_bytes)) as zf:
|
||||||
|
with zf.open(ARCHIVE_ENTRY) as raw_file:
|
||||||
|
for raw_line in raw_file:
|
||||||
|
line = raw_line.decode("utf-8").rstrip("\n")
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
columns = line.split("\t")
|
||||||
|
if len(columns) != 19:
|
||||||
|
continue
|
||||||
|
|
||||||
|
yield row_from_columns(columns)
|
||||||
|
|
||||||
|
|
||||||
|
def seed_database(db_path: Path, batch_size: int) -> int:
|
||||||
|
archive = download_archive()
|
||||||
|
|
||||||
|
inserted = 0
|
||||||
|
batch = []
|
||||||
|
|
||||||
|
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
with sqlite3.connect(str(db_path)) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("PRAGMA journal_mode=WAL")
|
||||||
|
cursor.execute(CREATE_TABLE_SQL)
|
||||||
|
for sql in CREATE_INDEXES_SQL:
|
||||||
|
cursor.execute(sql)
|
||||||
|
|
||||||
|
for row in stream_city_rows(archive):
|
||||||
|
batch.append(row)
|
||||||
|
if len(batch) >= batch_size:
|
||||||
|
cursor.executemany(INSERT_SQL, batch)
|
||||||
|
inserted += len(batch)
|
||||||
|
batch.clear()
|
||||||
|
|
||||||
|
if batch:
|
||||||
|
cursor.executemany(INSERT_SQL, batch)
|
||||||
|
inserted += len(batch)
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
return inserted
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = parse_args()
|
||||||
|
try:
|
||||||
|
inserted = seed_database(Path(args.db), args.batch_size)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
print(f"failed to seed cities database: {exc}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(f"seeded {inserted} city rows into {args.db}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user