Merge branch 'chubin:master' into master

This commit is contained in:
Sebin Thomas 2024-12-16 10:52:07 +05:30 committed by GitHub
commit 0c96b323f0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
106 changed files with 7363 additions and 2092 deletions

30
.golangci.yaml Normal file
View file

@ -0,0 +1,30 @@
run:
skip-dirs:
- pkg/curlator
linters:
enable-all: true
disable:
- wsl
- wrapcheck
- varnamelen
- gci
- exhaustivestruct
- exhaustruct
- gomnd
- gofmt
# to be fixed:
- ireturn
- gosec
- noctx
- interfacer
# deprecated:
- scopelint
- deadcode
- varcheck
- maligned
- ifshort
- nosnakecase
- structcheck
- golint

View file

@ -3,26 +3,23 @@ FROM golang:1-alpine as builder
WORKDIR /app
COPY ./share/we-lang/we-lang.go /app
COPY ./share/we-lang/go.mod /app
COPY ./share/we-lang/ /app
RUN apk add --no-cache git
RUN go get -u github.com/mattn/go-colorable && \
go get -u github.com/klauspost/lctime && \
go get -u github.com/mattn/go-runewidth && \
CGO_ENABLED=0 go build /app/we-lang.go
# Results in /app/we-lang
cd /app && CGO_ENABLED=0 go build .
FROM alpine:3
# Application stage
FROM alpine:3.16
WORKDIR /app
COPY ./requirements.txt /app
ENV LLVM_CONFIG=/usr/bin/llvm-config
ENV LLVM_CONFIG=/usr/bin/llvm11-config
RUN apk add --no-cache --virtual .build \
autoconf \
@ -30,7 +27,7 @@ RUN apk add --no-cache --virtual .build \
g++ \
gcc \
jpeg-dev \
llvm10-dev\
llvm11-dev\
make \
zlib-dev \
&& apk add --no-cache \
@ -41,7 +38,7 @@ RUN apk add --no-cache --virtual .build \
py3-gevent \
zlib \
jpeg \
llvm10 \
llvm11 \
libtool \
supervisor \
py3-numpy-dev \
@ -54,7 +51,7 @@ RUN apk add --no-cache --virtual .build \
pip install -r requirements.txt --no-cache-dir && \
apk del --no-cache -r .build
COPY --from=builder /app/we-lang /app/bin/we-lang
COPY --from=builder /app/wttr.in /app/bin/wttr.in
COPY ./bin /app/bin
COPY ./lib /app/lib
COPY ./share /app/share
@ -62,7 +59,7 @@ COPY share/docker/supervisord.conf /etc/supervisor/supervisord.conf
ENV WTTR_MYDIR="/app"
ENV WTTR_GEOLITE="/app/GeoLite2-City.mmdb"
ENV WTTR_WEGO="/app/bin/we-lang"
ENV WTTR_WEGO="/app/bin/wttr.in"
ENV WTTR_LISTEN_HOST="0.0.0.0"
ENV WTTR_LISTEN_PORT="8002"

9
Makefile Normal file
View file

