diff --git a/.gitignore b/.gitignore index b472a6a..5d3c754 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ data/ log/ .idea/ *.swp +*.mmdb +*.dat diff --git a/.ip2location.key b/.ip2location.key new file mode 100644 index 0000000..e69de29 diff --git a/.wegorc b/.wegorc new file mode 100644 index 0000000..1628cf3 --- /dev/null +++ b/.wegorc @@ -0,0 +1,7 @@ +{ + "APIKey": "", + "City": "London", + "Numdays": 3, + "Imperial": false, + "Lang": "en" +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6da660f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,54 @@ +FROM ubuntu:18.04 + +RUN apt-get update && \ + apt-get install -y curl \ + git \ + python \ + python-pip \ + python-dev \ + autoconf \ + libtool \ + gawk +RUN curl -O https://dl.google.com/go/go1.10.3.linux-amd64.tar.gz +RUN tar xvf go1.10.3.linux-amd64.tar.gz + +WORKDIR /app + +COPY ./bin /app/bin +COPY ./lib /app/lib +COPY ./share /app/share +COPY ./GeoLite2-City.mmdb /app +COPY ./requirements.txt /app +COPY ./.wegorc /root +COPY ./we-lang.go /app +COPY ./.ip2location.key /root +COPY ./airports.dat /app + +RUN export PATH=$PATH:/go/bin && \ + go get -u github.com/mattn/go-colorable && \ + go get -u github.com/klauspost/lctime && \ + go get -u github.com/mattn/go-runewidth && \ + export GOBIN="/root/go/bin" && \ + go install /app/we-lang.go + +RUN pip install -r requirements.txt + +RUN mkdir /app/cache +RUN mkdir -p /var/log/supervisor && \ + mkdir -p /etc/supervisor/conf.d +RUN chmod -R o+rw /var/log/supervisor && \ + chmod -R o+rw /var/run +COPY supervisord.conf /etc/supervisor/supervisord.conf + +ENV WTTR_MYDIR="/app" +ENV WTTR_GEOLITE="/app/GeoLite2-City.mmdb" +ENV WTTR_WEGO="/root/go/bin/we-lang" +ENV WTTR_LISTEN_HOST="0.0.0.0" +ENV WTTR_LISTEN_PORT="8002" + + +EXPOSE 8002 + +CMD ["/usr/local/bin/supervisord"] + + diff --git a/requirements.txt b/requirements.txt index 9a34166..69744ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ dnspython pylint cyrtranslit astral -timezonefinder +timezonefinder==2.1.2 Pillow pyte python-dateutil @@ -16,4 +16,6 @@ pyjq scipy babel pylru +pysocks +supervisor numba diff --git a/supervisord.conf b/supervisord.conf new file mode 100644 index 0000000..68304bb --- /dev/null +++ b/supervisord.conf @@ -0,0 +1,22 @@ +[supervisord] +nodaemon=true +logfile=/var/log/supervisor/supervisord.log +pidfile=/var/run/supervisord.pid + +[program:srv] +command=python /app/bin/srv.py +stderr_logfile=/var/log/supervisor/srv-stderr.log +stdout_logfile=/var/log/supervisor/srv-stdout.log + +[program:proxy] +command=python /app/bin/proxy.py +stderr_logfile=/var/log/supervisor/proxy-stderr.log +stdout_logfile=/var/log/supervisor/proxy-stdout.log + +[program:geoproxy] +command=python /app/bin/geo-proxy.py +stderr_logfile=/var/log/supervisor/geoproxy-stderr.log +stdout_logfile=/var/log/supervisor/geoproxy-stdout.log + +[include] +files=/etc/supervisor/conf.d/*.conf \ No newline at end of file diff --git a/we-lang.go b/we-lang.go new file mode 100644 index 0000000..a70c0e4 --- /dev/null +++ b/we-lang.go @@ -0,0 +1,1181 @@ +package main + +import ( + "bytes" + _ "crypto/sha512" + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "log" + "math" + "net/http" + "net/url" + "os" + "os/user" + "path" + "regexp" + "strconv" + "strings" + "time" + "unicode/utf8" + + "github.com/mattn/go-colorable" + "github.com/klauspost/lctime" + "github.com/mattn/go-runewidth" +) + +type configuration struct { + APIKey string + City string + Numdays int + Imperial bool + WindUnit bool + Inverse bool + Lang string + Narrow bool + LocationName string + WindMS bool + RightToLeft bool +} + +type cond struct { + ChanceOfRain string `json:"chanceofrain"` + FeelsLikeC int `json:",string"` + PrecipMM float32 `json:"precipMM,string"` + TempC int `json:"tempC,string"` + TempC2 int `json:"temp_C,string"` + Time int `json:"time,string"` + VisibleDistKM int `json:"visibility,string"` + WeatherCode int `json:"weatherCode,string"` + WeatherDesc []struct{ Value string } + WindGustKmph int `json:",string"` + Winddir16Point string + WindspeedKmph int `json:"windspeedKmph,string"` +} + +type astro struct { + Moonrise string + Moonset string + Sunrise string + Sunset string +} + +type weather struct { + Astronomy []astro + Date string + Hourly []cond + MaxtempC int `json:"maxtempC,string"` + MintempC int `json:"mintempC,string"` +} + +type loc struct { + Query string `json:"query"` + Type string `json:"type"` +} + +type resp struct { + Data struct { + Cur []cond `json:"current_condition"` + Err []struct{ Msg string } `json:"error"` + Req []loc `json:"request"` + Weather []weather `json:"weather"` + } `json:"data"` +} + +var ( + ansiEsc *regexp.Regexp + config configuration + configpath string + debug bool + windDir = map[string]string{ + "N": "\033[1m↓\033[0m", + "NNE": "\033[1m↓\033[0m", + "NE": "\033[1m↙\033[0m", + "ENE": "\033[1m↙\033[0m", + "E": "\033[1m←\033[0m", + "ESE": "\033[1m←\033[0m", + "SE": "\033[1m↖\033[0m", + "SSE": "\033[1m↖\033[0m", + "S": "\033[1m↑\033[0m", + "SSW": "\033[1m↑\033[0m", + "SW": "\033[1m↗\033[0m", + "WSW": "\033[1m↗\033[0m", + "W": "\033[1m→\033[0m", + "WNW": "\033[1m→\033[0m", + "NW": "\033[1m↘\033[0m", + "NNW": "\033[1m↘\033[0m", + } + unitRain = map[bool]string{ + false: "mm", + true: "in", + } + unitTemp = map[bool]string{ + false: "C", + true: "F", + } + unitVis = map[bool]string{ + false: "km", + true: "mi", + } + unitWind = map[int]string{ + 0: "km/h", + 1: "mph", + 2: "m/s", + } + slotTimes = [slotcount]int{9 * 60, 12 * 60, 18 * 60, 22 * 60} + codes = map[int][]string{ + 113: iconSunny, + 116: iconPartlyCloudy, + 119: iconCloudy, + 122: iconVeryCloudy, + 143: iconFog, + 176: iconLightShowers, + 179: iconLightSleetShowers, + 182: iconLightSleet, + 185: iconLightSleet, + 200: iconThunderyShowers, + 227: iconLightSnow, + 230: iconHeavySnow, + 248: iconFog, + 260: iconFog, + 263: iconLightShowers, + 266: iconLightRain, + 281: iconLightSleet, + 284: iconLightSleet, + 293: iconLightRain, + 296: iconLightRain, + 299: iconHeavyShowers, + 302: iconHeavyRain, + 305: iconHeavyShowers, + 308: iconHeavyRain, + 311: iconLightSleet, + 314: iconLightSleet, + 317: iconLightSleet, + 320: iconLightSnow, + 323: iconLightSnowShowers, + 326: iconLightSnowShowers, + 329: iconHeavySnow, + 332: iconHeavySnow, + 335: iconHeavySnowShowers, + 338: iconHeavySnow, + 350: iconLightSleet, + 353: iconLightShowers, + 356: iconHeavyShowers, + 359: iconHeavyRain, + 362: iconLightSleetShowers, + 365: iconLightSleetShowers, + 368: iconLightSnowShowers, + 371: iconHeavySnowShowers, + 374: iconLightSleetShowers, + 377: iconLightSleet, + 386: iconThunderyShowers, + 389: iconThunderyHeavyRain, + 392: iconThunderySnowShowers, + 395: iconHeavySnowShowers, + } + + iconUnknown = []string{ + " .-. ", + " __) ", + " ( ", + " `-’ ", + " • "} + iconSunny = []string{ + "\033[38;5;226m \\ / \033[0m", + "\033[38;5;226m .-. \033[0m", + "\033[38;5;226m ― ( ) ― \033[0m", + "\033[38;5;226m `-’ \033[0m", + "\033[38;5;226m / \\ \033[0m"} + iconPartlyCloudy = []string{ + "\033[38;5;226m \\ /\033[0m ", + "\033[38;5;226m _ /\"\"\033[38;5;250m.-. \033[0m", + "\033[38;5;226m \\_\033[38;5;250m( ). \033[0m", + "\033[38;5;226m /\033[38;5;250m(___(__) \033[0m", + " "} + iconCloudy = []string{ + " ", + "\033[38;5;250m .--. \033[0m", + "\033[38;5;250m .-( ). \033[0m", + "\033[38;5;250m (___.__)__) \033[0m", + " "} + iconVeryCloudy = []string{ + " ", + "\033[38;5;240;1m .--. \033[0m", + "\033[38;5;240;1m .-( ). \033[0m", + "\033[38;5;240;1m (___.__)__) \033[0m", + " "} + iconLightShowers = []string{ + "\033[38;5;226m _`/\"\"\033[38;5;250m.-. \033[0m", + "\033[38;5;226m ,\\_\033[38;5;250m( ). \033[0m", + "\033[38;5;226m /\033[38;5;250m(___(__) \033[0m", + "\033[38;5;111m ‘ ‘ ‘ ‘ \033[0m", + "\033[38;5;111m ‘ ‘ ‘ ‘ \033[0m"} + iconHeavyShowers = []string{ + "\033[38;5;226m _`/\"\"\033[38;5;240;1m.-. \033[0m", + "\033[38;5;226m ,\\_\033[38;5;240;1m( ). \033[0m", + "\033[38;5;226m /\033[38;5;240;1m(___(__) \033[0m", + "\033[38;5;21;1m ‚‘‚‘‚‘‚‘ \033[0m", + "\033[38;5;21;1m ‚’‚’‚’‚’ \033[0m"} + iconLightSnowShowers = []string{ + "\033[38;5;226m _`/\"\"\033[38;5;250m.-. \033[0m", + "\033[38;5;226m ,\\_\033[38;5;250m( ). \033[0m", + "\033[38;5;226m /\033[38;5;250m(___(__) \033[0m", + "\033[38;5;255m * * * \033[0m", + "\033[38;5;255m * * * \033[0m"} + iconHeavySnowShowers = []string{ + "\033[38;5;226m _`/\"\"\033[38;5;240;1m.-. \033[0m", + "\033[38;5;226m ,\\_\033[38;5;240;1m( ). \033[0m", + "\033[38;5;226m /\033[38;5;240;1m(___(__) \033[0m", + "\033[38;5;255;1m * * * * \033[0m", + "\033[38;5;255;1m * * * * \033[0m"} + iconLightSleetShowers = []string{ + "\033[38;5;226m _`/\"\"\033[38;5;250m.-. \033[0m", + "\033[38;5;226m ,\\_\033[38;5;250m( ). \033[0m", + "\033[38;5;226m /\033[38;5;250m(___(__) \033[0m", + "\033[38;5;111m ‘ \033[38;5;255m*\033[38;5;111m ‘ \033[38;5;255m* \033[0m", + "\033[38;5;255m *\033[38;5;111m ‘ \033[38;5;255m*\033[38;5;111m ‘ \033[0m"} + iconThunderyShowers = []string{ + "\033[38;5;226m _`/\"\"\033[38;5;250m.-. \033[0m", + "\033[38;5;226m ,\\_\033[38;5;250m( ). \033[0m", + "\033[38;5;226m /\033[38;5;250m(___(__) \033[0m", + "\033[38;5;228;5m ⚡\033[38;5;111;25m‘‘\033[38;5;228;5m⚡\033[38;5;111;25m‘‘ \033[0m", + "\033[38;5;111m ‘ ‘ ‘ ‘ \033[0m"} + iconThunderyHeavyRain = []string{ + "\033[38;5;240;1m .-. \033[0m", + "\033[38;5;240;1m ( ). \033[0m", + "\033[38;5;240;1m (___(__) \033[0m", + "\033[38;5;21;1m ‚‘\033[38;5;228;5m⚡\033[38;5;21;25m‘‚\033[38;5;228;5m⚡\033[38;5;21;25m‚‘ \033[0m", + "\033[38;5;21;1m ‚’‚’\033[38;5;228;5m⚡\033[38;5;21;25m’‚’ \033[0m"} + iconThunderySnowShowers = []string{ + "\033[38;5;226m _`/\"\"\033[38;5;250m.-. \033[0m", + "\033[38;5;226m ,\\_\033[38;5;250m( ). \033[0m", + "\033[38;5;226m /\033[38;5;250m(___(__) \033[0m", + "\033[38;5;255m *\033[38;5;228;5m⚡\033[38;5;255;25m*\033[38;5;228;5m⚡\033[38;5;255;25m* \033[0m", + "\033[38;5;255m * * * \033[0m"} + iconLightRain = []string{ + "\033[38;5;250m .-. \033[0m", + "\033[38;5;250m ( ). \033[0m", + "\033[38;5;250m (___(__) \033[0m", + "\033[38;5;111m ‘ ‘ ‘ ‘ \033[0m", + "\033[38;5;111m ‘ ‘ ‘ ‘ \033[0m"} + iconHeavyRain = []string{ + "\033[38;5;240;1m .-. \033[0m", + "\033[38;5;240;1m ( ). \033[0m", + "\033[38;5;240;1m (___(__) \033[0m", + "\033[38;5;21;1m ‚‘‚‘‚‘‚‘ \033[0m", + "\033[38;5;21;1m ‚’‚’‚’‚’ \033[0m"} + iconLightSnow = []string{ + "\033[38;5;250m .-. \033[0m", + "\033[38;5;250m ( ). \033[0m", + "\033[38;5;250m (___(__) \033[0m", + "\033[38;5;255m * * * \033[0m", + "\033[38;5;255m * * * \033[0m"} + iconHeavySnow = []string{ + "\033[38;5;240;1m .-. \033[0m", + "\033[38;5;240;1m ( ). \033[0m", + "\033[38;5;240;1m (___(__) \033[0m", + "\033[38;5;255;1m * * * * \033[0m", + "\033[38;5;255;1m * * * * \033[0m"} + iconLightSleet = []string{ + "\033[38;5;250m .-. \033[0m", + "\033[38;5;250m ( ). \033[0m", + "\033[38;5;250m (___(__) \033[0m", + "\033[38;5;111m ‘ \033[38;5;255m*\033[38;5;111m ‘ \033[38;5;255m* \033[0m", + "\033[38;5;255m *\033[38;5;111m ‘ \033[38;5;255m*\033[38;5;111m ‘ \033[0m"} + iconFog = []string{ + " ", + "\033[38;5;251m _ - _ - _ - \033[0m", + "\033[38;5;251m _ - _ - _ \033[0m", + "\033[38;5;251m _ - _ - _ - \033[0m", + " "} + + locale = map[string]string{ + "af": "af_ZA", + "ar": "ar_TN", + "az": "az_AZ", + "be": "be_BY", + "bg": "bg_BG", + "bs": "bs_BA", + "ca": "ca_ES", + "cs": "cs_CZ", + "cy": "cy_GB", + "da": "da_DK", + "de": "de_DE", + "el": "el_GR", + "eo": "eo", + "es": "es_ES", + "et": "et_EE", + "fa": "fa_IR", + "fi": "fi_FI", + "fr": "fr_FR", + "fy": "fy_NL", + "ga": "ga_IE", + "he": "he_IL", + "hr": "hr_HR", + "hu": "hu_HU", + "hy": "hy_AM", + "id": "id_ID", + "is": "is_IS", + "it": "it_IT", + "ja": "ja_JP", + "jv": "en_US", + "ka": "ka_GE", + "ko": "ko_KR", + "kk": "kk_KZ", + "ky": "ky_KG", + "lt": "lt_LT", + "lv": "lv_LV", + "mk": "mk_MK", + "ml": "ml_IN", + "nb": "nb_NO", + "nl": "nl_NL", + "nn": "nn_NO", + "pt": "pt_PT", + "pt-br":"pt_BR", + "pl": "pl_PL", + "ro": "ro_RO", + "ru": "ru_RU", + "sv": "sv_SE", + "sk": "sk_SK", + "sl": "sl_SI", + "sr": "sr_RS", + "sr-lat": "sr_RS@latin", + "sw": "sw_KE", + "th": "th_TH", + "tr": "tr_TR", + "uk": "uk_UA", + "uz": "uz_UZ", + "vi": "vi_VN", + "zu": "zu_ZA", + "zh": "zh_CN", + "zh-cn":"zh_CN", + "zh-tw":"zh_TW", + } + + localizedCaption = map[string]string{ + "af": "Weer verslag vir:", + "ar": "تقرير حالة ألطقس", + "az": "Hava proqnozu:", + "be": "Прагноз надвор'я для:", + "bg": "Прогноза за времето в:", + "bs": "Vremenske prognoze za:", + "ca": "Informe del temps per a:", + "cs": "Předpověď počasí pro:", + "cy": "Adroddiad tywydd ar gyfer:", + "da": "Vejret i:", + "de": "Wetterbericht für:", + "el": "Πρόγνωση καιρού για:", + "eo": "Veterprognozo por:", + "es": "El tiempo en:", + "et": "Ilmaprognoos:", + "fa": "اوه و بآ تیعضو شرازگ", + "fi": "Säätiedotus:", + "fr": "Prévisions météo pour:", + "fy": "Waarberjocht foar:", + "ga": "Réamhaisnéis na haimsire do:", + "he": ":ריוואה גזמ תיזחת", + "hr": "Vremenska prognoza za:", + "hu": "Időjárás előrejelzés:", + "hy": "Եղանակի տեսություն:", + "id": "Prakiraan cuaca:", + "it": "Previsioni meteo:", + "is": "Veðurskýrsla fyrir:", + "ja": "天気予報:", + "jv": "Weather forecast for:", + "ka": "ამინდის პროგნოზი:", + "kk": "Ауа райы:", + "ko": "일기 예보:", + "ky": "Аба ырайы:", + "lt": "Orų prognozė:", + "lv": "Laika ziņas:", + "mk": "Прогноза за времето во:", + "ml": "കാലാവസ്ഥ റിപ്പോർട്ട്:", + "nb": "Værmelding for:", + "nl": "Weerbericht voor:", + "nn": "Vêrmelding for:", + "pl": "Pogoda w:", + "pt": "Previsão do tempo para:", + "pt-br": "Previsão do tempo para:", + "ro": "Prognoza meteo pentru:", + "ru": "Прогноз погоды:", + "sk": "Predpoveď počasia pre:", + "sl": "Vremenska napoved za", + "sr": "Временска прогноза за:", + "sr-lat":"Vremenska prognoza za:", + "sv": "Väderleksprognos för:", + "sw": "Ripoti ya hali ya hewa, jiji la:", + "te": "వాతావరణ సమాచారము:", + "th": "รายงานสภาพอากาศ:", + "tr": "Hava beklentisi:", + "uk": "Прогноз погоди для:", + "uz": "Ob-havo bashorati:", + "vi": "Báo cáo thời tiết:", + "zu": "Isimo sezulu:", + "zh": "天气预报:", + "zh-cn": "天气预报:", + "zh-tw": "天氣預報:", + } + + daytimeTranslation = map[string][]string{ + "af":{"Oggend","Middag", "Vroegaand", "Laatnag"}, + "ar":{"صباح", "ظهر", "مساء", "ليل" }, + "az":{"Səhər", "Gün", "Axşam", "Gecə" }, + "be":{"Раніца", "Дзень", "Вечар", "Ноч" }, + "bg":{"Сутрин", "Обяд", "Вечер", "Нощ" }, + "bs":{"Ujutro", "Dan", "Večer", "Noć" }, + "cs":{"Ráno", "Ve dne", "Večer", "V noci"}, + "ca":{"Matí", "Dia", "Tarda", "Nit" }, + "cy":{"Bore", "Dydd", "Hwyr", "Nos" }, + "da":{"Morgen", "Middag", "Aften", "Nat" }, + "de":{"Früh", "Mittag", "Abend", "Nacht" }, + "el":{"Πρωί", "Μεσημέρι", "Απόγευμα", "Βράδυ" }, + "en":{"Morning","Noon", "Evening", "Night" }, + "eo":{"Mateno", "Tago", "Vespero", "Nokto" }, + "es":{"Mañana", "Día", "Tarde", "Noche" }, + "et":{"Hommik", "Päev", "Õhtu", "Öösel" }, + "fa":{ "حبص", "رهظ", "رصع", "بش" }, + "fi":{"Aamu", "Keskipäivä", "Ilta", "Yö" }, + "fr":{"Matin", "Après-midi", "Soir", "Nuit" }, + "fy":{"Moarns", "Middeis", "Jûns", "Nachts"}, + "ga":{"Maidin", "Nóin", "Tráthnóna","Oíche"}, + "he":{ "רקוב", "םוֹיְ", "ברֶעֶ", "הלָיְלַ" }, + "hr":{"Jutro", "Dan", "Večer", "Noć" }, + "hu":{"Reggel", "Dél", "Este", "Éjszaka"}, + "hy":{"Առավոտ", "Կեսօր", "Երեկո", "Գիշեր" }, + "id":{"Pagi", "Hari", "Petang", "Malam" }, + "it":{"Mattina","Pomeriggio", "Sera", "Notte" }, + "is":{"Morgunn","Dagur", "Kvöld", "Nótt" }, + "ja":{"朝", "昼", "夕", "夜" }, + "jv":{"Morning","Noon", "Evening", "Night" }, + "ka":{"დილა", "დღე", "საღამო", "ღამე" }, + "kk":{"Таң", "Күндіз", "Кеш", "Түн" }, + "ko":{"아침", "낮", "저녁", "밤" }, + "ky":{"Эртең", "Күн", "Кеч", "Түн" }, + "lt":{"Rytas", "Diena", "Vakaras", "Naktis"}, + "lv":{"Rīts", "Diena", "Vakars", "Nakts" }, + "mk":{"Утро", "Пладне", "Вечер", "Ноќ" }, + "ml":{ "മോണിംഗ്", "മധ്യാഹ്നം", "വൈകുന്നേരം", "രാത്രി",}, + "nl":{"'s Ochtends","'s Middags", "'s Avonds", "'s Nachts"}, + "nb":{"Morgen", "Middag", "Kveld", "Natt" }, + "nn":{"Morgon", "Middag", "Kveld", "Natt" }, + "pl":{"Ranek", "Dzień", "Wieczór", "Noc" }, + "pt":{"Manhã", "Meio-dia", "Tarde", "Noite" }, + "pt-br":{"Manhã", "Meio-dia", "Tarde", "Noite" }, + "ro":{"Dimineaţă","Amiază", "Seară", "Noapte"}, + "ru":{"Утро", "День", "Вечер", "Ночь" }, + "sk":{"Ráno", "Cez deň", "Večer", "V noci"}, + "sl":{"Jutro", "Dan", "Večer", "Noč" }, + "sr":{"Јутро", "Подне", "Вече", "Ноћ" }, + "sr-lat":{"Jutro", "Podne", "Veče", "Noć" }, + "sv":{"Morgon", "Eftermiddag","Kväll", "Natt" }, + "sw":{"Asubuhi","Adhuhuri", "Jioni", "Usiku" }, + "te":{"ఉదయం", "రోజు", "సాయంత్రం", "రాత్రి" }, + "th":{"เช้า", "วัน", "เย็น", "คืน" }, + "tr":{"Sabah", "Öğle", "Akşam", "Gece" }, + "uk":{"Ранок", "День", "Вечір", "Ніч" }, + "uz":{"Ertalab","Kunduzi", "Kechqurun","Kecha" }, + "vi":{"Sáng", "Trưa", "Chiều", "Tối" }, + "zh": {"早上", "中午", "傍晚", "夜间" }, + "zh-cn":{"早上", "中午", "傍晚", "夜间" }, + "zh-tw":{"早上", "中午", "傍晚", "夜間" }, + "zu":{"Morning","Noon", "Evening", "Night" }, + } +) + +// Add this languages: +// da tr hu sr jv zu +// More languages: https://developer.worldweatheronline.com/api/multilingual.aspx + +// const ( +// wuri = "https://api.worldweatheronline.com/premium/v1/weather.ashx?" +// suri = "https://api.worldweatheronline.com/premium/v1/search.ashx?" +// slotcount = 4 +// ) + +const ( + wuri = "http://127.0.0.1:5001/premium/v1/weather.ashx?" + suri = "http://127.0.0.1:5001/premium/v1/search.ashx?" + slotcount = 4 +) + +func configload() error { + b, err := ioutil.ReadFile(configpath) + if err == nil { + return json.Unmarshal(b, &config) + } + return err +} + +func configsave() error { + j, err := json.MarshalIndent(config, "", "\t") + if err == nil { + return ioutil.WriteFile(configpath, j, 0600) + } + return err +} + +func pad(s string, mustLen int) (ret string) { + ret = s + realLen := utf8.RuneCountInString(ansiEsc.ReplaceAllLiteralString(s, "")) + delta := mustLen - realLen + if delta > 0 { + if config.RightToLeft { + ret = strings.Repeat(" ", delta) + ret + "\033[0m" + } else { + ret += "\033[0m" + strings.Repeat(" ", delta) + } + } else if delta < 0 { + toks := ansiEsc.Split(s, 2) + tokLen := utf8.RuneCountInString(toks[0]) + esc := ansiEsc.FindString(s) + if tokLen > mustLen { + ret = fmt.Sprintf("%.*s\033[0m", mustLen, toks[0]) + } else { + ret = fmt.Sprintf("%s%s%s", toks[0], esc, pad(toks[1], mustLen-tokLen)) + } + } + return +} + +func formatTemp(c cond) string { + color := func(temp int, explicit_plus bool) string { + var col = 0 + if !config.Inverse { + col = 21 + switch temp { + case -15, -14, -13: + col = 27 + case -12, -11, -10: + col = 33 + case -9, -8, -7: + col = 39 + case -6, -5, -4: + col = 45 + case -3, -2, -1: + col = 51 + case 0, 1: + col = 50 + case 2, 3: + col = 49 + case 4, 5: + col = 48 + case 6, 7: + col = 47 + case 8, 9: + col = 46 + case 10, 11, 12: + col = 82 + case 13, 14, 15: + col = 118 + case 16, 17, 18: + col = 154 + case 19, 20, 21: + col = 190 + case 22, 23, 24: + col = 226 + case 25, 26, 27: + col = 220 + case 28, 29, 30: + col = 214 + case 31, 32, 33: + col = 208 + case 34, 35, 36: + col = 202 + default: + if temp > 0 { + col = 196 + } + } + } else { + col = 16 + switch temp { + case -15, -14, -13: + col = 17 + case -12, -11, -10: + col = 18 + case -9, -8, -7: + col = 19 + case -6, -5, -4: + col = 20 + case -3, -2, -1: + col = 21 + case 0, 1: + col = 30 + case 2, 3: + col = 28 + case 4, 5: + col = 29 + case 6, 7: + col = 30 + case 8, 9: + col = 34 + case 10, 11, 12: + col = 35 + case 13, 14, 15: + col = 36 + case 16, 17, 18: + col = 40 + case 19, 20, 21: + col = 59 + case 22, 23, 24: + col = 100 + case 25, 26, 27: + col = 101 + case 28, 29, 30: + col = 94 + case 31, 32, 33: + col = 166 + case 34, 35, 36: + col = 52 + default: + if temp > 0 { + col = 196 + } + } + } + if config.Imperial { + temp = (temp*18 + 320) / 10 + } + if explicit_plus { + return fmt.Sprintf("\033[38;5;%03dm+%d\033[0m", col, temp) + } else { + return fmt.Sprintf("\033[38;5;%03dm%d\033[0m", col, temp) + } + } + t := c.TempC + if t == 0 { + t = c.TempC2 + } + + hyphen := " - " + // if (config.Lang == "sl") { + // hyphen = "-" + // } + hyphen = ".." + + explicit_plus := false + if c.FeelsLikeC < t { + if c.FeelsLikeC < 0 && t > 0 { + explicit_plus = true + } + return pad(fmt.Sprintf("%s%s%s °%s", color(c.FeelsLikeC, false), hyphen, color(t, explicit_plus), unitTemp[config.Imperial]), 15) + } else if c.FeelsLikeC > t { + if t < 0 && c.FeelsLikeC > 0 { + explicit_plus = true + } + return pad(fmt.Sprintf("%s%s%s °%s", color(t, false), hyphen, color(c.FeelsLikeC, explicit_plus), unitTemp[config.Imperial]), 15) + } + return pad(fmt.Sprintf("%s °%s", color(c.FeelsLikeC, false), unitTemp[config.Imperial]), 15) +} + +func formatWind(c cond) string { + windInRightUnits := func(spd int) int { + if config.WindMS { + spd = (spd * 1000) / 3600 + } else { + if config.Imperial { + spd = (spd * 1000) / 1609 + } + } + return spd + } + color := func(spd int) string { + var col = 46 + switch spd { + case 1, 2, 3: + col = 82 + case 4, 5, 6: + col = 118 + case 7, 8, 9: + col = 154 + case 10, 11, 12: + col = 190 + case 13, 14, 15: + col = 226 + case 16, 17, 18, 19: + col = 220 + case 20, 21, 22, 23: + col = 214 + case 24, 25, 26, 27: + col = 208 + case 28, 29, 30, 31: + col = 202 + default: + if spd > 0 { + col = 196 + } + } + spd = windInRightUnits(spd) + + return fmt.Sprintf("\033[38;5;%03dm%d\033[0m", col, spd) + } + + unitWindString := unitWind[0] + if config.WindMS { + unitWindString = unitWind[2] + } else { + if config.Imperial { + unitWindString = unitWind[1] + } + } + + hyphen := " - " + // if (config.Lang == "sl") { + // hyphen = "-" + // } + hyphen = "-" + + cWindGustKmph := fmt.Sprintf("%s", color(c.WindGustKmph)) + cWindspeedKmph := fmt.Sprintf("%s", color(c.WindspeedKmph)) + if windInRightUnits(c.WindGustKmph) > windInRightUnits(c.WindspeedKmph) { + return pad(fmt.Sprintf("%s %s%s%s %s", windDir[c.Winddir16Point], cWindspeedKmph, hyphen, cWindGustKmph, unitWindString), 15) + } + return pad(fmt.Sprintf("%s %s %s", windDir[c.Winddir16Point], cWindspeedKmph, unitWindString), 15) +} + +func formatVisibility(c cond) string { + if config.Imperial { + c.VisibleDistKM = (c.VisibleDistKM * 621) / 1000 + } + return pad(fmt.Sprintf("%d %s", c.VisibleDistKM, unitVis[config.Imperial]), 15) +} + +func formatRain(c cond) string { + rainUnit := float32(c.PrecipMM) + if config.Imperial { + rainUnit = float32(c.PrecipMM) * 0.039 + } + if c.ChanceOfRain != "" { + return pad(fmt.Sprintf("%.1f %s | %s%%", rainUnit, unitRain[config.Imperial], c.ChanceOfRain), 15) + } + return pad(fmt.Sprintf("%.1f %s", rainUnit, unitRain[config.Imperial]), 15) +} + +func formatCond(cur []string, c cond, current bool) (ret []string) { + var icon []string + if i, ok := codes[c.WeatherCode]; !ok { + icon = iconUnknown + } else { + icon = i + } + if config.Inverse { + // inverting colors + for i, _ := range icon { + icon[i] = strings.Replace(icon[i], "38;5;226", "38;5;94", -1) + icon[i] = strings.Replace(icon[i], "38;5;250", "38;5;243", -1) + icon[i] = strings.Replace(icon[i], "38;5;21", "38;5;18", -1) + icon[i] = strings.Replace(icon[i], "38;5;255", "38;5;245", -1) + icon[i] = strings.Replace(icon[i], "38;5;111", "38;5;63", -1) + icon[i] = strings.Replace(icon[i], "38;5;251", "38;5;238", -1) + } + } + //desc := fmt.Sprintf("%-15.15v", c.WeatherDesc[0].Value) + desc := c.WeatherDesc[0].Value + if config.RightToLeft { + for runewidth.StringWidth(desc) < 15 { + desc = " " + desc + } + for runewidth.StringWidth(desc) > 15 { + _, size := utf8.DecodeLastRuneInString(desc) + desc = desc[size:len(desc)] + } + } else { + for runewidth.StringWidth(desc) < 15 { + desc += " " + } + for runewidth.StringWidth(desc) > 15 { + _, size := utf8.DecodeLastRuneInString(desc) + desc = desc[:len(desc)-size] + } + } + if current { + if config.RightToLeft { + desc = c.WeatherDesc[0].Value + if runewidth.StringWidth(desc) < 15 { + desc = strings.Repeat(" ", 15 - runewidth.StringWidth(desc)) + desc + } + } else { + desc = c.WeatherDesc[0].Value + } + } else { + if config.RightToLeft { + if frstRune, size := utf8.DecodeRuneInString(desc); frstRune != ' ' { + desc = "…" + desc[size:len(desc)] + for runewidth.StringWidth(desc) < 15 { + desc = " " + desc + } + } + } else { + if lastRune, size := utf8.DecodeLastRuneInString(desc); lastRune != ' ' { + desc = desc[:len(desc)-size] + "…" + //for numberOfSpaces < runewidth.StringWidth(fmt.Sprintf("%c", lastRune)) - 1 { + for runewidth.StringWidth(desc) < 15 { + desc = desc + " " + } + } + } + } + if (config.RightToLeft) { + ret = append(ret, fmt.Sprintf("%v %v %v", cur[0], desc, icon[0])) + ret = append(ret, fmt.Sprintf("%v %v %v", cur[1], formatTemp(c), icon[1])) + ret = append(ret, fmt.Sprintf("%v %v %v", cur[2], formatWind(c), icon[2])) + ret = append(ret, fmt.Sprintf("%v %v %v", cur[3], formatVisibility(c), icon[3])) + ret = append(ret, fmt.Sprintf("%v %v %v", cur[4], formatRain(c), icon[4])) + } else { + ret = append(ret, fmt.Sprintf("%v %v %v", cur[0], icon[0], desc)) + ret = append(ret, fmt.Sprintf("%v %v %v", cur[1], icon[1], formatTemp(c))) + ret = append(ret, fmt.Sprintf("%v %v %v", cur[2], icon[2], formatWind(c))) + ret = append(ret, fmt.Sprintf("%v %v %v", cur[3], icon[3], formatVisibility(c))) + ret = append(ret, fmt.Sprintf("%v %v %v", cur[4], icon[4], formatRain(c))) + } + return +} + +func justifyCenter(s string, width int) string { + appendSide := 0 + for runewidth.StringWidth(s) <= width { + if appendSide == 1 { + s = s + " " + appendSide = 0 + } else { + s = " " + s + appendSide = 1 + } + } + return s +} + +func reverse(s string) string { + r := []rune(s) + for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 { + r[i], r[j] = r[j], r[i] + } + return string(r) +} + +func printDay(w weather) (ret []string) { + hourly := w.Hourly + ret = make([]string, 5) + for i := range ret { + ret[i] = "│" + } + + // find hourly data which fits the desired times of day best + var slots [slotcount]cond + for _, h := range hourly { + c := int(math.Mod(float64(h.Time), 100)) + 60*(h.Time/100) + for i, s := range slots { + if math.Abs(float64(c-slotTimes[i])) < math.Abs(float64(s.Time-slotTimes[i])) { + h.Time = c + slots[i] = h + } + } + } + + if config.RightToLeft { + slots[0], slots[3] = slots[3], slots[0] + slots[1], slots[2] = slots[2], slots[1] + } + + for i, s := range slots { + if config.Narrow { + if i == 0 || i == 2 { + continue; + } + } + ret = formatCond(ret, s, false) + for i := range ret { + ret[i] = ret[i] + "│" + } + } + + d, _ := time.Parse("2006-01-02", w.Date) + // dateFmt := "┤ " + d.Format("Mon 02. Jan") + " ├" + + if val, ok := locale[config.Lang]; ok { + lctime.SetLocale(val) + } else { + lctime.SetLocale("en_US") + } + dateName := "" + if config.RightToLeft { + dow := lctime.Strftime("%a", d) + day := lctime.Strftime("%d", d) + month := lctime.Strftime("%b", d) + dateName = reverse(month) + " " + day + " " + reverse(dow) + } else { + dateName = lctime.Strftime("%a %d %b", d) + if config.Lang == "ko" { + dateName = lctime.Strftime("%b %d일 %a", d) + } + } + // appendSide := 0 + // // for utf8.RuneCountInString(dateName) <= dateWidth { + // for runewidth.StringWidth(dateName) <= dateWidth { + // if appendSide == 1 { + // dateName = dateName + " " + // appendSide = 0 + // } else { + // dateName = " " + dateName + // appendSide = 1 + // } + // } + + dateFmt := "┤" + justifyCenter(dateName, 12) + "├" + + trans := daytimeTranslation["en"] + if t, ok := daytimeTranslation[config.Lang]; ok { + trans = t + } + if (config.Narrow) { + + names := "│ "+ justifyCenter(trans[1], 16) + + "└──────┬──────┘" +justifyCenter(trans[3], 16) + " │" + + ret = append([]string{ + " ┌─────────────┐ ", + "┌───────────────────────" + dateFmt + "───────────────────────┐", + names, + "├──────────────────────────────┼──────────────────────────────┤"}, + ret...) + + return append(ret, + "└──────────────────────────────┴──────────────────────────────┘") + + } else { + + names := "" + if config.RightToLeft { + names = "│"+ justifyCenter(trans[3], 29) + "│ "+ justifyCenter(trans[2], 16) + + "└──────┬──────┘" +justifyCenter(trans[1], 16) + " │"+justifyCenter(trans[0], 29)+"│" + } else { + names = "│"+ justifyCenter(trans[0], 29) + "│ "+ justifyCenter(trans[1], 16) + + "└──────┬──────┘" +justifyCenter(trans[2], 16) + " │"+justifyCenter(trans[3], 29)+"│" + } + + ret = append([]string{ + " ┌─────────────┐ ", + "┌──────────────────────────────┬───────────────────────" + dateFmt + "───────────────────────┬──────────────────────────────┐", + names, + "├──────────────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────┤"}, + ret...) + + return append(ret, + "└──────────────────────────────┴──────────────────────────────┴──────────────────────────────┴──────────────────────────────┘") + } + return +} + +func unmarshalLang(body []byte, r *resp) error { + var rv map[string]interface{} + if err := json.Unmarshal(body, &rv); err != nil { + return err + } + if data, ok := rv["data"].(map[string]interface{}); ok { + if ccs, ok := data["current_condition"].([]interface{}); ok { + for _, cci := range ccs { + cc, ok := cci.(map[string]interface{}) + if !ok { + continue + } + langs, ok := cc["lang_"+config.Lang].([]interface{}) + if !ok || len(langs) == 0 { + continue + } + weatherDesc, ok := cc["weatherDesc"].([]interface{}) + if !ok || len(weatherDesc) == 0 { + continue + } + weatherDesc[0] = langs[0] + } + } + if ws, ok := data["weather"].([]interface{}); ok { + for _, wi := range ws { + w, ok := wi.(map[string]interface{}) + if !ok { + continue + } + if hs, ok := w["hourly"].([]interface{}); ok { + for _, hi := range hs { + h, ok := hi.(map[string]interface{}) + if !ok { + continue + } + langs, ok := h["lang_"+config.Lang].([]interface{}) + if !ok || len(langs) == 0 { + continue + } + weatherDesc, ok := h["weatherDesc"].([]interface{}) + if !ok || len(weatherDesc) == 0 { + continue + } + weatherDesc[0] = langs[0] + } + } + } + } + } + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(rv); err != nil { + return err + } else { + if err = json.NewDecoder(&buf).Decode(r); err != nil { + return err + } + } + return nil +} + +func getDataFromAPI() (ret resp) { + var params []string + + if len(config.APIKey) == 0 { + log.Fatal("No API key specified. Setup instructions are in the README.") + } + params = append(params, "key="+config.APIKey) + + // non-flag shortcut arguments will overwrite possible flag arguments + for _, arg := range flag.Args() { + if v, err := strconv.Atoi(arg); err == nil && len(arg) == 1 { + config.Numdays = v + } else { + config.City = arg + } + } + + if len(config.City) > 0 { + params = append(params, "q="+url.QueryEscape(config.City)) + } + params = append(params, "format=json") + params = append(params, "num_of_days="+strconv.Itoa(config.Numdays)) + params = append(params, "tp=3") + if config.Lang != "" { + params = append(params, "lang="+config.Lang) + } + + if debug { + fmt.Fprintln(os.Stderr, params) + } + + res, err := http.Get(wuri + strings.Join(params, "&")) + if err != nil { + log.Fatal(err) + } + defer res.Body.Close() + body, err := ioutil.ReadAll(res.Body) + if err != nil { + log.Fatal(err) + } + + if debug { + var out bytes.Buffer + json.Indent(&out, body, "", " ") + out.WriteTo(os.Stderr) + fmt.Println("\n") + } + + if config.Lang == "" { + if err = json.Unmarshal(body, &ret); err != nil { + log.Println(err) + } + } else { + if err = unmarshalLang(body, &ret); err != nil { + log.Println(err) + } + } + return +} + +func init() { + flag.IntVar(&config.Numdays, "days", 3, "Number of days of weather forecast to be displayed") + flag.StringVar(&config.Lang, "lang", "en", "Language of the report") + flag.StringVar(&config.City, "city", "New York", "City to be queried") + flag.BoolVar(&debug, "debug", false, "Print out raw json response for debugging purposes") + flag.BoolVar(&config.Imperial, "imperial", false, "Use imperial units") + flag.BoolVar(&config.Inverse, "inverse", false, "Use inverted colors") + flag.BoolVar(&config.Narrow, "narrow", false, "Narrow output (two columns)") + flag.StringVar(&config.LocationName, "location_name", "", "Location name (used in the caption)") + flag.BoolVar(&config.WindMS, "wind_in_ms", false, "Show wind speed in m/s") + flag.BoolVar(&config.RightToLeft, "right_to_left", false, "Right to left script") + configpath = os.Getenv("WEGORC") + if configpath == "" { + usr, err := user.Current() + if err != nil { + log.Fatalf("%v\nYou can set the environment variable WEGORC to point to your config file as a workaround.", err) + } + configpath = path.Join(usr.HomeDir, ".wegorc") + } + config.APIKey = "" + config.Imperial = false + config.Lang = "en" + err := configload() + if _, ok := err.(*os.PathError); ok { + log.Printf("No config file found. Creating %s ...", configpath) + if err2 := configsave(); err2 != nil { + log.Fatal(err2) + } + } else if err != nil { + log.Fatalf("could not parse %v: %v", configpath, err) + } + + ansiEsc = regexp.MustCompile("\033.*?m") +} + +func main() { + flag.Parse() + + r := getDataFromAPI() + + if r.Data.Req == nil || len(r.Data.Req) < 1 { + if r.Data.Err != nil && len(r.Data.Err) >= 1 { + log.Fatal(r.Data.Err[0].Msg) + } + log.Fatal("Malformed response.") + } + locationName := r.Data.Req[0].Query + if config.LocationName != "" { + locationName = config.LocationName + } + if config.Lang == "he" || config.Lang == "ar" || config.Lang == "fa" { + config.RightToLeft = true + } + if caption, ok := localizedCaption[config.Lang]; !ok { + // r.Data.Req[0].Type, + fmt.Printf("Weather report: %s\n\n", locationName) + } else { + if (config.RightToLeft) { + caption = locationName + " " + caption + space := strings.Repeat(" ", 125 - runewidth.StringWidth(caption)) + fmt.Printf("%s%s\n\n", space, caption) + } else { + fmt.Printf("%s %s\n\n", caption, locationName) + } + } + stdout := colorable.NewColorableStdout() + + if r.Data.Cur == nil || len(r.Data.Cur) < 1 { + log.Fatal("No weather data available.") + } + out := formatCond(make([]string, 5), r.Data.Cur[0], true) + for _, val := range out { + if config.RightToLeft { + space := strings.Repeat(" ", 93) + fmt.Fprintln(stdout, space, val) + } else { + fmt.Fprintln(stdout, val) + } + } + + if config.Numdays == 0 { + return + } + if r.Data.Weather == nil { + log.Fatal("No detailed weather forecast available.") + } + for _, d := range r.Data.Weather { + for _, val := range printDay(d) { + fmt.Fprintln(stdout, val) + } + } +}