mirror of
https://github.com/chubin/wttr.in
synced 2025-01-11 19:48:45 +00:00
Merge branch 'chubin:master' into master
This commit is contained in:
commit
0c96b323f0
106 changed files with 7363 additions and 2092 deletions
30
.golangci.yaml
Normal file
30
.golangci.yaml
Normal 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
|
21
Dockerfile
21
Dockerfile
|
@ -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
9
Makefile
Normal 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
169
README.md
|
@ -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 Powershell’s `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
BIN
San_Francisco.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 63 KiB |
53
bin/proxy.py
53
bin/proxy.py
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
69
cmd/srv.go
69
cmd/srv.go
|
@ -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))
|
||||
}
|
40
cmd/stat.go
40
cmd/stat.go
|
@ -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)
|
||||
}
|
|
@ -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
26
go.mod
Normal 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
80
go.sum
Normal 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
190
internal/config/config.go
Normal 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
774
internal/fmt/png/colors.go
Normal 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
10
internal/fmt/png/go.mod
Normal 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
8
internal/fmt/png/go.sum
Normal 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
224
internal/fmt/png/png.go
Normal 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)
|
||||
}
|
||||
}
|
88
internal/geo/ip/convert.go
Normal file
88
internal/geo/ip/convert.go
Normal 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
245
internal/geo/ip/ip.go
Normal 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, " "))
|
||||
}
|
84
internal/geo/ip/ip_test.go
Normal file
84
internal/geo/ip/ip_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
218
internal/geo/location/cache.go
Normal file
218
internal/geo/location/cache.go
Normal 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)
|
||||
}
|
145
internal/geo/location/convert.go
Normal file
145
internal/geo/location/convert.go
Normal 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)
|
||||
}
|
25
internal/geo/location/location.go
Normal file
25
internal/geo/location/location.go
Normal 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)
|
||||
}
|
77
internal/geo/location/nominatim.go
Normal file
77
internal/geo/location/nominatim.go
Normal 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
|
||||
}
|
39
internal/geo/location/nominatim_locationiq.go
Normal file
39
internal/geo/location/nominatim_locationiq.go
Normal 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
|
||||
}
|
42
internal/geo/location/nominatim_opencage.go
Normal file
42
internal/geo/location/nominatim_opencage.go
Normal 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
|
||||
}
|
44
internal/geo/location/response.go
Normal file
44
internal/geo/location/response.go
Normal 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),
|
||||
)}
|
||||
}
|
42
internal/geo/location/search.go
Normal file
42
internal/geo/location/search.go
Normal 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
124
internal/logging/logging.go
Normal 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,
|
||||
)
|
||||
}
|
84
internal/logging/suppress.go
Normal file
84
internal/logging/suppress.go
Normal 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
89
internal/processor/j1.go
Normal 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
|
||||
}
|
98
internal/processor/peak.go
Normal file
98
internal/processor/peak.go
Normal 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
|
||||
})
|
||||
}
|
356
internal/processor/processor.go
Normal file
356
internal/processor/processor.go
Normal 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,
|
||||
}
|
||||
}
|
72
internal/routing/routing.go
Normal file
72
internal/routing/routing.go
Normal 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
89
internal/stats/stats.go
Normal 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
14
internal/types/errors.go
Normal 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
8
internal/types/types.go
Normal file
|
@ -0,0 +1,8 @@
|
|||
package types
|
||||
|
||||
type CacheType string
|
||||
|
||||
const (
|
||||
CacheTypeDB = "db"
|
||||
CacheTypeFiles = "files"
|
||||
)
|
18
internal/util/files.go
Normal file
18
internal/util/files.go
Normal 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
26
internal/util/http.go
Normal 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
15
internal/util/yaml.go
Normal 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
192
internal/view/v1/api.go
Normal 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
172
internal/view/v1/cmd.go
Normal 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
400
internal/view/v1/format.go
Normal 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
213
internal/view/v1/icons.go
Normal 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
338
internal/view/v1/locale.go
Normal 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
130
internal/view/v1/view1.go
Normal 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
|
||||
}
|
|
@ -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",
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"),
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
48
lib/proxy_log.py
Normal 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=")])
|
|
@ -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 !
|
|||
Ní rabhamar ábalta do cheantar a aimsiú
|
||||
mar sin thugamar go dtí Oymyakon,
|
||||
tú 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ą iš š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 só 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 nå 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):
|
||||
|
|
|
@ -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": ("வானிலை அறிக்கை:", "வானிலை", "நேரம் மண்டலம்", "இப்போது", "விடியல்", "சூரிய உதயம்", "ஜெனித்", "சூரிய அஸ்தமனம்", "அந்தி"),
|
||||
}
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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__':
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
14
share/scripts/log-space.sh
Normal file
14
share/scripts/log-space.sh
Normal 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}')
|
|
@ -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
|
||||
|
|
|
@ -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 # عرض المعلومات حول المترجمين
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
67
share/translations/bn-help.txt
Normal file
67
share/translations/bn-help.txt
Normal 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
78
share/translations/bn.txt
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
67
share/translations/gl-help.txt
Normal file
67
share/translations/gl-help.txt
Normal 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
47
share/translations/gl.txt
Normal 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
|
70
share/translations/gu-help.txt
Normal file
70
share/translations/gu-help.txt
Normal 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
78
share/translations/gu.txt
Normal 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
|
69
share/translations/hi-help.txt
Normal file
69
share/translations/hi-help.txt
Normal 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
79
share/translations/hi.txt
Normal 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
|
|
@ -52,3 +52,4 @@
|
|||
: Havazás : Snow
|
||||
: Eső : Rain
|
||||
: Gyenge eső : Light Rain
|
||||
: Talajmenti köd : Shallow fog
|
||||
|
|
69
share/translations/lt-help.txt
Normal file
69
share/translations/lt-help.txt
Normal 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
47
share/translations/lt.txt
Normal 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 :
|
62
share/translations/messages/en.yaml
Normal file
62
share/translations/messages/en.yaml
Normal 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"
|
62
share/translations/messages/gu.yaml
Normal file
62
share/translations/messages/gu.yaml
Normal 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"
|
66
share/translations/mg-help.txt
Normal file
66
share/translations/mg-help.txt
Normal 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
77
share/translations/mg.txt
Normal 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 : Tafiotran’orampanala : 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 : Tafiotran’orampanala maivana : Light sleet
|
||||
320 : Tafiotran’orampanala 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
|
70
share/translations/mr-help.txt
Normal file
70
share/translations/mr-help.txt
Normal 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
78
share/translations/mr.txt
Normal 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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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 :
|
||||
|
|
66
share/translations/ta-help.txt
Normal file
66
share/translations/ta-help.txt
Normal 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
78
share/translations/ta.txt
Normal 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
|
70
share/translations/te-help.txt
Normal file
70
share/translations/te-help.txt
Normal 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 # అనువాదకుల గురించిన సమాచారాన్ని చూపుతుంది
|
||||
|
|
@ -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
|
||||
|
||||
|
|
66
share/translations/ukr-help.txt
Normal file
66
share/translations/ukr-help.txt
Normal 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
Loading…
Reference in a new issue