@ -0,0 +1,9 @@
srv: srv.go internal/*/*.go internal/*/*/*.go
go build -o srv -ldflags '-w -linkmode external -extldflags "-static"' ./
#go build -o srv ./
go-test:
go test ./...
lint:
golangci-lint run ./...

169
README.md
View file

@ -1,13 +1,17 @@
*wttr.in — the right way to check the weather!*
*wttr.in — the right way to ~check~ `curl` the weather!*
wttr.in is a console-oriented weather forecast service that supports various information
representation methods like terminal-oriented ANSI-sequences for console HTTP clients
(curl, httpie, or wget), HTML for web browsers, or PNG for graphical viewers.
wttr.in uses [wego](http://github.com/schachmat/wego) for visualization
and various data sources for weather forecast information.
Originally started as a small project, a wrapper for [wego](https://github.com/schachmat/wego),
intended to demonstrate the power of the console-oriented services,
*wttr.in* became a popular weather reporting service, handling tens of millions of queries daily.
You can see it running here: [wttr.in](http://wttr.in).
You can see it running here: [wttr.in](https://wttr.in).
[Documentation](https://wttr.in/:help) | [Usage](https://github.com/chubin/wttr.in#usage) | [One-line output](https://github.com/chubin/wttr.in#one-line-output) | [Data-rich output format](https://github.com/chubin/wttr.in#data-rich-output-format-v2) | [Map view](https://github.com/chubin/wttr.in#map-view-v3) | [Output formats](https://github.com/chubin/wttr.in#different-output-formats) | [Moon phases](https://github.com/chubin/wttr.in#moon-phases) | [Internationalization](https://github.com/chubin/wttr.in#internationalization-and-localization) | [Installation](https://github.com/chubin/wttr.in#installation)
## Usage
@ -17,23 +21,20 @@ You can access the service from a shell or from a Web browser like this:
Weather for City: Paris, France
\ / Clear
.-. 10 11 °C
― ( ) ― ↑ 11 km/h
`- 10 km
/ \ 0.0 mm
.-. 10 11 °C
― ( ) ― ↑ 11 km/h
`- 10 km
/ \ 0.0 mm
Here is an actual weather report for your location (it's live!):
Here is an example weather report:
![Weather Report](http://wttr.in/MyLocation.png?)
(It's not your actual location - GitHub's CDN hides your real IP address with its own IP address,
but it's still a live weather report in your language.)
![Weather Report](San_Francisco.png)
Or in PowerShell:
```PowerShell
Invoke-RestMethod http://wttr.in
Invoke-RestMethod https://wttr.in
```
Want to get the weather information for a specific location? You can add the desired location to the URL in your
@ -70,18 +71,25 @@ You can also use IP-addresses (direct) or domain names (prefixed with `@`) to sp
$ curl wttr.in/@github.com
$ curl wttr.in/@msu.ru
To get detailed information online, you can access the [/:help](http://wttr.in/:help) page:
To get detailed information online, you can access the [/:help](https://wttr.in/:help) page:
$ curl wttr.in/:help
### Weather Units
By default the USCS units are used for the queries from the USA and the metric system for the rest of the world.
You can override this behavior by adding `?u` or `?m` to a URL like this:
You can override this behavior by adding `?u`, `?m` or `?M` to a URL like this:
$ curl wttr.in/Amsterdam?u
$ curl wttr.in/Amsterdam?m
$ curl wttr.in/Amsterdam?u # USCS (used by default in US)
$ curl wttr.in/Amsterdam?m # metric (SI) (used by default everywhere except US)
$ curl wttr.in/Amsterdam?M # metric (SI), but show wind speed in m/s
If you have several options to pass, write them without delimiters in between for the one-letter options,
and use `&` as a delimiter for the long options with values:
$ curl 'wttr.in/Amsterdam?m2&lang=nl'
It would be a rough equivalent of `-m2 --lang nl` for the GNU CLI syntax.
## Supported output formats and views
@ -94,7 +102,16 @@ wttr.in currently supports five output formats:
* JSON for scripts and APIs;
* Prometheus metrics for scripts and APIs.
The ANSI and HTML formats are selected basing on the User-Agent string.
The ANSI and HTML formats are selected based on the User-Agent string.
To force plain text, which disables colors:
$ curl wttr.in/?T
To restrict output to glyphs available in standard console fonts (e.g. Consolas and Lucida Console):
$ curl wttr.in/?d
The PNG format can be forced by adding `.png` to the end of the query:
$ wget wttr.in/Paris.png
@ -128,6 +145,9 @@ You can embed a special wttr.in widget, that displays the weather condition for
## One-line output
One-line output format is convenient to be used to show weather info
in status bar of different programs, such as *tmux*, *weechat*, etc.
For one-line output format, specify additional URL parameter `format`:
```
@ -157,24 +177,25 @@ To specify your own custom output format, use the special `%`-notation:
```
c Weather condition,
C Weather condition textual name,
x weather contidion, plain-text symbol,
x Weather condition, plain-text symbol,
h Humidity,
t Temperature (Actual),
f Temperature (Feels Like),
w Wind,
l Location,
m Moonphase 🌑🌒🌓🌔🌕🌖🌗🌘,
M Moonday,
p precipitation (mm/3 hours),
P pressure (hPa),
m Moon phase 🌑🌒🌓🌔🌕🌖🌗🌘,
M Moon day,
p Precipitation (mm/3 hours),
P Pressure (hPa),
u UV index (1-12),
D Dawn*,
S Sunrise*,
z Zenith*,
s Sunset*,
d Dusk*,
T current time*,
Z local timezone.
T Current time*,
Z Local timezone.
(*times are shown in the local timezone)
```
@ -187,7 +208,10 @@ So, these two calls are the same:
$ curl wttr.in/London?format="%l:+%c+%t\n"
London: ⛅️ +7⁰C
```
Keep in mind, that when using in `tmux.conf`, you have to escape `%` with `%`, i.e. write there `%%` instead of `%`.
### tmux
When using in `tmux.conf`, you have to escape `%` with `%`, i.e. write there `%%` instead of `%`.
The output does not contain new line by default, when the %-notation is used, but it does contain it when preconfigured format (`1`,`2`,`3` etc.)
are used. To have the new line in the output when the %-notation is used, use '\n' and single quotes when doing a query from the shell.
@ -203,12 +227,48 @@ set -g status-right "$WEATHER ..."
```
![wttr.in in tmux status bar](https://wttr.in/files/example-tmux-status-line.png)
### WeeChat
To embed in to an IRC ([WeeChat](https://github.com/weechat/weechat)) client's existing status bar:
```
/alias add wttr /exec -pipe "/mute /set plugins.var.wttr" url:wttr.in/Montreal?format=%l:+%c+%f+%h+%p+%P+%m+%w+%S+%s;/wait 3 /item refresh wttr
/trigger add wttr timer 60000;0;0 "" "" "/wttr"
/item add wttr "" "${plugins.var.wttr}"
/eval /set weechat.bar.status.items ${weechat.bar.status.items},spacer,wttr
/eval /set weechat.startup.command_after_plugins ${weechat.startup.command_after_plugins};/wttr
/wttr
```
![wttr.in in WeeChat status bar](https://i.imgur.com/XkYiRU7.png)
### conky
Conky usage example:
```
${texeci 1800 curl wttr.in/kyiv_0pq_lang=uk.png
| convert - -transparent black $HOME/.config/conky/out.png}
${image $HOME/.config/conky/out.png -p 0,0}
```
![wttr.in in conky](https://user-images.githubusercontent.com/3875145/172178453-9e9ed9e3-9815-426a-9a21-afdd6e279fc8.png)
### IRC
IRC integration example:
* https://github.com/OpenSourceTreasure/Mirc-ASCII-weather-translate-pixel-editor
### Emojis support
To see emojis in terminal, you need:
1. Terminal support for emojis (was added to Cairo 1.15.8);
2. Font with emojis support.
For the Emoji font, we recommend *Noto Color Emoji*, and a good alternative option would be the *Emoji One* font;
For the emoji font, we recommend *Noto Color Emoji*, and a good alternative option would be the *Emoji One* font;
both of them support all necessary emoji glyphs.
Font configuration:
@ -249,9 +309,9 @@ cause strange effects similar to that described in #579.
In the experimental data-rich output format, that is available under the view code `v2`,
a lot of additional weather and astronomical information is available:
* Temperature, and precepetation changes forecast throughout the days;
* Temperature, and precipitation changes forecast throughout the days;
* Moonphase for today and the next three days;
* The current weather condition, temperature, humidity, windspeed and direction, pressure;
* The current weather condition, temperature, humidity, wind speed and direction, pressure;
* Timezone;
* Dawn, sunrise, noon, sunset, dusk time for he selected location;
* Precise geographical coordinates for the selected location.
@ -313,7 +373,7 @@ or directly in browser:
The map view currently supports three formats:
* PNG (for browser and messangers);
* PNG (for browser and messengers);
* Sixel (terminal inline images support);
* IIP (terminal with iterm2 inline images protocol support).
@ -327,7 +387,7 @@ Terminal with inline images protocols support:
| mlterm | X11 | yes | Sixel |
| kitty | X11 | yes | Kitty |
| wezterm | X11 | yes | IIP |
| aminal | X11 | yes | Sixel |
| Darktile | X11 | yes | Sixel |
| Jexer | X11 | yes | Sixel |
| GNOME Terminal | X11 | [in-progress](https://gitlab.gnome.org/GNOME/vte/-/issues/253) | Sixel |
| alacritty | X11 | [in-progress](https://github.com/alacritty/alacritty/issues/910) | Sixel |
@ -392,7 +452,7 @@ Most of these values are self-explanatory, aside from `weatherCode`. The `weathe
### Prometheus Metrics Output
The [Prometheus](https://github.com/prometheus/prometheus) Metrics format is a feature providing access to *wttr.in* data through an easy-to-parse format for monitoring systems, without requiring the user to create a complex script to reinterpret wttr.in's graphical output.
The [Prometheus](https://github.com/prometheus/prometheus) Metrics format is a feature providing access to *wttr.in* data through an easy-to-parse format for monitoring systems, without requiring the user to create a complex script to reinterpret wttr.in's graphical output.
To fetch information in Prometheus format, use the following syntax:
@ -429,18 +489,18 @@ in the full-output mode:
$ curl wttr.in/Moon
Get the Moon phase for a particular date by adding `@YYYY-MM-DD`:
Get the moon phase for a particular date by adding `@YYYY-MM-DD`:
$ curl wttr.in/Moon@2016-12-25
The Moon phase information uses [pyphoon](https://github.com/chubin/pyphoon) as its backend.
The moon phase information uses [pyphoon](https://github.com/chubin/pyphoon) as its backend.
To get the moon phase information in the online mode, use `%m`:
$ curl wttr.in/London?format=%m
🌖
Keep in mind that the Unicode representation of moonphases suffers 2 caveats:
Keep in mind that the Unicode representation of moon phases suffers 2 caveats:
- With some fonts, the representation `🌘` is ambiguous, for it either seem
almost-shadowed or almost-lit, depending on whether your terminal is in
@ -492,25 +552,12 @@ The third option is to choose the language using the DNS name used in the query:
wttr.in is currently translated into 54 languages, and the number of supported languages is constantly growing.
See [/:translation](http://wttr.in/:translation) to learn more about the translation process,
See [/:translation](https://wttr.in/:translation) to learn more about the translation process,
to see the list of supported languages and contributors, or to know how you can help to translate wttr.in
in your language.
![Queries to wttr.in in various languages](https://pbs.twimg.com/media/C7hShiDXQAES6z1.jpg)
## Windows Users
There are currently two Windows related issues that prevent the examples found on this page from working exactly as expected out of the box. Until Microsoft fixes the issues, there are a few workarounds. To circumvent both issues you may use a shell such as `bash` on the [Windows Subsystem for Linux (WSL)](https://docs.microsoft.com/en-us/windows/wsl/install-win10) or read on for alternative solutions.
### Garbage characters in the output
There is a limitation of the current Win32 version of `curl`. Until the [Win32 curl issue](https://github.com/chubin/wttr.in/issues/18#issuecomment-474145551) is resolved and rolled out in a future Windows release, it is recommended that you use Powershells `Invoke-Web-Request` command instead:
- `(Invoke-WebRequest http://wttr.in).Content`
### Missing or double wide diagonal wind direction characters
The second issue is regarding the width of the diagonal arrow glyphs that some Windows Terminal Applications such as the default `conhost.exe` use. At the time of writing this, `ConEmu.exe`, `ConEmu64.exe` and Terminal Applications built on top of ConEmu such as Cmder (`cmder.exe`) use these double-wide glyphs by default. The result is the same with all of these programs, either a missing character for certain wind directions or a broken table in the output or both. Some third-party Terminal Applications have addressed the wind direction glyph issue but that fix depends on the font and the Terminal Application you are using.
One way to display the diagonal wind direction glyphs in your Terminal Application is to use [Windows Terminal](https://www.microsoft.com/en-us/p/windows-terminal-preview/9n0dx20hk701?activetab=pivot:overviewtab) which is currently available in the [Microsoft Store](https://www.microsoft.com/en-us/p/windows-terminal-preview/9n0dx20hk701?activetab=pivot:overviewtab). Windows Terminal is currently a preview release and will be rolled out as the default Terminal Application in an upcoming release. If your output is still skewed after using Windows Terminal then try maximizing the terminal window.
Another way you can display the diagonal wind direction is to swap out the problematic characters with forward and backward slashes as shown [here](https://github.com/chubin/wttr.in/issues/18#issuecomment-405640892).
## Installation
To install the application:
@ -530,9 +577,9 @@ wttr.in has the following external dependencies:
* [wego](https://github.com/schachmat/wego), weather client for terminal
After you install [golang](https://golang.org/doc/install), install `wego`:
$ go get -u github.com/schachmat/wego
$ go install github.com/schachmat/wego
```bash
go install github.com/schachmat/wego@latest
```
### Install Python dependencies
@ -554,13 +601,15 @@ You can install most of them using `pip`.
Some python package use LLVM, so install it first:
$ apt-get install llvm-7 llvm-7-dev
```bash
apt-get install llvm-7 llvm-7-dev
```
If `virtualenv` is used:
$ virtualenv -p python3 ve
$ ve/bin/pip3 install -r requirements.txt
$ ve/bin/python3 bin/srv.py
```bash
virtualenv -p python3 ve
ve/bin/pip3 install -r requirements.txt
ve/bin/python3 bin/srv.py
```
Also, you need to install the geoip2 database.
You can use a free database GeoLite2 that can be downloaded from (http://dev.maxmind.com/geoip/geoip2/geolite2/).

BIN
San_Francisco.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View file

@ -36,11 +36,15 @@ MYDIR = os.path.abspath(
os.path.dirname(os.path.dirname('__file__')))
sys.path.append("%s/lib/" % MYDIR)
import proxy_log
import globals
from globals import PROXY_CACHEDIR, PROXY_HOST, PROXY_PORT, USE_METNO, USER_AGENT, MISSING_TRANSLATION_LOG
from metno import create_standard_json_from_metno, metno_request
from translations import PROXY_LANGS
# pylint: enable=wrong-import-position
proxy_logger = proxy_log.LoggerWWO(globals.PROXY_LOG_ACCESS, globals.PROXY_LOG_ERRORS)
def is_testmode():
"""Server is running in the wttr.in test mode"""
@ -90,7 +94,7 @@ def _cache_file(path, query):
digest = hashlib.sha1(("%s %s" % (path, query)).encode("utf-8")).hexdigest()
digest_number = ord(digest[0].upper())
expiry_interval = 60*(digest_number+90)
expiry_interval = 60*(digest_number+180)
timestamp = "%010d" % (int(time.time())//expiry_interval*expiry_interval)
filename = os.path.join(PROXY_CACHEDIR, timestamp, path, query)
@ -133,7 +137,7 @@ def translate(text, lang):
def _log_unknown_translation(lang, text):
with open(MISSING_TRANSLATION_LOG % lang, "a") as f_missing_translation:
f_missing_translation.write(text)
f_missing_translation.write(text+"\n")
if "," in text:
terms = text.split(",")
@ -233,10 +237,11 @@ def _fetch_content_and_headers(path, query_string, **kwargs):
if content is None:
srv = _find_srv_for_query(path, query_string)
url = '%s/%s?%s' % (srv, path, query_string)
url = "%s/%s?%s" % (srv, path, query_string)
attempts = 10
response = None
error = ""
while attempts:
try:
response = requests.get(url, timeout=2, **kwargs)
@ -244,11 +249,19 @@ def _fetch_content_and_headers(path, query_string, **kwargs):
attempts -= 1
continue
try:
json.loads(response.content)
data = json.loads(response.content)
error = data.get("data", {}).get("error", "")
if error:
try:
error = error[0]["msg"]
except (ValueError, IndexError):
error = "invalid error format: %s" % error
break
except ValueError:
attempts -= 1
error = "invalid response"
proxy_logger.log(query_string, error)
_touch_empty_file(path, query_string)
if response:
headers = {}
@ -262,18 +275,8 @@ def _fetch_content_and_headers(path, query_string, **kwargs):
return content, headers
@APP.route("/<path:path>")
def proxy(path):
"""
Main proxy function. Handles incoming HTTP queries.
"""
def _make_query(path, query_string):
lang = request.args.get('lang', 'en')
query_string = request.query_string.decode("utf-8")
query_string = query_string.replace('sr-lat', 'sr')
query_string = query_string.replace('lang=None', 'lang=en')
content = ""
headers = ""
if _is_metno():
path, query, days = metno_request(path, query_string)
if USER_AGENT == '':
@ -288,6 +291,25 @@ def proxy(path):
query_string += "&includelocation=yes"
content, headers = _fetch_content_and_headers(path, query_string)
return content, headers
@APP.route("/<path:path>")
def proxy(path):
"""
Main proxy function. Handles incoming HTTP queries.
"""
lang = request.args.get('lang', 'en')
query_string = request.query_string.decode("utf-8")
query_string = query_string.replace('sr-lat', 'sr')
query_string = query_string.replace('lang=None', 'lang=en')
content = ""
headers = ""
content, headers = _make_query(path, query_string)
# _log_query(path, query_string, error)
content = add_translations(content, lang)
return content, 200, headers
@ -295,6 +317,7 @@ def proxy(path):
if __name__ == "__main__":
#app.run(host='0.0.0.0', port=5001, debug=False)
#app.debug = True
if len(sys.argv) == 1:
bind_addr = "0.0.0.0"
SERVER = WSGIServer((bind_addr, PROXY_PORT), APP)

View file

@ -1,79 +0,0 @@
package main
import (
"log"
"net/http"
"sync"
"time"
"github.com/robfig/cron"
)
var peakRequest30 sync.Map
var peakRequest60 sync.Map
func initPeakHandling() {
c := cron.New()
// cronTime := fmt.Sprintf("%d,%d * * * *", 30-prefetchInterval/60, 60-prefetchInterval/60)
c.AddFunc("24 * * * *", prefetchPeakRequests30)
c.AddFunc("54 * * * *", prefetchPeakRequests60)
c.Start()
}
func savePeakRequest(cacheDigest string, r *http.Request) {
_, min, _ := time.Now().Clock()
if min == 30 {
peakRequest30.Store(cacheDigest, *r)
} else if min == 0 {
peakRequest60.Store(cacheDigest, *r)
}
}
func prefetchRequest(r *http.Request) {
processRequest(r)
}
func syncMapLen(sm *sync.Map) int {
count := 0
f := func(key, value interface{}) bool {
// Not really certain about this part, don't know for sure
// if this is a good check for an entry's existence
if key == "" {
return false
}
count++
return true
}
sm.Range(f)
return count
}
func prefetchPeakRequests(peakRequestMap *sync.Map) {
peakRequestLen := syncMapLen(peakRequestMap)
log.Printf("PREFETCH: Prefetching %d requests\n", peakRequestLen)
if peakRequestLen == 0 {
return
}
sleepBetweenRequests := time.Duration(prefetchInterval*1000/peakRequestLen) * time.Millisecond
peakRequestMap.Range(func(key interface{}, value interface{}) bool {
go func(r http.Request) {
prefetchRequest(&r)
}(value.(http.Request))
peakRequestMap.Delete(key)
time.Sleep(sleepBetweenRequests)
return true
})
}
func prefetchPeakRequests30() {
prefetchPeakRequests(&peakRequest30)
}
func prefetchPeakRequests60() {
prefetchPeakRequests(&peakRequest60)
}

View file

@ -1,147 +0,0 @@
package main
import (
"fmt"
"io/ioutil"
"log"
"math/rand"
"net"
"net/http"
"strings"
"time"
)
func processRequest(r *http.Request) responseWithHeader {
var response responseWithHeader
if dontCache(r) {
return get(r)
}
cacheDigest := getCacheDigest(r)
foundInCache := false
savePeakRequest(cacheDigest, r)
cacheBody, ok := lruCache.Get(cacheDigest)
if ok {
cacheEntry := cacheBody.(responseWithHeader)
// if after all attempts we still have no answer,
// we try to make the query on our own
for attempts := 0; attempts < 300; attempts++ {
if !ok || !cacheEntry.InProgress {
break
}
time.Sleep(30 * time.Millisecond)
cacheBody, ok = lruCache.Get(cacheDigest)
cacheEntry = cacheBody.(responseWithHeader)
}
if cacheEntry.InProgress {
log.Printf("TIMEOUT: %s\n", cacheDigest)
}
if ok && !cacheEntry.InProgress && cacheEntry.Expires.After(time.Now()) {
response = cacheEntry
foundInCache = true
}
}
if !foundInCache {
lruCache.Add(cacheDigest, responseWithHeader{InProgress: true})
response = get(r)
if response.StatusCode == 200 || response.StatusCode == 304 {
lruCache.Add(cacheDigest, response)
} else {
log.Printf("REMOVE: %d response for %s from cache\n", response.StatusCode, cacheDigest)
lruCache.Remove(cacheDigest)
}
}
return response
}
func get(req *http.Request) responseWithHeader {
client := &http.Client{}
queryURL := fmt.Sprintf("http://%s%s", req.Host, req.RequestURI)
proxyReq, err := http.NewRequest(req.Method, queryURL, req.Body)
if err != nil {
log.Printf("Request: %s\n", err)
}
// proxyReq.Header.Set("Host", req.Host)
// proxyReq.Header.Set("X-Forwarded-For", req.RemoteAddr)
for header, values := range req.Header {
for _, value := range values {
proxyReq.Header.Add(header, value)
}
}
res, err := client.Do(proxyReq)
if err != nil {
panic(err)
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Println(err)
}
return responseWithHeader{
InProgress: false,
Expires: time.Now().Add(time.Duration(randInt(1000, 1500)) * time.Second),
Body: body,
Header: res.Header,
StatusCode: res.StatusCode,
}
}
// implementation of the cache.get_signature of original wttr.in
func getCacheDigest(req *http.Request) string {
userAgent := req.Header.Get("User-Agent")
queryHost := req.Host
queryString := req.RequestURI
clientIPAddress := readUserIP(req)
lang := req.Header.Get("Accept-Language")
return fmt.Sprintf("%s:%s%s:%s:%s", userAgent, queryHost, queryString, clientIPAddress, lang)
}
// return true if request should not be cached
func dontCache(req *http.Request) bool {
// dont cache cyclic requests
loc := strings.Split(req.RequestURI, "?")[0]
if strings.Contains(loc, ":") {
return true
}
return false
}
func readUserIP(r *http.Request) string {
IPAddress := r.Header.Get("X-Real-Ip")
if IPAddress == "" {
IPAddress = r.Header.Get("X-Forwarded-For")
}
if IPAddress == "" {
IPAddress = r.RemoteAddr
var err error
IPAddress, _, err = net.SplitHostPort(IPAddress)
if err != nil {
log.Printf("ERROR: userip: %q is not IP:port\n", IPAddress)
}
}
return IPAddress
}
func randInt(min int, max int) int {
return min + rand.Intn(max-min)
}

View file

@ -1,69 +0,0 @@
package main
import (
"context"
"log"
"net"
"net/http"
"time"
lru "github.com/hashicorp/golang-lru"
)
const uplinkSrvAddr = "127.0.0.1:9002"
const uplinkTimeout = 30
const prefetchInterval = 300
const lruCacheSize = 12800
var lruCache *lru.Cache
type responseWithHeader struct {
InProgress bool // true if the request is being processed
Expires time.Time // expiration time of the cache entry
Body []byte
Header http.Header
StatusCode int // e.g. 200
}
func init() {
var err error
lruCache, err = lru.New(lruCacheSize)
if err != nil {
panic(err)
}
dialer := &net.Dialer{
Timeout: uplinkTimeout * time.Second,
KeepAlive: uplinkTimeout * time.Second,
DualStack: true,
}
http.DefaultTransport.(*http.Transport).DialContext = func(ctx context.Context, network, _ string) (net.Conn, error) {
return dialer.DialContext(ctx, network, uplinkSrvAddr)
}
initPeakHandling()
}
func copyHeader(dst, src http.Header) {
for k, vv := range src {
for _, v := range vv {
dst.Add(k, v)
}
}
}
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// printStat()
response := processRequest(r)
copyHeader(w.Header(), response.Header)
w.Header().Set("Access-Control-Allow-Origin", "*")
w.WriteHeader(response.StatusCode)
w.Write(response.Body)
})
log.Fatal(http.ListenAndServe(":8082", nil))
}

View file

@ -1,40 +0,0 @@
package main
import (
"log"
"sync"
"time"
)
type safeCounter struct {
v map[int]int
mux sync.Mutex
}
func (c *safeCounter) inc(key int) {
c.mux.Lock()
c.v[key]++
c.mux.Unlock()
}
// func (c *safeCounter) val(key int) int {
// c.mux.Lock()
// defer c.mux.Unlock()
// return c.v[key]
// }
//
// func (c *safeCounter) reset(key int) int {
// c.mux.Lock()
// defer c.mux.Unlock()
// result := c.v[key]
// c.v[key] = 0
// return result
// }
var queriesPerMinute safeCounter
func printStat() {
_, min, _ := time.Now().Clock()
queriesPerMinute.inc(min)
log.Printf("Processed %d requests\n", min)
}

View file

@ -29,12 +29,12 @@ The map view currently supports three formats:
| mlterm | X11 | yes | Sixel |
| kitty | X11 | yes | Kitty |
| wezterm | X11 | yes | IIP |
| aminal | X11 | yes | Sixel |
| Darktile | X11 | yes | Sixel |
| Jexer | X11 | yes | Sixel |
| GNOME Terminal | X11 | [in-progress](https://gitlab.gnome.org/GNOME/vte/-/issues/253) | Sixel |
| alacritty | X11 | [in-progress](https://github.com/alacritty/alacritty/issues/910) | Sixel |
| st | X11 | [stixel](https://github.com/vizs/stixel) or [st-sixel](https://github.com/galatolofederico/st-sixel) | Sixel |
| Konsole | X11 | [requested](https://bugs.kde.org/show_bug.cgi?id=391781) | Sixel |
| Konsole | X11 | yes | Sixel |
| DomTerm | Web | yes | Sixel |
| Yaft | FB | yes | Sixel |
| iTerm2 | Mac OS X| yes | IIP |

26
go.mod Normal file
View file

@ -0,0 +1,26 @@
module github.com/chubin/wttr.in
go 1.16
require (
github.com/alecthomas/kong v0.7.1 // indirect
github.com/denisenkom/go-mssqldb v0.0.0-20200910202707-1e08a3fab204 // indirect
github.com/go-sql-driver/mysql v1.5.0 // indirect
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect
github.com/hashicorp/golang-lru v0.6.0
github.com/itchyny/gojq v0.12.11 // indirect
github.com/klauspost/lctime v0.1.0 // indirect
github.com/lib/pq v1.8.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mattn/go-sqlite3 v1.14.16 // indirect
github.com/robfig/cron v1.2.0
github.com/samonzeweb/godb v1.0.8 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/smartystreets/assertions v1.2.0 // indirect
github.com/smartystreets/goconvey v1.6.4 // indirect
github.com/stretchr/testify v1.8.1 // indirect
github.com/zsefvlol/timezonemapper v1.0.0 // indirect
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

80
go.sum Normal file
View file

@ -0,0 +1,80 @@
github.com/alecthomas/assert/v2 v2.1.0/go.mod h1:b/+1DI2Q6NckYi+3mXyH3wFb8qG37K/DuK80n7WefXA=
github.com/alecthomas/kong v0.7.1 h1:azoTh0IOfwlAX3qN9sHWTxACE2oV8Bg2gAwBsMwDQY4=
github.com/alecthomas/kong v0.7.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U=
github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.0.0-20200910202707-1e08a3fab204/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4=
github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/itchyny/gojq v0.12.11 h1:YhLueoHhHiN4mkfM+3AyJV6EPcCxKZsOnYf+aVSwaQw=
github.com/itchyny/gojq v0.12.11/go.mod h1:o3FT8Gkbg/geT4pLI0tF3hvip5F3Y/uskjRz9OYa38g=
github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE=
github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/klauspost/lctime v0.1.0 h1:nINsuFc860M9cyYhT6vfg6U1USh7kiVBj/s/2b04U70=
github.com/klauspost/lctime v0.1.0/go.mod h1:OwdMhr8tbQvusAsnilqkkgDQqivWlqyg0w5cfXkLiDk=
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
github.com/samonzeweb/godb v1.0.8 h1:WRn6nq0FChYOzh+w8SgpXHUkEhL7W6ZqkCf5Ninx7Uc=
github.com/samonzeweb/godb v1.0.8/go.mod h1:LNDt3CakfBwpRY4AD0y/QPTbj+jB6O17tSxQES0p47o=
github.com/samonzeweb/godb v1.0.15 h1:HyNb8o1w109as9KWE8ih1YIBe8jC4luJ22f1XNacW38=
github.com/samonzeweb/godb v1.0.15/go.mod h1:SxCHqyireDXNrZApknS9lGUEutA43x9eJF632ecbK5Q=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/zsefvlol/timezonemapper v1.0.0 h1:HXqkOzf01gXYh2nDQcDSROikFgMaximnhE8BY9SyF6E=
github.com/zsefvlol/timezonemapper v1.0.0/go.mod h1:cVUCOLEmc/VvOMusEhpd2G/UBtadL26ZVz2syODXDoQ=
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-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
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-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

190
internal/config/config.go Normal file
View file

@ -0,0 +1,190 @@
package config
import (
"log"
"os"
"gopkg.in/yaml.v3"
"github.com/chubin/wttr.in/internal/types"
"github.com/chubin/wttr.in/internal/util"
)
// Config of the program.
type Config struct {
Cache
Geo
Logging
Server
Uplink
}
// Logging configuration.
type Logging struct {
// AccessLog path.
AccessLog string `yaml:"accessLog,omitempty"`
// ErrorsLog path.
ErrorsLog string `yaml:"errorsLog,omitempty"`
// Interval between access log flushes, in seconds.
Interval int `yaml:"interval,omitempty"`
}
// Server configuration.
type Server struct {
// PortHTTP is port where HTTP server must listen.
// If 0, HTTP is disabled.
PortHTTP int `yaml:"portHttp,omitempty"`
// PortHTTP is port where the HTTPS server must listen.
// If 0, HTTPS is disabled.
PortHTTPS int `yaml:"portHttps,omitempty"`
// TLSCertFile contains path to cert file for TLS Server.
TLSCertFile string `yaml:"tlsCertFile,omitempty"`
// TLSCertFile contains path to key file for TLS Server.
TLSKeyFile string `yaml:"tlsKeyFile,omitempty"`
}
// Uplink configuration.
type Uplink struct {
// Address1 contains address of the uplink server in form IP:PORT
// for format=j1 queries.
Address1 string `yaml:"address1,omitempty"`
// Address2 contains address of the uplink server in form IP:PORT
// for format=* queries.
Address2 string `yaml:"address2,omitempty"`
// Address3 contains address of the uplink server in form IP:PORT
// for all other queries.
Address3 string `yaml:"address3,omitempty"`
// Timeout for upstream queries.
Timeout int `yaml:"timeout,omitempty"`
// PrefetchInterval contains time (in milliseconds) indicating,
// how long the prefetch procedure should take.
PrefetchInterval int `yaml:"prefetchInterval,omitempty"`
}
// Cache configuration.
type Cache struct {
// Size of the main cache.
Size int `yaml:"size,omitempty"`
}
// Geo contains geolocation configuration.
type Geo struct {
// IPCache contains the path to the IP Geodata cache.
IPCache string `yaml:"ipCache,omitempty"`
// IPCacheDB contains the path to the SQLite DB with the IP Geodata cache.
IPCacheDB string `yaml:"ipCacheDb,omitempty"`
IPCacheType types.CacheType `yaml:"ipCacheType,omitempty"`
// LocationCache contains the path to the Location Geodata cache.
LocationCache string `yaml:"locationCache,omitempty"`
// LocationCacheDB contains the path to the SQLite DB with the Location Geodata cache.
LocationCacheDB string `yaml:"locationCacheDb,omitempty"`
LocationCacheType types.CacheType `yaml:"locationCacheType,omitempty"`
Nominatim []Nominatim `yaml:"nominatim"`
}
type Nominatim struct {
Name string
// Type describes the type of the location service.
// Supported types: iq.
Type string
URL string
Token string
}
// Default contains the default configuration.
func Default() *Config {
return &Config{
Cache{
Size: 12800,
},
Geo{
IPCache: "/wttr.in/cache/ip2l",
IPCacheDB: "/wttr.in/cache/geoip.db",
IPCacheType: types.CacheTypeDB,
LocationCache: "/wttr.in/cache/loc",
LocationCacheDB: "/wttr.in/cache/geoloc.db",
LocationCacheType: types.CacheTypeDB,
Nominatim: []Nominatim{
{
Name: "locationiq",
Type: "iq",
URL: "https://eu1.locationiq.com/v1/search",
Token: os.Getenv("NOMINATIM_LOCATIONIQ"),
},
{
Name: "opencage",
Type: "opencage",
URL: "https://api.opencagedata.com/geocode/v1/json",
Token: os.Getenv("NOMINATIM_OPENCAGE"),
},
},
},
Logging{
AccessLog: "/wttr.in/log/access.log",
ErrorsLog: "/wttr.in/log/errors.log",
Interval: 300,
},
Server{
PortHTTP: 8083,
PortHTTPS: 8084,
TLSCertFile: "/wttr.in/etc/fullchain.pem",
TLSKeyFile: "/wttr.in/etc/privkey.pem",
},
Uplink{
Address1: "127.0.0.1:9002",
Address2: "127.0.0.1:9002",
Address3: "127.0.0.1:9002",
Timeout: 30,
PrefetchInterval: 300,
},
}
}
// Load config from file.
func Load(filename string) (*Config, error) {
var (
config Config
data []byte
err error
)
data, err = os.ReadFile(filename)
if err != nil {
return nil, err
}
err = util.YamlUnmarshalStrict(data, &config)
if err != nil {
return nil, err
}
return &config, nil
}
func (c *Config) Dump() []byte {
data, err := yaml.Marshal(c)
if err != nil {
// should never happen.
log.Fatalln("config.Dump():", err)
}
return data
}

774
internal/fmt/png/colors.go Normal file
View file

@ -0,0 +1,774 @@
package main
// Source: https://www.ditig.com/downloads/256-colors.json
var ansiColorsDB = [][3]float64{
{
0, 0, 0,
},
{
128, 0, 0,
},
{
0, 128, 0,
},
{
128, 128, 0,
},
{
0, 0, 128,
},
{
128, 0, 128,
},
{
0, 128, 128,
},
{
192, 192, 192,
},
{
128, 128, 128,
},
{
255, 0, 0,
},
{
0, 255, 0,
},
{
255, 255, 0,
},
{
0, 0, 255,
},
{
255, 0, 255,
},
{
0, 255, 255,
},
{
255, 255, 255,
},
{
0, 0, 0,
},
{
0, 0, 95,
},
{
0, 0, 135,
},
{
0, 0, 175,
},
{
0, 0, 215,
},
{
0, 0, 255,
},
{
0, 95, 0,
},
{
0, 95, 95,
},
{
0, 95, 135,
},
{
0, 95, 175,
},
{
0, 95, 215,
},
{
0, 95, 255,
},
{
0, 135, 0,
},
{
0, 135, 95,
},
{
0, 135, 135,
},
{
0, 135, 175,
},
{
0, 135, 215,
},
{
0, 135, 255,
},
{
0, 175, 0,
},
{
0, 175, 95,
},
{
0, 175, 135,
},
{
0, 175, 175,
},
{
0, 175, 215,
},
{
0, 175, 255,
},
{
0, 215, 0,
},
{
0, 215, 95,
},
{
0, 215, 135,
},
{
0, 215, 175,
},
{
0, 215, 215,
},
{
0, 215, 255,
},
{
0, 255, 0,
},
{
0, 255, 95,
},
{
0, 255, 135,
},
{
0, 255, 175,
},
{
0, 255, 215,
},
{
0, 255, 255,
},
{
95, 0, 0,
},
{
95, 0, 95,
},
{
95, 0, 135,
},
{
95, 0, 175,
},
{
95, 0, 215,
},
{
95, 0, 255,
},
{
95, 95, 0,
},
{
95, 95, 95,
},
{
95, 95, 135,
},
{
95, 95, 175,
},
{
95, 95, 215,
},
{
95, 95, 255,
},
{
95, 135, 0,
},
{
95, 135, 95,
},
{
95, 135, 135,
},
{
95, 135, 175,
},
{
95, 135, 215,
},
{
95, 135, 255,
},
{
95, 175, 0,
},
{
95, 175, 95,
},
{
95, 175, 135,
},
{
95, 175, 175,
},
{
95, 175, 215,
},
{
95, 175, 255,
},
{
95, 215, 0,
},
{
95, 215, 95,
},
{
95, 215, 135,
},
{
95, 215, 175,
},
{
95, 215, 215,
},
{
95, 215, 255,
},
{
95, 255, 0,
},
{
95, 255, 95,
},
{
95, 255, 135,
},
{
95, 255, 175,
},
{
95, 255, 215,
},
{
95, 255, 255,
},
{
135, 0, 0,
},
{
135, 0, 95,
},
{
135, 0, 135,
},
{
135, 0, 175,
},
{
135, 0, 215,
},
{
135, 0, 255,
},
{
135, 95, 0,
},
{
135, 95, 95,
},
{
135, 95, 135,
},
{
135, 95, 175,
},
{
135, 95, 215,
},
{
135, 95, 255,
},
{
135, 135, 0,
},
{
135, 135, 95,
},
{
135, 135, 135,
},
{
135, 135, 175,
},
{
135, 135, 215,
},
{
135, 135, 255,
},
{
135, 175, 0,
},
{
135, 175, 95,
},
{
135, 175, 135,
},
{
135, 175, 175,
},
{
135, 175, 215,
},
{
135, 175, 255,
},
{
135, 215, 0,
},
{
135, 215, 95,
},
{
135, 215, 135,
},
{
135, 215, 175,
},
{
135, 215, 215,
},
{
135, 215, 255,
},
{
135, 255, 0,
},
{
135, 255, 95,
},
{
135, 255, 135,
},
{
135, 255, 175,
},
{
135, 255, 215,
},
{
135, 255, 255,
},
{
175, 0, 0,
},
{
175, 0, 95,
},
{
175, 0, 135,
},
{
175, 0, 175,
},
{
175, 0, 215,
},
{
175, 0, 255,
},
{
175, 95, 0,
},
{
175, 95, 95,
},
{
175, 95, 135,
},
{
175, 95, 175,
},
{
175, 95, 215,
},
{
175, 95, 255,
},
{
175, 135, 0,
},
{
175, 135, 95,
},
{
175, 135, 135,
},
{
175, 135, 175,
},
{
175, 135, 215,
},
{
175, 135, 255,
},
{
175, 175, 0,
},
{
175, 175, 95,
},
{
175, 175, 135,
},
{
175, 175, 175,
},
{
175, 175, 215,
},
{
175, 175, 255,
},
{
175, 215, 0,
},
{
175, 215, 95,
},
{
175, 215, 135,
},
{
175, 215, 175,
},
{
175, 215, 215,
},
{
175, 215, 255,
},
{
175, 255, 0,
},
{
175, 255, 95,
},
{
175, 255, 135,
},
{
175, 255, 175,
},
{
175, 255, 215,
},
{
175, 255, 255,
},
{
215, 0, 0,
},
{
215, 0, 95,
},
{
215, 0, 135,
},
{
215, 0, 175,
},
{
215, 0, 215,
},
{
215, 0, 255,
},
{
215, 95, 0,
},
{
215, 95, 95,
},
{
215, 95, 135,
},
{
215, 95, 175,
},
{
215, 95, 215,
},
{
215, 95, 255,
},
{
215, 135, 0,
},
{
215, 135, 95,
},
{
215, 135, 135,
},
{
215, 135, 175,
},
{
215, 135, 215,
},
{
215, 135, 255,
},
{
215, 175, 0,
},
{
215, 175, 95,
},
{
215, 175, 135,
},
{
215, 175, 175,
},
{
215, 175, 215,
},
{
215, 175, 255,
},
{
215, 215, 0,
},
{
215, 215, 95,
},
{
215, 215, 135,
},
{
215, 215, 175,
},
{
215, 215, 215,
},
{
215, 215, 255,
},
{
215, 255, 0,
},
{
215, 255, 95,
},
{
215, 255, 135,
},
{
215, 255, 175,
},
{
215, 255, 215,
},
{
215, 255, 255,
},
{
255, 0, 0,
},
{
255, 0, 95,
},
{
255, 0, 135,
},
{
255, 0, 175,
},
{
255, 0, 215,
},
{
255, 0, 255,
},
{
255, 95, 0,
},
{
255, 95, 95,
},
{
255, 95, 135,
},
{
255, 95, 175,
},
{
255, 95, 215,
},
{
255, 95, 255,
},
{
255, 135, 0,
},
{
255, 135, 95,
},
{
255, 135, 135,
},
{
255, 135, 175,
},
{
255, 135, 215,
},
{
255, 135, 255,
},
{
255, 175, 0,
},
{
255, 175, 95,
},
{
255, 175, 135,
},
{
255, 175, 175,
},
{
255, 175, 215,
},
{
255, 175, 255,
},
{
255, 215, 0,
},
{
255, 215, 95,
},
{
255, 215, 135,
},
{
255, 215, 175,
},
{
255, 215, 215,
},
{
255, 215, 255,
},
{
255, 255, 0,
},
{
255, 255, 95,
},
{
255, 255, 135,
},
{
255, 255, 175,
},
{
255, 255, 215,
},
{
255, 255, 255,
},
{
8, 8, 8,
},
{
18, 18, 18,
},
{
28, 28, 28,
},
{
38, 38, 38,
},
{
48, 48, 48,
},
{
58, 58, 58,
},
{
68, 68, 68,
},
{
78, 78, 78,
},
{
88, 88, 88,
},
{
98, 98, 98,
},
{
108, 108, 108,
},
{
118, 118, 118,
},
{
128, 128, 128,
},
{
138, 138, 138,
},
{
148, 148, 148,
},
{
158, 158, 158,
},
{
168, 168, 168,
},
{
178, 178, 178,
},
{
188, 188, 188,
},
{
198, 198, 198,
},
{
208, 208, 208,
},
{
218, 218, 218,
},
{
228, 228, 228,
},
{
238, 238, 238,
},
}

10
internal/fmt/png/go.mod Normal file
View file

@ -0,0 +1,10 @@
module example.com/m/v2
go 1.20
require (
github.com/chubin/vt10x v0.0.0-20231112153020-ef4f56837bf1 // indirect
github.com/fogleman/gg v1.3.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
golang.org/x/image v0.14.0 // indirect
)

8
internal/fmt/png/go.sum Normal file
View file

@ -0,0 +1,8 @@
github.com/chubin/vt10x v0.0.0-20231112153020-ef4f56837bf1 h1:CHg5BTAJZmCjBaAAQrD92s248JHH3JTsLlaC6QBJo/Y=
github.com/chubin/vt10x v0.0.0-20231112153020-ef4f56837bf1/go.mod h1:mQssL2gI1LTqWgbffl6DESqe6QkAF67ujBdzSe4bWkU=
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=

224
internal/fmt/png/png.go Normal file
View file

@ -0,0 +1,224 @@
package main
import (
"fmt"
"log"
"os"
"strings"
"github.com/chubin/vt10x"
"github.com/fogleman/gg"
)
func StringSliceToRuneSlice(s string) [][]rune {
strings := strings.Split(s, "\n")
result := make([][]rune, len(strings))
i := 0
for _, str := range strings {
if len(str) == 0 {
continue
}
result[i] = []rune(str)
i++
}
return result
}
func maxRowLength(rows [][]rune) int {
maxLen := 0
for _, row := range rows {
if len(row) > maxLen {
maxLen = len(row)
}
}
return maxLen
}
func GeneratePng() {
runes := StringSliceToRuneSlice(`
Weather report: Hochstadt an der Aisch, Germany
\ / Partly cloudy
_ /"".-. +5(2) °C
\_( ). 9 km/h
/(___(__) 10 km
0.0 mm
Sat 11 Nov
Noon Night
_'/"".-. Patchy rain po _'/"".-. Patchy rain po
,\_( ). +6(3) °C ,\_( ). +5(2) °C
/(___(__) 22-29 km/h /(___(__) 14-20 km/h
10 km 10 km
0.1 mm | 86% 0.0 mm | 89%
Sun 12 Nov
Noon Night
\ / Partly cloudy .-. Light drizzle
_ /"".-. +8(7) °C ( ). +5(2) °C
\_( ). 7-8 km/h (___(__) 13-18 km/h
/(___(__) 10 km 2 km
0.0 mm | 0% 0.3 mm | 76%
`)
// Dimensions of each rune in pixels
runeWidth := 8
runeHeight := 14
// Compute the width and height of the final image
imageWidth := runeWidth * maxRowLength(runes)
imageHeight := runeHeight * len(runes)
// Create a new context with the computed dimensions
dc := gg.NewContext(imageWidth, imageHeight)
// fontPath := "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf"
// fontPath := "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc"
fontPath := "/usr/share/fonts/truetype/lexi/LexiGulim.ttf"
err := dc.LoadFontFace(fontPath, 13)
if err != nil {
log.Fatal(err)
}
// Loop through each rune in the array and draw it on the context
for i, row := range runes {
for j, char := range row {
// Compute the x and y coordinates for drawing the current rune
x := float64(j*runeWidth + runeWidth/2)
y := float64(i*runeHeight + runeHeight/2)
// Set the appropriate color for the current rune
if char == '#' {
dc.SetRGB(0, 0, 0) // Black
} else if char == '@' {
dc.SetRGB(1, 0, 0) // Red
} else {
dc.SetRGB(1, 1, 1) // White
}
character := string(char)
// if char == ' ' {
// character = fmt.Sprint(j % 10)
// }
dc.DrawRectangle(x, y, x+float64(runeWidth), y+float64(runeHeight))
dc.Fill()
// Draw a rectangle with the rune's dimensions and color
dc.DrawString(character, x, y) // Draw the character centered on the canvas
// dc.DrawStringAnchored(character, x, y, 0.5, 0.5) // Draw the character centered on the canvas
}
}
// Save the image to a PNG file
err = dc.SavePNG("output.png")
if err != nil {
fmt.Println("Error saving PNG:", err)
return
}
fmt.Println("PNG generated successfully")
}
func GeneratePngFromANSI(input []byte, outputFile string) error {
// Dimensions of each rune in pixels
runeWidth := 8
runeHeight := 14
fontSize := 13.0
// fontPath := "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf"
fontPath := "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc"
imageCols := 80
imageRows := 25
// Compute the width and height of the final image
imageWidth := runeWidth * imageCols
imageHeight := runeHeight * imageRows
// Create terminal and feed it with input.
term := vt10x.New(vt10x.WithSize(imageCols, imageRows))
_, err := term.Write([]byte("\033[20h"))
if err != nil {
return fmt.Errorf("virtual terminal write error: %w", err)
}
_, err = term.Write(input)
if err != nil {
return fmt.Errorf("virtual terminal write error: %w", err)
}
// Create a new context with the computed dimensions
dc := gg.NewContext(imageWidth, imageHeight)
err = dc.LoadFontFace(fontPath, fontSize) // Set font size to 96
if err != nil {
return fmt.Errorf("error loading font: %w", err)
}
// Loop through each rune in the array and draw it on the context
for i := 0; i < imageRows; i++ {
for j := 0; j < imageCols; j++ {
// Compute the x and y coordinates for drawing the current rune
x := float64(j * runeWidth)
y := float64(i * runeHeight)
cell := term.Cell(j, i)
character := string(cell.Char)
dc.DrawRectangle(x, y, float64(runeWidth), float64(runeHeight))
bg := colorANSItoRGB(cell.BG)
dc.SetRGB(bg[0], bg[1], bg[2])
dc.Fill()
fg := colorANSItoRGB(cell.FG)
dc.SetRGB(fg[0], fg[1], fg[2])
// Draw a rectangle with the rune's dimensions and color
dc.DrawString(character, x, y+float64(runeHeight)-3) // Draw the character centered on the canvas
// dc.DrawStringAnchored(character, x, y, 0.5, 0.5) // Draw the character centered on the canvas
}
}
// Save the image to a PNG file
err = dc.SavePNG(outputFile)
if err != nil {
return fmt.Errorf("error saving png: %w", err)
}
return nil
}
func colorANSItoRGB(colorANSI vt10x.Color) [3]float64 {
defaultBG := vt10x.Color(0)
defaultFG := vt10x.Color(8)
if colorANSI == vt10x.DefaultFG {
colorANSI = defaultFG
}
if colorANSI == vt10x.DefaultBG {
colorANSI = defaultBG
}
if colorANSI > 255 {
return [3]float64{127, 127, 127}
}
return ansiColorsDB[colorANSI]
}
func main() {
data, err := os.ReadFile("zh-text.txt")
if err != nil {
log.Fatalln(err)
}
err = GeneratePngFromANSI(data, "output.png")
if err != nil {
log.Fatalln(err)
}
}

View file

@ -0,0 +1,88 @@
package ip
import (
"fmt"
"log"
"path/filepath"
"github.com/samonzeweb/godb"
"github.com/samonzeweb/godb/adapters/sqlite"
"github.com/chubin/wttr.in/internal/util"
)
//nolint:cyclop
func (c *Cache) ConvertCache() error {
dbfile := c.config.Geo.IPCacheDB
err := util.RemoveFileIfExists(dbfile)
if err != nil {
return err
}
db, err := godb.Open(sqlite.Adapter, dbfile)
if err != nil {
return err
}
err = createTable(db, "Address")
if err != nil {
return err
}
log.Println("listing cache entries...")
files, err := filepath.Glob(filepath.Join(c.config.Geo.IPCache, "*"))
if err != nil {
return err
}
log.Printf("going to convert %d entries\n", len(files))
block := []Address{}
for i, file := range files {
ip := filepath.Base(file)
loc, err := c.Read(ip)
if err != nil {
log.Println("invalid entry for", ip)
continue
}
block = append(block, *loc)
if i%1000 != 0 || i == 0 {
continue
}
err = db.BulkInsert(&block).Do()
if err != nil {
return err
}
block = []Address{}
log.Println("converted", i+1, "entries")
}
// inserting the rest.
err = db.BulkInsert(&block).Do()
if err != nil {
return err
}
log.Println("converted", len(files), "entries")
return nil
}
func createTable(db *godb.DB, tableName string) error {
createTable := fmt.Sprintf(
`create table %s (
name text not null primary key,
fullName text not null,
lat text not null,
long text not null);
`, tableName)
_, err := db.CurrentDB().Exec(createTable)
return err
}

245
internal/geo/ip/ip.go Normal file
View file

@ -0,0 +1,245 @@
package ip
import (
"fmt"
"log"
"net/http"
"os"
"path"
"regexp"
"strconv"
"strings"
"github.com/samonzeweb/godb"
"github.com/samonzeweb/godb/adapters/sqlite"
"github.com/chubin/wttr.in/internal/config"
"github.com/chubin/wttr.in/internal/routing"
"github.com/chubin/wttr.in/internal/types"
"github.com/chubin/wttr.in/internal/util"
)
// Address information.
type Address struct {
IP string `db:"ip,key"`
CountryCode string `db:"countryCode"`
Country string `db:"country"`
Region string `db:"region"`
City string `db:"city"`
Latitude float64 `db:"latitude"`
Longitude float64 `db:"longitude"`
}
func (l *Address) String() string {
if l.Latitude == -1000 {
return fmt.Sprintf(
"%s;%s;%s;%s",
l.CountryCode, l.Country, l.Region, l.City)
}
return fmt.Sprintf(
"%s;%s;%s;%s;%v;%v",
l.CountryCode, l.Country, l.Region, l.City, l.Latitude, l.Longitude)
}
// Cache provides access to the IP Geodata cache.
type Cache struct {
config *config.Config
db *godb.DB
}
// NewCache returns new cache reader for the specified config.
func NewCache(config *config.Config) (*Cache, error) {
db, err := godb.Open(sqlite.Adapter, config.Geo.IPCacheDB)
if err != nil {
return nil, err
}
// Needed for "upsert" implementation in Put()
db.UseErrorParser()
return &Cache{
config: config,
db: db,
}, nil
}
// Read returns location information from the cache, if found,
// or types.ErrNotFound if not found. If the entry is found, but its format
// is invalid, types.ErrInvalidCacheEntry is returned.
//
// Format:
//
// [CountryCode];Country;Region;City;[Latitude];[Longitude]
//
// Example:
//
// DE;Germany;Free and Hanseatic City of Hamburg;Hamburg;53.5736;9.9782
//
func (c *Cache) Read(addr string) (*Address, error) {
if c.config.Geo.IPCacheType == types.CacheTypeDB {
return c.readFromCacheDB(addr)
}
return c.readFromCacheFile(addr)
}
func (c *Cache) readFromCacheFile(addr string) (*Address, error) {
bytes, err := os.ReadFile(c.cacheFile(addr))
if err != nil {
return nil, types.ErrNotFound
}
return NewAddressFromString(addr, string(bytes))
}
func (c *Cache) readFromCacheDB(addr string) (*Address, error) {
result := Address{}
err := c.db.Select(&result).
Where("IP = ?", addr).
Do()
if err != nil {
return nil, err
}
return &result, nil
}
func (c *Cache) Put(addr string, loc *Address) error {
if c.config.Geo.IPCacheType == types.CacheTypeDB {
return c.putToCacheDB(loc)
}
return c.putToCacheFile(addr, loc)
}
func (c *Cache) putToCacheDB(loc *Address) error {
err := c.db.Insert(loc).Do()
// it should work like this:
//
// target := dberror.UniqueConstraint{}
// if errors.As(err, &target) {
//
// See: https://github.com/samonzeweb/godb/pull/23
//
// But for some reason it does not work,
// so the dirty hack is used:
if strings.Contains(fmt.Sprint(err), "UNIQUE constraint failed") {
return c.db.Update(loc).Do()
}
return err
}
func (c *Cache) putToCacheFile(addr string, loc fmt.Stringer) error {
return os.WriteFile(c.cacheFile(addr), []byte(loc.String()), 0o600)
}
// cacheFile returns path to the cache entry for addr.
func (c *Cache) cacheFile(addr string) string {
return path.Join(c.config.Geo.IPCache, addr)
}
// NewAddressFromString parses the location cache entry s,
// and return location, or error, if the cache entry is invalid.
func NewAddressFromString(addr, s string) (*Address, error) {
var (
lat float64 = -1000
long float64 = -1000
err error
)
parts := strings.Split(s, ";")
if len(parts) < 4 {
return nil, types.ErrInvalidCacheEntry
}
if len(parts) >= 6 {
lat, err = strconv.ParseFloat(parts[4], 64)
if err != nil {
return nil, types.ErrInvalidCacheEntry
}
long, err = strconv.ParseFloat(parts[5], 64)
if err != nil {
return nil, types.ErrInvalidCacheEntry
}
}
return &Address{
IP: addr,
CountryCode: parts[0],
Country: parts[1],
Region: parts[2],
City: parts[3],
Latitude: lat,
Longitude: long,
}, nil
}
// Response provides routing interface to the geo cache.
//
// Temporary workaround to switch IP addresses handling to the Go server.
// Handles two queries:
//
// - /:geo-ip-put?ip=IP&value=VALUE
// - /:geo-ip-get?ip=IP
//
//nolint:cyclop
func (c *Cache) Response(r *http.Request) *routing.Cadre {
var (
respERR = &routing.Cadre{Body: []byte("ERR")}
respOK = &routing.Cadre{Body: []byte("OK")}
)
if ip := util.ReadUserIP(r); ip != "127.0.0.1" {
log.Printf("geoIP access from %s rejected\n", ip)
return nil
}
if r.URL.Path == "/:geo-ip-put" {
ip := r.URL.Query().Get("ip")
value := r.URL.Query().Get("value")
if !validIP4(ip) || value == "" {
log.Printf("invalid geoIP put query: ip='%s' value='%s'\n", ip, value)
return respERR
}
location, err := NewAddressFromString(ip, value)
if err != nil {
return respERR
}
err = c.Put(ip, location)
if err != nil {
return respERR
}
return respOK
}
if r.URL.Path == "/:geo-ip-get" {
ip := r.URL.Query().Get("ip")
if !validIP4(ip) {
return respERR
}
result, err := c.Read(ip)
if result == nil || err != nil {
return respERR
}
return &routing.Cadre{Body: []byte(result.String())}
}
return nil
}
func validIP4(ipAddress string) bool {
re := regexp.MustCompile(
`^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$`)
return re.MatchString(strings.Trim(ipAddress, " "))
}

View file

@ -0,0 +1,84 @@
package ip_test
import (
"testing"
"github.com/stretchr/testify/require"
. "github.com/chubin/wttr.in/internal/geo/ip"
"github.com/chubin/wttr.in/internal/types"
)
//nolint:funlen
func TestParseCacheEntry(t *testing.T) {
t.Parallel()
tests := []struct {
addr string
input string
expected Address
err error
}{
{
"1.2.3.4",
"DE;Germany;Free and Hanseatic City of Hamburg;Hamburg;53.5736;9.9782",
Address{
IP: "1.2.3.4",
CountryCode: "DE",
Country: "Germany",
Region: "Free and Hanseatic City of Hamburg",
City: "Hamburg",
Latitude: 53.5736,
Longitude: 9.9782,
},
nil,
},
{
"1.2.3.4",
"ES;Spain;Madrid, Comunidad de;Madrid;40.4165;-3.70256;28223;Orange Espagne SA;orange.es",
Address{
IP: "1.2.3.4",
CountryCode: "ES",
Country: "Spain",
Region: "Madrid, Comunidad de",
City: "Madrid",
Latitude: 40.4165,
Longitude: -3.70256,
},
nil,
},
{
"1.2.3.4",
"US;United States of America;California;Mountain View",
Address{
IP: "1.2.3.4",
CountryCode: "US",
Country: "United States of America",
Region: "California",
City: "Mountain View",
Latitude: -1000,
Longitude: -1000,
},
nil,
},
// Invalid entries
{
"1.2.3.4",
"DE;Germany;Free and Hanseatic City of Hamburg;Hamburg;53.5736;XXX",
Address{},
types.ErrInvalidCacheEntry,
},
}
for _, tt := range tests {
result, err := NewAddressFromString(tt.addr, tt.input)
if tt.err == nil {
require.NoError(t, err)
require.Equal(t, *result, tt.expected)
} else {
require.ErrorIs(t, err, tt.err)
}
}
}

View file

@ -0,0 +1,218 @@
package location
import (
"encoding/json"
"errors"
"fmt"
"os"
"path"
"strconv"
"strings"
"github.com/samonzeweb/godb"
"github.com/samonzeweb/godb/adapters/sqlite"
log "github.com/sirupsen/logrus"
"github.com/zsefvlol/timezonemapper"
"github.com/chubin/wttr.in/internal/config"
"github.com/chubin/wttr.in/internal/types"
)
// Cache is an implemenation of DB/file-based cache.
//
// At the moment, it is an implementation for the location cache,
// but it should be generalized to cache everything.
type Cache struct {
config *config.Config
db *godb.DB
searcher *Searcher
indexField string
filesCacheDir string
}
// NewCache returns new cache reader for the specified config.
func NewCache(config *config.Config) (*Cache, error) {
var (
db *godb.DB
err error
)
if config.Geo.LocationCacheType == types.CacheTypeDB {
log.Debugln("using db for location cache")
db, err = godb.Open(sqlite.Adapter, config.Geo.LocationCacheDB)
if err != nil {
return nil, err
}
log.Debugln("db file:", config.Geo.LocationCacheDB)
// Needed for "upsert" implementation in Put()
db.UseErrorParser()
}
return &Cache{
config: config,
db: db,
indexField: "name",
filesCacheDir: config.Geo.LocationCache,
searcher: NewSearcher(config),
}, nil
}
// Resolve returns location information for specified location.
// If the information is found in the cache, it is returned.
// If it is not found, the external service is queried,
// and the result is stored in the cache.
func (c *Cache) Resolve(location string) (*Location, error) {
location = normalizeLocationName(location)
loc, err := c.Read(location)
if !errors.Is(err, types.ErrNotFound) {
return loc, err
}
log.Debugln("geo/location: not found in cache:", location)
loc, err = c.searcher.Search(location)
if err != nil {
return nil, err
}
loc.Name = location
loc.Timezone = latLngToTimezoneString(loc.Lat, loc.Lon)
err = c.Put(location, loc)
if err != nil {
return nil, err
}
return loc, nil
}
// Read returns location information from the cache, if found,
// or types.ErrNotFound if not found. If the entry is found, but its format
// is invalid, types.ErrInvalidCacheEntry is returned.
func (c *Cache) Read(addr string) (*Location, error) {
if c.config.Geo.LocationCacheType == types.CacheTypeFiles {
return c.readFromCacheFile(addr)
}
return c.readFromCacheDB(addr)
}
func (c *Cache) readFromCacheFile(name string) (*Location, error) {
var (
fileLoc = struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
Timezone string `json:"timezone"`
Address string `json:"address"`
}{}
location Location
)
bytes, err := os.ReadFile(c.cacheFile(name))
if err != nil {
return nil, types.ErrNotFound
}
err = json.Unmarshal(bytes, &fileLoc)
if err != nil {
return nil, err
}
// normalize name
name = strings.TrimSpace(
strings.TrimRight(
strings.TrimLeft(name, `"`), `"`))
timezone := fileLoc.Timezone
if timezone == "" {
timezone = timezonemapper.LatLngToTimezoneString(fileLoc.Latitude, fileLoc.Longitude)
}
location = Location{
Name: name,
Lat: fmt.Sprint(fileLoc.Latitude),
Lon: fmt.Sprint(fileLoc.Longitude),
Timezone: timezone,
Fullname: fileLoc.Address,
}
return &location, nil
}
func (c *Cache) readFromCacheDB(addr string) (*Location, error) {
result := Location{}
err := c.db.Select(&result).
Where(c.indexField+" = ?", addr).
Do()
if strings.Contains(fmt.Sprint(err), "no rows in result set") {
return nil, types.ErrNotFound
}
if err != nil {
return nil, fmt.Errorf("readFromCacheDB: %w", err)
}
return &result, nil
}
func (c *Cache) Put(addr string, loc *Location) error {
log.Infoln("geo/location: storing in cache:", loc)
if c.config.Geo.IPCacheType == types.CacheTypeDB {
return c.putToCacheDB(loc)
}
return c.putToCacheFile(addr, loc)
}
func (c *Cache) putToCacheDB(loc *Location) error {
err := c.db.Insert(loc).Do()
if strings.Contains(fmt.Sprint(err), "UNIQUE constraint failed") {
return c.db.Update(loc).Do()
}
return err
}
func (c *Cache) putToCacheFile(addr string, loc fmt.Stringer) error {
return os.WriteFile(c.cacheFile(addr), []byte(loc.String()), 0o600)
}
// cacheFile returns path to the cache entry for addr.
func (c *Cache) cacheFile(item string) string {
return path.Join(c.filesCacheDir, item)
}
// normalizeLocationName converts name into the standard location form
// with the following steps:
// - remove excessive spaces,
// - remove quotes,
// - convert to lover case.
func normalizeLocationName(name string) string {
name = strings.ReplaceAll(name, `"`, " ")
name = strings.ReplaceAll(name, `'`, " ")
name = strings.TrimSpace(name)
name = strings.Join(strings.Fields(name), " ")
return strings.ToLower(name)
}
// latLngToTimezoneString returns timezone for lat, lon,
// or an empty string if they are invalid.
func latLngToTimezoneString(lat, lon string) string {
latFloat, err := strconv.ParseFloat(lat, 64)
if err != nil {
log.Errorln("geoloc: latLngToTimezoneString:", err)
return ""
}
lonFloat, err := strconv.ParseFloat(lon, 64)
if err != nil {
log.Errorln("geoloc: latLngToTimezoneString:", err)
return ""
}
return timezonemapper.LatLngToTimezoneString(latFloat, lonFloat)
}

View file

@ -0,0 +1,145 @@
package location
import (
"database/sql"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"github.com/samonzeweb/godb"
"github.com/samonzeweb/godb/adapters/sqlite"
)
// ConvertCache converts files-based cache into the DB-based cache.
// If reset is true, the DB cache is created from scratch.
//
//nolint:funlen,cyclop
func (c *Cache) ConvertCache(reset bool) error {
var (
dbfile = c.config.Geo.LocationCacheDB
tableName = "Location"
cacheFiles = c.filesCacheDir
known = map[string]bool{}
)
if reset {
err := removeDBIfExists(dbfile)
if err != nil {
return err
}
}
db, err := godb.Open(sqlite.Adapter, dbfile)
if err != nil {
return err
}
if reset {
err = createTable(db, tableName)
if err != nil {
return err
}
}
log.Println("listing cache entries...")
files, err := filepath.Glob(filepath.Join(cacheFiles, "*"))
if err != nil {
return err
}
log.Printf("going to convert %d entries\n", len(files))
block := []Location{}
for i, file := range files {
ip := filepath.Base(file)
loc, err := c.Read(ip)
if err != nil {
log.Println("invalid entry for", ip)
continue
}
// Skip too long location names.
if len(loc.Name) > 25 {
continue
}
// Skip duplicates.
if known[loc.Name] {
log.Println("skipping", loc.Name)
continue
}
singleLocation := Location{}
err = db.Select(&singleLocation).
Where("name = ?", loc.Name).
Do()
if !errors.Is(err, sql.ErrNoRows) {
log.Println("found in db:", loc.Name)
continue
}
known[loc.Name] = true
// Skip some invalid names.
if strings.Contains(loc.Name, "\n") {
continue
}
block = append(block, *loc)
if i%1000 != 0 || i == 0 {
continue
}
log.Println("going to insert new entries")
err = db.BulkInsert(&block).Do()
if err != nil {
return err
}
block = []Location{}
log.Println("converted", i+1, "entries")
}
// inserting the rest.
err = db.BulkInsert(&block).Do()
if err != nil {
return err
}
log.Println("converted", len(files), "entries")
return nil
}
func createTable(db *godb.DB, tableName string) error {
createTable := fmt.Sprintf(
`create table %s (
name text not null primary key,
displayName text not null,
lat text not null,
lon text not null,
timezone text not null);
`, tableName)
_, err := db.CurrentDB().Exec(createTable)
return err
}
func removeDBIfExists(filename string) error {
_, err := os.Stat(filename)
if err != nil {
if !os.IsNotExist(err) {
return err
}
// no db file
return nil
}
return os.Remove(filename)
}

View file

@ -0,0 +1,25 @@
package location
import (
"encoding/json"
"log"
)
type Location struct {
Name string `db:"name,key" json:"name"`
Lat string `db:"lat" json:"latitude"`
Lon string `db:"lon" json:"longitude"`
Timezone string `db:"timezone" json:"timezone"`
Fullname string `db:"displayName" json:"address"`
}
// String returns string representation of location.
func (l *Location) String() string {
bytes, err := json.Marshal(l)
if err != nil {
// should never happen
log.Fatalln(err)
}
return string(bytes)
}

View file

@ -0,0 +1,77 @@
package location
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"github.com/chubin/wttr.in/internal/types"
log "github.com/sirupsen/logrus"
)
type Nominatim struct {
name string
url string
token string
typ string
}
type locationQuerier interface {
Query(*Nominatim, string) (*Location, error)
}
func NewNominatim(name, typ, url, token string) *Nominatim {
return &Nominatim{
name: name,
url: url,
token: token,
typ: typ,
}
}
func (n *Nominatim) Query(location string) (*Location, error) {
var data locationQuerier
switch n.typ {
case "iq":
data = &locationIQ{}
case "opencage":
data = &locationOpenCage{}
default:
return nil, fmt.Errorf("%s: %w", n.name, types.ErrUnknownLocationService)
}
return data.Query(n, location)
}
func makeQuery(url string, result interface{}) error {
var errResponse struct {
Error string
}
log.Debugln("nominatim:", url)
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
err = json.Unmarshal(body, &errResponse)
if err == nil && errResponse.Error != "" {
return fmt.Errorf("%w: %s", types.ErrUpstream, errResponse.Error)
}
log.Debugln("nominatim: response: ", string(body))
err = json.Unmarshal(body, &result)
if err != nil {
return err
}
return nil
}

View file

@ -0,0 +1,39 @@
package location
import (
"fmt"
"net/url"
"github.com/chubin/wttr.in/internal/types"
)
type locationIQ []struct {
Name string `db:"name,key"`
Lat string `db:"lat"`
Lon string `db:"lon"`
//nolint:tagliatelle
Fullname string `db:"displayName" json:"display_name"`
}
func (data *locationIQ) Query(n *Nominatim, location string) (*Location, error) {
url := fmt.Sprintf(
"%s?q=%s&format=json&language=native&limit=1&key=%s",
n.url, url.QueryEscape(location), n.token)
err := makeQuery(url, data)
if err != nil {
return nil, fmt.Errorf("%s: %w", n.name, err)
}
if len(*data) != 1 {
return nil, fmt.Errorf("%w: %s: invalid response", types.ErrUpstream, n.name)
}
nl := &(*data)[0]
return &Location{
Lat: nl.Lat,
Lon: nl.Lon,
Fullname: nl.Fullname,
}, nil
}

View file

@ -0,0 +1,42 @@
package location
import (
"fmt"
"net/url"
"github.com/chubin/wttr.in/internal/types"
)
type locationOpenCage struct {
Results []struct {
Name string `db:"name,key"`
Geometry struct {
Lat float64 `db:"lat"`
Lng float64 `db:"lng"`
}
Fullname string `json:"formatted"`
} `json:"results"`
}
func (data *locationOpenCage) Query(n *Nominatim, location string) (*Location, error) {
url := fmt.Sprintf(
"%s?q=%s&language=native&limit=1&key=%s",
n.url, url.QueryEscape(location), n.token)
err := makeQuery(url, data)
if err != nil {
return nil, fmt.Errorf("%s: %w", n.name, err)
}
if len(data.Results) != 1 {
return nil, fmt.Errorf("%w: %s: invalid response", types.ErrUpstream, n.name)
}
nl := data.Results[0]
return &Location{
Lat: fmt.Sprint(nl.Geometry.Lat),
Lon: fmt.Sprint(nl.Geometry.Lng),
Fullname: nl.Fullname,
}, nil
}

View file

@ -0,0 +1,44 @@
package location
import (
"encoding/json"
"fmt"
"log"
"net/http"
"github.com/chubin/wttr.in/internal/routing"
)
// Response provides routing interface to the geo cache.
func (c *Cache) Response(r *http.Request) *routing.Cadre {
var (
locationName = r.URL.Query().Get("location")
loc *Location
bytes []byte
err error
)
if locationName == "" {
return errorResponse("location is not specified")
}
loc, err = c.Resolve(locationName)
if err != nil {
log.Println("geo/location error:", locationName)
return errorResponse(fmt.Sprint(err))
}
bytes, err = json.Marshal(loc)
if err != nil {
return errorResponse(fmt.Sprint(err))
}
return &routing.Cadre{Body: bytes}
}
func errorResponse(s string) *routing.Cadre {
return &routing.Cadre{Body: []byte(
fmt.Sprintf(`{"error": %q}`, s),
)}
}

View file

@ -0,0 +1,42 @@
package location
import "github.com/chubin/wttr.in/internal/config"
type Provider interface {
Query(location string) (*Location, error)
}
type Searcher struct {
providers []Provider
}
// NewSearcher returns a new Searcher for the specified config.
func NewSearcher(config *config.Config) *Searcher {
providers := []Provider{}
for _, p := range config.Geo.Nominatim {
providers = append(providers, NewNominatim(p.Name, p.Type, p.URL, p.Token))
}
return &Searcher{
providers: providers,
}
}
// Search makes queries through all known providers,
// and returns response, as soon as it is not nil.
// If all responses were nil, the last response is returned.
func (s *Searcher) Search(location string) (*Location, error) {
var (
err error
result *Location
)
for _, p := range s.providers {
result, err = p.Query(location)
if result != nil && err == nil {
return result, nil
}
}
return result, err
}

124
internal/logging/logging.go Normal file
View file

@ -0,0 +1,124 @@
package logging
import (
"fmt"
"net/http"
"os"
"sync"
"time"
"github.com/chubin/wttr.in/internal/util"
)
// Logging request.
//
// RequestLogger logs all incoming HTTP requests.
type RequestLogger struct {
buf map[logEntry]int
filename string
m sync.Mutex
period time.Duration
lastFlush time.Time
}
type logEntry struct {
Proto string
IP string
URI string
UserAgent string
}
// NewRequestLogger returns a new RequestLogger for the specified log file.
// Flush logging entries after period of time.
//
// If filename is empty, no log will be written, and all logging entries
// will be silently dropped.
func NewRequestLogger(filename string, period time.Duration) *RequestLogger {
return &RequestLogger{
buf: map[logEntry]int{},
filename: filename,
m: sync.Mutex{},
period: period,
}
}
// Log logs information about a HTTP request.
func (rl *RequestLogger) Log(r *http.Request) error {
le := logEntry{
Proto: "http",
IP: util.ReadUserIP(r),
URI: r.RequestURI,
UserAgent: r.Header.Get("User-Agent"),
}
if r.TLS != nil {
le.Proto = "https"
}
// Do not log 127.0.0.1 connections
if le.IP == "127.0.0.1" {
return nil
}
rl.m.Lock()
rl.buf[le]++
rl.m.Unlock()
if time.Since(rl.lastFlush) > rl.period {
return rl.flush()
}
return nil
}
// flush stores log data to disk, and flushes the buffer.
func (rl *RequestLogger) flush() error {
rl.m.Lock()
defer rl.m.Unlock()
// It is possible, that while waiting the mutex,
// the buffer was already flushed.
if time.Since(rl.lastFlush) <= rl.period {
return nil
}
if rl.filename != "" {
// Generate log output.
output := ""
for k, hitsNumber := range rl.buf {
output += fmt.Sprintf("%s %3d %s\n", time.Now().Format(time.RFC3339), hitsNumber, k.String())
}
// Open log file.
//nolint:nosnakecase
f, err := os.OpenFile(rl.filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o600)
if err != nil {
return err
}
defer f.Close()
// Save output to log file.
_, err = f.Write([]byte(output))
if err != nil {
return err
}
}
// Flush buffer.
rl.buf = map[logEntry]int{}
rl.lastFlush = time.Now()
return nil
}
// String returns string representation of logEntry.
func (e *logEntry) String() string {
return fmt.Sprintf(
"%s %s %s %s",
e.Proto,
e.IP,
e.URI,
e.UserAgent,
)
}

View file

@ -0,0 +1,84 @@
package logging
import (
"os"
"strings"
"sync"
)
// LogSuppressor provides io.Writer interface for logging
// with lines suppression. For usage with log.Logger.
type LogSuppressor struct {
filename string
suppress []string
linePrefix string
logFile *os.File
m sync.Mutex
}
// NewLogSuppressor creates a new LogSuppressor for specified
// filename and lines to be suppressed.
//
// If filename is empty, log entries will be printed to stderr.
func NewLogSuppressor(filename string, suppress []string, linePrefix string) *LogSuppressor {
return &LogSuppressor{
filename: filename,
suppress: suppress,
linePrefix: linePrefix,
}
}
// Open opens log file.
func (ls *LogSuppressor) Open() error {
var err error
if ls.filename == "" {
return nil
}
//nolint:nosnakecase
ls.logFile, err = os.OpenFile(ls.filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o600)
return err
}
// Close closes log file.
func (ls *LogSuppressor) Close() error {
if ls.filename == "" {
return nil
}
return ls.logFile.Close()
}
// Write writes p to log, and returns number f bytes written.
// Implements io.Writer interface.
func (ls *LogSuppressor) Write(p []byte) (int, error) {
var output string
if ls.filename == "" {
return os.Stdin.Write(p)
}
ls.m.Lock()
defer ls.m.Unlock()
lines := strings.Split(string(p), ls.linePrefix)
for _, line := range lines {
if (func(line string) bool {
for _, suppress := range ls.suppress {
if strings.Contains(line, suppress) {
return true
}
}
return false
})(line) {
continue
}
output += line
}
return ls.logFile.Write([]byte(output))
}

89
internal/processor/j1.go Normal file
View file

@ -0,0 +1,89 @@
package processor
import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
)
func getAny(req *http.Request, tr1, tr2, tr3 *http.Transport) (*ResponseWithHeader, error) {
uri := strings.ReplaceAll(req.URL.RequestURI(), "%", "%25")
u, err := url.Parse(uri)
if err != nil {
return nil, err
}
format := u.Query().Get("format")
if format == "j1" {
return getJ1(req, tr1)
} else if format != "" {
return getFormat(req, tr2)
}
// log.Println(req.URL.Query())
// log.Println()
return getDefault(req, tr3)
}
func getJ1(req *http.Request, transport *http.Transport) (*ResponseWithHeader, error) {
return getUpstream(req, transport)
}
func getFormat(req *http.Request, transport *http.Transport) (*ResponseWithHeader, error) {
return getUpstream(req, transport)
}
func getDefault(req *http.Request, transport *http.Transport) (*ResponseWithHeader, error) {
return getUpstream(req, transport)
}
func getUpstream(req *http.Request, transport *http.Transport) (*ResponseWithHeader, error) {
client := &http.Client{
Transport: transport,
}
queryURL := fmt.Sprintf("http://%s%s", req.Host, req.RequestURI)
proxyReq, err := http.NewRequest(req.Method, queryURL, req.Body)
if err != nil {
return nil, err
}
// proxyReq.Header.Set("Host", req.Host)
// proxyReq.Header.Set("X-Forwarded-For", req.RemoteAddr)
for header, values := range req.Header {
for _, value := range values {
proxyReq.Header.Add(header, value)
}
}
if proxyReq.Header.Get("X-Forwarded-For") == "" {
proxyReq.Header.Set("X-Forwarded-For", ipFromAddr(req.RemoteAddr))
}
res, err := client.Do(proxyReq)
if err != nil {
return nil, err
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
return &ResponseWithHeader{
InProgress: false,
Expires: time.Now().Add(time.Duration(randInt(1000, 1500)) * time.Second),
Body: body,
Header: res.Header,
StatusCode: res.StatusCode,
}, nil
}

View file

@ -0,0 +1,98 @@
package processor
import (
"log"
"net/http"
"sync"
"time"
"github.com/robfig/cron"
)
func (rp *RequestProcessor) startPeakHandling() error {
var err error
c := cron.New()
// cronTime := fmt.Sprintf("%d,%d * * * *", 30-prefetchInterval/60, 60-prefetchInterval/60)
err = c.AddFunc(
"24 * * * *",
func() { rp.prefetchPeakRequests(&rp.peakRequest30) },
)
if err != nil {
return err
}
err = c.AddFunc(
"54 * * * *",
func() { rp.prefetchPeakRequests(&rp.peakRequest60) },
)
if err != nil {
return err
}
c.Start()
return nil
}
// registerPeakRequest registers requests coming in the peak time.
// Such requests can be prefetched afterwards just before the peak time comes.
func (rp *RequestProcessor) savePeakRequest(cacheDigest string, r *http.Request) {
if _, min, _ := time.Now().Clock(); min == 30 {
rp.peakRequest30.Store(cacheDigest, *r)
} else if min == 0 {
rp.peakRequest60.Store(cacheDigest, *r)
}
}
func (rp *RequestProcessor) prefetchRequest(r *http.Request) error {
_, err := rp.ProcessRequest(r)
return err
}
func syncMapLen(sm *sync.Map) int {
count := 0
f := func(key, value interface{}) bool {
// Not really certain about this part, don't know for sure
// if this is a good check for an entry's existence
if key == "" {
return false
}
count++
return true
}
sm.Range(f)
return count
}
func (rp *RequestProcessor) prefetchPeakRequests(peakRequestMap *sync.Map) {
peakRequestLen := syncMapLen(peakRequestMap)
if peakRequestLen == 0 {
return
}
log.Printf("PREFETCH: Prefetching %d requests\n", peakRequestLen)
sleepBetweenRequests := time.Duration(rp.config.Uplink.PrefetchInterval*1000/peakRequestLen) * time.Millisecond
peakRequestMap.Range(func(key interface{}, value interface{}) bool {
req, ok := value.(http.Request)
if !ok {
log.Println("missing value for:", key)
return true
}
go func(r http.Request) {
err := rp.prefetchRequest(&r)
if err != nil {
log.Println("prefetch request:", err)
}
}(req)
peakRequestMap.Delete(key)
time.Sleep(sleepBetweenRequests)
return true
})
}

View file

@ -0,0 +1,356 @@
package processor
import (
"context"
"fmt"
"log"
"math/rand"
"net"
"net/http"
"strings"
"sync"
"time"
lru "github.com/hashicorp/golang-lru"
"github.com/chubin/wttr.in/internal/config"
geoip "github.com/chubin/wttr.in/internal/geo/ip"
geoloc "github.com/chubin/wttr.in/internal/geo/location"
"github.com/chubin/wttr.in/internal/routing"
"github.com/chubin/wttr.in/internal/stats"
"github.com/chubin/wttr.in/internal/util"
)
// plainTextAgents contains signatures of the plain-text agents.
func plainTextAgents() []string {
return []string{
"curl",
"httpie",
"lwp-request",
"wget",
"python-httpx",
"python-requests",
"openbsd ftp",
"powershell",
"fetch",
"aiohttp",
"http_get",
"xh",
"nushell",
}
}
type ResponseWithHeader struct {
InProgress bool // true if the request is being processed
Expires time.Time // expiration time of the cache entry
Body []byte
Header http.Header
StatusCode int // e.g. 200
}
// RequestProcessor handles incoming requests.
type RequestProcessor struct {
peakRequest30 sync.Map
peakRequest60 sync.Map
lruCache *lru.Cache
stats *stats.Stats
router routing.Router
upstreamTransport1 *http.Transport
upstreamTransport2 *http.Transport
upstreamTransport3 *http.Transport
config *config.Config
geoIPCache *geoip.Cache
geoLocation *geoloc.Cache
}
// NewRequestProcessor returns new RequestProcessor.
func NewRequestProcessor(config *config.Config) (*RequestProcessor, error) {
lruCache, err := lru.New(config.Cache.Size)
if err != nil {
return nil, err
}
dialer := &net.Dialer{
Timeout: time.Duration(config.Uplink.Timeout) * time.Second,
KeepAlive: time.Duration(config.Uplink.Timeout) * time.Second,
DualStack: true,
}
transport1 := &http.Transport{
DialContext: func(ctx context.Context, network, _ string) (net.Conn, error) {
return dialer.DialContext(ctx, network, config.Uplink.Address1)
},
}
transport2 := &http.Transport{
DialContext: func(ctx context.Context, network, _ string) (net.Conn, error) {
return dialer.DialContext(ctx, network, config.Uplink.Address2)
},
}
transport3 := &http.Transport{
DialContext: func(ctx context.Context, network, _ string) (net.Conn, error) {
return dialer.DialContext(ctx, network, config.Uplink.Address3)
},
}
geoCache, err := geoip.NewCache(config)
if err != nil {
return nil, err
}
geoLocation, err := geoloc.NewCache(config)
if err != nil {
return nil, err
}
rp := &RequestProcessor{
lruCache: lruCache,
stats: stats.New(),
upstreamTransport1: transport1,
upstreamTransport2: transport2,
upstreamTransport3: transport3,
config: config,
geoIPCache: geoCache,
geoLocation: geoLocation,
}
// Initialize routes.
rp.router.AddPath("/:stats", rp.stats)
rp.router.AddPath("/:geo-ip-get", rp.geoIPCache)
rp.router.AddPath("/:geo-ip-put", rp.geoIPCache)
rp.router.AddPath("/:geo-location", rp.geoLocation)
return rp, nil
}
// Start starts async request processor jobs, such as peak handling.
func (rp *RequestProcessor) Start() error {
return rp.startPeakHandling()
}
func (rp *RequestProcessor) ProcessRequest(r *http.Request) (*ResponseWithHeader, error) {
var (
response *ResponseWithHeader
ip = util.ReadUserIP(r)
)
if ip != "127.0.0.1" {
rp.stats.Inc("total")
}
// Main routing logic.
if rh := rp.router.Route(r); rh != nil {
result := rh.Response(r)
if result != nil {
return fromCadre(result), nil
}
}
if resp, ok := redirectInsecure(r); ok {
rp.stats.Inc("redirects")
return resp, nil
}
if dontCache(r) {
rp.stats.Inc("uncached")
return getAny(r, rp.upstreamTransport1, rp.upstreamTransport2, rp.upstreamTransport3)
}
// processing cached request
cacheDigest := getCacheDigest(r)
rp.savePeakRequest(cacheDigest, r)
response = rp.processRequestFromCache(r)
if response != nil {
return response, nil
}
return rp.processUncachedRequest(r)
}
// processRequestFromCache processes requests using the cache.
// If no entry in cache found, nil is returned.
func (rp *RequestProcessor) processRequestFromCache(r *http.Request) *ResponseWithHeader {
var (
cacheEntry ResponseWithHeader
cacheDigest = getCacheDigest(r)
ok bool
)
cacheBody, _ := rp.lruCache.Get(cacheDigest)
cacheEntry, ok = cacheBody.(ResponseWithHeader)
if !ok {
return nil
}
// If after all attempts we still have no answer,
// respond with an error message.
// (WAS: we try to make the query on our own)
for attempts := 0; attempts < 300; attempts++ {
if !ok || !cacheEntry.InProgress {
break
}
time.Sleep(30 * time.Millisecond)
cacheBody, _ = rp.lruCache.Get(cacheDigest)
v, ok := cacheBody.(ResponseWithHeader)
if ok {
cacheEntry = v
}
}
if cacheEntry.InProgress {
// log.Printf("TIMEOUT: %s\n", cacheDigest)
return &ResponseWithHeader{
InProgress: false,
Expires: time.Now().Add(time.Duration(randInt(1000, 1500)) * time.Second),
Body: []byte("This query is already being processed"),
StatusCode: 200,
}
}
if ok && !cacheEntry.InProgress && cacheEntry.Expires.After(time.Now()) {
rp.stats.Inc("cache1")
return &cacheEntry
}
return nil
}
// processUncachedRequest processes requests that were not found in the cache.
func (rp *RequestProcessor) processUncachedRequest(r *http.Request) (*ResponseWithHeader, error) {
var (
cacheDigest = getCacheDigest(r)
ip = util.ReadUserIP(r)
response *ResponseWithHeader
err error
)
// Indicate, that the request is being handled.
rp.lruCache.Add(cacheDigest, ResponseWithHeader{InProgress: true})
// Response was not found in cache.
// Starting real handling.
format := r.URL.Query().Get("format")
if len(format) != 0 {
rp.stats.Inc("format")
if format == "j1" {
rp.stats.Inc("format=j1")
}
}
// Count, how many IP addresses are known.
_, err = rp.geoIPCache.Read(ip)
if err == nil {
rp.stats.Inc("geoip")
}
response, err = getAny(r, rp.upstreamTransport1, rp.upstreamTransport2, rp.upstreamTransport3)
if err != nil {
return nil, err
}
if response.StatusCode == 200 || response.StatusCode == 304 || response.StatusCode == 404 {
rp.lruCache.Add(cacheDigest, *response)
} else {
log.Printf("REMOVE: %d response for %s from cache\n", response.StatusCode, cacheDigest)
rp.lruCache.Remove(cacheDigest)
}
return response, nil
}
// getCacheDigest is an implementation of the cache.get_signature of original wttr.in.
func getCacheDigest(req *http.Request) string {
userAgent := req.Header.Get("User-Agent")
queryHost := req.Host
queryString := req.RequestURI
clientIPAddress := util.ReadUserIP(req)
lang := req.Header.Get("Accept-Language")
return fmt.Sprintf("%s:%s%s:%s:%s", userAgent, queryHost, queryString, clientIPAddress, lang)
}
// dontCache returns true if req should not be cached.
func dontCache(req *http.Request) bool {
// dont cache cyclic requests
loc := strings.Split(req.RequestURI, "?")[0]
return strings.Contains(loc, ":")
}
// redirectInsecure returns redirection response, and bool value, if redirection was needed,
// if the query comes from a browser, and it is insecure.
//
// Insecure queries are marked by the frontend web server
// with X-Forwarded-Proto header:
// `proxy_set_header X-Forwarded-Proto $scheme;`.
func redirectInsecure(req *http.Request) (*ResponseWithHeader, bool) {
if isPlainTextAgent(req.Header.Get("User-Agent")) {
return nil, false
}
if req.TLS != nil || strings.ToLower(req.Header.Get("X-Forwarded-Proto")) == "https" {
return nil, false
}
target := "https://" + req.Host + req.URL.Path
if len(req.URL.RawQuery) > 0 {
target += "?" + req.URL.RawQuery
}
body := []byte(fmt.Sprintf(`<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="%s">here</A>.
</BODY></HTML>
`, target))
return &ResponseWithHeader{
InProgress: false,
Expires: time.Now().Add(time.Duration(randInt(1000, 1500)) * time.Second),
Body: body,
Header: http.Header{"Location": []string{target}},
StatusCode: 301,
}, true
}
// isPlainTextAgent returns true if userAgent is a plain-text agent.
func isPlainTextAgent(userAgent string) bool {
userAgentLower := strings.ToLower(userAgent)
for _, signature := range plainTextAgents() {
if strings.Contains(userAgentLower, signature) {
return true
}
}
return false
}
func randInt(min int, max int) int {
return min + rand.Intn(max-min)
}
// ipFromAddr returns IP address from a ADDR:PORT pair.
func ipFromAddr(s string) string {
pos := strings.LastIndex(s, ":")
if pos == -1 {
return s
}
return s[:pos]
}
// fromCadre converts Cadre into a responseWithHeader.
func fromCadre(cadre *routing.Cadre) *ResponseWithHeader {
return &ResponseWithHeader{
Body: cadre.Body,
Expires: cadre.Expires,
StatusCode: 200,
InProgress: false,
}
}

View file

@ -0,0 +1,72 @@
package routing
import (
"net/http"
"time"
)
// CadreFormat specifies how the shot data is formatted.
type CadreFormat int
const (
// CadreFormatANSI represents Terminal ANSI format.
CadreFormatANSI = iota
// CadreFormatHTML represents HTML.
CadreFormatHTML
// CadreFormatPNG represents PNG.
CadreFormatPNG
)
// Cadre contains result of a query execution.
type Cadre struct {
// Body contains the data of Cadre, formatted as Format.
Body []byte
// Format of the shot.
Format CadreFormat
// Expires contains the time of the Cadre expiration,
// or 0 if it does not expire.
Expires time.Time
}
// Handler can handle queries and return views.
type Handler interface {
Response(*http.Request) *Cadre
}
type routeFunc func(*http.Request) bool
type route struct {
routeFunc
Handler
}
// Router keeps a routing table, and finds queries handlers, based on its rules.
type Router struct {
rt []route
}
// Route returns a query handler based on its content.
func (r *Router) Route(req *http.Request) Handler {
for _, re := range r.rt {
if re.routeFunc(req) {
return re.Handler
}
}
return nil
}
// AddPath adds route for a static path.
func (r *Router) AddPath(path string, handler Handler) {
r.rt = append(r.rt, route{routePath(path), handler})
}
func routePath(path string) routeFunc {
return routeFunc(func(req *http.Request) bool {
return req.URL.Path == path
})
}

89
internal/stats/stats.go Normal file
View file

@ -0,0 +1,89 @@
package stats
import (
"bytes"
"fmt"
"net/http"
"sync"
"time"
"github.com/chubin/wttr.in/internal/routing"
)
// Stats holds processed requests statistics.
type Stats struct {
m sync.Mutex
v map[string]int
startTime time.Time
}
// New returns new Stats.
func New() *Stats {
return &Stats{
v: map[string]int{},
startTime: time.Now(),
}
}
// Inc key by one.
func (c *Stats) Inc(key string) {
c.m.Lock()
c.v[key]++
c.m.Unlock()
}
// Get current key counter value.
func (c *Stats) Get(key string) int {
c.m.Lock()
defer c.m.Unlock()
return c.v[key]
}
// Reset key counter.
func (c *Stats) Reset(key string) int {
c.m.Lock()
defer c.m.Unlock()
result := c.v[key]
c.v[key] = 0
return result
}
// Show returns current statistics formatted as []byte.
func (c *Stats) Show() []byte {
var b bytes.Buffer
c.m.Lock()
defer c.m.Unlock()
uptime := time.Since(c.startTime) / time.Second
fmt.Fprintf(&b, "%-20s: %v\n", "Running since", c.startTime.Format(time.RFC3339))
fmt.Fprintf(&b, "%-20s: %d\n", "Uptime (min)", uptime/60)
fmt.Fprintf(&b, "%-20s: %d\n", "Total queries", c.v["total"])
if uptime != 0 {
fmt.Fprintf(&b, "%-20s: %d\n", "Throughput (QpM)", c.v["total"]*60/int(uptime))
}
fmt.Fprintf(&b, "%-20s: %d\n", "Cache L1 queries", c.v["cache1"])
if c.v["total"] != 0 {
fmt.Fprintf(&b, "%-20s: %d\n", "Cache L1 queries (%)", (100*c.v["cache1"])/c.v["total"])
}
fmt.Fprintf(&b, "%-20s: %d\n", "Upstream queries", c.v["total"]-c.v["cache1"])
fmt.Fprintf(&b, "%-20s: %d\n", "Queries with format", c.v["format"])
fmt.Fprintf(&b, "%-20s: %d\n", "Queries with format=j1", c.v["format=j1"])
fmt.Fprintf(&b, "%-20s: %d\n", "Queries with known IP", c.v["geoip"])
return b.Bytes()
}
func (c *Stats) Response(*http.Request) *routing.Cadre {
return &routing.Cadre{
Body: c.Show(),
}
}

14
internal/types/errors.go Normal file
View file

@ -0,0 +1,14 @@
package types
import "errors"
var (
ErrNotFound = errors.New("cache entry not found")
ErrInvalidCacheEntry = errors.New("invalid cache entry format")
ErrUpstream = errors.New("upstream error")
// ErrNoServersConfigured means that there are no servers to run.
ErrNoServersConfigured = errors.New("no servers configured")
ErrUnknownLocationService = errors.New("unknown location service")
)

8
internal/types/types.go Normal file
View file

@ -0,0 +1,8 @@
package types
type CacheType string
const (
CacheTypeDB = "db"
CacheTypeFiles = "files"
)

18
internal/util/files.go Normal file
View file

@ -0,0 +1,18 @@
package util
import "os"
// RemoveFileIfExists removes filename if exists, or does nothing if the file
// is not there. Returns an error, if it occurred during deletion.
func RemoveFileIfExists(filename string) error {
_, err := os.Stat(filename)
if err != nil {
if !os.IsNotExist(err) {
return err
}
// no db file
return nil
}
return os.Remove(filename)
}

26
internal/util/http.go Normal file
View file

@ -0,0 +1,26 @@
package util
import (
"log"
"net"
"net/http"
)
// ReadUserIP returns IP address of the client from http.Request,
// taking into account the HTTP headers.
func ReadUserIP(r *http.Request) string {
IPAddress := r.Header.Get("X-Real-Ip")
if IPAddress == "" {
IPAddress = r.Header.Get("X-Forwarded-For")
}
if IPAddress == "" {
IPAddress = r.RemoteAddr
var err error
IPAddress, _, err = net.SplitHostPort(IPAddress)
if err != nil {
log.Printf("ERROR: userip: %q is not IP:port\n", IPAddress)
}
}
return IPAddress
}

15
internal/util/yaml.go Normal file
View file

@ -0,0 +1,15 @@
package util
import (
"bytes"
"gopkg.in/yaml.v3"
)
// YamlUnmarshalStrict unmarshals YAML data with an error when unknown fields are present.
func YamlUnmarshalStrict(in []byte, out interface{}) error {
dec := yaml.NewDecoder(bytes.NewReader(in))
dec.KnownFields(true)
return dec.Decode(out)
}

192
internal/view/v1/api.go Normal file
View file

@ -0,0 +1,192 @@
//nolint:forbidigo,funlen,nestif,goerr113,gocognit,cyclop
package v1
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"strconv"
"strings"
)
//nolint:tagliatelle
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"`
}
//nolint:tagliatelle
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"`
}
func (g *global) getDataFromAPI() (*resp, error) {
var (
ret resp
params []string
)
if len(g.config.APIKey) == 0 {
return nil, fmt.Errorf("no API key specified. Setup instructions are in the README")
}
params = append(params, "key="+g.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 {
g.config.Numdays = v
} else {
g.config.City = arg
}
}
if len(g.config.City) > 0 {
params = append(params, "q="+url.QueryEscape(g.config.City))
}
params = append(params, "format=json", "num_of_days="+strconv.Itoa(g.config.Numdays), "tp=3")
if g.config.Lang != "" {
params = append(params, "lang="+g.config.Lang)
}
if g.debug {
fmt.Fprintln(os.Stderr, params)
}
res, err := http.Get(wuri + strings.Join(params, "&"))
if err != nil {
return nil, err
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
if g.debug {
var out bytes.Buffer
err := json.Indent(&out, body, "", " ")
if err != nil {
return nil, err
}
_, err = out.WriteTo(os.Stderr)
if err != nil {
return nil, err
}
fmt.Print("\n\n")
}
if g.config.Lang == "" {
if err = json.Unmarshal(body, &ret); err != nil {
return nil, err
}
} else {
if err = g.unmarshalLang(body, &ret); err != nil {
return nil, err
}
}
return &ret, nil
}
func (g *global) 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_"+g.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_"+g.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
}
if err := json.NewDecoder(&buf).Decode(r); err != nil {
return err
}
return nil
}

172
internal/view/v1/cmd.go Normal file
View file

@ -0,0 +1,172 @@
// This code represents wttr.in view v1.
// It is based on wego (github.com/schachmat/wego) from which it diverged back in 2016.
//nolint:forbidigo,funlen,gocognit,cyclop
package v1
import (
"encoding/json"
"errors"
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"os/user"
"path"
"regexp"
"strings"
"github.com/mattn/go-colorable"
"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 global struct {
ansiEsc *regexp.Regexp
config Configuration
configpath string
debug bool
}
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 (g *global) configload() error {
b, err := ioutil.ReadFile(g.configpath)
if err == nil {
return json.Unmarshal(b, &g.config)
}
return err
}
func (g *global) configsave() error {
j, err := json.MarshalIndent(g.config, "", "\t")
if err == nil {
return ioutil.WriteFile(g.configpath, j, 0o600)
}
return err
}
func (g *global) init() {
flag.IntVar(&g.config.Numdays, "days", 3, "Number of days of weather forecast to be displayed")
flag.StringVar(&g.config.Lang, "lang", "en", "Language of the report")
flag.StringVar(&g.config.City, "city", "New York", "City to be queried")
flag.BoolVar(&g.debug, "debug", false, "Print out raw json response for debugging purposes")
flag.BoolVar(&g.config.Imperial, "imperial", false, "Use imperial units")
flag.BoolVar(&g.config.Inverse, "inverse", false, "Use inverted colors")
flag.BoolVar(&g.config.Narrow, "narrow", false, "Narrow output (two columns)")
flag.StringVar(&g.config.LocationName, "location_name", "", "Location name (used in the caption)")
flag.BoolVar(&g.config.WindMS, "wind_in_ms", false, "Show wind speed in m/s")
flag.BoolVar(&g.config.RightToLeft, "right_to_left", false, "Right to left script")
g.configpath = os.Getenv("WEGORC")
if g.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)
}
g.configpath = path.Join(usr.HomeDir, ".wegorc")
}
g.config.APIKey = ""
g.config.Imperial = false
g.config.Lang = "en"
err := g.configload()
var pathError *os.PathError
if errors.Is(err, pathError) {
log.Printf("No config file found. Creating %s ...", g.configpath)
if err2 := g.configsave(); err2 != nil {
log.Fatal(err2)
}
} else if err != nil {
log.Fatalf("could not parse %v: %v", g.configpath, err)
}
g.ansiEsc = regexp.MustCompile("\033.*?m")
}
func Cmd() error {
g := global{}
g.init()
flag.Parse()
r, err := g.getDataFromAPI()
if err != nil {
return err
}
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 g.config.LocationName != "" {
locationName = g.config.LocationName
}
if g.config.Lang == "he" || g.config.Lang == "ar" || g.config.Lang == "fa" {
g.config.RightToLeft = true
}
if caption, ok := localizedCaption()[g.config.Lang]; !ok {
fmt.Printf("Weather report: %s\n\n", locationName)
} else {
if g.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 := g.formatCond(make([]string, 5), r.Data.Cur[0], true)
for _, val := range out {
if g.config.RightToLeft {
fmt.Fprint(stdout, strings.Repeat(" ", 94))
} else {
fmt.Fprint(stdout, " ")
}
fmt.Fprintln(stdout, val)
}
if g.config.Numdays == 0 {
return nil
}
if r.Data.Weather == nil {
log.Fatal("No detailed weather forecast available.")
}
for _, d := range r.Data.Weather {
lines, err := g.printDay(d)
if err != nil {
return err
}
for _, val := range lines {
fmt.Fprintln(stdout, val)
}
}
return nil
}

400
internal/view/v1/format.go Normal file
View file

@ -0,0 +1,400 @@
//nolint:funlen,nestif,cyclop,gocognit,gocyclo
package v1
import (
"fmt"
"strings"
"unicode/utf8"
"github.com/mattn/go-runewidth"
)
func windDir() map[string]string {
return 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",
}
}
func (g *global) formatTemp(c cond) string {
color := func(temp int, explicitPlus bool) string {
var col int
//nolint:dupl
if !g.config.Inverse {
// Extremely cold temperature must be shown with violet
// because dark blue is too dark
col = 165
switch temp {
case -15, -14, -13:
col = 171
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 g.config.Imperial {
temp = (temp*18 + 320) / 10
}
if explicitPlus {
return fmt.Sprintf("\033[38;5;%03dm+%d\033[0m", col, temp)
}
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 = ".."
explicitPlus1 := false
explicitPlus2 := false
if c.FeelsLikeC != t {
if t > 0 {
explicitPlus1 = true
}
if c.FeelsLikeC > 0 {
explicitPlus2 = true
}
if explicitPlus1 {
explicitPlus2 = false
}
return g.pad(
fmt.Sprintf("%s(%s) °%s",
color(t, explicitPlus1),
color(c.FeelsLikeC, explicitPlus2),
unitTemp()[g.config.Imperial]),
15)
}
return g.pad(fmt.Sprintf("%s °%s", color(c.FeelsLikeC, false), unitTemp()[g.config.Imperial]), 15)
}
func (g *global) formatWind(c cond) string {
unitWindString := unitWind(0, g.config.Lang)
if g.config.WindMS {
unitWindString = unitWind(2, g.config.Lang)
} else if g.config.Imperial {
unitWindString = unitWind(1, g.config.Lang)
}
hyphen := "-"
cWindGustKmph := speedToColor(c.WindGustKmph, windInRightUnits(c.WindGustKmph, g.config.WindMS, g.config.Imperial))
cWindspeedKmph := speedToColor(c.WindspeedKmph, windInRightUnits(c.WindspeedKmph, g.config.WindMS, g.config.Imperial))
if windInRightUnits(c.WindGustKmph, g.config.WindMS, g.config.Imperial) >
windInRightUnits(c.WindspeedKmph, g.config.WindMS, g.config.Imperial) {
return g.pad(
fmt.Sprintf("%s %s%s%s %s", windDir()[c.Winddir16Point], cWindspeedKmph, hyphen, cWindGustKmph, unitWindString),
15)
}
return g.pad(fmt.Sprintf("%s %s %s", windDir()[c.Winddir16Point], cWindspeedKmph, unitWindString), 15)
}
func windInRightUnits(spd int, windMS, imperial bool) int {
if windMS {
spd = (spd * 1000) / 3600
} else if imperial {
spd = (spd * 1000) / 1609
}
return spd
}
func speedToColor(spd, spdConverted int) string {
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
}
}
return fmt.Sprintf("\033[38;5;%03dm%d\033[0m", col, spdConverted)
}
func (g *global) formatVisibility(c cond) string {
if g.config.Imperial {
c.VisibleDistKM = (c.VisibleDistKM * 621) / 1000
}
return g.pad(fmt.Sprintf("%d %s", c.VisibleDistKM, unitVis(g.config.Imperial, g.config.Lang)), 15)
}
func (g *global) formatRain(c cond) string {
rainUnit := c.PrecipMM
if g.config.Imperial {
rainUnit = c.PrecipMM * 0.039
}
if c.ChanceOfRain != "" {
return g.pad(fmt.Sprintf(
"%.1f %s | %s%%",
rainUnit,
unitRain(g.config.Imperial, g.config.Lang),
c.ChanceOfRain), 15)
}
return g.pad(fmt.Sprintf("%.1f %s", rainUnit, unitRain(g.config.Imperial, g.config.Lang)), 15)
}
func (g *global) formatCond(cur []string, c cond, current bool) []string {
var (
ret []string
icon []string
)
if i, ok := codes()[c.WeatherCode]; !ok {
icon = getIcon("iconUnknown")
} else {
icon = i
}
if g.config.Inverse {
// inverting colors
for i := range icon {
icon[i] = strings.ReplaceAll(icon[i], "38;5;226", "38;5;94")
icon[i] = strings.ReplaceAll(icon[i], "38;5;250", "38;5;243")
icon[i] = strings.ReplaceAll(icon[i], "38;5;21", "38;5;18")
icon[i] = strings.ReplaceAll(icon[i], "38;5;255", "38;5;245")
icon[i] = strings.ReplaceAll(icon[i], "38;5;111", "38;5;63")
icon[i] = strings.ReplaceAll(icon[i], "38;5;251", "38;5;238")
}
}
// desc := fmt.Sprintf("%-15.15v", c.WeatherDesc[0].Value)
desc := c.WeatherDesc[0].Value
if g.config.RightToLeft {
for runewidth.StringWidth(desc) < 15 {
desc = " " + desc
}
for runewidth.StringWidth(desc) > 15 {
_, size := utf8.DecodeLastRuneInString(desc)
desc = desc[size:]
}
} else {
for runewidth.StringWidth(desc) < 15 {
desc += " "
}
for runewidth.StringWidth(desc) > 15 {
_, size := utf8.DecodeLastRuneInString(desc)
desc = desc[:len(desc)-size]
}
}
if current {
if g.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 g.config.RightToLeft {
if frstRune, size := utf8.DecodeRuneInString(desc); frstRune != ' ' {
desc = "…" + desc[size:]
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 += " "
}
}
}
}
if g.config.RightToLeft {
ret = append(
ret,
fmt.Sprintf("%v %v %v", cur[0], desc, icon[0]),
fmt.Sprintf("%v %v %v", cur[1], g.formatTemp(c), icon[1]),
fmt.Sprintf("%v %v %v", cur[2], g.formatWind(c), icon[2]),
fmt.Sprintf("%v %v %v", cur[3], g.formatVisibility(c), icon[3]),
fmt.Sprintf("%v %v %v", cur[4], g.formatRain(c), icon[4]))
} else {
ret = append(
ret,
fmt.Sprintf("%v %v %v", cur[0], icon[0], desc),
fmt.Sprintf("%v %v %v", cur[1], icon[1], g.formatTemp(c)),
fmt.Sprintf("%v %v %v", cur[2], icon[2], g.formatWind(c)),
fmt.Sprintf("%v %v %v", cur[3], icon[3], g.formatVisibility(c)),
fmt.Sprintf("%v %v %v", cur[4], icon[4], g.formatRain(c)))
}
return ret
}
func justifyCenter(s string, width int) string {
appendSide := 0
for runewidth.StringWidth(s) <= width {
if appendSide == 1 {
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 (g *global) pad(s string, mustLen int) string {
var ret string
ret = s
realLen := utf8.RuneCountInString(g.ansiEsc.ReplaceAllLiteralString(s, ""))
delta := mustLen - realLen
if delta > 0 {
if g.config.RightToLeft {
ret = strings.Repeat(" ", delta) + ret + "\033[0m"
} else {
ret += "\033[0m" + strings.Repeat(" ", delta)
}
} else if delta < 0 {
toks := g.ansiEsc.Split(s, 2)
tokLen := utf8.RuneCountInString(toks[0])
esc := g.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, g.pad(toks[1], mustLen-tokLen))
}
}
return ret
}

213
internal/view/v1/icons.go Normal file
View file

@ -0,0 +1,213 @@
package v1
//nolint:funlen
func getIcon(name string) []string {
icon := map[string][]string{
"iconUnknown": {
" .-. ",
" __) ",
" ( ",
" `- ",
" • ",
},
"iconSunny": {
"\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": {
"\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": {
" ",
"\033[38;5;250m .--. \033[0m",
"\033[38;5;250m .-( ). \033[0m",
"\033[38;5;250m (___.__)__) \033[0m",
" ",
},
"iconVeryCloudy": {
" ",
"\033[38;5;240;1m .--. \033[0m",
"\033[38;5;240;1m .-( ). \033[0m",
"\033[38;5;240;1m (___.__)__) \033[0m",
" ",
},
"iconLightShowers": {
"\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": {
"\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": {
"\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": {
"\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": {
"\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": {
"\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": {
"\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": {
"\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": {
"\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": {
"\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": {
"\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": {
"\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": {
"\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": {
" ",
"\033[38;5;251m _ - _ - _ - \033[0m",
"\033[38;5;251m _ - _ - _ \033[0m",
"\033[38;5;251m _ - _ - _ - \033[0m",
" ",
},
}
return icon[name]
}
func codes() map[int][]string {
return map[int][]string{
113: getIcon("iconSunny"),
116: getIcon("iconPartlyCloudy"),
119: getIcon("iconCloudy"),
122: getIcon("iconVeryCloudy"),
143: getIcon("iconFog"),
176: getIcon("iconLightShowers"),
179: getIcon("iconLightSleetShowers"),
182: getIcon("iconLightSleet"),
185: getIcon("iconLightSleet"),
200: getIcon("iconThunderyShowers"),
227: getIcon("iconLightSnow"),
230: getIcon("iconHeavySnow"),
248: getIcon("iconFog"),
260: getIcon("iconFog"),
263: getIcon("iconLightShowers"),
266: getIcon("iconLightRain"),
281: getIcon("iconLightSleet"),
284: getIcon("iconLightSleet"),
293: getIcon("iconLightRain"),
296: getIcon("iconLightRain"),
299: getIcon("iconHeavyShowers"),
302: getIcon("iconHeavyRain"),
305: getIcon("iconHeavyShowers"),
308: getIcon("iconHeavyRain"),
311: getIcon("iconLightSleet"),
314: getIcon("iconLightSleet"),
317: getIcon("iconLightSleet"),
320: getIcon("iconLightSnow"),
323: getIcon("iconLightSnowShowers"),
326: getIcon("iconLightSnowShowers"),
329: getIcon("iconHeavySnow"),
332: getIcon("iconHeavySnow"),
335: getIcon("iconHeavySnowShowers"),
338: getIcon("iconHeavySnow"),
350: getIcon("iconLightSleet"),
353: getIcon("iconLightShowers"),
356: getIcon("iconHeavyShowers"),
359: getIcon("iconHeavyRain"),
362: getIcon("iconLightSleetShowers"),
365: getIcon("iconLightSleetShowers"),
368: getIcon("iconLightSnowShowers"),
371: getIcon("iconHeavySnowShowers"),
374: getIcon("iconLightSleetShowers"),
377: getIcon("iconLightSleet"),
386: getIcon("iconThunderyShowers"),
389: getIcon("iconThunderyHeavyRain"),
392: getIcon("iconThunderySnowShowers"),
395: getIcon("iconHeavySnowShowers"),
}
}

338
internal/view/v1/locale.go Normal file
View file

@ -0,0 +1,338 @@
package v1
//nolint:funlen
func locale() map[string]string {
return map[string]string{
"af": "af_ZA",
"am": "am_ET",
"ar": "ar_TN",
"az": "az_AZ",
"be": "be_BY",
"bg": "bg_BG",
"bn": "bn_IN",
"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",
"eu": "eu_ES",
"fa": "fa_IR",
"fi": "fi_FI",
"fr": "fr_FR",
"fy": "fy_NL",
"ga": "ga_IE",
"gl": "gl_ES",
"he": "he_IL",
"hi": "hi_IN",
"hr": "hr_HR",
"hu": "hu_HU",
"hy": "hy_AM",
"ia": "ia",
"id": "id_ID",
"is": "is_IS",
"it": "it_IT",
"ja": "ja_JP",
"jv": "en_US",
"ka": "ka_GE",
"kk": "kk_KZ",
"ko": "ko_KR",
"ky": "ky_KG",
"lt": "lt_LT",
"lv": "lv_LV",
"mg": "mg_MG",
"mk": "mk_MK",
"ml": "ml_IN",
"mr": "mr_IN",
"nb": "nb_NO",
"nl": "nl_NL",
"nn": "nn_NO",
"oc": "oc_FR",
"pl": "pl_PL",
"pt-br": "pt_BR",
"pt": "pt_PT",
"ro": "ro_RO",
"ru": "ru_RU",
"sk": "sk_SK",
"sl": "sl_SI",
"sr-lat": "sr_RS@latin",
"sr": "sr_RS",
"sv": "sv_SE",
"sw": "sw_KE",
"ta": "ta_IN",
"th": "th_TH",
"tr": "tr_TR",
"uk": "uk_UA",
"uz": "uz_UZ",
"vi": "vi_VN",
"zh-cn": "zh_CN",
"zh-tw": "zh_TW",
"zh": "zh_CN",
"zu": "zu_ZA",
}
}
//nolint:funlen
func localizedCaption() map[string]string {
return map[string]string{
"af": "Weer verslag vir:",
"am": "የአየር ሁኔታ ዘገባ ለ ፥",
"ar": "تقرير حالة ألطقس",
"az": "Hava proqnozu:",
"be": "Прагноз надвор'я для:",
"bg": "Прогноза за времето в:",
"bn": "আবহাওয়া সঙ্ক্রান্ত তথ্য",
"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:",
"eu": "Eguraldia:",
"fa": "اوه و بآ تیعضو شرازگ",
"fi": "Säätiedotus:",
"fr": "Prévisions météo pour:",
"fy": "Waarberjocht foar:",
"ga": "Réamhaisnéis na haimsire do:",
"gl": "Previsión do tempo en:",
"he": ":ריוואה גזמ תיזחת",
"hi": "मौसम की जानकारी",
"hr": "Vremenska prognoza za:",
"hu": "Időjárás előrejelzés:",
"hy": "Եղանակի տեսություն:",
"ia": "Le tempore a:",
"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": "കാലാവസ്ഥ റിപ്പോർട്ട്:",
"mr": "हवामानाचा अंदाज:",
"nb": "Værmelding for:",
"nl": "Weerbericht voor:",
"nn": "Vêrmelding for:",
"oc": "Previsions metèo per:",
"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:",
"ta": "வானிலை அறிக்கை",
"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": "天氣預報:",
"mg": "Vinavina toetr'andro hoan'ny:",
}
}
//nolint:misspell,funlen
func daytimeTranslation() map[string][]string {
return map[string][]string{
"af": {"Oggend", "Middag", "Vroegaand", "Laatnag"},
"am": {"ጠዋት", "ከሰዓት በኋላ", "ምሽት", "ሌሊት"},
"ar": {"ﺎﻠﻠﻴﻟ", "ﺎﻠﻤﺳﺍﺀ", "ﺎﻠﻈﻫﺭ", "ﺎﻠﺼﺑﺎﺣ"},
"az": {"Səhər", "Gün", "Axşam", "Gecə"},
"be": {"Раніца", "Дзень", "Вечар", "Ноч"},
"bg": {"Сутрин", "Обяд", "Вечер", "Нощ"},
"bn": {"সকাল", "দুপুর", "সন্ধ্যা", "রাত্রি"},
"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": {"Morgen", "Mittag", "Abend", "Nacht"},
"el": {"Πρωί", "Μεσημέρι", "Απόγευμα", "Βράδυ"},
"en": {"Morning", "Noon", "Evening", "Night"},
"eo": {"Mateno", "Tago", "Vespero", "Nokto"},
"es": {"Mañana", "Mediodía", "Tarde", "Noche"},
"et": {"Hommik", "Päev", "Õhtu", "Öösel"},
"eu": {"Goiza", "Eguerdia", "Arratsaldea", "Gaua"},
"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"},
"gl": {"Mañá", "Mediodía", "Tarde", "Noite"},
"he": {"רקוב", "םוֹיְ", "ברֶעֶ", "הלָיְלַ"},
"hi": {"प्रातःकाल", "दोपहर", "सायंकाल", "रात"},
"hr": {"Jutro", "Dan", "Večer", "Noć"},
"hu": {"Reggel", "Dél", "Este", "Éjszaka"},
"hy": {"Առավոտ", "Կեսօր", "Երեկո", "Գիշեր"},
"ia": {"Matino", "Mediedie", "Vespere", "Nocte"},
"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": {"രാവിലെ", "മധ്യാഹ്നം", "വൈകുന്നേരം", "രാത്രി"},
"mr": {"सकाळ", "दुपार", "संध्याकाळ", "रात्र"},
"nl": {"'s Ochtends", "'s Middags", "'s Avonds", "'s Nachts"},
"nb": {"Morgen", "Middag", "Kveld", "Natt"},
"nn": {"Morgon", "Middag", "Kveld", "Natt"},
"oc": {"Matin", "Jorn", "Vèspre", "Nuèch"},
"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"},
"ta": {"காலை", "நண்பகல்", "சாயங்காலம்", "இரவு"},
"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"},
"mg": {"Maraina", "Tolakandro", "Ariva", "Alina"},
}
}
func unitTemp() map[bool]string {
return map[bool]string{
false: "C",
true: "F",
}
}
func localizedRain() map[string]map[bool]string {
return map[string]map[bool]string{
"en": {
false: "mm",
true: "in",
},
"be": {
false: "мм",
true: "in",
},
"ru": {
false: "мм",
true: "in",
},
"uk": {
false: "мм",
true: "in",
},
}
}
func localizedVis() map[string]map[bool]string {
return map[string]map[bool]string{
"en": {
false: "km",
true: "mi",
},
"be": {
false: "км",
true: "mi",
},
"ru": {
false: "км",
true: "mi",
},
"uk": {
false: "км",
true: "mi",
},
}
}
func localizedWind() map[string]map[int]string {
return map[string]map[int]string{
"en": {
0: "km/h",
1: "mph",
2: "m/s",
},
"be": {
0: "км/г",
1: "mph",
2: "м/c",
},
"ru": {
0: "км/ч",
1: "mph",
2: "м/c",
},
"tr": {
0: "km/sa",
1: "mph",
2: "m/s",
},
"uk": {
0: "км/год",
1: "mph",
2: "м/c",
},
}
}
func unitWind(unit int, lang string) string {
translation, ok := localizedWind()[lang]
if !ok {
translation = localizedWind()["en"]
}
return translation[unit]
}
func unitVis(unit bool, lang string) string {
translation, ok := localizedVis()[lang]
if !ok {
translation = localizedVis()["en"]
}
return translation[unit]
}
func unitRain(unit bool, lang string) string {
translation, ok := localizedRain()[lang]
if !ok {
translation = localizedRain()["en"]
}
return translation[unit]
}

130
internal/view/v1/view1.go Normal file
View file

@ -0,0 +1,130 @@
package v1
import (
"math"
"time"
"github.com/klauspost/lctime"
)
func slotTimes() []int {
return []int{9 * 60, 12 * 60, 18 * 60, 22 * 60}
}
//nolint:funlen,gocognit,cyclop
func (g *global) printDay(w weather) ([]string, error) {
var (
ret = []string{}
dateName string
names string
)
hourly := w.Hourly
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 g.config.RightToLeft {
slots[0], slots[3] = slots[3], slots[0]
slots[1], slots[2] = slots[2], slots[1]
}
for i, s := range slots {
if g.config.Narrow {
if i == 0 || i == 2 {
continue
}
}
ret = g.formatCond(ret, s, false)
for i := range ret {
ret[i] += "│"
}
}
d, _ := time.Parse("2006-01-02", w.Date)
// dateFmt := "┤ " + d.Format("Mon 02. Jan") + " ├"
if val, ok := locale()[g.config.Lang]; ok {
err := lctime.SetLocale(val)
if err != nil {
return nil, err
}
} else {
err := lctime.SetLocale("en_US")
if err != nil {
return nil, err
}
}
if g.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 g.config.Lang == "ko" {
dateName = lctime.Strftime("%b %d일 %a", d)
}
if g.config.Lang == "zh" || g.config.Lang == "zh-tw" || g.config.Lang == "zh-cn" {
dateName = lctime.Strftime("%b%d日%A", d)
}
}
dateFmt := "┤" + justifyCenter(dateName, 12) + "├"
trans := daytimeTranslation()["en"]
if t, ok := daytimeTranslation()[g.config.Lang]; ok {
trans = t
}
if g.config.Narrow {
names := "│ " + justifyCenter(trans[1], 16) +
"└──────┬──────┘" + justifyCenter(trans[3], 16) + " │"
ret = append([]string{
" ┌─────────────┐ ",
"┌───────────────────────" + dateFmt + "───────────────────────┐",
names,
"├──────────────────────────────┼──────────────────────────────┤",
},
ret...)
return append(ret,
"└──────────────────────────────┴──────────────────────────────┘"),
nil
}
if g.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) + "│"
}
//nolint:lll
ret = append([]string{
" ┌─────────────┐ ",
"┌──────────────────────────────┬───────────────────────" + dateFmt + "───────────────────────┬──────────────────────────────┐",
names,
"├──────────────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────┤",
},
ret...)
//nolint:lll
return append(ret,
"└──────────────────────────────┴──────────────────────────────┴──────────────────────────────┴──────────────────────────────┘"),
nil
}

View file

@ -316,5 +316,5 @@ LOCALE = {
"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", "zh": "zh_TW", "zu": "zu_ZA",
"uz": "uz_UZ", "vi": "vi_VN", "zh": "zh_TW", "zu": "zu_ZA", "mg": "mg_MG",
}

View file

@ -5,10 +5,22 @@ to select data source basing on location, or on the user's preferences.
## Possible data sources
* OpenWeatherMap
* AccuWeather
* Windy.com
* yr.no
* [Open weather map](https://openweathermap.org/)
* [Accu weather](https://www.accuweather.com/)
* [Windy](https://www.windy.com/?26.953,75.711,5)
* [Yr](https://www.yr.no/nb)
* [BBC WeatherFeeds](https://support.bbc.co.uk/platform/feeds/WeatherFeeds.htm)
* http://www.bom.gov.au
* https://weather.gc.ca
* [Bom](http://www.bom.gov.au)
* [IMD](https://mausam.imd.gov.in/)
* [darksky](https://darksky.net/forecast/40.7127,-74.0059/us12/en)
* [weather bug](https://www.weatherbug.com/)
* [weather underground](https://www.wunderground.com/)
* [brightsky](https://brightsky.dev/)
## Air Quality sources
* http://aqicn.org/
* https://docs.airnowapi.org/
* https://www2.purpleair.com/community/faq#hc-access-the-json

View file

@ -30,7 +30,7 @@ DESCRIPTION = {
"Temperature in Fahrenheit",
"temperature_fahrenheit"),
"uvIndex": (
"Ultaviolet Radiation Index",
"Ultraviolet Radiation Index",
"uv_index"),
"visibility": (
"Visible Distance in Kilometres",
@ -91,15 +91,15 @@ DESCRIPTION = {
# astronomy fields with time
"moonrise": (
"Minutes since start of the day untill the moon appears above the horizon",
"Minutes since start of the day until the moon appears above the horizon",
"astronomy_moonrise_min"),
"moonset": (
"Minutes since start of the day untill the moon disappears below the horizon",
"Minutes since start of the day until the moon disappears below the horizon",
"astronomy_moonset_min"),
"sunrise": (
"Minutes since start of the day untill the sun appears above the horizon",
"Minutes since start of the day until the sun appears above the horizon",
"astronomy_sunrise_min"),
"sunset": (
"Minutes since start of the day untill the moon disappears below the horizon",
"Minutes since start of the day until the moon disappears below the horizon",
"astronomy_sunset_min"),
}

View file

@ -148,7 +148,7 @@ def _script_category(char):
default, Cyrillic, Greek, Han, Hiragana
"""
if char in emoji.UNICODE_EMOJI:
if emoji.is_emoji(char):
return "Emoji"
cat = unicodedata2.script_cat(char)[0]

View file

@ -35,6 +35,10 @@ PNG_CACHE = os.path.join(_DATADIR, "cache/png")
LRU_CACHE = os.path.join(_DATADIR, "cache/lru")
LOG_FILE = os.path.join(_LOGDIR, 'main.log')
PROXY_LOG_ACCESS = os.path.join(_LOGDIR, 'proxy-access.log')
PROXY_LOG_ERRORS = os.path.join(_LOGDIR, 'proxy-errors.log')
MISSING_TRANSLATION_LOG = os.path.join(_LOGDIR, 'missing-translation/%s.log')
ALIASES = os.path.join(MYDIR, "share/aliases")
@ -79,14 +83,26 @@ PLAIN_TEXT_AGENTS = [
"lwp-request",
"wget",
"python-requests",
"python-httpx",
"openbsd ftp",
"powershell",
"fetch",
"aiohttp",
"http_get",
"xh",
"nushell",
]
PLAIN_TEXT_PAGES = [':help', ':bash.function', ':translation', ':iterm2']
TRANSLATION_TABLE = str.maketrans({
'\u2196': '\u256E', # '↖' -> '╮'
'\u2197': '\u256D', # '↗' -> '╭'
'\u2198': '\u2570', # '↘' -> '╰'
'\u2199': '\u256F', # '↙' -> '╯'
'\u26A1': '\u250C\u2518'
})
_IPLOCATION_ORDER = os.environ.get(
"WTTR_IPLOCATION_ORDER",
'geoip,ip2location,ipinfo')

View file

@ -40,6 +40,7 @@ import json
import os
import socket
import sys
import random
import geoip2.database
import pycountry
@ -48,7 +49,6 @@ import requests
from globals import GEOLITE, GEOLOCATOR_SERVICE, IP2LCACHE, IP2LOCATION_KEY, NOT_FOUND_LOCATION, \
ALIASES, BLACKLIST, IATA_CODES_FILE, IPLOCATION_ORDER, IPINFO_TOKEN
GEOIP_READER = geoip2.database.Reader(GEOLITE)
COUNTRY_MAP = {"Russian Federation": "Russia"}
@ -99,7 +99,10 @@ def _geolocator(location):
"""
try:
geo = requests.get('%s/%s' % (GEOLOCATOR_SERVICE, location)).text
if random.random() < 0:
geo = requests.get('%s/%s' % (GEOLOCATOR_SERVICE, location)).text
else:
geo = requests.get("http://127.0.0.1:8085/:geo-location?location=%s" % location).text
except requests.exceptions.ConnectionError as exception:
print("ERROR: %s" % exception)
return None
@ -109,6 +112,8 @@ def _geolocator(location):
try:
answer = json.loads(geo.encode('utf-8'))
if "error" in answer:
return None
return answer
except ValueError as exception:
print("ERROR: %s" % exception)
@ -129,6 +134,7 @@ def _ipcachewrite(ip_addr, location):
The latitude and longitude are optional elements.
"""
return
cachefile = os.path.join(IP2LCACHE, ip_addr)
if not os.path.exists(IP2LCACHE):
os.makedirs(IP2LCACHE)
@ -144,20 +150,29 @@ def _ipcache(ip_addr):
Returns a triple of (CITY, REGION, COUNTRY) or None
TODO: When cache becomes more robust, transition to using latlong
"""
cachefile = os.path.join(IP2LCACHE, ip_addr)
if os.path.exists(cachefile):
try:
_, country, region, city, *_ = open(cachefile, 'r').read().split(';')
return city, region, country
except ValueError:
# cache entry is malformed: should be
# [ccode];country;region;city;[lat];[long];...
return None
else:
_debug_log("[_ipcache] %s not found" % ip_addr)
## Use Geo IP service when available
r = requests.get("http://127.0.0.1:8085/:geo-ip-get?ip=%s" % ip_addr)
if r.status_code == 200 and ";" in r.text:
_, country, region, city, *_ = r.text.split(';')
return city, region, country
return None
# cachefile = os.path.join(IP2LCACHE, ip_addr)
#
# if os.path.exists(cachefile):
# try:
# _, country, region, city, *_ = open(cachefile, 'r').read().split(';')
# return city, region, country
# except ValueError:
# # cache entry is malformed: should be
# # [ccode];country;region;city;[lat];[long];...
# return None
# else:
# _debug_log("[_ipcache] %s not found" % ip_addr)
# return None
def _ip2location(ip_addr):
""" Convert IP address `ip_addr` to a location name using ip2location.
@ -334,11 +349,13 @@ def _get_hemisphere(location):
"""
if all(location):
location_string = ", ".join(location)
else:
return True
geolocation = _geolocator(location_string)
if geolocation is None:
return True
return geolocation["latitude"] > 0
return float(geolocation["latitude"]) > 0
def _fully_qualified_location(location, region, country):
@ -389,12 +406,17 @@ def location_processing(location, ip_addr):
if location and location.lstrip('~ ').startswith('@'):
try:
location, region, country = _get_location(
socket.gethostbyname(
location.lstrip('~ ')[1:]))
location = '~' + location
location = _fully_qualified_location(location, region, country)
hide_full_address = not force_show_full_address
if (location.lstrip('~ ')[1:] == ""):
location, region, country = NOT_FOUND_LOCATION, None, None
else:
location, region, country = _get_location(
socket.gethostbyname(
location.lstrip('~ ')[1:]))
location = '~' + location
location = _fully_qualified_location(location, region, country)
hide_full_address = not force_show_full_address
except:
location, region, country = NOT_FOUND_LOCATION, None, None
@ -475,7 +497,13 @@ def _main_():
print(city)
shutil.move(filename, os.path.join("/wttr.in/cache/ip2l-broken-format", ip_address))
def _trace_ip():
print(_geoip("108.5.186.108"))
print(_get_location("108.5.186.108"))
print(location_processing("", "108.5.186.108"))
if __name__ == "__main__":
_main_()
_trace_ip()
#_main_()
#print(_geoip("173.216.90.56"))

View file

@ -177,6 +177,8 @@ def hpa_to_mb(hpa):
def hpa_to_in(hpa):
return round(hpa * 0.02953, 2)
def hpa_to_mmHg(hpa):
return round(hpa * 0.75006157584566 , 3)
def group_hours_to_days(lat, lng, hourlies, days_to_return):
tf = timezonefinder.TimezoneFinder()
@ -345,6 +347,7 @@ def _convert_hour(hour):
"visibility": 'not yet implemented', # str(details['vis_km']),
"visibilityMiles": 'not yet implemented', # str(details['vis_miles']),
"pressure": str(hpa_to_mb(details['air_pressure_at_sea_level'])),
"pressure_mmHg": str(hpa_to_mmHg(details['air_pressure_at_sea_level'])),
"pressureInches": str(hpa_to_in(details['air_pressure_at_sea_level'])),
"cloudcover": 'not yet implemented', # Convert from cloud_area_fraction?? str(details['cloud']),
# metno doesn't have FeelsLikeC, but we-lang.go is using it in calcs,

View file

@ -79,11 +79,14 @@ def parse_query(args):
return result
if 'A' in q:
result['force-ansi'] = True
if 'd' in q:
result['dumb'] = True
if 'n' in q:
result['narrow'] = True
if 'm' in q:
result['use_metric'] = True
if 'M' in q:
result['use_metric'] = True
result['use_ms_for_wind'] = True
if 'u' in q:
result['use_imperial'] = True

48
lib/proxy_log.py Normal file
View file

@ -0,0 +1,48 @@
"""
Logger of proxy queries
"""
# pylint: disable=consider-using-with,too-few-public-methods
import datetime
class Logger:
"""
Generic logger.
For specific loggers, _shorten_query() should be rewritten.
"""
def __init__(self, filename_access, filename_errors):
self._filename_access = filename_access
self._filename_errors = filename_errors
self._log_access = open(filename_access, "a", encoding="utf-8")
self._log_errors = open(filename_errors, "a", encoding="utf-8")
def _shorten_query(self, query):
return query
def log(self, query, error):
"""
Log `query` and `error`
"""
message = str(datetime.datetime.now())
query = self._shorten_query(query)
if error != "":
message += " ERR " + query + " " + error
self._log_errors.write(message+"\n")
else:
message += " OK " + query
self._log_access.write(message+"\n")
class LoggerWWO(Logger):
"""
WWO logger.
"""
def _shorten_query(self, query):
return "".join([x for x in query.split("&") if x.startswith("q=")])

View file

@ -5,30 +5,30 @@ Translation of almost everything.
"""
FULL_TRANSLATION = [
"am", "ar", "af", "be", "ca", "da", "de", "el", "et",
"fr", "fa", "hi", "hu", "ia", "id", "it",
"am", "ar", "af", "be", "bn", "ca", "da", "de", "el", "et",
"fr", "fa", "gl", "hi", "hu", "ia", "id", "it", "lt", "mg",
"nb", "nl", "oc", "pl", "pt-br", "ro",
"ru", "tr", "th", "uk", "vi", "zh-cn", "zh-tw"
"ru", "ta", "tr", "th", "uk", "vi", "zh-cn", "zh-tw",
]
PARTIAL_TRANSLATION = [
"az", "bg", "bs", "cy", "cs",
"eo", "es", "eu", "fi", "ga", "hi", "hr",
"hy", "is", "ja", "jv", "ka", "kk",
"ko", "ky", "lt", "lv", "mk", "ml", "nl", "fy",
"nn", "pt", "pt-br", "sk", "sl", "sr", "sr-lat",
"sv", "sw", "te", "uz",
"zh", "zu", "he",
"ko", "ky", "lv", "mk", "ml", "mr", "nl", "fy",
"nn", "pt", "pt-br", "sk", "sl", "sr",
"sr-lat", "sv", "sw", "te", "uz", "zh",
"zu", "he",
]
PROXY_LANGS = [
"af", "am", "ar", "az", "be", "bs", "ca",
"af", "am", "ar", "az", "be", "bn", "bs", "ca",
"cy", "de", "el", "eo", "et", "eu", "fa", "fr",
"fy", "ga", "he", "hr", "hu", "hy",
"fy", "ga", "gl", "he", "hr", "hu", "hy",
"ia", "id", "is", "it", "ja", "kk",
"lv", "mk", "nb", "nn", "oc", "ro",
"ru", "sl", "th", "pt-br", "uk", "uz",
"vi", "zh-cn", "zh-tw",
"lt", "lv", "mg", "mk", "mr", "nb", "nn", "oc",
"ro", "ru", "sl", "th", "pt-br", "uk",
"uz", "vi", "zh-cn", "zh-tw",
]
SUPPORTED_LANGS = FULL_TRANSLATION + PARTIAL_TRANSLATION
@ -65,6 +65,11 @@ een van die koudste permanent bewoonde plekke op aarde.
Не успяхме да открием вашето местоположение
така че ви доведохме в Оймякон,
едно от най-студените постоянно обитавани места на планетата.
""",
'bn' : u"""
ি, আপন অবস আমর ইনি
, আমর আপন ি এসি ওয়মিকন,
ি তলতম জন-বসতি একটি
""",
'bs': u"""
Nismo mogli pronaći vašu lokaciju,
@ -129,6 +134,11 @@ Nous espérons qu'il fait meilleur chez vous !
rabhamar ábalta do cheantar a aimsiú
mar sin thugamar go dtí Oymyakon,
ceann do na ceantair bhuanáitrithe is fuaire ar domhan.
""",
'gl': u"""
Non logramos atopar a túa localización
polo que te trouxemos até Oimiakón,
un dos lugares máis fríos e permamentemente deshabitados do planeta.
""",
'hi': u"""
हम आपक जन असमर ,
@ -176,6 +186,11 @@ Ci auguriamo che le condizioni dove lei si trova siano migliori!
지정된 장소를 찾을 없습니다,
대신 오이먀콘의 일기 예보를 표시합니다,
오이먀콘은 지구상에서 가장 추운 곳에 위치한 마을입니다!
""",
'lt': u"""
Mums nepavyko rasti jūsų vietovės,
todėl mes nukreipėme jus į Omjakoną,
vieną šalčiausių nuolatinių gyvenviečių planetoje.
""",
'lv': u"""
Mēs nevarējām atrast jūsu atrašanās vietu tādēļ nogādājām jūs Oimjakonā,
@ -185,6 +200,11 @@ vienā no aukstākajām apdzīvotajām vietām uz planētas.
Неможевме да ја пронајдеме вашата локација,
затоа ве однесовме во Ојмајкон,
еден од најладните трајно населени места на планетата.
""",
'mr': u"""
आमह मच थळ पडल .
हण आम ओयम आणल आह,
ि आपल रहवर सर वसि एक आह.
""",
'nb': u"""
Vi kunne ikke finne din lokasjon,
@ -298,6 +318,15 @@ một trong những nơi lạnh nhất có người sinh sống trên trái đ
我們找不到您的位置
所以我們帶您到奧伊米亞康
這個星球上有人類定居最冷之處
""",
'mg': u"""
Tsy hita ny toerana misy anao koa nentinay tany Oymyakon ianao,
iray amin'ireo toerana mangatsiaka indrindra tsisy mponina eto an-tany.
""",
'ta': u"""
உஙகள இரிடத எஙகள கணிி ியவி
எனவ கள உஙகள ஓமி அழ வந.
ிரகதி ி ிரநதரம வசி இடஙகளி ஒன.
""",
},
@ -308,6 +337,7 @@ một trong những nơi lạnh nhất có người sinh sống trên trái đ
'ar': u'موقع غير معروف',
'be': u'Невядомае месцазнаходжанне',
'bg': u'Неизвестно местоположение',
'bn': u'অজানা অবস্থান',
'bs': u'Nepoznatoja lokacija',
'ca': u'Ubicació desconeguda',
'cs': u'Neznámá poloha',
@ -322,6 +352,7 @@ một trong những nơi lạnh nhất có người sinh sống trên trái đ
'fi': u'Tuntematon sijainti',
'fr': u'Emplacement inconnu',
'ga': u'Ceantar anaithnid',
'gl': u'Localización descoñecida',
'hi': u'अज्ञात स्थान',
'hu': u'Ismeretlen lokáció',
'hy': u'Անհայտ գտնվելու վայր',
@ -332,8 +363,10 @@ một trong những nơi lạnh nhất có người sinh sống trên trái đ
'ja': u'未知の場所です',
'ko': u'알 수 없는 장소',
'kk': u'',
'lt': u'Nežinoma vietovė',
'lv': u'Nezināma atrašanās vieta',
'mk': u'Непозната локација',
'mr': u'अज्ञात स्थळ',
'nb': u'Ukjent sted',
'nl': u'Onbekende locatie',
'oc': u'Emplaçament desconegut',
@ -355,6 +388,8 @@ một trong những nơi lạnh nhất có người sinh sống trên trái đ
'zh': u'未知地点',
'vi': u'Địa điểm không xác định',
'zh-tw': u'未知位置',
'mg': u'Toerana tsy fantatra',
'ta': u'தெரியாத இடம்',
},
'LOCATION': {
@ -363,6 +398,7 @@ một trong những nơi lạnh nhất có người sinh sống trên trái đ
'ar': u'الموقع',
'be': u'Месцазнаходжанне',
'bg': u'Местоположение',
'bn': u'অবস্থান',
'bs': u'Lokacija',
'ca': u'Ubicació',
'cs': u'Poloha',
@ -377,6 +413,7 @@ một trong những nơi lạnh nhất có người sinh sống trên trái đ
'fi': u'Tuntematon sijainti',
'fr': u'Emplacement',
'ga': u'Ceantar',
'gl': u'Localización',
'hi': u'स्थान',
'hu': u'Lokáció',
'hy': u'Դիրք',
@ -387,8 +424,10 @@ một trong những nơi lạnh nhất có người sinh sống trên trái đ
'ja': u'位置情報',
'ko': u'위치',
'kk': u'',
'lt': u'Vietovė',
'lv': u'Atrašanās vieta',
'mk': u'Локација',
'mr': u'स्थळ',
'nb': u'Sted',
'nl': u'Locatie',
'oc': u'Emplaçament',
@ -409,6 +448,8 @@ một trong những nơi lạnh nhất có người sinh sống trên trái đ
'uk': u'Місцезнаходження',
'vi': u'Địa điểm',
'zh-tw': u'位置',
'mg': u'Toerana',
'ta': u'இடம்',
},
'CAPACITY_LIMIT_REACHED': {
@ -434,10 +475,10 @@ U kan vir https://twitter.com/igor_chubin volg vir opdaterings.
======================================================================================
""",
'ar': u"""
نأسف, نفذت منا طلبات إستعلام خدمة الطقس في هذه اللحظة.
هذا التقرير الجوي للمدينة الإفتراضية (فقط لنريك, الشكل الذي تبدو عليه).
سوف نحصل علي طلبات إستعلام جديدة في أقرب وقت ممكن.
يمكنك متابعة https://twitter.com/igor_chubin من أجل الحصول علي أخر المستجدات.
عذرًا ، استعلامات خدمة الطقس نفذت في الوقت الحالي.
هذا هو تقرير الطقس للمدينة الافتراضية (فقط لتظهر لك كيف تبدو).
سوف نتلقى استعلامات جديدة في أقرب وقت ممكن.
يمكنك متابعة https://twitter.com/igor_chubin لآخر المستجدات.
======================================================================================
""",
'be': u"""
@ -452,6 +493,12 @@ U kan vir https://twitter.com/igor_chubin volg vir opdaterings.
Ето доклад за града по подразбиране (просто да видите как изглежда).
Ще осогурим допълнителни заявки максимално бързо.
Може да последвате https://twitter.com/igor_chubin за обновления.
""",
'bn': u"""
ি, এই আবহওয পরি আম ইর হয আসছ
এখ িফল শহর আবহওয রতিদন রয (এটি খত মন আপন জন)
আমর নত ইর ওয় যবস করছি
আপড জন আপনি https://twitter.com/igor_chubin অনসরণ করত
""",
'bs': u"""
Žao mi je, mi ponestaje upita i vremenska prognoza u ovom trenutku.
@ -508,6 +555,13 @@ Seo duit réamhaisnéis na haimsire don chathair réamhshocraithe (chun é a tha
Gheobhaimid iarratais nua chomh luath agus is feidir.
Lean orainn ar https://twitter.com/igor_chubin don eolas is déanaí.
======================================================================================
""",
'gl': u"""
Desculpa, estamos a chegar ao límite de peticións ao servizo meteorolóxico neste momento.
Aquí está a previsión do tempo para a cidade por defecto (tan para amosarche un exemplo).
Imos obter máis peticións tan pronto como poidamos.
Podes seguir https://twitter.com/igor_chubin para estares actualizada.
======================================================================================
""",
'hi': u"""
षम कर, इस समय हम सम पर नह कर रह
@ -557,6 +611,13 @@ Potete seguire https://twitter.com/igor_chubin per gli aggiornamenti.
쿼리 요청이 가능한 빨리 이루어질 있도록 하겠습니다.
업데이트 소식을 원하신다면 https://twitter.com/igor_chubin 팔로우 해주세요.
======================================================================================
""",
'lt': u"""
Atsiprašome, šiuo metu pasiekėme orų prognozės paslaugos užklausų ribą.
Štai orų prognozė numatomam miestui (tam, kad parodytume, kaip ji atrodo).
Naujas užklausas priimsime, kai tik galėsime.
Atnaujinimus galite sekti https://twitter.com/igor_chubin
======================================================================================
""",
'lv': u"""
Atvainojiet, uz doto brīdi mēs esam mazliet noslogoti.
@ -571,6 +632,13 @@ Jūs varat sekot https://twitter.com/igor_chubin lai redzētu visus jaunumus.
Ќе добиеме нови барања најбрзо што можеме.
Следете го https://twitter.com/igor_chubin за известувања
======================================================================================
""",
'mr': u"""
षमस, षण आम हव पर कर शकत .
एक वनिि शहर हव अहव आह (वळ कस िसत खवणकरि).
आम लवकर लवकर करण रयत कर.
अदवत ि https://twitter.com/igor_chubin अनसरण कर शकत.
======================================================================================
""",
'nb': u"""
Beklager, vi kan ikke værtjenesten for øyeblikket.
@ -690,6 +758,18 @@ Bạn có thể theo dõi https://twitter.com/igor_chubin để cập nhật th
我們將盡快取得新的資料
您可以追蹤 https://twitter.com/igor_chubin 以取得更新
======================================================================================
""",
'mg': u"""
Miala tsiny fa misedra olana ny sampan-draharaha momba ny toetrandro amin'izao fotoana izao.
Ity ny tatitra momba ny toetr'andro ho an'ny tanàna mahazatra (mba hampisehoana anao ny endriny).
Haivaly aminao haingana ny fangatahanao.
Azonao atao ny manaraka ny pejy https://twitter.com/igor_chubin.
""",
'ta': u"""
மனிகவ, தற ி ினவலகள எஙகளிடம இல.
இயலி நகரதி ி அறி இத (அத எபபடி இர எனபத உஙகள ிபதற).
ி ிி ி ினவலகள .
ிகள கள https://twitter.com/igor_chubin ஐப ிடரல.
""",
},
@ -704,11 +784,13 @@ Bạn có thể theo dõi https://twitter.com/igor_chubin để cập nhật th
'af': u'Nuwe eienskap: veeltalige name vir liggings \033[92mwttr.in/станция+Восток\033[0m (in UTF-8) en ligging soek \033[92mwttr.in/~Kilimanjaro\033[0m (plaas net ~ vooraan)',
'be': u'Новыя магчымасці: назвы месц на любой мове \033[92mwttr.in/станция+Восток\033[0m (в UTF-8) i пошук месц \033[92mwttr.in/~Kilimanjaro\033[0m (трэба дадаць ~ ў пачатак)',
'bg': u'Нова функционалност: многоезични имена на места\033[92mwttr.in/станция+Восток\033[0m (в UTF-8) и в търсенето \033[92mwttr.in/~Kilimanjaro\033[0m (добавете ~ преди)',
'bn': u'নতুন ফিচার : বহুভাষিক অবস্থানের নাম \ 033 [92mwttr.in/станция+Восток\033 [0m (UTF-8)] এবং অবস্থান অনুসন্ধান \ 033 [92mwttr.in/~Kilimanjaro\033 [0m (শুধু আগে ~ যোগ করুন)',
'bs': u'XXXXXXXXXXXXXXXXXXXX: XXXXXXXXXXXXXXXXXXXXXXXXXXXXX\033[92mwttr.in/станция+Восток\033[0m (XX UTF-8) XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
'ca': u'Noves funcionalitats: noms d\'ubicació multilingües \033[92mwttr.in/станция+Восток\033[0m (en UTF-8) i la ubicació de recerca \033[92mwttr.in/~Kilimanjaro\033[0m (només cal afegir ~ abans)',
'es': u'Nuevas funcionalidades: los nombres de las ubicaciones en varios idiomas \033[92mwttr.in/станция+Восток\033[0m (em UTF-8) y la búsqueda por ubicaciones \033[92mwttr.in/~Kilimanjaro\033[0m (tan solo inserte ~ al principio)',
'fa': u'قابلیت جدید: پشتیبانی از نام چند زبانه مکانها \033[92mwttr.in/станция+Восток\033[0m (در فرمت UTF-8) و جسجتوی مکان ها \033[92mwttr.in/~Kilimanjaro\033[0m (فقط قبل از اون ~ اضافه کنید)',
'fr': u'Nouvelles fonctionnalités: noms d\'emplacements multilingues \033[92mwttr.in/станция+Восток\033[0m (en UTF-8) et recherche d\'emplacement \033[92mwttr.in/~Kilimanjaro\033[0m (ajouter ~ devant)',
'gl': u'Nova funcionalidade: nomes de localizacións en varios idiomas\033[92mwttr.in/станция+Восток\033[0m (en UTF-8) e procuras de localizacións \033[92mwttr.in/~Kilimanjaro\033[0m (engade ~ antes)',
'mk': u'Нова функција: повеќе јазично локациски имиња \033[92mwttr.in/станция+Восток\033[0m (во UTF-8) и локациско пребарување \033[92mwttr.in/~Kilimanjaro\033[0m (just add ~ before)',
'nb': u'Ny funksjon: flerspråklige stedsnavn \033[92mwttr.in/станция+Восток\033[0m (i UTF-8) og lokasjonssøk \033[92mwttr.in/~Kilimanjaro\033[0m (bare legg til ~ foran)',
'nl': u'Nieuwe functie: tweetalige locatie namen \033[92mwttr.in/станция+Восток\033[0m (in UTF-8) en locatie zoeken \033[92mwttr.in/~Kilimanjaro\033[0m (zet er gewoon een ~ voor)',
@ -723,8 +805,10 @@ Bạn có thể theo dõi https://twitter.com/igor_chubin để cập nhật th
'it': u'Nuove funzionalità: nomi delle località multilingue \033[92mwttr.in/станция+Восток\033[0m (in UTF-8) e ricerca della località \033[92mwttr.in/~Kilimanjaro\033[0m (basta premettere ~)',
'ko': u'새로운 기능: 다국어로 대응된 위치 \033[92mwttr.in/서울\033[0m (UTF-8에서) 장소 검색 \033[92mwttr.in/~Kilimanjaro\033[0m (앞에 ~를 붙이세요)',
'kk': u'',
'lt': u'Naujiena: daugiakalbiai vietovių pavadinimai \033[92mwttr.in/станция+Восток\033[0m (UTF-8) ir vietovių paieška \033[92mwttr.in/~Kilimanjaro\033[0m (tiesiog priekyje pridėkite ~)',
'lv': u'Jaunums: Daudzvalodu atrašanās vietu nosaukumi \033[92mwttr.in/станция+Восток\033[0m (in UTF-8) un dabas objektu meklēšana \033[92mwttr.in/~Kilimanjaro\033[0m (tikai priekšā pievieno ~)',
'mk': u'Нова функција: повеќе јазично локациски имиња \033[92mwttr.in/станция+Восток\033[0m (во UTF-8) и локациско пребарување \033[92mwttr.in/~Kilimanjaro\033[0m (just add ~ before)',
'mr': u'नवीन वैशिष्ट्य: स्थळांची बहुभाषिक नावे \033[92mwttr.in/станция+Восток\033[0m (UTF-8 मध्ये) आणि स्थळ शोध \033[92mwttr.in/~Kilimanjaro\033[0m (फक्त आधी ~ जोडा)',
'oc': u'Novèla foncionalitat : nom de lòc multilenga \033[92mwttr.in/станция+Восток\033[0m (en UTF-8) e recèrca de lòc \033[92mwttr.in/~Kilimanjaro\033[0m (solament ajustatz ~ abans)',
'pl': u'Nowa funkcjonalność: wielojęzyczne nazwy lokalizacji \033[92mwttr.in/станция+Восток\033[0m (w UTF-8) i szukanie lokalizacji \033[92mwttr.in/~Kilimanjaro\033[0m (poprzedź zapytanie ~ - znakiem tyldy)',
'pt': u'Nova funcionalidade: nomes de localidades em várias línguas \033[92mwttr.in/станция+Восток\033[0m (em UTF-8) e procura por localidades \033[92mwttr.in/~Kilimanjaro\033[0m (é só colocar ~ antes)',
@ -740,15 +824,18 @@ Bạn có thể theo dõi https://twitter.com/igor_chubin để cập nhật th
'uk': u'Спробуйте: назви місць будь-якою мовою \033[92mwttr.in/станція+Восток\033[0m (в UTF-8) та пошук місць \033[92mwttr.in/~Kilimanjaro\033[0m (потрібно додати ~ спочатку)',
'vi': u'Chức năng mới: tên địa điểm đa ngôn ngữ \033[92mwttr.in/станция+Восток\033[0m (dùng UTF-8) và tìm kiếm địa điểm \033[92mwttr.in/~Kilimanjaro\033[0m (chỉ cần thêm ~ phía trước)',
'zh-tw': u'新功能:多語言地點名稱 \033[92mwttr.in/станция+Восток\033[0m (使用 UTF-8 編碼)與位置搜尋 \033[92mwttr.in/~Kilimanjaro\033[0m (只要在地點前加 ~ 就可以了)',
'mg': u'Fanatsrana vaovao: anarana toerana amin\'ny fiteny maro\033[92mwttr.in/станция+Восток\033[0m (en UTF-8) sy fitadiavana toerana \033[92mwttr.in/~Kilimanjaro\033[0m (ampio ~ fotsiny eo aloha)',
'ta': u'புதிய அம்சம்: பன்மொழி இருப்பிடப் பெயர்கள் \033[92mwttr.in/станция+Восток\033[0m (UTF-8 இல்) மற்றும் இருப்பிடத் தேடல் \033[92mwttr.in/~Kilimanjaro\033[0m (முன் ~ஐச் சேர்க்கவும்)',
},
'FOLLOW_ME': {
'en': u'Follow \033[46m\033[30m@igor_chubin\033[0m for wttr.in updates',
'ar': u'تابع \033[46m\033[30m@igor_chubin\033[0m من أجل wttr.in أخر مستجدات',
'ar': u'لآخر المستجدات تابع \033[46m\033[30m@igor_chubin\033[0m',
'af': u'Volg \033[46m\033[30m@igor_chubin\033[0m vir wttr.in opdaterings',
'am': u'ለተጨማሪ wttr.in ዜና እና መረጃ \033[46m\033[30m@igor_chubin\033[0m ን ይከተሉ',
'be': u'Сачыце за \033[46m\033[30m@igor_chubin\033[0m за навінамі wttr.in',
'bg': u'Последвай \033[46m\033[30m@igor_chubin\033[0m за обновления свързани с wttr.in',
'bn': u'wttr.in আপডেটের জন্য \033[46m\033[30m@igor_chubin\033[0m কে অনুসরণ করুন',
'bs': u'XXXXXX \033[46m\033[30m@igor_chubin\033[0m XXXXXXXXXXXXXXXXXXX',
'ca': u'Segueix \033[46m\033[30m@igor_chubin\033[0m per actualitzacions de wttr.in',
'es': u'Sigue a \033[46m\033[30m@igor_chubin\033[0m para enterarte de las novedades de wttr.in',
@ -758,6 +845,7 @@ Bạn có thể theo dõi https://twitter.com/igor_chubin để cập nhật th
'fr': u'Suivez \033[46m\033[30m@igor_Chubin\033[0m pour rester informé sur wttr.in',
'de': u'Folgen Sie \033[46m\033[30mhttps://twitter.com/igor_chubin\033[0m für wttr.in Updates',
'ga': u'Lean \033[46m\033[30m@igor_chubin\033[0m don wttr.in eolas is deanaí',
'gl': u'Segue a \033[46m\033[30m@igor_chubin\033[0m para actualizacións sobre wttr.in',
'hi': u'अपडेट के लिए फॉलो करें \033[46m\033[30m@igor_chubin\033[0m',
'hu': u'Kövesd \033[46m\033[30m@igor_chubin\033[0m-t további wttr.in információkért',
'hy': u'Նոր ֆիչռների համար հետևեք՝ \033[46m\033[30m@igor_chubin\033[0m',
@ -766,8 +854,10 @@ Bạn có thể theo dõi https://twitter.com/igor_chubin để cập nhật th
'it': u'Seguite \033[46m\033[30m@igor_chubin\033[0m per aggiornamenti a wttr.in',
'ko': u'wttr.in의 업데이트 소식을 원하신다면 \033[46m\033[30m@igor_chubin\033[0m 을 팔로우 해주세요',
'kk': u'',
'lt': u'wttr.in atnaujinimus sekite \033[46m\033[30m@igor_chubin\033[0m',
'lv': u'Seko \033[46m\033[30m@igor_chubin\033[0m , lai uzzinātu wttr.in jaunumus',
'mk': u'Следете \033[46m\033[30m@igor_chubin\033[0m за wttr.in новости',
'mr': u'wttr.in च्या अद्यावत माहितीसाठी \033[46m\033[30m@igor_chubin\033[0m चे अनुसरण करा',
'nb': u'Følg \033[46m\033[30m@igor_chubin\033[0m for wttr.in oppdateringer',
'nl': u'Volg \033[46m\033[30m@igor_chubin\033[0m voor wttr.in updates',
'oc': u'Seguissètz \033[46m\033[30m@igor_Chubin\033[0m per demorar informat sus wttr.in',
@ -786,15 +876,18 @@ Bạn có thể theo dõi https://twitter.com/igor_chubin để cập nhật th
'uk': u'Нові можливості wttr.in публікуються тут: \033[46m\033[30m@igor_chubin\033[0m',
'vi': u'Theo dõi \033[46m\033[30m@igor_chubin\033[0m để cập nhật thông tin về wttr.in',
'zh-tw': u'追蹤 \033[46m\033[30m@igor_chubin\033[0m 以取得更多 wttr.in 的動態',
'mg': u'Araho ao ny pejy \033[46m\033[30m@igor_Chubin\033[0m raha toa ka te hahazo vaovao momban\'ny wttr.in',
'ta': u'wttr.in புதுப்பிப்புகளுக்கு \033[46m\033[30m@igor_chubin\033[0m ஐப் பின்தொடரவும்',
},
}
CAPTION = {
"af": u"Weer verslag vir:",
"am": u"የአየር ሁኔታ ሪፖርት ለ",
"ar": u"تقرير جوي",
"ar": u"تقرير حالة الطقس",
"az": u"Hava proqnozu:",
"be": u"Прагноз надвор'я для:",
"bg": u"Прогноза за времето в:",
"bn": u"আবহাওয়ার প্রতিবেদন:",
"bs": u"Vremenske prognoze za:",
"ca": u"Informe del temps per a:",
"cs": u"Předpověď počasí pro:",
@ -831,6 +924,7 @@ CAPTION = {
"lv": u"Laika ziņas:",
"mk": u"Прогноза за времето во:",
"ml": u"കാലാവസ്ഥ റിപ്പോർട്ട്:",
"mr": u"हवामान अहवाल:",
"nb": u"Værmelding for:",
"nl": u"Weerbericht voor:",
"nn": u"Vêrmelding for:",
@ -855,6 +949,8 @@ CAPTION = {
"zh": u"天气预报:",
"zu": u"Isimo sezulu:",
"zh-tw": u"天氣報告:",
"mg": u"Toetr\'andro any :",
"ta": u"வானிலை அறிக்கை:",
}
def get_message(message_name, lang):

View file

@ -9,12 +9,13 @@ V2_TRANSLATION = {
"en": ("Weather report for:", "Weather", "Timezone", "Now", "Dawn", "Sunrise", "Zenith", "Sunset", "Dusk"),
"af": ("Weer verslag vir:", "", "", "", "", "", "", "", ""),
"am": ("የአየር ሁኔታ ዘገባ ለ ፥", "የአየር ሁኔታ", "የጊዜ ሰቅ", "አሁን", "ንጋት", "የፀሐይ መውጫ", "", "የፀሐይ መጥለቅ", "ምሽት"),
"ar": ("تقرير جوي:", "حالة الجو", "المنطقة الزمنية", "الآن ", "الفجر", "شروق الشمس", "الذروة", "غروب الشمس", "الغسق"),
"ar": ("تقرير حالة الطقس:", "حالة الطقس", "المنطقة الزمنية", "الآن ", "الفجر", "شروق الشمس", "الذروة", "غروب الشمس", "الغسق"),
"az": ("Hava proqnozu:", "Hava", "Saat zonası", "İndi", "Şəfəq", "Günəş çıxdı", "Zenit", "Gün batımı", "Toran"),
"be": ("Прагноз надвор'я для:", "Надвор'е", "Часавая зона", "Цяпер", "Світанак", "Усход сонца", "Зеніт", "Захад сонца", "Змярканне"),
"bg": ("Прогноза за времето в:", "", "", "", "", "", "", "", ""),
"bn" : ("আবহাওয়া প্রতিবেদন:", "আবহাওয়া", "টাইমজোন", "এখন", "ভোর", "সূর্যোদয়", "সুবিন্দু", "সূর্যাস্ত", "সন্ধ্যা"),
"bs": ("Vremenske prognoze za:", "", "", "", "", "", "", "", ""),
"ca": ("Informe del temps per a:", "", "", "", "", "", "", "", ""),
"ca": ("Informe del temps per a:", "Oratge", "Zona horària", "Ara", "Albada", "Sortida", "Zenit", "Posta", "Crepuscle"),
"cs": ("Předpověď počasí pro:", "", "", "", "", "", "", "", ""),
"cy": ("Adroddiad tywydd ar gyfer:", "", "", "", "", "", "", "", ""),
"da": ("Vejret i:", "Vejret", "Tidszone", "Nu", "Daggry", "Solopgang", "Zenit", "Solnedgang", "Skumring"),
@ -28,7 +29,7 @@ V2_TRANSLATION = {
"fi": ("Säätiedotus:", "", "", "", "", "", "", "", ""),
"fr": ("Prévisions météo pour :", "Météo", "Fuseau Horaire", "Heure", "Aube", "Lever du Soleil", "Zénith", "Coucher du Soleil", "Crépuscule"),
"fy": ("Waarberjocht foar:", "", "", "", "", "", "", "", ""),
"ga": ("Réamhaisnéis na haimsire do:", "", "", "", "", "", "", "", ""),
"ga": ("Réamhaisnéis na haimsire do:", "Aimsir", "Crios ama", "Anois", "Breacadh an lae", "Éirí na gréine", "Forar", "Dul faoi na gréine", "Coineascar"),
"he": (":ריוואה גזמ תיזחת", "", "", "", "", "", "", "", ""),
"hi": ("मौसम की जानकारी", "मौसम", "समय मण्डल", "अभी", "उदय", "सूर्योदय", "चरम बिन्दु", "सूर्यास्त", "संध्याकाल"),
"hr": ("Vremenska prognoza za:", "", "", "", "", "", "", "", ""),
@ -44,10 +45,11 @@ V2_TRANSLATION = {
"kk": ("Ауа райы:", "", "", "", "", "", "", "", ""),
"ko": ("일기 예보:", "날씨", "시간대", "현재", "새벽", "일출", "정오", "일몰", "황혼"),
"ky": ("Аба ырайы:", "", "", "", "", "", "", "", ""),
"lt": ("Orų prognozė:", "", "", "", "", "", "", "", ""),
"lt": ("Orų prognozė:", "Orai", "Laiko zona", "Dabar", "Aušra", "Saulėtekis", "Zenitas", "Saulėlydis", "Sutemos"),
"lv": ("Laika ziņas:", "", "", "", "", "", "", "", ""),
"mk": ("Прогноза за времето во:", "", "", "", "", "", "", "", ""),
"ml": ("കാലാവസ്ഥ റിപ്പോർട്ട്:", "", "", "", "", "", "", "", ""),
"ml": ("കാലാവസ്ഥ റിപ്പോർട്ട്:", "കാലാവസ്", "സമയ മേഖല", "ഇപ്പോൾ", "പ്രഭാതത്തെ", "ഉച്ചതിരിഞ്ഞ്","സൂര്യോദയം", "പരമോന്നത", "സൂര്യാസ്തമയം", "സന്ധ്യ"),
"mr": ("हवामान अहवालाचे ठिकाण:", "हवामान", "कालक्षेत्र", "आता", "पहाट", "सूर्योदय", "शिखरबिंदु", "सूर्यास्त", "संध्याकाळ"),
"nb": ("Værmelding for:", "", "", "", "", "", "", "", ""),
"nl": ("Weerbericht voor:", "Weer", "Tijdzone", "Nu", "Dageraad", "Zonsopkomst", "Zenit", "Zonsondergang", "Schemering"),
"nn": ("Vêrmelding for:", "", "", "", "", "", "", "", ""),
@ -63,7 +65,7 @@ V2_TRANSLATION = {
"sr-lat": ("Vremenska prognoza za:", "", "", "", "", "", "", "", ""),
"sv": ("Väderleksprognos för:", "", "", "", "", "", "", "", ""),
"sw": ("Ripoti ya hali ya hewa, jiji la:", "", "", "", "", "", "", "", ""),
"te": ("వాతావరణ సమాచారము:", "", "", "", "", "", "", "", ""),
"te": ("వాతావరణ సమాచారము:", "వాతావరణం", "కాల మండలం", "ప్రస్తుతం", "తెల్లవారుజాము", "సూర్యోదయం", "ఉన్నత స్థానం", "సూర్యాస్తమయం", "సందెచీకటి"),
"th": ("รายงานสภาพอากาศ:", "", "", "", "", "", "", "", ""),
"tr": ("Hava beklentisi:", "Hava Durumu", "Zaman Dilimi", "Şimdi", "Şafak", "Gün Doğumu", "Doruk", "Gün Batımı", "Akşam"),
"uk": ("Прогноз погоди для:", "Погода", "Часовий пояс", "Зараз", "Світанок", "Схід сонця", "Зеніт", "Захід сонця", "Сутінки"),
@ -72,4 +74,6 @@ V2_TRANSLATION = {
"zh": ("天气预报:", "天气", "时区", "当前", "黎明", "日出", "正午", "日落", "黄昏"),
"zh-tw": ("天氣預報:", "天氣", "時區", "目前", "黎明", "日出", "日正當中", "日落", "黃昏"),
"zu": ("Isimo sezulu:", "", "", "", "", "", "", "", ""),
"mg": ("Vinavina toetr'andro hoany :", "Toetr'andro", "Faritra ora", "Ora", "Mangirandratsy", "Maneno akoho", "Mitatao vovonana", "Masoandro milentika", "Crépuscule"),
"ta": ("வானிலை அறிக்கை:", "வானிலை", "நேரம் மண்டலம்", "இப்போது", "விடியல்", "சூரிய உதயம்", "ஜெனித்", "சூரிய அஸ்தமனம்", "அந்தி"),
}

View file

@ -25,7 +25,7 @@ from astral.sun import sun
import pytz
from constants import WWO_CODE, WEATHER_SYMBOL, WIND_DIRECTION, WEATHER_SYMBOL_WIDTH_VTE, WEATHER_SYMBOL_PLAIN
from constants import WWO_CODE, WEATHER_SYMBOL, WEATHER_SYMBOL_WI_NIGHT, WEATHER_SYMBOL_WI_DAY, WIND_DIRECTION, WIND_DIRECTION_WI, WEATHER_SYMBOL_WIDTH_VTE, WEATHER_SYMBOL_PLAIN
from weather_data import get_weather_data
from . import v2
from . import v3
@ -36,6 +36,7 @@ PRECONFIGURED_FORMAT = {
'2': r'%c 🌡️%t 🌬️%w\n',
'3': r'%l: %c %t\n',
'4': r'%l: %c 🌡️%t 🌬️%w\n',
'69': r'nice',
}
MOON_PHASES = (
@ -81,8 +82,21 @@ def render_condition(data, query):
"""Emoji encoded weather condition (c)
"""
weather_condition = WEATHER_SYMBOL[WWO_CODE[data['weatherCode']]]
spaces = " "*(WEATHER_SYMBOL_WIDTH_VTE.get(weather_condition) - 1)
if query.get("view") == "v2n":
weather_condition = WEATHER_SYMBOL_WI_NIGHT.get(
WWO_CODE.get(
data['weatherCode'], "Unknown"))
spaces = " "
elif query.get("view") == "v2d":
weather_condition = WEATHER_SYMBOL_WI_DAY.get(
WWO_CODE.get(
data['weatherCode'], "Unknown"))
spaces = " "
else:
weather_condition = WEATHER_SYMBOL.get(
WWO_CODE.get(
data['weatherCode'], "Unknown"))
spaces = " "*(3 - WEATHER_SYMBOL_WIDTH_VTE.get(weather_condition, 1))
return weather_condition + spaces
@ -154,6 +168,14 @@ def render_pressure(data, query):
answer += 'hPa'
return answer
def render_uv_index(data, query):
"""
UV Index (u)
"""
answer = data.get('uvIndex', '')
return answer
def render_wind(data, query):
"""
wind (w)
@ -170,7 +192,10 @@ def render_wind(data, query):
degree = ""
if degree:
wind_direction = WIND_DIRECTION[int(((degree+22.5)%360)/45.0)]
if query.get("view") in ["v2n", "v2d"]:
wind_direction = WIND_DIRECTION_WI[int(((degree+22.5)%360)/45.0)]
else:
wind_direction = WIND_DIRECTION[int(((degree+22.5)%360)/45.0)]
else:
wind_direction = ""
@ -213,7 +238,8 @@ def render_moonday(_, query):
# this is just a temporary solution
def get_geodata(location):
text = requests.get("http://localhost:8004/%s" % location).text
# text = requests.get("http://localhost:8004/%s" % location).text
text = requests.get("http://127.0.0.1:8083/:geo-location?location=%s" % location).text
return json.loads(text)
@ -268,6 +294,7 @@ FORMAT_SYMBOL = {
'p': render_precipitation,
'o': render_precipitation_chance,
'P': render_pressure,
"u": render_uv_index,
}
FORMAT_SYMBOL_ASTRO = {
@ -359,7 +386,11 @@ def format_weather_data(query, parsed_query, data):
if format_line in PRECONFIGURED_FORMAT:
format_line = PRECONFIGURED_FORMAT[format_line]
if format_line == "j1":
if format_line in ["j1", "j2"]:
# j2 is a lightweight j1, without 'hourly' in 'weather' (weather forecast)
if "weather" in data["data"] and format_line == "j2":
for i in range(len(data["data"]["weather"])):
del data["data"]["weather"][i]["hourly"]
return render_json(data['data'])
if format_line == "p1":
return prometheus.render_prometheus(data['data'])

View file

@ -44,6 +44,9 @@ def get_moon(parsed_query):
if parsed_query.get('no-terminal', False):
stdout = globals.remove_ansi(stdout)
if parsed_query.get('dumb', False):
stdout = stdout.translate(globals.TRANSLATION_TABLE)
if html:
p = Popen(
["bash", globals.ANSI2HTML, "--palette=solarized", "--bg=dark"],

View file

@ -38,7 +38,7 @@ from astral import moon, sun
from scipy.interpolate import interp1d
from babel.dates import format_datetime
from globals import WWO_KEY, remove_ansi
from globals import WWO_KEY, TRANSLATION_TABLE, remove_ansi
import constants
import translations
import parse_query
@ -321,7 +321,7 @@ def draw_emoji(data, config):
weather_symbol = constants.WEATHER_SYMBOL_WI_NIGHT
weather_symbol_width_vte = constants.WEATHER_SYMBOL_WIDTH_VTE_WI
elif config.get("view") == "v2d":
weather_symbol = constants.WEATHER_SYMBOL_WI_NIGHT
weather_symbol = constants.WEATHER_SYMBOL_WI_DAY
weather_symbol_width_vte = constants.WEATHER_SYMBOL_WIDTH_VTE_WI
else:
weather_symbol = constants.WEATHER_SYMBOL
@ -419,13 +419,19 @@ def generate_panel(data_parsed, geo_data, config):
max_width = 72
if config.get("use_imperial"):
feels_like_query = "[.data.weather[] | .hourly[]] | .[].FeelsLikeF"
temp_query = "[.data.weather[] | .hourly[]] | .[].tempF"
wind_speed_query = "[.data.weather[] | .hourly[]] | .[].windspeedMiles"
else:
feels_like_query = "[.data.weather[] | .hourly[]] | .[].FeelsLikeC"
temp_query = "[.data.weather[] | .hourly[]] | .[].tempC"
wind_speed_query = "[.data.weather[] | .hourly[]] | .[].windspeedKmph"
precip_mm_query = "[.data.weather[] | .hourly[]] | .[].precipMM"
precip_chance_query = "[.data.weather[] | .hourly[]] | .[].chanceofrain"
feels_like_query = "[.data.weather[] | .hourly[]] | .[].FeelsLikeC"
temp_query = "[.data.weather[] | .hourly[]] | .[].tempC"
weather_code_query = "[.data.weather[] | .hourly[]] | .[].weatherCode"
wind_direction_query = "[.data.weather[] | .hourly[]] | .[].winddirDegree"
wind_speed_query = "[.data.weather[] | .hourly[]] | .[].windspeedKmph"
output = ""
@ -509,7 +515,7 @@ def textual_information(data_parsed, geo_data, config, html_output=False):
format_line = "%c %C, %t, %h, %w, %P"
current_condition = data_parsed['data']['current_condition'][0]
query = {}
query = config
weather_line = wttr_line.render_line(format_line, current_condition, query)
output.append('Weather: %s' % weather_line)
@ -567,13 +573,16 @@ def textual_information(data_parsed, geo_data, config, html_output=False):
city_only = True
suffix = ", Крым"
latitude = float(geo_data["latitude"])
longitude = float(geo_data["longitude"])
if config["full_address"]:
output.append('Location: %s%s [%5.4f,%5.4f]' \
% (
_shorten_full_location(config["full_address"], city_only=city_only),
suffix,
geo_data["latitude"],
geo_data["longitude"],
latitude,
longitude,
))
output = [
@ -587,7 +596,7 @@ def textual_information(data_parsed, geo_data, config, html_output=False):
# }}}
# get_geodata {{{
def get_geodata(location):
text = requests.get("http://localhost:8004/%s" % location).text
text = requests.get("http://127.0.0.1:8083/:geo-location?location=%s" % location).text
return json.loads(text)
# }}}
@ -629,6 +638,8 @@ def main(query, parsed_query, data):
output += textual_information(data_parsed, geo_data, parsed_query)
if parsed_query.get('no-terminal', False):
output = remove_ansi(output)
if parsed_query.get('dumb', False):
output = output.translate(TRANSLATION_TABLE)
return output
if __name__ == '__main__':

View file

@ -13,8 +13,8 @@ from gevent.subprocess import Popen, PIPE
sys.path.insert(0, "..")
from translations import get_message, SUPPORTED_LANGS
from globals import WEGO, NOT_FOUND_LOCATION, DEFAULT_LOCATION, ANSI2HTML, \
error, remove_ansi
from globals import WEGO, TRANSLATION_TABLE, NOT_FOUND_LOCATION, \
DEFAULT_LOCATION, ANSI2HTML, error, remove_ansi
def get_wetter(parsed_query):
@ -31,6 +31,7 @@ def get_wetter(parsed_query):
returncode = 0
if not location_not_found:
stdout, stderr, returncode = _wego_wrapper(location, parsed_query)
first_line, stdout = _wego_postprocessing(location, parsed_query, stdout)
if location_not_found or \
(returncode != 0 \
@ -57,13 +58,8 @@ def get_wetter(parsed_query):
not_found_footer = "\n".join("\033[48;5;91m " + x + " \033[0m"
for x in not_found_footer.splitlines() if x) + "\n"
stdout = not_found_header + "\n----\n" + stdout + not_found_footer
if "\n" in stdout:
first_line, stdout = _wego_postprocessing(location, parsed_query, stdout)
else:
first_line = ""
stdout = not_found_header + "\n----\n" + stdout + not_found_footer
if html:
return _htmlize(stdout, first_line, parsed_query)
@ -130,6 +126,9 @@ def _wego_postprocessing(location, parsed_query, stdout):
if parsed_query.get('no-city', False):
stdout = "\n".join(stdout.splitlines()[2:]) + "\n"
if parsed_query.get('dumb', False):
stdout = stdout.translate(TRANSLATION_TABLE)
if full_address \
and parsed_query.get('format', 'txt') != 'png' \
and (not parsed_query.get('no-city')

View file

@ -17,7 +17,7 @@ import fmt.png
import parse_query
from translations import get_message, FULL_TRANSLATION, PARTIAL_TRANSLATION, SUPPORTED_LANGS
from buttons import add_buttons
from globals import get_help_file, remove_ansi, \
from globals import get_help_file, remove_ansi, TRANSLATION_TABLE, \
BASH_FUNCTION_FILE, TRANSLATION_FILE, LOG_FILE, \
NOT_FOUND_LOCATION, \
MALFORMED_RESPONSE_HTML_PAGE, \
@ -210,6 +210,9 @@ def _response(parsed_query, query, fast_mode=False):
# so we handle it with all available logic
loc = (parsed_query['orig_location'] or "").lower()
if parsed_query.get("view"):
if not parsed_query.get("location"):
parsed_query["location"] = loc
output = wttr_line(query, parsed_query)
elif loc == 'moon' or loc.startswith('moon@'):
output = get_moon(parsed_query)
@ -236,6 +239,8 @@ def _response(parsed_query, query, fast_mode=False):
message = get_message('FOLLOW_ME', parsed_query['lang'])
if parsed_query.get('no-terminal', False):
message = remove_ansi(message)
if parsed_query.get('dumb', False):
message = message.translate(TRANSLATION_TABLE)
output += '\n' + message + '\n'
return cache.store(cache_signature, output)

View file

@ -6,22 +6,22 @@ gevent
dnspython
pylint
cyrtranslit
astral
astral>=2.0,<=2.2
timezonefinder==2.1.2
pytz
pyte
python-dateutil
python-dateutil>=2.5.0,<=2.8.1
diagram
pyjq
scipy
numpy
pillow
babel
pylru
pylru>=1.0.7,<=1.2.1
pysocks
supervisor
numba
emoji
emoji>=1.6.0,<=1.7.0
grapheme
pycountry
aiohttp>=3.10.11 # not directly required, pinned by Snyk to avoid a vulnerability

View file

@ -1,3 +1,5 @@
MDW : MDW Chicago
mdw : MDW Chicago
Msk : Moscow
Moskva : Moscow
Moskau : Moscow
@ -50,3 +52,5 @@ Kashan : ~Kashan,Iran
Baku : Baku,Az
Rome : Rome, Italia
YYZ : Toronto Pearson Airport
brasilia : Palacio da Alvorada,Brasilia
Tula : Tula,Ru

View file

@ -30,6 +30,7 @@ View options:
1 # current weather + today's forecast
2 # current weather + today's + tomorrow's forecast
A # ignore User-Agent and force ANSI output format (terminal)
d # restrict output to standard console font glyphs
F # do not show the "Follow" line
n # narrow version (only day and night)
q # quiet version (no "Weather report" text)

View file

@ -0,0 +1,14 @@
#!/usr/bin/env bash
LOG_DIR="/wttr.in/log"
LOG_FILE="$LOG_DIR/diskspace.log"
DISK=/wttr.in
log() {
mkdir -p "$LOG_DIR"
echo "$(date +"[%Y-%m-%d %H:%M:%S]") $*" | tee -a "$LOG_FILE"
}
log $(df -k "$DISK" | tail -1 | awk '{print $4}')

View file

@ -5,7 +5,8 @@ wttr.in is translated in NUMBER_OF_LANGUAGES languages:
Translated/improved/corrected by:
* Afrikaans: Casper Labuschage @casperl (on github)
* Arabic Besher Aladdam @akai54 (on github)
* Arabic Besher Aladdam @akai54 (on github),
Ahmed D. ALi @_nakanakaii (twitter) / @nakanakaii (github)
* Arhamic Robel Kassa @rawbubble
* Armenian: Aram Bayadyan @aramix, Mikayel Ghazaryan @mkdotam,
Grigor Khachatryan @grigortw
@ -14,6 +15,7 @@ Translated/improved/corrected by:
* Basque: Iker Sagasti (@isagasti on github)
* Belarusian: Igor Chubin, Anton Zhavoronkov @edogby (on github)
* Bosnian: Ismar Kunc @ismarkunc
* Bengali: Nazia Tasnim (@appledora on github)
* Bulgarian: Vladimir Vitkov @zeridon (on github)
* Brazilian-PT: Tupã Negreiros @TupaNegreiros (on github)
* Catalan: Angel Jarabo @legna29A
@ -23,12 +25,13 @@ Translated/improved/corrected by:
* Croatian: Siniša Kusić @ku5ic
* Czech: Juraj Kakody
* Danish: Kim Schulz @kimusan (on github)
* Dutch: Youri Claes
* Dutch: Youri Claes, Edwin Martin @edwinm
* Esperanto: Igor Chubin
* Estonian: Jaan Jänesmäe @janesmae (on github)
* Finnish: @Maxifi
* French: Igor Chubin, @daftaupe, @iago-lito
* Frisian: Anne Douwe Bouma @anned20 (on github)
* Galician Diego Blanco @diego-treitos (on github)
* German: Igor Chubin, @MAGICC (https://kthx.at)
* Greek: Panayotis Vryonis and @Petewg (on github)
* Hebrew: E.R.
@ -43,7 +46,9 @@ Translated/improved/corrected by:
* Kazakh: Akku Tutkusheva, Oleg Tropinin
* Korean: Jeremy Bae @opt9, Jung Winter @res_tin
* Latvian: Gunārs Danovskis
* Lithuanian Juras Rutavičius
* Macedonian: Matej Plavevski @MatejMecka
* Marathi: Sanket Pandit Garade (@sanketgarade on github)
* Norwegian: Fredrik Fjeld @fredrikfjeld
* Nynorsk: Kevin Brubeck Unhammer (https://unhammer.org/k/)
* Occitan: Quentin PAGÈS @Quenty-tolosan (gh)
@ -51,14 +56,17 @@ Translated/improved/corrected by:
* Polish: Wojtek Łukasiewicz @wojtuch (on github)
* Portuguese: Fernando Bitti Loureiro @fbitti (on github)
* Romanian: Gabriel Moruz
* Russian: Igor Chubin
* Russian: Igor Chubin, @layerex (on github)
* Serbian: Milan Stevanović @FathVader
* Slovak: Juraj Kakody
* Slovenian: B.S.
* Spanish: Fernando Bitti Loureiro @fbitti (on github)
* Swedish: John Eriksson
* Swahili: Joel Mukuthu
* Turkish: Atabey Kaygun, Yilmaz @edigu, Volkan Tokmak(@volkanto)
* Tamil: Parthiban @parthi1984 (on github)
* Telugu: Pavan Srinivas Mamidala @pavansrinivasmamidala (on github),
Vishal Boddu @bodduv (on github)
* Turkish: Atabey Kaygun, Yilmaz @edigu, Volkan Tokmak(@volkanto), Oğuz Ersen
* Thai: Vatunyoo Suwannapisit @kerlos
* Ukrainian: Igor Chubin, Serhiy @pavse
* Uzbek: Shukhrat Mukimov

View file

@ -5,52 +5,53 @@
أنواع الأماكن المدعومة:
/paris # أسم المدينة
/paris # اسم المدينة
/~Eiffel+tower # أي مكان
/Москва # أسم يونيكود ﻷي مكان بأي لغة
/muc # airport code (3 letters)
/@stackoverflow.com # أسم النطاق
/Москва # اسم يونيكود ﻷي مكان بأي لغة
/muc # الاسم النمطي للمطار (3 احرف)
/@stackoverflow.com # اسم النطاق
/94107 # رمز المنطقة
/-78.46,106.79 # GPS إحداثيات الـ
الأماكن الخاصة:
/moon # مرحلة القمر (أضف ,+US أو ,+France لهؤلاء المدن)
/moon@2016-10-25 # مرحلة القمر بتاريخ (@2016-10-25)
/moon # مرحلة القمر (أضف ,+US أو ,+France لمدينة معينة)
/moon@2016-10-25 # مرحلة القمر بتاريخ (2016-10-25)
الوحدات:
m # المتريّ (SI) (يستخدم في العادة في كل الأماكن ما عدا الولايات المتحدة)
m # النظام المتريّ (SI) (يستخدم في العادة في كل الأماكن ما عدا الولايات المتحدة)
u # وحدات القياس العرفية الأمريكية (يستخدم في العادة في الولايات المتحدة)
M # إظهار سرعة الرياح بوحدة م/ث
خيارات العرض:
0 # فقط الطقس الحالي
1 # الطقس الحالي + 1 يوم
2 # الطقس الحالي + 2 يوم
A # تجاهل الوكيل المستخدم وقم بإجبار تنسيق المعهد القومى الأمريكى للتنميط (الطرفية)
0 # الطقس الحالي فقط
1 # الطقس الحالي وطقس اليوم
2 # الطقس الحالي وطقس اليوم ويوم غد
A # تجاهل الوكيل المستخدم وقم بتطبيق نمط ANSI (الطرفية)
F # "لا تظهر سطر "المتابعة
n # النسخة الضيقة (النهار والليل فقط)
n # النسخة الخفيفة جدا (النهار والليل فقط)
q # النسخة الصامتة (من غير عبارة "تقرير جوي")
Q # النسخة الصامتة كليا (من غير عبارة "تقرير جوي", من غير أسم المدينة)
T # إطفاء تسلسل الطرفية (من غير ألوان)
Q # النسخة الصامتة كليا (بدون عبارة "تقرير جوي" أو اسم المدينة)
T # العودة لنمط تسلسل الطرفية (من غير ألوان)
PNG خيارات:
/paris.png # png إنشاء صورة بصيغة
/paris.png # PNG إنشاء صورة بصيغة
p # إضافة إطار حول المخرج
t # الشفافية 150
transparency=... # الشفافية من 0 إلي 255 (255 = غير شفاف)
transparency=... # الشفافية من 0 إلي 255 (255 = غير شفاف)
background=... # لون الخلفية بنمط RRGGBB
يمكن جمع الخيارات:
/Paris?0pq
/Paris?0pq&lang=fr
/Paris_0pq.png # خيار الملف يتم تحديده في ما بعد PNG في
/Paris_0pq.png # يتم تحديد خيار ملف PNG بعد الشرطة السفلية
/Rome_0pq_lang=it.png # الخيارات الطويلة يتم فصلهم عن طريق شرطة سفلية
حصر المكان:
الترجمة:
$ curl fr.wttr.in/Paris
$ curl wttr.in/paris?lang=fr
@ -63,7 +64,7 @@
روابط URLs خاصة :
/:help # إظهار هذه الصفحة
/:bash.function # wttr() bash إظهار الميزة الخاصةبـ
/:translation # إظهار المعلومات حول المترجمين
/:help # عرض هذه الصفحة
/:bash.function # wttr() bash عرض الميزة الخاصةبـ
/:translation # عرض المعلومات حول المترجمين

View file

@ -1,47 +1,47 @@
113: صَافٍ : Clear
113: مُشْمِسٌ : Sunny
116: غَائِمٌ جزئياً‏ : Partly cloudy
119: غَائِمٌ : Cloudy
122: مُلبَّد بالغُيُوم : Overcast
143: ضَبَاب خَفيف : Mist
176: مِنَ الْمُمْكِنِ هُطول أمطار متفرِّقة : Patchy rain possible
179: مِنَ الْمُمْكِنِ هُطول ثُلُوج متفرِّقة : Patchy snow possible
182: مِنَ الْمُمْكِنِ هُطول المطر الثلجي متفرِّقة : Patchy sleet possible
185: مِنَ الْمُمْكِنِ هُطول أمطار متفرِّقة : Patchy freezing drizzle possible
200: مِنَ الْمُمْكِنِ ظُهورَ الرَعد : Thundery outbreaks possible
227: ثُلُوج غَزِيْرَةُ : Blowing snow
230: عاصِفة ثلجية : Blizzard
248: ضَّبَابُ كَثيف : Fog
260: ضَّبَابُ جامِد : Freezing fog
263: رَّذاذُ مَطَرِ متفرِّق : Patchy light drizzle
266: رَّذاذُ مَطَرِ : Light drizzle
281: رَّذاذُ مُتَجمَّد : Freezing drizzle
284: رَّذاذُ مُتَجمَّد غَزير : Heavy freezing drizzle
293: أمطار خَفيفة متفرِّقة : Patchy light rain
296: أمطار خَفيفة : Light rain
299: أمطار مُعْتَدِلَةُ في بَعْضِ الأوْقات : Moderate rain at times
302: أمطار مُعْتَدِلَةُ : Moderate rain
305: أمطار كَثيفة في بَعْضِ الأوْقات : Heavy rain at times
308: أمطار كَثيفة : Heavy rain
311: أمطار مُتَجمَّدة خَفيفة : Light freezing rain
314: أمطار مُتَجمَّدة مُعْتَدِلَةُ أو كَثيفة : Moderate or heavy freezing rain
317: مطر ثلجي خَفيف : Light sleet
320: مطر ثلجي مُعْتَدِلَ أو كَثيف : Moderate or heavy sleet
323: ثُلُوج خَفيفة متفرِّقة : Patchy light snow
326: ثُلُوج خَفيفة : Light snow
329: ثُلُوج مُعْتَدِلَةُ متفرِّقة : Patchy moderate snow
332: ثُلُوج مُعْتَدِلَةُ : Moderate snow
335: ثُلُوج كَثيفة متفرِّقة : Patchy heavy snow
338: ثُلُوج كَثيفة : Heavy snow
350: حُبَيْبات جليدية : Ice pellets
353: رَّذاذُ مَطَرٌ خَفِيف : Light rain shower
356: رَّذاذُ مَطَرٌ مُعْتَدِلَ أو كَثيف : Moderate or heavy rain shower
359: رَّذاذُ مطر غَزِيْرَ : Torrential rain shower
362: رَّذاذُ مطر ثلجي خَفيف : Light sleet showers
365: رَّذاذُ مطر ثلجي مُعْتَدِلَ أو كَثيف : Moderate or heavy sleet showers
368: رَّذاذُ ثلجي خَفيف : Light snow showers
371: رَّذاذُ ثلجي خَفيف : Moderate or heavy snow showers
386: أمطار خَفيفة متفرِّقة مَصحوبة بالرَعد : Patchy light rain with thunder
389: أمطار مُعْتَدِلَةُ أو كَثيفة متفرِّقة مَصحوبة بالرَعد : Moderate or heavy rain with thunder
392: ثُلُوج خَفيفة متفرِّقة مَصحوبة بالرَعد : Patchy light snow with thunder
395: ثُلُوج مُعْتَدِلَةُ أو كَثيفة متفرِّقة مَصحوبة بالرَعد : Moderate or heavy snow with thunder
113: صاف : Clear
113: مشمس : Sunny
116: غائم جزئياً‏ : Partly cloudy
119: غائم : Cloudy
122: ملبد بالغيوم : Overcast
143: شبورة : Mist
176: احتمال هطول أمطار متفرقة : Patchy rain possible
179: احتمال هطول ثلوج متفرقة : Patchy snow possible
182: احتمال هطول المطر الثلجي متفرقة : Patchy sleet possible
185: احتمال هطول أمطار متفرقة : Patchy freezing drizzle possible
200: احتمال ظهور الرعد : Thundery outbreaks possible
227: هبوب ثلجية : Blowing snow
230: عاصفة ثلجية : Blizzard
248: ضباب : Fog
260: ضباب بارد : Freezing fog
263: رذاذ خفيف متقطع : Patchy light drizzle
266: رذاذ خفيف : Light drizzle
281: رذاذ بارد : Freezing drizzle
284: رذاذ غزير بارد : Heavy freezing drizzle
293: أمطار خفيفة متفرقة : Patchy light rain
296: أمطار خفيفة : Light rain
299: أمطار معتدلة أحياناً : Moderate rain at times
302: أمطار معتدلة : Moderate rain
305: أمطار كثيفة أحياناً : Heavy rain at times
308: أمطار كثيفة : Heavy rain
311: أمطار باردة خفيفة : Light freezing rain
314: أمطار باردة معتدلة أو كثيفة : Moderate or heavy freezing rain
317: مطر ثلجي خفيف : Light sleet
320: مطر ثلجي معتدل أو كثيف : Moderate or heavy sleet
323: ثلوج خفيفة متفرقة : Patchy light snow
326: ثلوج خفيفة : Light snow
329: ثلوج معتدلة متفرقة : Patchy moderate snow
332: ثلوج معتدلة : Moderate snow
335: ثلوج كثيفة متفرقة : Patchy heavy snow
338: ثلوج كثيفة : Heavy snow
350: حبيبات جليدية : Ice pellets
353: رذاذ مطر خفيف : Light rain shower
356: رذاذ مطر معتدل أو كثيف : Moderate or heavy rain shower
359: رذاذ مطر غزير : Torrential rain shower
362: رذاذ مطر ثلجي خفيف : Light sleet showers
365: رذاذ مطر ثلجي معتدل أو كثيف : Moderate or heavy sleet showers
368: رذاذ ثلجي خفيف : Light snow showers
371: رذاذ ثلجي معتدل أو كثيف : Moderate or heavy snow showers
386: أمطار خفيفة متفرقة مصحوبة بالرعد : Patchy light rain with thunder
389: أمطار معتدلة أو كثيفة متفرقة مصحوبة بالرعد : Moderate or heavy rain with thunder
392: ثلوج خفيفة متفرقة مصحوبة بالرعد : Patchy light snow with thunder
395: ثلوج معتدلة أو كثيفة متفرقة مصحوبة بالرعد : Moderate or heavy snow with thunder

View file

@ -0,0 +1,67 @@
ব্যবহার:
$ curl wttr.in # এখন যেখানে আছ
$ curl wttr.in/cdg # প্যারিস - চার্লস ডি গল বিমানবন্দরে আবহাওয়ার পূর্বাভাস
গৃহীত কমান্ডের ধরন:
/paris # শহরের নাম
/~Eiffel+tower # যেকোনো স্থানের নাম
/Москва # ইউনিকোড নাম বা যেকোনো ভাষায় যেকোনো স্থানের নাম
/muc # বিমানবন্দর কোড (3 অক্ষর)
/@stackoverflow.com # ডোমেন নাম
/94107 # জিপ কোড (শুধুমাত্র মার্কিন যুক্তরাষ্ট্রে)
/-78.46,106.79 # জিপিএস স্থানাঙ্ক
বিশেষ কমান্ড:
/moon # চাঁদের পর্যায়গুলি (একই নামের শহরগুলি অ্যাক্সেস করতে যোগ করুন, + US বা, + France)
/moon@2016-10-25 # এই তারিখের জন্য চাঁদের পর্যায়গুলি (@ 2016-10-25)
ইউনিট:
?m # মেট্রিক সিস্টেম (মার্কিন যুক্তরাষ্ট্র ছাড়া সব জায়গায় ডিফল্ট)
?u # USCS (মার্কিন যুক্তরাষ্ট্রের জন্য ডিফল্ট)
?M # বাতাসের গতি m / s তে প্রদর্শন করে
বিকল্প প্রদর্শন :
?0 # শুধুমাত্র আজ
?1 # আজ + আগামীকাল
?2 # আজ + 2 দিন
?n # সংক্ষিপ্ত সংস্করণ (শুধুমাত্র দিন এবং রাত)
?q # নীরব সংস্করণ (কোন "আবহাওয়ার পূর্বাভাস" হেডার নেই)
?Q # অতি-নীরব সংস্করণ (কোন "আবহাওয়ার পূর্বাভাস" হেডার নেই, শহরের নাম নেই)
?T # ডিজেবল্ড টার্মিনালের জন্য এস্কেপ সিকুএন্সে (কোনও রঙ নেই)
বিকল্প PNG:
/paris.png # একটি PNG ফাইল তৈরি করুন
?p # আউটপুটের চারপাশে একটি ফ্রেম যুক্ত করুন
?t # স্বচ্ছতা 150 (স্বচ্ছতা 150)
transparency=... # 0 থেকে 255 পর্যন্ত স্বচ্ছতা (255 = অস্বছ)
বিকল্প একত্রিত করুন:
/Paris?0pq
/Paris?0pq&lang=fr
/Paris_0pq.png # পিএনজি মোডে বিকল্পগুলি _ এর পরে নির্দিষ্ট করা হয়
/Rome_0pq_lang=it.png # দীর্ঘ বিকল্পগুলি আন্ডারস্কোর দ্বারা পৃথক করা হয় _
ভাষান্তর :
$ curl fr.wttr.in/Paris
$ curl wttr.in/paris?lang=fr
$ curl -H "Accept-Language: fr" wttr.in/paris
সাপোরটেড ভাষাসমূহ :
FULL_TRANSLATION (সম্পূর্ণ সাপোর্ট )
PARTIAL_TRANSLATION (অসম্পূর্ণ সাপোর্ট )
বিশেষ ইউআরএলসমূহ:
/:help # এই পেইজটি প্রদর্শন করুন
/:bash.function # রেকমেন্ডেড ব্যাশ ফাংশন wttr ()
/:translation # wttr.in এর অনুবাদ সম্পর্কে তথ্য প্রদর্শন করুন

78
share/translations/bn.txt Normal file
View file

@ -0,0 +1,78 @@
: বজ্রঝড়ের সঙ্গে ভারী বৃষ্টি ও শিলাবৃষ্টি : Heavy rain and hail with thunderstorm
: বজ্রঝড়ের সঙ্গে ভারী বৃষ্টি : Heavy rain with thunderstorm
: বজ্রঝড়ের সঙ্গে হালকা বৃষ্টি ও শিলাবৃষ্টি : Light rain and hail with thunderstorm
: হালকা বৃষ্টি আর তুষারপাত : Light rain and snow shower
: বজ্রবিদ্যুৎ সহ হালকা বৃষ্টি : Light rain with thunderstorm
: হালকা তুষারপাত : Light snow shower
: আংশিক কুয়াশা : Partial fog
: বজ্রবিদ্যুৎ সহ বৃষ্টি ও শিলাবৃষ্টি : Rain and hail with thunderstorm
: বজ্রবিদ্যুৎ সহ বৃষ্টি : Rain with thunderstorm
: অগভীর কুয়াশা : Shallow fog
: ধোঁয়া : Smoke
: আকস্মিক ঝড়ো বাতাস : Squalls
: আশেপাশে বজ্রঝড় : Thunderstorm in vicinity
: তুষার : Snow
: বৃষ্টি : Rain
: হালকা বৃষ্টি, ঝরনা বৃষ্টি : Light Rain, Rain Shower
: মাঝারী বৃষ্টিপাত : Rain Shower
: টুকরো টুকরো কুয়াশা : Patches of fog
: গুঁড়ি গুঁড়ি বৃষ্টি : Drizzle
: হালকা গুঁড়ি গুঁড়ি বৃষ্টি : Light drizzle
: হালকা ভাসমান তুষার : Low drifting snow
: হালকা বৃষ্টি ও তুষারপাত : Light rain and snow
: আশেপাশে বৃষ্টিপাত : Shower in vicinity
: বজ্রবিদ্যুৎ সহ বৃষ্টি : Rain with thunderstorm
: বৃষ্টি ও তুষারপাত : Rain and snow shower
: বজ্রঝড় : Thunderstorm
: গুঁড়ি গুঁড়ি মাঝারী বৃষ্টি : Drizzle and rain
: বজ্রঝড়ের সঙ্গে শিলাবৃষ্টি : Hail with thunderstorm
: কুয়াশা : Haze
: হালকা গুঁড়ি গুঁড়ি বৃষ্টি : Light drizzle and rain
: হালকা বৃষ্টি এবং বজ্রঝড় সহ ছোট শিলাবৃষ্টি/তুষারপাত : Light rain and small hail/snow pallets with thunderstorm
113 : পরিষ্কার : Clear
113 : রৌদ্রজ্জ্বল : Sunny
116 : আংশিক মেঘলা : Partly cloudy
119 : মেঘলা : Cloudy
122 : মেঘাচ্ছন্ন : Overcast
143 : কুয়াশা : Mist
176 : অল্প বৃষ্টি হতে পারে : Patchy rain possible
179 : অল্প তুষারপাত হতে পারে : Patchy snow possible
182 : অল্প শিলাবৃষ্টি হতে পারে : Patchy sleet possible
185 : ঠাণ্ডা হিমশীতল বৃষ্টির সম্ভাবনা : Patchy freezing drizzle possible
200 : বজ্রপাতের প্রাদুর্ভাবের সম্ভাবনা : Thundery outbreaks possible
227 : উড়ন্ত তুষার : Blowing snow
230 : তুষারঝড় : Blizzard
248 : কুয়াশা : Fog
260 : হিমশীতল কুয়াশা : Freezing fog
263 : খণ্ড খণ্ড হালকা গুঁড়ি গুঁড়ি বৃষ্টি : Patchy light drizzle
266 : হালকা গুঁড়ি গুঁড়ি বৃষ্টি : Light drizzle
281 : হিমশীতল গুঁড়ি গুঁড়ি বৃষ্টি : Freezing drizzle
284 : ভারী হিমশীতল গুঁড়ি গুঁড়ি বৃষ্টি : Heavy freezing drizzle
293 : খণ্ড খণ্ড হালকা বৃষ্টি : Patchy light rain
296 : হালকা বৃষ্টি : Light rain
299 : মাঝে মাঝে মাঝারি বৃষ্টি : Moderate rain at times
302 : মাঝারি বৃষ্টি : Moderate rain
305 : মাঝে মাঝে ভারী বৃষ্টি : Heavy rain at times
308 : ভারী বৃষ্টি : Heavy rain
311 : হিমশীতল হালকা বৃষ্টি : Light freezing rain
314 : মাঝারি বা ভারী হিমায়িত বৃষ্টি : Moderate or heavy freezing rain
317 : হালকা শিলা : Light sleet
320 : মাঝারি বা ভারী শিলাবৃষ্টি : Moderate or heavy sleet
323 : খণ্ড খণ্ড হালকা তুষারপাত : Patchy light snow
326 : হালকা তুষারপাত : Light snow
329 : খণ্ড খণ্ড মাঝারি তুষারপাত : Patchy moderate snow
332 : মাঝারি তুষারপাত : Moderate snow
335 : খণ্ড খণ্ড ভারী তুষারপাত : Patchy heavy snow
338 : ভারী তুষারপাত : Heavy snow
350 : বরফ প্যালেট : Ice pellets
353 : হালকা বৃষ্টির ঝরনা : Light rain shower
356 : মাঝারি বা ভারী বৃষ্টির ঝরনা : Moderate or heavy rain shower
359 : মুষলধারে বৃষ্টি : Torrential rain shower
362 : হালকা বরফমিশ্রিত ঝিরঝির বৃষ্টি : Light sleet showers
365 : মাঝারি বা ভারী বরফমিশ্রিত ঝিরঝির বৃষ্টি : Moderate or heavy sleet showers
368 : হাল্কা তুষারপাত : Light snow showers
371 : মাঝারি বা ভারী তুষারপাত : Moderate or heavy snow showers
386 : বজ্রসহ খণ্ড খণ্ড হালকা বৃষ্টি : Patchy light rain with thunder
389 : বজ্রসহ মাঝারি বা ভারী বৃষ্টি : Moderate or heavy rain with thunder
392 : বজ্রপাত সহ খণ্ড খণ্ড হালকা তুষারপাত : Patchy light snow with thunder
395 : মাঝারি বা ভারী তুষারপাত সহ বজ্রপাত : Moderate or heavy snow with thunder

View file

@ -7,6 +7,7 @@
: Regen : Rain
: Leichter Regen : Light Rain
: Regenschauer : Rain Shower
: Regen in der näheren Umgebung : Shower in vicinity
113: Wolkenlos : Clear
113: Sonnig : Sunny
116: Leicht Bewölkt : Partly cloudy

View file

@ -4,7 +4,7 @@
: Pluie légère et averses : Light rain and snow shower
: Pluies légères orageuses : Light rain with thunderstorm
: Chutes de neige légères : Light snow shower
: Nappes de brouillard : Parftial fog
: Nappes de brouillard : Partial fog
: Orage de pluie et de grêle : Rain and hail with thunderstorm
: Pluies orageuses : Rain with thunderstorm
: Brouillard léger : Shallow fog

View file

@ -0,0 +1,67 @@
Instrucións:
$ curl wttr.in # o tempo na sua localización actual
$ curl wttr.in/muc # o tempo no aeroporto de Múnic
Tipos de localización soportados:
/paris # o nome dunha cidade
/~Eiffel+tower # o nome de calquera lugar famoso
/Москва # nome Unicode de calquera lugar en calquera idioma
/muc # o código dun aeroporto (3 letras)
/@stackoverflow.com # o nome dun dominio web
/94107 # um código de área
/-78.46,106.79 # coordenadas do GPS
Lugares especiais:
/moon # A fase da lúa (crecente ,+US ou ,+France para estas cidades)
/moon@2016-10-25 # A fase da lúa nunha determinada data (@2016-10-25)
Unidades:
?m # Métricas (SI) (por defecto en todos os lugares agás en EEUU)
?u # Sistema Unificado de Clasificación de Solo ou USCS (por defecto en EEUU)
?M # Amosar a velocidade do vento en m/s
Opcións de visualización:
?0 # Soamente o clima actual
?1 # O clima actual + a previsión de 1 dia
?2 # O clima actual + a previsión de 2 dias
?n # Versión curta (só o dia e a noite)
?q # Versión breve (sen o texto de "Previsión do Tempo")
?Q # Versión superbreve (sen "Previsión do Tempo" e o nome da cidade)
?T # Desactiva as secuencias de escape no terminal (sen cores)
Opións de PNG:
/paris.png # Xera unha imaxe PNG
?p # Amece un borde ao redor da imaxe
?t # Transparencia 150
transparency=... # Transparencia de 0 a 255 (255 = sen transparencia)
As opcións poden ser usadas en conxunto:
/Paris?0pq
/Paris?0pq&lang=fr
/Paris_0pq.png # Em PNG as opcións especificanse depois do caracter _
/Rome_0pq_lang=it.png # Nunha secuencia longa de opcións, poden ser separadas polo caracter _
Localizaión:
$ curl fr.wttr.in/Paris
$ curl wttr.in/paris?lang=fr
$ curl -H "Accept-Language: fr" wttr.in/paris
Linguas soportadas:
FULL_TRANSLATION (soportadas)
PARTIAL_TRANSLATION (en proceso)
URLs especiais:
/:help # Amosa esta páxina
/:bash.function # Suxire unha función wttr() en bash
/:translation # Amosa información respecto dos tradutores

47
share/translations/gl.txt Normal file
View file

@ -0,0 +1,47 @@
113: Despexado : Clear
113: Solleiro : Sunny
116: Parcialmente Nubrado : Partly cloudy
119: Nubrado : Cloudy
122: Cuberto : Overcast
143: Neboa : Mist
176: Posibel choiva : Patchy rain possible
179: Posibel neve : Patchy snow possible
182: Posibel auganeve : Patchy sleet possible
185: Posibel barruzo xeado : Patchy freezing drizzle possible
200: Posibeis treboadas : Thundery outbreaks possible
227: Cebrisca : Blowing snow
230: Treboada de neve : Blizzard
248: Brétema : Fog
260: Brétema xeada : Freezing fog
263: Barruzo lixeiro casual : Patchy light drizzle
266: Barruzo lixeiro : Light drizzle
281: Barruzo xeado : Freezing drizzle
284: Barruzo xeado forte : Heavy freezing drizzle
293: Chuvisca casual : Patchy light rain
296: Chuvisca : Light rain
299: Choiva casual : Moderate rain at times
302: Choiva : Moderate rain
305: Dioivo casual : Heavy rain at times
308: Dioivo : Heavy rain
311: Orballo xeado : Light freezing rain
314: Xistra : Moderate or heavy freezing rain
317: Auganeve lixeira : Light sleet
320: Auganeve : Moderate or heavy sleet
323: Nevarisca casual : Patchy light snow
326: Nevarisca : Light snow
329: Nevada casual : Patchy moderate snow
332: Nevada : Moderate snow
335: Nevada forte casual : Patchy heavy snow
338: Nevada forte : Heavy snow
350: Sarabia : Ice pellets
353: Orballo : Light rain shower
356: Bategada : Moderate or heavy rain shower
359: Choiva torrencial : Torrential rain shower
362: Torba : Light sleet showers
365: Torba forte : Moderate or heavy sleet showers
368: Choiva con neve lixeira : Light snow showers
371: Choiva con neve forte : Moderate or heavy snow showers
386: Barruzo casual con tronos : Patchy light rain with thunder
389: Treboada : Moderate or heavy rain with thunder
392: Nevarisca casual con tronos : Patchy light snow with thunder
395: Nevada con tronos : Moderate or heavy snow with thunder

View file

@ -0,0 +1,70 @@
Usage:
$ curl wttr.in # current location
$ curl wttr.in/muc # weather in the Munich airport
Supported location types:
/paris # city name
/~Eiffel+tower # any location (+ for spaces)
/Москва # Unicode name of any location in any language
/muc # airport code (3 letters)
/@stackoverflow.com # domain name
/94107 # area codes
/-78.46,106.79 # GPS coordinates
Moon phase information:
/moon # Moon phase (add ,+US or ,+France for these cities)
/moon@2016-10-25 # Moon phase for the date (@2016-10-25)
Units:
m # metric (SI) (used by default everywhere except US)
u # USCS (used by default in US)
M # show wind speed in m/s
View options:
0 # only current weather
1 # current weather + today's forecast
2 # current weather + today's + tomorrow's forecast
A # ignore User-Agent and force ANSI output format (terminal)
F # do not show the "Follow" line
n # narrow version (only day and night)
q # quiet version (no "Weather report" text)
Q # superquiet version (no "Weather report", no city name)
T # switch terminal sequences off (no colors)
PNG options:
/paris.png # generate a PNG file
p # add frame around the output
t # transparency 150
transparency=... # transparency from 0 to 255 (255 = not transparent)
background=... # background color in form RRGGBB, e.g. 00aaaa
Options can be combined:
/Paris?0pq
/Paris?0pq&lang=fr
/Paris_0pq.png # in PNG the file mode are specified after _
/Rome_0pq_lang=it.png # long options are separated with underscore
Localization:
$ curl fr.wttr.in/Paris
$ curl wttr.in/paris?lang=fr
$ curl -H "Accept-Language: fr" wttr.in/paris
Supported languages:
FULL_TRANSLATION (supported)
PARTIAL_TRANSLATION (in progress)
Special URLs:
/:help # show this page
/:bash.function # show recommended bash function wttr()
/:translation # show the information about the translators

78
share/translations/gu.txt Normal file
View file

@ -0,0 +1,78 @@
: Fortes pluies et orages de grêle : Heavy rain and hail with thunderstorm
: Fortes pluies orageuses : Heavy rain with thunderstorm
: Orages de pluie et grêle légères : Light rain and hail with thunderstorm
: Pluie légère et averses : Light rain and snow shower
: Pluies légères orageuses : Light rain with thunderstorm
: Chutes de neige légères : Light snow shower
: Nappes de brouillard : Partial fog
: Orage de pluie et de grêle : Rain and hail with thunderstorm
: Pluies orageuses : Rain with thunderstorm
: Brouillard léger : Shallow fog
: Brume : Smoke
: Grains : Squalls
: Orages proches : Thunderstorm in vicinity
: Neige : Snow
: Pluie : Rain
: Pluie légère, Averses : Light Rain, Rain Shower
: Averses : Rain Shower
: Nappes de brouillard : Patches of fog
: Bruine : Drizzle
: Bruine légère : Light drizzle
: Chasse-neige basse : Low drifting snow
: Pluie et neige légères : Light rain and snow
: Averses proches : Shower in vicinity
: Pluie et orages : Rain with thunderstorm
: Averse de pluie et neige mêlées : Rain and snow shower
: Orage : Thunderstorm
: Bruine et pluie : Drizzle and rain
: Orage de grêle : Hail with thunderstorm
: Brume : Haze
: Bruine legère et pluie : Light drizzle and rain
: Orage, pluie légère et grèle / neige roulée : Light rain and small hail/snow pallets with thunderstorm
113 : Temps clair : Clear
113 : Ensoleillé : Sunny
116 : Partiellement couvert : Partly cloudy
119 : Nuageux : Cloudy
122 : Couvert : Overcast
143 : Brumeux : Mist
176 : Pluies éparses possibles : Patchy rain possible
179 : Chutes de neige éparses possibles : Patchy snow possible
182 : Chutes éparses de neige fondue possibles : Patchy sleet possible
185 : Bruines givrantes éparses possibles : Patchy freezing drizzle possible
200 : Orages possibles : Thundery outbreaks possible
227 : Poudrerie : Blowing snow
230 : Blizzard : Blizzard
248 : Brouillard : Fog
260 : Brouillard givrant : Freezing fog
263 : Bruines éparses et légères : Patchy light drizzle
266 : Bruine légère : Light drizzle
281 : Bruine givrante : Freezing drizzle
284 : Forte bruine givrante : Heavy freezing drizzle
293 : Pluies éparses et légères : Patchy light rain
296 : Pluie légère : Light rain
299 : Pluie modérée intermittente : Moderate rain at times
302 : Pluie modérée : Moderate rain
305 : Forte pluie intermittente : Heavy rain at times
308 : Forte pluie : Heavy rain
311 : Pluie verglaçante légère : Light freezing rain
314 : Pluie verglaçante modérée à forte : Moderate or heavy freezing rain
317 : Chutes légères de neige fondue : Light sleet
320 : Chutes de neige fondue modérées à fortes : Moderate or heavy sleet
323 : Chutes de neige éparses et légères : Patchy light snow
326 : Chutes de neige légères : Light snow
329 : Chutes de neige éparses et modérées : Patchy moderate snow
332 : Chutes de neige modérées : Moderate snow
335 : Fortes chutes de neige éparses : Patchy heavy snow
338 : Fortes chutes de neige : Heavy snow
350 : Grésil : Ice pellets
353 : Averses légères : Light rain shower
356 : Averses modérées à fortes : Moderate or heavy rain shower
359 : Averses torrentielles : Torrential rain shower
362 : Averses légères de neige fondue : Light sleet showers
365 : Averses de neige fondue modérées à fortes : Moderate or heavy sleet showers
368 : Averses de neige légères : Light snow showers
371 : Averses de neige modérées à fortes : Moderate or heavy snow showers
386 : Pluies orageuses légères et éparses : Patchy light rain with thunder
389 : Pluies orageuses modérées à fortes : Moderate or heavy rain with thunder
392 : Chutes de neige orageuses légères et éparses : Patchy light snow with thunder
395 : Chutes de neige orageuses modérées à fortes : Moderate or heavy snow with thunder

View file

@ -0,0 +1,69 @@
उपयोग:
    $ curl wttr.in          # वर्तमान स्थान के मौसम की जानकारी 
    $ curl wttr.in/muc      # म्यूनिख हवाई अड्डे का मौसम
समर्थित स्थान प्रकार:
    /paris                  # शहर का नाम
    /~Eiffel+tower          # कोई भी स्थान (एक से अधिक शब्दो को जोड़ने के लिए + का उपयोग करे)
    /Москва                 # किसी भी भाषा में किसी भी स्थान का यूनिकोड नाम
    /muc                    # एयरपोर्ट कोड (3 अक्षर)
    /@stackoverflow.com     # डोमेन नाम
    /94107                  # क्षेत्र कोड
    /-78.46,106.79          # जीपीएस निर्देशांक
चंद्र चरण की जानकारी:
    /moon                   # चंद्रमा चरण (शहरों के लिए +US या, +France जोड़ें)
    /moon@2016-10-25        # तिथि के लिए चंद्र चरण (@2016-10-25)
इकाइयां:
    m                       # मेट्रिक (एसआई) (यूएस को छोड़कर हर जगह डिफ़ॉल्ट रूप से उपयोग किया जाता है)
    u                       # यूएससीएस (यूएस में डिफ़ॉल्ट रूप से प्रयुक्त)
    M                       # हवा की गति मीटर/सेकंड में दिखाएं
विकल्प देखें:
    0                       # केवल वर्तमान मौसम
    1                       # वर्तमान मौसम और आज का पूर्वानुमान
    2                       # वर्तमान मौसम और आज का और कल का पूर्वानुमान
    A                       # युसेर-एजेंट को अनदेखा करें और एएनएसआई आउटपुट (टर्मिनल) को बाध्य करें
    F                       # "फॉलो" लाइन न दिखाएं
    n                       # संकीर्ण संस्करण (केवल दिन और रात के लिये)
    q                       # शांत संस्करण (कोई "मौसम रिपोर्ट" पाठ नहीं)
    Q                       # अति शांत संस्करण ("मौसम की जानकारी" और शहर का नाम नहीं)
    T                       # टर्मिनल अनुक्रम बंद करें (रंगो के बिना)
पीएनजी विकल्प:
    /paris.png              # पीएनजी फ़ाइल जनरेट करें
    p                       # आउटपुट के चारों ओर फ्रेम जोड़ें
    t                       # पारदर्शिता 150
    transparency=...        # पारदर्शिता 0 से 255 तक (255 = पारदर्शी नहीं)
    background=...          # RRGGBB के रूप में पृष्ठभूमि का रंग, जैसे की 00aaaa
विकल्पों को जोड़ा जा सकता है:
    /Paris?0pq
    /Paris?0pq&lang=fr
    /Paris_0pq.png          # पीएनजी में फ़ाइल मोड को "_" के बाद निर्दिष्ट किया जाता है
    /Rome_0pq_lang=it.png   # लंबे विकल्पों को अंडरस्कोर से अलग किया जाता है
स्थानीयकरण:
    $ curl fr.wttr.in/Paris
    $ curl wttr.in/paris?lang=fr
    $ curl -H "Accept-Language: fr" wttr.in/paris
समर्थित भाषाएँ:
    FULL_TRANSLATION (समर्थित)
    PARTIAL_TRANSLATION (प्रगति में)
विशेष यूआरएल:
    /:help                  # इस पेज को दिखाएं
    /:bash.function         # अनुशंसित बैश फ़ंक्शन wttr () दिखाएं
    /:translation           # अनुवादकों के बारे में जानकारी दिखाएं

79
share/translations/hi.txt Normal file
View file

@ -0,0 +1,79 @@
: तेज बारिश और गरज के साथ ओलावृष्टि : Heavy rain and hail with thunderstorm
: गरज के साथ तेज बारिश : Heavy rain with thunderstorm
: गरज के साथ हल्की बारिश और ओलावृष्टि : Light rain and hail with thunderstorm
: हल्की बारिश और बर्फ की बौछार : Light rain and snow shower
: गरज के साथ हल्की बारिश : Light rain with thunderstorm
: हल्की बर्फ़ की बौछार : Light snow shower
: आंशिक कोहरा : Partial fog
: घना कोहरा : Patchy fog
: गरज के साथ बारिश और ओलावृष्टि : Rain and hail with thunderstorm
: गरज के साथ बारिश : Rain with thunderstorm
: उथला कोहरा : Shallow fog
: धुआं : Smoke
: तूफ़ान : Squalls
: आसपास में आंधी : Thunderstorm in vicinity
: हिमपात : Snow
: वर्षा : Rain
: हल्की बारिश, बारिश की बौछार : Light Rain, Rain Shower
: बारिश की बौछार : Rain Shower
: कोहरे के धब्बे : Patches of fog
: बूंदा बांदी : Drizzle
: हल्की बूंदाबांदी : Light drizzle
: कम बहती बर्फ : Low drifting snow
: हल्की बारिश और हिमपात : Light rain and snow
: आस-पास शावर : Shower in vicinity
: गरज के साथ बारिश : Rain with thunderstorm
: बारिश और बर्फ की बौछार : Rain and snow shower
: आंधी तूफान : Thunderstorm
: बूंदा बांदी और बारिश : Drizzle and rain
: गरज के साथ ओलावृष्टि : Hail with thunderstorm
: धुंध : Haze
: हल्की बूंदा बांदी और बारिश : Light drizzle and rain
: गरज के साथ हल्की बारिश और छोटे-छोटे ओले/बर्फ की पट्टियां : Light rain and small hail/snow pallets with thunderstorm
113 : स्पष्ट : Clear
113 : धूपदार : Sunny
116 : आंशिक रूप से बादल छाएंगे : Partly cloudy
119 : बादल : Cloudy
122 : घटाटोप : Overcast
143 : कोहरा : Mist
176 : हल्की बारिश संभव : Patchy rain possible
179 : हल्की बर्फ़बारी संभव : Patchy snow possible
182 : हल्की ओले के साथ वर्षा संभव : Patchy sleet possible
185 : जमने वाली हल्की बूंदाबांदी संभव : Patchy freezing drizzle possible
200 : गरज का प्रकोप संभव : Thundery outbreaks possible
227 : उड़ाने वाली बर्फ : Blowing snow
230 : बर्फानी तूफान : Blizzard
248 : कोहरा : Fog
260 : अत्यधिक ठंडा कोहरा : Freezing fog
263 : हल्की हल्की बूंदा बांदी : Patchy light drizzle
266 : हल्की बूंदाबांदी : Light drizzle
281 : जमा देने वाली हवा : Freezing drizzle
284 : भारी जमने वाली बूंदा बांदी : Heavy freezing drizzle
293 : हल्की हल्की बारिश : Patchy light rain
296 : हलकी बारिश : Light rain
299 : कभी-कभी मध्यम बारिश : Moderate rain at times
302 : औसत दर्जे की वर्षा : Moderate rain
305 : रुक-रुक कर हो रही भारी बारिश : Heavy rain at times
308 : भारी वर्षा : Heavy rain
311 : हल्की जमने वाली बारिश : Light freezing rain
314 : मध्यम या भारी जमने वाली बारिश : Moderate or heavy freezing rain
317 : हल्की ओले के साथ वर्षा : Light sleet
320 : मध्यम या भारी ओले के साथ वर्षा : Moderate or heavy sleet
323 : हल्की हल्की बर्फ : Patchy light snow
326 : हल्की बर्फ : Light snow
329 : हल्की मध्यम हिमपात : Patchy moderate snow
332 : मध्यम हिमपात : Moderate snow
335 : हल्की भारी हिमपात : Patchy heavy snow
338 : भारी हिमपात : Heavy snow
350 : ओले के साथ वर्षा : Ice pellets
353 : हल्की बोछारे : Light rain shower
356 : मध्यम या भारी बारिश की बौछार : Moderate or heavy rain shower
359 : मूसलाधार बारिश की बौछार : Torrential rain shower
362 : हल्की ओले के साथ बौछारें : Light sleet showers
365 : मध्यम से भारी ओले के साथ बौछारें : Moderate or heavy sleet showers
368 : हल्की बर्फ़बारी : Light snow showers
371 : मध्यम या भारी हिमपात की बौछार : Moderate or heavy snow showers
386 : गरज के साथ हल्की बारिश : Patchy light rain with thunder
389 : गरज के साथ मध्यम या भारी बारिश : Moderate or heavy rain with thunder
392 : गरज के साथ हल्की हल्की बर्फ़ : Patchy light snow with thunder
395 : गरज के साथ मध्यम या भारी हिमपात : Moderate or heavy snow with thunder

View file

@ -52,3 +52,4 @@
: Havazás : Snow
: Eső : Rain
: Gyenge eső : Light Rain
: Talajmenti köd : Shallow fog

View file

@ -0,0 +1,69 @@
Naudojimas:
$ curl wttr.in # dabartinė vietovė
$ curl wttr.in/plq # oras Palangos oro uoste
Palaikomos vietovių rūšys:
/panemunė # miesto pavadinimas
/~Eiffel+tower # bet kuri vietovė (+ vietoj tarpų)
/Магілёў # bet kurios vietovės pavadinimas Unikodu
/plq # oro uosto kodas (3 raidės)
/@stackoverflow.com # domeno vardas
/94107 # pašto kodas (tik JAV)
/-78.46,106.79 # GPS koordinatės
Mėnulio fazių informacija:
/moon # Mėnulio fazė (pridėkite ,+US arba +,France šio pavadinimo miestams)
/moon@2016-10-25 # Mėnulio fazė datai (@2016-10-25)
Matai:
?m # metrai (SI) (pagal nutylėjimą, naudojama visu išskyrus JAV)
?u # USCS (pagal nutylėjimą, naudojama JAV)
?M # vėjo greitis m/s
Rodymo parinktys:
?0 # tik faktiniai orai
?1 # faktiniai orai + šiandienos prognozė
?2 # faktiniai orai + šiandienos + rytojaus prognozės
A # ignoruoti naudotojo agentą (User-Agent) ir priverstinai formatuoti išvestį į ANSI (terminale)
F # nerodyti eilutės apie atnaujinimų sekimą
n # siaura versija (tik diena ir naktis)
q # tylesnė versija (be teksto „Orų prognozė“)
Q # labai tyli versija (be teksto „Orų prognozė“ ir be vietovės pavadinimo
T # išjungti terminalo sekas (be spalvų)
PNG parinktys:
/panemunė.png # sukurti PNG failą
p # apvesti išvestį rėmeliu
t # skaidrumas 150
transparency=... # skaidrumas nuo 0 iki 255 (255 = neskaidrus)
background=... # fono spalva RRGGBB forma, pvz., 00aaaa
Parinktis galima jungti:
/Panemunė?0pq
/Panemunė?0pq&lang=lt
/Panemunė_0pq.png # PNG failo pobūdis nurodomas po _
/Rēzekne_0pq_lang=lv.png # ilgavardės parinktys atskiriamos apatiniu brūkšniu
Kalbos:
$ curl lt.wttr.in/Panemunė
$ curl wttr.in/panemunė?lang=lt
$ curl -H "Accept-Language: lt" wttr.in/panemunė
Palaikomos kalbos
FULL_TRANSLATION (išverstos)
PARTIAL_TRANSLATION (tebeverčiamos)
Ypatingi URL:
/:help # rodyti šį puslapį
/:bash.function # rodyti rekomenduojamą bash funkciją wttr()
/:translation # rodyti informaciją apie vertėjus

47
share/translations/lt.txt Normal file
View file

@ -0,0 +1,47 @@
113: Giedra : Clear :
113: Saulėta : Sunny :
116: Nepastoviai debesuota : Partly cloudy :
119: Debesuota su pragiedruliais : Cloudy :
122: Debesuota : Overcast :
143: Migla : Mist :
176: Galimas silpnas lietus : Patchy rain possible :
179: Galimas nedidelis snygis : Patchy snow possible :
182: Galima nedidelė šlapdriba : Patchy sleet possible :
185: Galima nedidelė lijundra : Patchy freezing drizzle possible :
200: Spėjama perkūnija : Thundery outbreaks possible :
227: Pustymas : Blowing snow :
230: Pūga : Blizzard :
248: Rūkas : Fog :
260: Šarma : Freezing fog :
263: Protarpiais dulksna : Patchy light drizzle :
266: Dulksna : Light drizzle :
281: Lijundra : Freezing drizzle :
284: Stipri lijundra : Heavy freezing drizzle :
293: Protarpiais silpnas lietus : Patchy light rain :
296: Silpnas lietus : Light rain :
299: Protarpiais lietus : Moderate rain at times :
302: Lietus : Moderate rain :
305: Protarpiais stiprus lietus : Heavy rain at times :
308: Stiprus lietus : Heavy rain :
311: Silpna lijundra : Light freezing rain :
314: Vidutinė arba stipri lijundra : Moderate or heavy freezing rain :
317: Lengva šlapdriba : Light sleet :
320: Vidutinė arba stipri šlapdriba : Moderate or heavy sleet :
323: Protarpiais lengvas snygis : Patchy light snow :
326: Lengvas snygis : Light snow :
329: Protarpiais vidutinis snygis : Patchy moderate snow :
332: Snygis : Moderate snow :
335: Protarpiais stiprus snygis : Patchy heavy snow :
338: Stiprus snygis : Heavy snow :
350: Kruša : Ice pellets :
353: Nesmarki liūtis : Light rain shower :
356: Vidutinė arba smarki liūtis : Moderate or heavy rain shower :
359: Smarki liūtis : Torrential rain shower :
362: Protarpiais lengva šlapdriba : Light sleet showers :
365: Protarpiais vidutinė/smarki šlapdriba : Moderate or heavy sleet showers :
368: Protarpiais lengvas snygis : Light snow showers :
371: Protarpiais vidutinis/sunkus snygis : Moderate or heavy snow showers :
386: Protarpiais lengvas lietūs su perkūnija : Patchy light rain with thunder :
389: Vidutinis/sunkus lietus su perkūnija : Moderate or heavy rain with thunder :
392: Protarpiais lengvas snygis su perkūija : Patchy light snow with thunder :
395: Vidutinis/sunkus snygis su perkūnija : Moderate or heavy snow with thunder :

View file

@ -0,0 +1,62 @@
lang:
name: English
code: "en"
issue: ""
translators:
- "Igor Chubin @igor_chubin"
locale: "en_US"
translated:
help: true
weather: true
messages: true
messages:
caption: 'Weather report for:'
location: Location
unknown_location: Unknown location
capacity_limit_reached: |2
Sorry, we are running out of queries to the weather service at the moment.
Here is the weather report for the default city (just to show you what it looks like).
We will get new queries as soon as possible.
You can follow https://twitter.com/igor_chubin for the updates.
======================================================================================
follow_me: |-
Follow \\e[46m\\e[30m@igor_chubin\\e[0m for wttr.in updates
new_feature: |-
New feature: multilingual location names \\e[92mwttr.in/станция+Восток\\e[0m (in UTF-8) and location search \\e[92mwttr.in/~Kilimanjaro\\e[0m (just add ~ before)
not_found_message: |2
We were unable to find your location
so we have brought you to Oymyakon,
one of the coldest permanently inhabited locales on the planet.
views:
v1:
morning: "Morning"
noon: "Noon"
evening: "Evening"
night: "Night"
v2:
weather_report_for: "Weather report for:"
weather: "Weather"
timezone: "Timezone"
now: "Now"
dawn: "Dawn"
sunrise: "Sunrise"
zenith: "Zenith"
sunset: "Sunset"
dusk: "Dusk"

View file

@ -0,0 +1,62 @@
lang:
name: Gujarati
code: gu
issue: "774"
translators:
- @wylited (on GitHub)
locale: gu_IN
translated:
help: false
weather: false
messages: false
messages:
caption: 'Weather report for:'
location: Location
unknown_location: Unknown location
capacity_limit_reached: |2
Sorry, we are running out of queries to the weather service at the moment.
Here is the weather report for the default city (just to show you what it looks like).
We will get new queries as soon as possible.
You can follow https://twitter.com/igor_chubin for the updates.
======================================================================================
follow_me: |-
Follow \\e[46m\\e[30m@igor_chubin\\e[0m for wttr.in updates
new_feature: |-
New feature: multilingual location names \\e[92mwttr.in/станция+Восток\\e[0m (in UTF-8) and location search \\e[92mwttr.in/~Kilimanjaro\\e[0m (just add ~ before)
not_found_message: |2
We were unable to find your location
so we have brought you to Oymyakon,
one of the coldest permanently inhabited locales on the planet.
views:
v1:
morning: "Morning"
noon: "Noon"
evening: "Evening"
night: "Night"
v2:
weather_report_for: "Weather report for:"
weather: "Weather"
timezone: "Timezone"
now: "Now"
dawn: "Dawn"
sunrise: "Sunrise"
zenith: "Zenith"
sunset: "Sunset"
dusk: "Dusk"

View file

@ -0,0 +1,66 @@
Fampiasana azy:
$ curl wttr.in # toetr'andro eo amin'ny toerana misy anao
$ curl wttr.in/antananarivo # totr'andro any Antananarivo
Karazana toerana azo ampesaina:
/fianarantsoa # nom de la ville
/~Eiffel+tower # anaran-toerana rehetra
/Москва # anarana Unikody na anaran-toerana rehetra amin'ny fiteny rehetra
/tnr # kaody ny seranam-piaramanidina (litera 3)
/@stackoverflow.com # anarana domaina (rohy)
/94107 # Kaody postaly (hoan'ny Etazonia iany)
/-78.46,106.79 # coordonnées GPS
Toerana somary miavaka:
/moon # Dignana ny volana(ampio ,+US ou ,+France raha toa ka misy toerana mitondra anio anarana io)
/moon@2016-10-25 # Dignana ny volana hoan'ny daty iray(@2016-10-25)
Refy:
?m # rafitra metrika (fampiasain'ny rehetra afatsy ny Amerika Avaratra)
?u # USCS (Fampiasan'ny Etazonia)
?M # mampiseho ny hafainganam-pandehan'ny rivotra amin'ny metatra isan-segondra
Fomba fampisehoana:
?0 # androany fotsiny
?1 # androany sy rampitso
?2 # androany miampy roa andro
?n # kinova fohy (atoandro sy ariva fotsiny)
?q # kinova tsotra (tsisy "Vinavina ny totrandro androany")
?Q # version super-silencieuse (pas d'en-tête "Prévisions météo pour", pas de nom de la ville)
?T # séquences d'échappement pour terminaux désactivées (pas de couleurs)
Fomba fampisehoana sary PNG:
/antananarivo.png # mamoka sary PNG
?p # manisy kadra manodidina ilay seho mivoaka
?t # transparency 150 (fangaraharana 150)
transparency=... # fangaraharana ao anatin'ny 0 atramin'ny 255 (255 = tsisy fangaraharana)
Manambatra anireo safidy:
/antananarivo?0pq
/antananarivo?0pq&lang=mg
/antananarivo_0pq.png # raha toa ka mampiasa fampisehoana aminn'ny sary PNG dia asina tsipik'ambany `_` manelanelana azy
/Rome_0pq_lang=it.png # ireo safidy lava dia sarahina amin'ny tsipik'ambany `_` ian'ny koa
Toerana:
$ curl fr.wttr.in/antananarivo
$ curl wttr.in/antananarivo?lang=mg
$ curl -H "Accept-Language: mg" wttr.in/paris
Langues supportées:
FULL_TRANSLATION (Voadika teny tanteraka)
PARTIAL_TRANSLATION (Voadika teny ampahany)
URLs particulières:
/:help # mampiseho ito pejy ito
/:bash.function # sosokevitra fonction bash wttr()
/:translation # mampahafantra ny momba ny fandikanteny ao amin'ny wttr.in

77
share/translations/mg.txt Normal file
View file

@ -0,0 +1,77 @@
: Oram-be manavandra sy oram-baratra : Heavy rain and hail with thunderstorm
: Oram-be sy oram-baratra : Heavy rain with thunderstorm
: Oram-baratra malefaka sy manavandra : Light rain and hail with thunderstorm
: Orana malefaka sy ranomandry : Light rain and snow shower
: Oranam-baratra malefaka : Light rain with thunderstorm
: Orana malefaka ranomandry : Light snow shower
: Zavona ampahany : Partial fog
: Oram-baratra manavandra : Rain and hail with thunderstorm
: Zavona malefaka : Shallow fog
: Zavona : Smoke
: Oram-baratra : Squalls
: Orages proches : Thunderstorm in vicinity
: Oram-panala : Snow
: Orana : Rain
: Orana malefaka : Light Rain, Rain Shower
: Ranonorana : Rain Shower
: Vongan-zavona : Patches of fog
: Pitipitik'orana : Drizzle
: Pitipitik'orana : Light drizzle
: Oram-panala mitsoka ambany : Low drifting snow
: Oram-panala malefaka : Light rain and snow
: Oram-be manakaiky : Shower in vicinity
: Oram-baratra : Rain with thunderstorm
: Oram-be sy oram-panala : Rain and snow shower
: Ora-mikija : Thunderstorm
: Pitipitik'orana : Drizzle and rain
: Oram-be manavandra : Hail with thunderstorm
: Zavona : Haze
: Zavona arahin'orana : Light drizzle and rain
: Ora-mikija, orana malefaka et avandra / Oram-panala : Light rain and small hail/snow pallets with thunderstorm
113 : Tsara ny andro : Clear
113 : Masoandro mibalika : Sunny
116 : Rakodrahona ampahany : Partly cloudy
119 : Rakodrahona : Cloudy
122 : Manjombona : Overcast
143 : Zavona : Mist
176 : Mety hanorana matevina : Patchy rain possible
179 : Mety hisy oram-panala matevina : Patchy snow possible
182 : Mety hanerika kely : Patchy sleet possible
185 : Mety hisy zavona mandry sy matevina : Patchy freezing drizzle possible
200 : Mety hisy Ora-mikija : Thundery outbreaks possible
227 : Oram-panala : Blowing snow
230 : Tafiotranorampanala : Blizzard
248 : Zavona : Fog
260 : Zavona mangatsiaka : Freezing fog
263 : Zavona matevina : Patchy light drizzle
266 : Zavona malefakaa : Light drizzle
281 : Zavona mandry : Freezing drizzle
284 : Zavona mangatsika mafy : Heavy freezing drizzle
293 : Orana matevina : Patchy light rain
296 : Orana malefaka : Light rain
299 : Orana malefaka miverimberina : Moderate rain at times
302 : Orana malefaka : Moderate rain
305 : Oram-be miverimberina : Heavy rain at times
308 : Oram-be : Heavy rain
311 : Orana mangatsiaka : Light freezing rain
314 : Orana malefaka na mafy mangatsiaka : Moderate or heavy freezing rain
317 : Tafiotranorampanala maivana : Light sleet
320 : Tafiotranorampanala malefaka na mafy : Moderate or heavy sleet
323 : Oram-panala matevina : Patchy light snow
326 : Oram-panala maivana : Light snow
329 : Oram-panala malefaka : Patchy moderate snow
332 : Latsakoram-panala malefaka : Moderate snow
335 : Oram-panala mavesatra sy matevina : Patchy heavy snow
338 : Oram-panala mafy : Heavy snow
350 : Pitipitika ranomandro : Ice pellets
353 : Oram-be malefaka : Light rain shower
356 : Orana antoniny sy mavesatra : Moderate or heavy rain shower
359 : Oram-be mikija : Torrential rain shower
362 : Pitipitik'oram-panala mikija : Light sleet showers
365 : Pitipitik'oram-panala malefaka na mavesatra : Moderate or heavy sleet showers
368 : Pitipitik'oram-panala maivana : Light snow showers
371 : Rotsak'oram-panala maivana sy mavesatra : Moderate or heavy snow showers
386 : Oram-baratra matevina : Patchy light rain with thunder
389 : Oram-baratra malefaka na mavesatra : Moderate or heavy rain with thunder
392 : Rotsak'oram-panala maivana arahim-baratra : Patchy light snow with thunder
395 : Rotsak'oram-panala malefaka na mavesatra arahim-baratra : Moderate or heavy snow with thunder

View file

@ -0,0 +1,70 @@
वापर:
$ curl wttr.in # वर्तमान स्थळाचे हवामान
$ curl wttr.in/muc # म्युनिक विमानतळावरील हवामान
उपलब्धीत/प्रयोज्य स्थळांचे प्रकार:
/paris # शहराचे नाव
/~Eiffel+tower # कोणत्याही स्थळाचे नाव (रिकाम्या ठिकाणी (स्पेस ऐवजी) +)
/Москва # युनिकोड स्वरूपात कोणत्याही भाषेतील कोणत्याही स्थळाचे नाव
/muc # विमातळाचे संकेत (कोड) (३ अक्षरे)
/@stackoverflow.com # संकेतस्थळाचे डोमेन नाव
/94107 # क्षेत्र कोड
/-78.46,106.79 # जीपीएस सहनिर्देशक (रेखांश, अक्षांश)
चंद्राच्या कलेची माहिती:
/moon # चंद्राची कला (विशिष्ट स्थळासाठी +US, +France इत्यादी जोडा)
/moon@2016-10-25 # विशिष्ट दिनी चंद्राची कला (@2016-10-25)
एकक:
m # दशमान (मेट्रिक/SI) (अमेरिका वगळता सर्वत्र वापरली जाते)
u # USCS (अमेरिकेत वापरली जाते)
M # वाऱ्याचा वेग मीटर प्रति सेकंद (m/s) मध्ये दाखवा
दृश्य पर्याय:
0 # केवळ वर्तमान हवामान
1 # वर्तमान + आजचा हवामान अंदाज
2 # वर्तमान + आजचा + उद्याचा हवामान अंदाज
A # (टर्मिनल मध्ये) युसर-एजन्ट दुर्लक्षित करून एएनएसआय (ANSI) स्वरूप वापरा
F # "अनुसरण करा" (फॉलो) ची ओळ अदृश्य करा
n # अरुंद स्वरूप (फक्त दुपार व रात्र)
q # शांत स्वरूप ("हवामान अंदाज" मजकूर अदृश्य)
Q # अतिशांत स्वरूप ("हवामान अंदाज" मजकूर व शहराचे नाव अदृश्य)
T # टर्मिनल सिक्वेन्स(क्रम) बंद (बेरंगीत)
PNG पर्याय:
/paris.png # PNG फाईल निर्माण करा
p # प्रतिमेभोवती चौकट जोडा
t # 150 पारदर्शकता
transparency=... # 0 ते 255 पारदर्शकता (255 = अपारदर्शक)
background=... # RRGGBB (लाल हिरवा निळा) स्वरूपात पार्श्वभूमीचा रंग, उदा. 00aaaa
पर्याय एकत्र करू शकता:
/Paris?0pq
/Paris?0pq&lang=fr
/Paris_0pq.png # PNG फाईलच्या बाबतीत पर्याय अधोरेखे "_" नंतर लिहिले जातात
/Rome_0pq_lang=it.png # लांब पर्याय हे अधोरेखेने विभाजित केले जातात
भाषांतर:
$ curl fr.wttr.in/Paris
$ curl wttr.in/paris?lang=fr
$ curl -H "Accept-Language: fr" wttr.in/paris
उपलब्ध भाषा:
FULL_TRANSLATION (उपलब्ध)
PARTIAL_TRANSLATION (काम चालू)
विशेष दुवे:
/:help # हे पृष्ठ दाखवा
/:bash.function # शिफारस केलेले बॅश "wttr()" कार्य दाखवा
/:translation # भाषांतर करणाऱ्यांची माहिती दाखवा

78
share/translations/mr.txt Normal file
View file

@ -0,0 +1,78 @@
: मुसळधार पाऊस, गारा व झंझावात : Heavy rain and hail with thunderstorm
: मुसळधार पाऊस व झंझावात : Heavy rain with thunderstorm
: हलका पाऊस, गारा व झंझावात : Light rain and hail with thunderstorm
: हलका पाऊस व हिमवर्षाव : Light rain and snow shower
: हलका पाऊस व झंझावात : Light rain with thunderstorm
: हलका हिमवर्षाव : Light snow shower
: आंशिक दाट धुके : Partial fog
: पाऊस, गारा व झंझावात : Rain and hail with thunderstorm
: पाऊस व झंझावात : Rain with thunderstorm
: उथळ दाट धुके : Shallow fog
: धूर : Smoke
: चंडवात : Squalls
: झंझावात जवळपास : Thunderstorm in vicinity
: हिमवर्षाव : Snow
: पाऊस : Rain
: हलका पाऊस, पावसाच्या सरी : Light Rain, Rain Shower
: पावसाच्या सरी : Rain Shower
: तुरळक दाट धुके : Patches of fog
: रिमझिम : Drizzle
: हलका रिमझिम पाऊस : Light drizzle
: कमी वाहणारे बर्फ : Low drifting snow
: हलका पाऊस आणि बर्फ : Light rain and snow
: पावसाची सर जवळपास : Shower in vicinity
: पाऊस व झंझावात : Rain with thunderstorm
: पाऊस व हिमवर्षाव : Rain and snow shower
: झंझावात : Thunderstorm
: रिमझिम व पाऊस : Drizzle and rain
: गारा व झंझावात : Hail with thunderstorm
: विरळ धुके : Haze
: हलके रिमझिम व पाऊस : Light drizzle and rain
: झंझावात सह हलका पाऊस व लहान गारा : Light rain and small hail/snow pallets with thunderstorm
113 : स्वच्छ : Clear
113 : ऊन : Sunny
116 : काहीसे ढगाळ : Partly cloudy
119 : ढगाळ : Cloudy
122 : मळभ : Overcast
143 : धुके : Mist
176 : तुरळक पावसाची शक्यता : Patchy rain possible
179 : तुरळक बर्फाची शक्यता : Patchy snow possible
182 : तुरळक हिमयुक्त पावसाची शक्यता : Patchy sleet possible
185 : तुरळक थंड रिमझिमची शक्यता : Patchy freezing drizzle possible
200 : झंझावाताची शक्यता : Thundery outbreaks possible
227 : वाहणारा बर्फ : Blowing snow
230 : हिमवादळ : Blizzard
248 : दाट धुके : Fog
260 : थंड दाट धुके : Freezing fog
263 : तुरळक रिमझिम : Patchy light drizzle
266 : हलके रिमझिम : Light drizzle
281 : थंड रिमझिम : Freezing drizzle
284 : अतिथंड रिमझिम : Heavy freezing drizzle
293 : तुरळक हलका पाऊस : Patchy light rain
296 : हलका पाऊस : Light rain
299 : अधूनमधून हलका पाऊस : Moderate rain at times
302 : मध्यम पाऊस : Moderate rain
305 : अधूनमधून मुसळधार पाऊस : Heavy rain at times
308 : मुसळधार पाऊस : Heavy rain
311 : हलका थंड पाऊस : Light freezing rain
314 : मध्यम ते अतिथंड पाऊस : Moderate or heavy freezing rain
317 : हलका हिमयुक्त पाऊस : Light sleet
320 : मध्यम ते अतिथंड हिमयुक्त पाऊस : Moderate or heavy sleet
323 : तुरळक हलका हिमवर्षाव : Patchy light snow
326 : हलका हिमवर्षाव : Light snow
329 : तुरळक मध्यम हिमवर्षाव : Patchy moderate snow
332 : मध्यम हिमवर्षाव : Moderate snow
335 : तुरळक जोरदार हिमवर्षाव : Patchy heavy snow
338 : जोरदार हिमवर्षाव : Heavy snow
350 : मऊ गारा : Ice pellets
353 : पावसाच्या हलक्या सरी : Light rain shower
356 : पावसाच्या मध्यम ते जोरात सरी : Moderate or heavy rain shower
359 : मुसळधार पावसाच्या सरी : Torrential rain shower
362 : हिमयुक्त पावसाच्या हलक्या सरी : Light sleet showers
365 : हिमयुक्त पावसाच्या मध्यम ते जोरात सरी : Moderate or heavy sleet showers
368 : बर्फाच्या हलक्या सरी : Light snow showers
371 : बर्फाच्या मध्यम ते जोरात सरी : Moderate or heavy snow showers
386 : तुरळक हलका पाऊस व गडगडाट : Patchy light rain with thunder
389 : मध्यम ते जोरात पाऊस व गडगडाट : Moderate or heavy rain with thunder
392 : तुरळक हलका बर्फ व गडगडाट : Patchy light snow with thunder
395 : मध्यम ते जोरात बर्फ व गडगडाट : Moderate or heavy snow with thunder

View file

@ -1,45 +1,55 @@
Gebruik:
$ curl wttr.in # huidige locatie
$ curl wttr.in/muc # het weer in Munic's vliegveld
$ curl wttr.in/muc # weer op de luchthaven van München
Ondersteunde locatie soorten:
Ondersteunde locatiesoorten:
/paris # stadsnaam
/~Eiffel+tower # elke locatie
/Москва # Unicode naam van elke locatie in elke taal
/muc # vliegveld codes (3 letters)
/~Eiffel+tower # elke locatie (+ for spaties)
/Москва # Unicodenaam van elke locatie in elke taal
/muc # vliegveldcode (3 letters)
/@stackoverflow.com # domeinnaam
/94107 # gebieds code
/-78.46,106.76 # GPS coördinaten
/94107 # gebiedscode
/-78.46,106.76 # GPS-coördinaten
Specialen locaties:
Maanstand informatie:
/moon # Maanstand (voeg ,+US or ,+France voor deze plekken)
/moon # Maanstand (voeg ,+US of ,+France toe voor deze plekken)
/moon@2016-10-25 # Maanstand op deze datum (@2016-10-25)
Eenheden:
?m # metriek (SI) (gebruikt als standaard overal behalve US)
?u # USCS (standaard in US)
?M # laat wind snelheid in m/s zien
m # metriek (SI) (overal gebruikt als standaard behalve in US)
u # USCS (standaard in US)
M # laat windsnelheid in m/s zien
Beeld opties:
?0 # alleen huidig weer
?1 # huidig weer + 1 dag
?2 # huidig weer + 2 dag
?n # smalle versie (alleen dag en nacht)
?q # stille versie (geen "Weerbericht" tekst)
?Q # superstille versie (geen " Weerbericht", geen stadsnaam)
?T # wissel terminal volgorde off (geen kleur)
0 # alleen huidig weer
1 # huidig weer + verwachting voor vandaag
2 # huidig weer + verwachting voor vandaag en morgen
A # negeer User-Agent en forceer ANSI uitvoerformaat (terminal)
F # Toon niet de "Follow" regel
n # smalle versie (alleen dag en nacht)
q # stille versie (geen "Weerbericht" tekst)
Q # superstille versie (geen "Weerbericht", geen stadsnaam)
T # schakel terminalcodes uit (geen kleur)
PNG opties:
/paris.png # genereerd een PNG bestand
?p # voegt een frame rond de output
/paris.png # genereert een PNG-bestand
?p # voegt een rand toe rond de uitvoer
?t # transparantie 150
transparency=... # transparantie van 0 to 255 (255 is niet doorzichtig)
transparency=... # transparantie van 0 to 255 (255 is ondoorzichtig)
background=... # achtergrondkleur in formaat RRGGBB, bijv. 00aaaa
Opties kunnen worden gecombineerd:
/Paris?0pq
/Paris?0pq&lang=fr
/Paris_0pq.png # in PNG het bestandstype specificeren achter _
/Rome_0pq_lang=it.png # lange opties worden gescheiden met _
Lokalisatie:
@ -49,12 +59,12 @@ Lokalisatie:
Ondersteunde talen:
de fr id it nb ru (ondersteund)
ar az be bg bs ca cy cs da el eo es et fi hi hr hu hy is ja jv ka kk ko ky lt lv mk ml nl nn pt pl ro sk sl sr sr-lat sv sw th tr uk uz vi zh zu he (mee bezig)
FULL_TRANSLATION (supported)
PARTIAL_TRANSLATION (in progress)
Speciale URLs:
/:help # laat help pagina zien
/:bash.function # laat voorgestelde wttr() functie zien voor in bash
/:translation # laat the informatie van de vertalers zien
/:help # toon helppagina
/:bash.function # toon voorgestelde wttr() bash-functie
/:translation # toon informatie van de vertalers

View file

@ -1,48 +1,79 @@
113: Helder : Clear
113: Zonnig : Sunny
116: Deels bewolkt : Partly cloudy
119: Bewolkt : Cloudy
122: Bewolkt : Overcast
143: Mist : Mist
176: Mogelijk plaatselijk regen : Patchy rain possible
179: Mogelijk plaatselijk sneeuw : Patchy snow possible
182: Mogelijk plaatselijk hagel : Patchy sleet possible
185: Mogelijk plaatselijk ijzel : Patchy freezing drizzle possible
200: Mogelijk een onweersbui : Thundery outbreaks possible
227: Sneeuw winden : Blowing snow
230: Sneeuwstorm : Blizzard
248: Mist : Fog
260: Rijp : Freezing fog
263: Plaatselijk motregen : Patchy light drizzle
266: Motregen : Light drizzle
281: Ijzel : Freezing drizzle
284: Zware ijzel : Heavy freezing drizzle
293: Plaatselijk lichte regen : Patchy light rain
296: Lichte regen : Light rain
299: Af en toe regen : Moderate rain at times
302: Regen : Moderate rain
305: Af en toe zware regen : Heavy rain at times
308: Zware regen : Heavy rain
311: Lichte ijzel : Light freezing rain
314: Zware ijzel : Moderate or heavy freezing rain
317: Lichte hagel : Light sleet
320: Zware hagel : Moderate or heavy sleet
323: Plaatselijk lichte sneeuw : Patchy light snow
326: Lichte sneeuw : Light snow
329: Plaatselijke sneeuw : Patchy moderate snow
332: Sneeuw : Moderate snow
335: Plaatselijk zware sneeuw : Patchy heavy snow
338: Zware sneeuw : Heavy snow
350: Hagel : Ice pellets
353: Lichte regenbuien : Light rain shower
356: Regenbuien : Moderate or heavy rain shower
359: Extreem zware regenbuien : Torrential rain shower
362: Lichte hagelbuien : Light sleet showers
365: Hagelbuien : Moderate or heavy sleet showers
368: Lichte sneeuwbuien : Light snow showers
371: Sneeuwbuien : Moderate or heavy snow showers
386: Plaatselijk lichte regen met on: Patchy light rain with thunder weer
389: Regen met onweer : Moderate or heavy rain with thunder
392: Plaatselijk lichte sneeuw met o: Patchy light snow with thunder nweer
395: Sneeuw met onweer : Moderate or heavy snow with thunder
: Zware regen en hagel met onweer : Heavy rain and hail with thunderstorm
: Zware regen met onweer : Heavy rain with thunderstorm
: Lichte regen en hagel met onweer : Light rain and hail with thunderstorm
: Lichte regen en sneeuwbui : Light rain and snow shower
: Lichte regen met onweer : Light rain with thunderstorm
: Lichte sneeuwbui : Light snow shower
: Plaatselijke mist : Partial fog
: Regen en hagel met onweer : Rain and hail with thunderstorm
: Regen met onweer : Rain with thunderstorm
: Lichte mist : Shallow fog
: Nevel : Smoke
: Windstoten : Squalls
: Onweer in de buurt : Thunderstorm in vicinity
: Sneeuw : Snow
: Regen : Rain
: Lichte regen, regenbuien : Light Rain, Rain Shower
: Regenbuien : Rain Shower
: Plaatselijke mist : Patches of fog
: Motregen : Drizzle
: Lichte motregen : Light drizzle
: Laag stuifsneeuw : Low drifting snow
: Lichte regen en sneeuw : Light rain and snow
: Buien in de buurt : Shower in vicinity
: Regen met onweer : Rain with thunderstorm
: Regen en sneeuwbuien : Rain and snow shower
: Onweer : Thunderstorm
: Motregen en regen : Drizzle and rain
: Hagel met onweer : Hail with thunderstorm
: Nevel : Haze
: Lichte motregen en regen : Light drizzle and rain
: Lichte regen en korrelhagel/sneeuwpallets met onweer : Light rain and small hail/snow pallets with thunderstorm
113: Helder : Clear
113: Zonnig : Sunny
116: Deels bewolkt : Partly cloudy
119: Bewolkt : Cloudy
122: Geheel bewolkt : Overcast
143: Mist : Mist
176: Mogelijk plaatselijk regen : Patchy rain possible
179: Mogelijk plaatselijk sneeuw : Patchy snow possible
182: Mogelijk plaatselijk hagel : Patchy sleet possible
185: Mogelijk plaatselijk ijzel : Patchy freezing drizzle possible
200: Mogelijk een onweersbui : Thundery outbreaks possible
227: Sneeuwwinden : Blowing snow
230: Sneeuwstorm : Blizzard
248: Mist : Fog
260: Rijp : Freezing fog
263: Plaatselijk motregen : Patchy light drizzle
266: Motregen : Light drizzle
281: Ijzel : Freezing drizzle
284: Zware ijzel : Heavy freezing drizzle
293: Plaatselijk lichte regen : Patchy light rain
296: Lichte regen : Light rain
299: Af en toe regen : Moderate rain at times
302: Regen : Moderate rain
305: Af en toe zware regen : Heavy rain at times
308: Zware regen : Heavy rain
311: Lichte ijzel : Light freezing rain
314: Zware ijzel : Moderate or heavy freezing rain
317: Lichte hagel : Light sleet
320: Zware hagel : Moderate or heavy sleet
323: Plaatselijk lichte sneeuw : Patchy light snow
326: Lichte sneeuw : Light snow
329: Plaatselijke sneeuw : Patchy moderate snow
332: Sneeuw : Moderate snow
335: Plaatselijk zware sneeuw : Patchy heavy snow
338: Zware sneeuw : Heavy snow
350: Hagel : Ice pellets
353: Lichte regenbuien : Light rain shower
356: Regenbuien : Moderate or heavy rain shower
359: Extreem zware regenbuien : Torrential rain shower
362: Lichte hagelbuien : Light sleet showers
365: Hagelbuien : Moderate or heavy sleet showers
368: Lichte sneeuwbuien : Light snow showers
371: Sneeuwbuien : Moderate or heavy snow showers
386: Plaatselijk lichte regen met onweeer : Patchy light rain with thunder
389: Regen met onweer : Moderate or heavy rain with thunder
392: Plaatselijk lichte sneeuw met onweer : Patchy light snow with thunder
395: Sneeuw met onweer : Moderate or heavy snow with thunder

View file

@ -29,6 +29,8 @@ Ustawienia wyświetlania:
?0 # Pokaż jedynie aktualną pogodę
?1 # Pokaż pogodę na jutro
?2 # Pokaż pogodę na pojutrze
?A # Zignoruj User-Agent i wymuś format wyjścia ANSI (terminal)
?F # nie pokazuj linii "Subskrybuj"
?n # Wersja kompaktowa (tylko noc i dzień)
?q # Wersja okrojona (bez tekstu 'Pogoda w')
?Q # Wersja bardziej okrojona (bez tekstu 'Pogoda w' i nazwy miasta)
@ -36,10 +38,11 @@ Ustawienia wyświetlania:
Opcje PNG:
/paris.png # generuje plik PNG
?p # dodaje obramowanie do obrazka
/paris.png # Generuje plik PNG
?p # Dodaje obramowanie do obrazka
?t # Przezroczystość 150
transparency=... # Przezroczystość między 0 a 255 (255 = brak przezroczystości)
background=... # Kolor tła w formie RRGGBB
Opcje mogą być ze sobą łączone:

View file

@ -3,24 +3,24 @@ Instruções:
$ curl wttr.in # o tempo na sua localização atual
$ curl wttr.in/muc # o tempo no aeroporto de Munique
Tipos de localização suportados:
Tipos de locais suportados:
/paris # o nome de uma cidade
/~Eiffel+tower # o nome de qualquer lugar famoso
/Москва # nome Unicode de qualquer lugar em qualquer idioma
/~Eiffel+tower # o nome de um lugar famoso
/Москва # o nome Unicode de qualquer lugar em qualquer idioma
/muc # o código de um aeroporto (3 letras)
/@stackoverflow.com # o nome de um domínio web
/94107 # um código de área
/-78.46,106.79 # coordenadas do GPS
/-78.46,106.79 # as coordenadas do GPS de um lugar
Lugares especiais:
/moon # A fase da lua (crescente ,+US ou ,+France para estas cidades)
/moon@2016-10-25 # A fase da lua em uma determinada data (@2016-10-25)
/moon # a fase da ua (crescente ,+US ou ,+France para estas cidades)
/moon@2016-10-25 # a fase da Lua em uma determinada data (@2016-10-25)
Unidades:
?m # métricas (SI) (o padrão em todos os lugares exceto nos EUA)
?m # Sistema Internacional de Unidades (SI) (o padrão em todos os lugares exceto nos EUA)
?u # Sistema Unificado de Clasificaçāo de Solo ou USCS (o padrão nos EUA)
?M # mostrar a velocidade do vento em m/s
@ -48,13 +48,13 @@ As opções podem ser usadas em conjunto:
/Paris_0pq.png # em PNG as opções se especificam depois do caracter _
/Rome_0pq_lang=it.png # uma longa sequência de opções podem ser separadas pelo caracter _
Localizaçāo:
Tradução:
$ curl fr.wttr.in/Paris
$ curl wttr.in/paris?lang=fr
$ curl -H "Accept-Language: fr" wttr.in/paris
Línguas suportadas:
Idiomas suportadas:
FULL_TRANSLATION (suportadas)
PARTIAL_TRANSLATION (em andamento)
@ -63,5 +63,4 @@ URLs especiais:
/:help # mostra esta página
/:bash.function # sugere uma função wttr() em bash
/:translation # mostra informação a respeito dos tradutores
/:translation # mostra informações a respeito dos tradutores

View file

@ -17,8 +17,8 @@
266: Chuvisco : Light drizzle
281: Chuvisco gelado : Freezing drizzle
284: Chuvisco muito gelado : Heavy freezing drizzle
293: Garoa irregular : Patchy light rain
296: Garoa : Light rain
293: Chuvisco irregular : Patchy light rain
296: Chuvisco : Light rain
299: Chuva moderada ocasional : Moderate rain at times
302: Chuva moderada : Moderate rain
305: Chuva forte ocasional : Heavy rain at times
@ -34,48 +34,14 @@
335: Neve forte irregular : Patchy heavy snow
338: Neve forte : Heavy snow
350: Pelotas de gelo : Ice pellets
353: Chuveiro de garoa : Light rain shower
356: Chuveiro de chuva moderada ou forte : Moderate or heavy rain shower
359: Chuveiro de chuva torrencial : Torrential rain shower
362: Chuveiro de granizo fraco : Light sleet showers
365: Chuveiro de granizo moderada ou forte : Moderate or heavy sleet showers
368: Chuveiro de neve fraca : Light snow showers
371: Chuveiro de neve moderada ou forte : Moderate or heavy snow showers
353: Chuva fraca : Light rain shower
356: Chuva moderada ou forte : Moderate or heavy rain shower
359: Chuva torrencial : Torrential rain shower
362: Chuva de granizo fraco : Light sleet showers
365: Chuva de granizo moderada ou forte : Moderate or heavy sleet showers
368: Chuva com neve fraca : Light snow showers
371: Chuva com neve moderada ou forte : Moderate or heavy snow showers
386: Tempestate com garoa irregular : Patchy light rain with thunder
389: Tempestade com chuva moderada ou forte : Moderate or heavy rain with thunder
392: Tempestade com neve fraca : Patchy light snow with thunder
395: Tempestade com neve moderada ou forte : Moderate or heavy snow with thunder

View file

@ -1,58 +1,76 @@
: Снегопад : Low drifting snow :
: Местами туман : Patches of fog :
: Морось : Drizzle :
: Лёгкая морось : Light drizzle :
: Снег : Snow :
: Стелющийся туман : Shallow fog :
: Ледяной дождь : Freezing rain :
: Проливной дождь : Rain shower :
: Лёгкий снег с дождём : Light rain and snow :
: Снег с дождём : Snow shower :
: Сильный снег с дождём : Heave snow shower :
113: Ясно : Clear :
113: Солнечно : Sunny :
116: Переменная облачность : Partly cloudy :
119: Облачно : Cloudy :
122: Пасмурно : Overcast :
143: Дымка : Mist :
176: Местами дождь : Patchy rain possible :
179: Местами снег : Patchy snow possible :
182: Местами дождь со снегом : Patchy sleet possible :
185: Местами замерзающая морось : Patchy freezing drizzle possible :
200: Местами грозы : Thundery outbreaks possible :
227: Позёмок : Blowing snow :
230: Метель : Blizzard :
248: Туман : Fog :
260: Переохлажденный туман : Freezing fog :
263: Местами слабая морось : Patchy light drizzle :
266: Слабая морось : Light drizzle :
281: Замерзающая морось : Freezing drizzle :
284: Сильная замерзающая морось : Heavy freezing drizzle :
293: Местами небольшой дождь : Patchy light rain :
296: Небольшой дождь : Light rain :
299: Временами умеренный дождь : Moderate rain at times :
302: Умеренный дождь : Moderate rain :
305: Временами сильный дождь : Heavy rain at times :
308: Сильный дождь : Heavy rain :
311: Слабый переохлажденный дождь : Light freezing rain :
314: Умеренный или сильный переохлажденный дождь : Moderate or heavy freezing rain :
317: Небольшой дождь со снегом : Light sleet :
320: Умеренный или сильный дождь со снегом : Moderate or heavy sleet :
323: Местами небольшой снег : Patchy light snow :
326: Небольшой снег : Light snow :
329: Местами умеренный снег : Patchy moderate snow :
332: Умеренный снег : Moderate snow :
335: Местами сильный снег : Patchy heavy snow :
338: Сильный снег : Heavy snow :
350: Ледяной дождь : Ice pellets :
353: Небольшой ливневый дождь : Light rain shower :
356: Умеренный или сильный ливневый дождь : Moderate or heavy rain shower :
359: Очень сильный ливень : Torrential rain shower :
362: Небольшой ливневый дождь со снегом : Light sleet showers :
365: Небольшой снег : Moderate or heavy sleet showers :
368: Небольшой ливневый дождь со снегом : Light snow showers :
371: Умеренный или сильный снежный ливень : Moderate or heavy snow showers :
386: Местами небольшой дождь с грозой : Patchy light rain with thunder :
389: Умеренный или сильный дождь с грозой : Moderate or heavy rain with thunder :
392: Местами небольшой снег с грозой : Patchy light snow with thunder :
395: Умеренный или сильный снег с грозой : Moderate or heavy snow with thunder :
: Позёмок : Low drifting snow :
: Местами туман : Patches of fog :
: Морось : Drizzle :
: Лёгкая морось : Light drizzle :
: Снег : Snow :
: Стелющийся туман : Shallow fog :
: Ледяной дождь : Freezing rain :
: Проливной дождь : Rain shower :
: Лёгкий снег с дождём : Light rain and snow :
: Снег : Snow shower :
: Сильный снег : Heavy snow shower :
: Дымка : Haze :
: Снег с ливнем : Heavy rain and snow shower :
: Снег с ливнем и грозой : Heavy rain and snow with thunderstorm :
: Ливень с грозой : Heavy rain with thunderstorm :
: Морось с грозой и градом : Light rain and hail with thunderstorm :
: Сильный снег с моросью : Light rain and snow shower :
: Морось с грозой : Light rain with thunderstorm :
: Небольшой снег : Light snow shower :
: Местами туман : Partial fog :
: Дождь с грозой и градом : Rain and hail with thunderstorm :
: Дождь с грозой и несильным градом : Rain and small hail/snow pallets with thunderstorm :
: Снег с дождём : Rain and snow shower :
: Дождь с грозой : Rain with thunderstorm :
: Гроза поблизости : Shower in vicinity :
: Дымка : Smoke :
: Шквальный ветер : Squalls :
: Гроза : Thunderstorm :
: Дождь поблизости : Thunderstorm in vicinity :
113: Ясно : Clear :
113: Солнечно : Sunny :
116: Переменная облачность : Partly cloudy :
119: Облачно : Cloudy :
122: Пасмурно : Overcast :
143: Дымка : Mist :
176: Местами дождь : Patchy rain possible :
179: Местами снег : Patchy snow possible :
182: Местами дождь со снегом : Patchy sleet possible :
185: Местами замерзающая морось : Patchy freezing drizzle possible :
200: Местами грозы : Thundery outbreaks possible :
227: Позёмок : Blowing snow :
230: Метель : Blizzard :
248: Туман : Fog :
260: Переохлаждённый туман : Freezing fog :
263: Местами слабая морось : Patchy light drizzle :
266: Слабая морось : Light drizzle :
281: Замерзающая морось : Freezing drizzle :
284: Сильная замерзающая морось : Heavy freezing drizzle :
293: Местами небольшой дождь : Patchy light rain :
296: Небольшой дождь : Light rain :
299: Временами умеренный дождь : Moderate rain at times :
302: Умеренный дождь : Moderate rain :
305: Временами сильный дождь : Heavy rain at times :
308: Сильный дождь : Heavy rain :
311: Слабый переохлаждённый дождь : Light freezing rain :
314: Умеренный или сильный переохлаждённый дождь : Moderate or heavy freezing rain :
317: Небольшой дождь со снегом : Light sleet :
320: Умеренный или сильный дождь со снегом : Moderate or heavy sleet :
323: Местами небольшой снег : Patchy light snow :
326: Небольшой снег : Light snow :
329: Местами умеренный снег : Patchy moderate snow :
332: Умеренный снег : Moderate snow :
335: Местами сильный снег : Patchy heavy snow :
338: Сильный снег : Heavy snow :
350: Ледяной дождь : Ice pellets :
353: Небольшой ливневый дождь : Light rain shower :
356: Умеренный или сильный ливневый дождь : Moderate or heavy rain shower :
359: Очень сильный ливень : Torrential rain shower :
362: Небольшой ливневый дождь со снегом : Light sleet showers :
365: Небольшой снег : Moderate or heavy sleet showers :
368: Небольшой ливневый дождь со снегом : Light snow showers :
371: Умеренный или сильный снежный ливень : Moderate or heavy snow showers :
386: Местами небольшой дождь с грозой : Patchy light rain with thunder :
389: Умеренный или сильный дождь с грозой : Moderate or heavy rain with thunder :
392: Местами небольшой снег с грозой : Patchy light snow with thunder :
395: Умеренный или сильный снег с грозой : Moderate or heavy snow with thunder :

View file

@ -0,0 +1,66 @@
பயன்பாடு:
$ curl wttr.in # தற்போதைய இடம்
$ curl wttr.in/cdg # பாரிஸ் - சார்லஸ் டி கோல் விமான நிலையத்தில் வானிலை முன்னறிவிப்பு
ஏற்றுக்கொள்ளப்பட்ட வகைகள்:
/paris # நகரத்தின் பெயர்
/~Eiffel+tower # எந்த இடம்
/Москва # யூனிகோட் பெயர் அல்லது எந்த மொழியிலும் எந்த இடம்
/muc # விமான நிலைய குறியீடு (3 எழுத்துகள்)
/@stackoverflow.com # டொமைன் பெயர்
/94107 # அஞ்சல் குறியீடு (அமெரிக்காவில் மட்டும்)
/-78.46,106.79 # ஜிபிஎஸ் ஒருங்கிணைப்புகள்
சிறப்பு வகைகள்:
/moon # சந்திரனின் கட்டங்கள் (அதே பெயரில் உள்ள நகரங்களை அணுக, + அமெரிக்கா அல்லது + பிரான்ஸ் சேர்க்கவும்)
/moon@2016-10-25 # இந்த தேதிக்கான சந்திரனின் கட்டங்கள் (@2016-10-25)
அலகுகள்:
?m # மெட்ரிக் அமைப்பு (அமெரிக்காவைத் தவிர எல்லா இடங்களிலும் இயல்புநிலை)
?u # USCS (அமெரிக்க ஐக்கிய நாடுகளுக்கு இயல்புநிலை)
?M # காற்றின் வேகத்தை m/s இல் காட்டுகிறது
காட்சி விருப்பம்:
?0 # இன்று மட்டும்
?1 # இன்று + நாளை
?2 # இன்று + 2 நாட்கள்
?n # குறுகிய பதிப்பு (பகல் மற்றும் இரவு மட்டும்)
?q # சைலண்ட் பதிப்பு ("தலைப்புக்கான வானிலை முன்னறிவிப்பு" இல்லை)
?Q # சூப்பர்-சைலண்ட் பதிப்பு ("வானிலை முன்னறிவிப்பு" தலைப்பு இல்லை, நகரத்தின் பெயர் இல்லை)
?T # முடக்கப்பட்ட டெர்மினல்களுக்கான தப்பிக்கும் காட்சிகள் (வண்ணங்கள் இல்லை)
PNG விருப்பங்கள்:
/paris.png # ஒரு PNG கோப்பை உருவாக்கவும்
?p # வெளியீட்டைச் சுற்றி ஒரு சட்டத்தைச் சேர்க்கவும்
?t # வெளிப்படைத்தன்மை 150
transparency=... # 0 முதல் 255 வரை வெளிப்படைத்தன்மை (255 = வெளிப்படைத்தன்மை இல்லை)
விருப்பங்களை இணைக்கவும்:
/Paris?0pq
/Paris?0pq&lang=fr
/Paris_0pq.png # PNG பயன்முறையில் விருப்பங்கள் _ க்குப் பிறகு குறிப்பிடப்படுகின்றன
/Rome_0pq_lang=it.png # நீண்ட விருப்பங்கள் அடிக்கோடிட்டால் பிரிக்கப்படுகின்றன _
இடம்:
$ curl fr.wttr.in/Paris
$ curl wttr.in/paris?lang=fr
$ curl -H "Accept-Language: fr" wttr.in/paris
ஆதரிக்கப்படும் மொழிகள்:
FULL_TRANSLATION (முழு மொழிபெயர்ப்பு)
PARTIAL_TRANSLATION (முழுமையற்ற மொழிபெயர்ப்பு)
URLs குறிப்பாக:
/:help # இந்தப் பக்கத்தைக் காட்டவும்
/:bash.function # பரிந்துரைக்கப்பட்ட bash செயல்பாடு wttr()
/:translation # மொழிபெயர்ப்பு பற்றிய தகவலைக் காட்டுகிறது wttr.in

78
share/translations/ta.txt Normal file
View file

@ -0,0 +1,78 @@
: இடியுடன் கூடிய கனமழை மற்றும் ஆலங்கட்டி மழை : Heavy rain and hail with thunderstorm
: இடியுடன் கூடிய கனமழை : Heavy rain with thunderstorm
: லேசான மழை மற்றும் இடியுடன் கூடிய ஆலங்கட்டி மழை : Light rain and hail with thunderstorm
: லேசான மழை மற்றும் பனி மழை : Light rain and snow shower
: லேசான மழை மற்றும் இடியுடன் கூடிய மழை : Light rain with thunderstorm
: லேசான பனி மழை : Light snow shower
: பகுதி மூடுபனி : Partial fog
: இடியுடன் கூடிய ஆலங்கட்டி மழை : Rain and hail with thunderstorm
: இடியுடன் கூடிய மழை : Rain with thunderstorm
: லேசான மூடுபனி : Shallow fog
: புகை : Smoke
: பலமான காற்று : Squalls
: இடியுடன் கூடிய மழை அருகில் : Thunderstorm in vicinity
: பனி : Snow
: மழை : Rain
: லேசான மழை, மழை பொழிவு : Light Rain, Rain Shower
: மழை பொழிவு : Rain Shower
: மூடுபனி திட்டுகள் : Patches of fog
: தூறல் : Drizzle
: லேசான தூறல் : Light drizzle
: குறைந்த பனிப்பொழிவு : Low drifting snow
: லேசான மழை மற்றும் பனி : Light rain and snow
: அருகாமையில் மழை : Shower in vicinity
: இடியுடன் கூடிய மழை : Rain with thunderstorm
: மழை மற்றும் பனி மழை : Rain and snow shower
: இடியுடன் கூடிய மழை : Thunderstorm
: தூறல் மற்றும் மழை : Drizzle and rain
: இடியுடன் கூடிய ஆலங்கட்டி மழை : Hail with thunderstorm
: மூட்டம் : Haze
: லேசான தூறல் மற்றும் மழை : Light drizzle and rain
: லேசான தூறல் மற்றும் இடியுடன் கூடிய சிறிய ஆலங்கட்டி மழை/பனிப் பலகைகள் : Light rain and small hail/snow pallets with thunderstorm
113 : தெளிந்த வானம் : Clear
113 : வெயில் : Sunny
116 : ஒரளவு மேகமூட்டம் : Partly cloudy
119 : மேகமூட்டம் : Cloudy
122 : முற்றிலும் மேகமூட்டம் : Overcast
143 : பனி மூட்டம் : Mist
176 : சீரற்ற மழை சாத்தியம் : Patchy rain possible
179 : சீரற்ற பனி சாத்தியம் : Patchy snow possible
182 : பனிப்பொழிவு சாத்தியம் : Patchy sleet possible
185 : உறைபனி தூறல் சாத்தியம் : Patchy freezing drizzle possible
200 : இடியுடன் கூடிய மழை சாத்தியமாகும் : Thundery outbreaks possible
227 : வீசும் பனி : Blowing snow
230 : பனிப்புயல் : Blizzard
248 : மூடுபனி : Fog
260 : உறைபனி மூடுபனி : Freezing fog
263 : மெல்லிய தூறல் : Patchy light drizzle
266 : லேசான தூறல் : Light drizzle
281 : உறையும் தூறல் : Freezing drizzle
284 : கடும் உறைபனி தூறல் : Heavy freezing drizzle
293 : சீரற்ற லேசான மழை : Patchy light rain
296 : லேசான மழை : Light rain
299 : அவ்வப்போது மிதமான மழை பெய்யும் : Moderate rain at times
302 : மிதமான மழை : Moderate rain
305 : அவ்வப்போது பலத்த மழை : Heavy rain at times
308 : பலத்த மழை : Heavy rain
311 : லேசான உறைபனி மழை : Light freezing rain
314 : மிதமான அல்லது கடுமையான உறைபனி மழை : Moderate or heavy freezing rain
317 : லேசான தூறல் : Light sleet
320 : மிதமான அல்லது கடுமையான தூறல் : Moderate or heavy sleet
323 : சீரற்ற லேசான பனி : Patchy light snow
326 : லேசான பனி : Light snow
329 : சீரற்ற மிதமான பனி : Patchy moderate snow
332 : மிதமான பனி : Moderate snow
335 : சீரற்ற கடுமையான பனி : Patchy heavy snow
338 : கடுமையான பனி : Heavy snow
350 : பனி துகள்கள் : Ice pellets
353 : லேசான சாரல் மழை : Light rain shower
356 : மிதமான அல்லது கடுமையான சாரல் மழை : Moderate or heavy rain shower
359 : சாரல் மழை : Torrential rain shower
362 : லேசான தூறல் மழை : Light sleet showers
365 : மிதமான அல்லது கனத்த தூறல் மழை பெய்யும் : Moderate or heavy sleet showers
368 : லேசான பனி மழை : Light snow showers
371 : மிதமான அல்லது கனத்த பனி மழை : Moderate or heavy snow showers
386 : சீரற்ற இடியுடன் கூடிய லேசான மழை : Patchy light rain with thunder
389 : இடியுடன் கூடிய மிதமான அல்லது பலத்த மழை : Moderate or heavy rain with thunder
392 : சீரற்ற இடியுடன் கூடிய லேசான பனி : Patchy light snow with thunder
395 : மிதமான அல்லது கனத்த இடியுடன் கூடிய பனி : Moderate or heavy snow with thunder

View file

@ -0,0 +1,70 @@
వాడుకొనుట:
$ curl wttr.in # ప్రస్తుత స్థానం
$ curl wttr.in/muc # మునిక్ విమానాశ్రయంలో వాతావరణం
నిర్వహించబడిన స్థాన రకాలు:
/paris # పట్టనం పేరు
/~Eiffel+tower # ఏదైనా ప్రదేశం (+ స్పేస్ కోసం)
/Москва # ఏ భాషలోనైనా ఏదైనా స్థానం యొక్క యూనికోడ్ పేరు
/muc # విమానాశ్రయం కోడ్ (3 అక్షరాలు)
/@stackoverflow.com # డొమైన్ పేరు
/94107 # ప్రాంతం సంకేతాలు (ఏరియా కోడ్‌లు)
/-78.46,106.79 # జిపియస్ కోఆర్డినేట్‌లు
చంద్రుని దశ సమాచారం:
/moon # చంద్రుని దశ (,+US లేదా ,+France జోడించు ఈ నగరాల కోసం)
/moon@2016-10-25 # తేదీకి చంద్ర దశ (@2016-10-25)
యూనిట్లు:
m # మెట్రిక్ పద్ధతి
u # USCS (అమెరికా సంయుక్త రాష్ట్రాల్లో అప్రమేయంగా ఉపయోగించబడుతుంది)
M # గాలి వేగం m/sలో
ఎంపికలను వీక్షించండి:
0 # ప్రస్తుత వాతావరణం మాత్రమే
1 # ప్రస్తుత వాతావరణం + నేటి సూచన
2 # ప్రస్తుత వాతావరణం + నేటి మరియు రేపటి సూచన
A # ANSI ఫార్మాట్‌లో అవుట్‌పుట్
F # "ఫాలో" లైన్ చూపించవద్దు
n # చిన్న సంస్కరణ (పగలు మరియు రాత్రి మాత్రమే)
q # చాలా చిన్న సంస్కరణ ("వాతావరణ నివేదిక" వచనం లేదు)
Q # అతి చిన్న సంస్కరణ ("వాతావరణ నివేదిక" లేదు, నగరం పేరు లేదు)
T # క్లోజ్ టెర్మినల్ సీక్వెన్స్ (రంగులు లేకుండా)
PNG ఎంపికలు:
/paris.png # PNG ఫైల్‌ను రూపొందించండి
p # అవుట్‌పుట్ చుట్టూ ఫ్రేమ్‌ని జోడించండి
t # పారదర్శకత 150
transparency=... # పారదర్శకత 0 నుండి 255 వరకు (255 = పారదర్శకం కాదు)
background=... # నేపథ్య రంగు RRGGBB రూపంలో ఉదాహరణకు 00aaaa
ఎంపికలు కలపవచ్చు:
/Paris?0pq
/Paris?0pq&lang=fr
/Paris_0pq.png # PNGలో (ఫైల్ మోడ్‌ను _ తర్వాత పేర్కొనవచ్చు)
/Rome_0pq_lang=it.png # పొడవైన ఎంపికలను అండర్‌స్కోర్‌తో వేరు చేయవచ్చు
స్థానికీకరణ:
$ curl fr.wttr.in/Paris
$ curl wttr.in/paris?lang=fr
$ curl -H "Accept-Language: fr" wttr.in/paris
నిర్వహించబడిన భాషలు:
am ar af be bn ca da de el et fr fa hi hu ia id it lt mg nb nl oc pl pt-br ro ru ta tr th uk vi zh-cn zh-tw (supported)
az bg bs cy cs eo es eu fi ga hi hr hy is ja jv ka kk ko ky lv mk ml mr nl fy nn pt pt-br sk sl sr sr-lat sv sw te uz zh zu he (in progress)
ప్రత్యేక URLs:
/:help # ఈ పేజీని చూపుతుంది
/:bash.function # సిఫార్సు చేయబడిన bash ఫంక్షన్‌ను wttr() చూపుతుంది
/:translation # అనువాదకుల గురించిన సమాచారాన్ని చూపుతుంది

View file

@ -3,45 +3,46 @@ Komut Satırı Kullanımı:
$ curl wttr.in # bulunduğunuz konum
$ curl wttr.in/esb # Esenboğa havalimanında hava durumu
Desteklenen konum tipleri:
Desteklenen konum türleri:
/istanbul # şehir adı
/~Anıtkabir # herhangi bir konum
/Москва # Herhangi bir dilde herhangi bir unicode konum
/~Anıtkabir # herhangi bir konum (boşluk için + kullanın)
/Москва # herhangi bir dildeki herhangi bir konumun Unicode adı
/esb # havalimanı kodu (3 harfli)
/@stackoverflow.com # alan adı
/@stackoverflow.com # etki alanı adı
/06800 # posta kodları
/39.925325,32.836987 # Yer belirleme sistemi (GPS) koordinatları
Özel konumlar:
Ay evresi bilgisi:
/moon # Ay evresi (Şehir veya ülke için +TR veya +İstanbul ekleyin)
/moon # Ay evresi (bu isimdeki şehirler için ,+US veya ,+France ekleyin)
/moon@2016-10-25 # Belirli bir tarih için ay evresi (@2016-10-25)
Ölçü birimleri:
?m # metrik sistem (SI) (ABD dışında her yer için varsayılan)
?u # USCS (ABD için varsayılan)
?M # Rüzgar hızını metre/saniye (m/s) olarak göster
m # metrik sistem (SI) (ABD dışında her yer için varsayılan)
u # USCS (ABD için varsayılan)
M # Rüzgar hızını metre/saniye (m/s) olarak göster
Seçenekleri görüntüle:
?0 # sadece bugün için hava durumu
?1 # Bugün ve 1 gün sonrası için hava durumu
?2 # Bugün ve 2 gün sonrası için hava durumu
?A # Kullanıcı aracısı (User-Agent) bilgilerini göz ardı eder ve terminalde ANSI formatında çıktı için zorlar
?F # "Follow (Takip et)" satırını göstermez
?n # dar görünüm (sadece gece ve gündüz)
?q # sessiz görünüm ("Hava durumu" yazısı yok)
?Q # aşırı sessiz görünüm ("Hava durumu" yazısı ve şehir adı yok)
?T # terminal geçişlerini kapatır (renkler yok)
0 # yalnızca bugün için hava durumu
1 # Bugün ve 1 gün sonrası için hava durumu
2 # Bugün ve 2 gün sonrası için hava durumu
A # Kullanıcı aracısı (User-Agent) bilgilerini göz ardı eder ve terminalde ANSI çıktı biçimini zorlar
F # "Follow (Takip et)" satırını göstermez
n # dar görünüm (yalnızca gece ve gündüz)
q # sessiz görünüm ("Hava durumu" yazısı yok)
Q # aşırı sessiz görünüm ("Hava durumu" yazısı ve şehir adı yok)
T # terminal geçişlerini kapatır (renkler yok)
PNG seçenekleri:
/istanbul.png # bir PNG dosyası üretir
?p # çıktı etrafına bir çerçeve ekler
?t # 150 birim saydamlık
p # çıktı etrafına bir çerçeve ekler
t # 150 birim saydamlık
transparency=... # 0 ila 255 arasında saydamlık (255 = saydam değil)
background=... # KKYYMM biçiminde arka plan rengi, örn. 00aaaa
Seçenekler birleştirilebilir:
@ -64,5 +65,6 @@ Desteklenen diller:
Özel adresler:
/:help # bu sayfayı göster
/:bash.function # önerilen bash fonksiyonu wttr() içeriğini göster
/:bash.function # tavsiye edilen wttr() bash fonksiyonunu göster
/:translation # çevirmenler hakkındaki bilgileri göster

View file

@ -0,0 +1,66 @@
Використання:
$ curl wttr.in # поточне місцеположення
$ curl wttr.in/kbp # погода в аеропорту Бориспіль (код ICAO: KBP)
Підтримуються наступні типи місцеположень:
/paris # місто
/~Eiffel+tower # будь-яке місцеположення
/Киів # юнікодне ім'я будь-якого місцеположення будь-якою мовою
/muc # код аеропорту ICAO (3 літери)
/@stackoverflow.com # доменне им'я
/94107 # поштовый індекс (тільки для США)
/-78.46,106.79 # GPS-координати
Спеціальні умовні місцеположення:
/moon # Фаза Місяця (додайте ,+US або ,+France для міста Moon у США або Франції)
/moon@2016-10-25 # Фаза Місяця для вказаної дати (@2016-10-25)
Одиниці вимірювань:
?m # метричні (СІ) (використовуються всюди крім США)
?u # USCS (використовуються у США)
?M # показувати швидкість вітру в м/с
Опції відображення:
?0 # тільки поточна погода
?1 # погода сьогодні + 1 день
?2 # погода сьогодні + 2 дня
?n # вузька версія (тільки день та ніч)
?q # тиха версія (без тексту "Прогноз погоди")
?Q # надтиха версія (без "Прогноз погоди", немає назви міста)
?T # відключити послідовності терміналу (без кольорів)
PNG-опції:
/paris.png # сгенерувати PNG-файл
?p # добавити рамку навколо
?t # transparency=150 (прозорість 150)
transparency=... # прозорість від 0 до 255 (255 = не прозорий)
Опції можна комбінувати:
/Paris?0pq
/Paris?0pq&lang=fr
/Paris_0pq.png # в PNG-запитах опції вказуються після знаку _
/Rome_0pq_lang=it.png # довгі опції розділяются знаком підкреслення _
Локалізація:
$ curl fr.wttr.in/Paris
$ curl wttr.in/paris?lang=fr
$ curl -H "Accept-Language: fr" wttr.in/paris
Мови що підтримуються:
FULL_TRANSLATION (підтримується)
PARTIAL_TRANSLATION (в процесі)
Спеціальні строрінки:
/:help # показати цю сторінку
/:bash.function # показати рекомендовану функцію wttr()
/:translation # показати список перекладачів wttr.in

Some files were not shown because too many files have changed in this diff Show more