feat: Add location search

This commit is contained in:
2026-02-21 01:50:59 +03:00
parent f520473ed3
commit 26af8af8a9
9 changed files with 417 additions and 104 deletions
+99
View File
@@ -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
}
-56
View File
@@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"strings"
"time"
"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
}
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) {
res, err := d.http.NewRequest().
SetContext(ctx).
+11 -21
View File
@@ -12,7 +12,6 @@ import (
type APIProvider interface {
Get(ctx context.Context, locationID string) ([]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 {
@@ -29,18 +28,11 @@ func New(api APIProvider, fallback FallbackProvider) Provider {
return Provider{
api: api,
fallback: fallback,
timeout: time.Second,
timeout: 2 * time.Second,
}
}
func (p Provider) SearchLocations(ctx context.Context, query string) ([]prayer.Location, error) {
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
}
var ErrEmptyTimes = errors.New("diyanet did not return any prayer times")
func (p Provider) Get(ctx context.Context, locationID string) ([]prayer.Times, error) {
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)
}
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
@@ -66,16 +58,14 @@ func (p Provider) GetByCoords(ctx context.Context, coords prayer.Coordinates) ([
return times, nil
}
fallbackTimes, fallbackErr := p.fallback.GetByCoords(ctx, coords)
if fallbackErr != 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", fallbackErr)
}
if len(fallbackTimes) == 0 {
return nil, fmt.Errorf("failed to get prayer times from fallback provider: %w", errors.New("empty prayer times result"))
times, err = p.fallback.GetByCoords(ctx, coords)
if err != nil {
return nil, fmt.Errorf("failed to get prayer times from fallback provider: %w", err)
}
return fallbackTimes, nil
if len(times) == 0 {
return nil, fmt.Errorf("fallback provider did not return any prayer times")
}
return times, nil
}
+7 -5
View File
@@ -12,11 +12,13 @@ type Coordinates struct {
}
type Location struct {
ID int `json:"id"`
NameTR string `json:"name_tr"`
NameEN string `json:"name_en"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
ID int `json:"id"`
Name string `json:"name"`
ASCIIName string `json:"ascii_name"`
AlternateNames string `json:"alternate_names"`
CountryCode string `json:"country_code"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
}
type Times struct {