Merge branch 'master' into basque_translations_update
119
README.md
|
|
@ -80,12 +80,12 @@ You can override this behavior by adding `?u` or `?m` to a URL like this:
|
|||
$ curl wttr.in/Amsterdam?u
|
||||
$ curl wttr.in/Amsterdam?m
|
||||
|
||||
## Supported output formats
|
||||
## Supported output formats and views
|
||||
|
||||
wttr.in currently supports four output formats:
|
||||
wttr.in currently supports five output formats:
|
||||
|
||||
* ANSI for the terminal;
|
||||
* ANSI for the terminal, one-line mode;
|
||||
* Plain-text for the terminal and scripts;
|
||||
* HTML for the browser;
|
||||
* PNG for the graphical viewers;
|
||||
* JSON for scripts and APIs.
|
||||
|
|
@ -122,53 +122,6 @@ You can embed a special wttr.in widget, that displays the weather condition for
|
|||
|
||||

|
||||
|
||||
## JSON output
|
||||
|
||||
The JSON format is a feature providing access to wttr.in data through an easy-to-parse format, without requiring the user to create a complex script to reinterpret wttr.in's graphical output.
|
||||
|
||||
To fetch information in JSON format, use the following syntax:
|
||||
|
||||
$ curl wttr.in/Detroit?format=j1
|
||||
|
||||
This will fetch information on the Detroit region in JSON format. The j1 format code is used to allow for the use of other layouts for the JSON output.
|
||||
|
||||
The result will look something like the following:
|
||||
|
||||
{
|
||||
"current_condition": [
|
||||
{
|
||||
"FeelsLikeC": "25",
|
||||
"FeelsLikeF": "76",
|
||||
"cloudcover": "100",
|
||||
"humidity": "76",
|
||||
"observation_time": "04:08 PM",
|
||||
"precipMM": "0.2",
|
||||
"pressure": "1019",
|
||||
"temp_C": "22",
|
||||
"temp_F": "72",
|
||||
"uvIndex": 5,
|
||||
"visibility": "16",
|
||||
"weatherCode": "122",
|
||||
"weatherDesc": [
|
||||
{
|
||||
"value": "Overcast"
|
||||
}
|
||||
],
|
||||
"weatherIconUrl": [
|
||||
{
|
||||
"value": ""
|
||||
}
|
||||
],
|
||||
"winddir16Point": "NNE",
|
||||
"winddirDegree": "20",
|
||||
"windspeedKmph": "7",
|
||||
"windspeedMiles": "4"
|
||||
}
|
||||
],
|
||||
...
|
||||
|
||||
Most of these values are self-explanatory, aside from `weatherCode`. The `weatherCode` is an enumeration which you can find at either [the WorldWeatherOnline website](https://www.worldweatheronline.com/developer/api/docs/weather-icons.aspx) or [in the wttr.in source code](https://github.com/chubin/wttr.in/blob/master/lib/constants.py).
|
||||
|
||||
## One-line output
|
||||
|
||||
For one-line output format, specify additional URL parameter `format`:
|
||||
|
|
@ -209,6 +162,14 @@ To specify your own custom output format, use the special `%`-notation:
|
|||
p precipitation (mm),
|
||||
o Probability of Precipitation,
|
||||
P pressure (hPa),
|
||||
|
||||
D Dawn*,
|
||||
S Sunrise*,
|
||||
z Zenith*,
|
||||
s Sunset*,
|
||||
d Dusk*.
|
||||
|
||||
(times are shown in the local timezone)
|
||||
```
|
||||
|
||||
So, these two calls are the same:
|
||||
|
|
@ -316,6 +277,54 @@ The result, should look like:
|
|||
|
||||

|
||||
|
||||
## JSON output
|
||||
|
||||
The JSON format is a feature providing access to wttr.in data through an easy-to-parse format, without requiring the user to create a complex script to reinterpret wttr.in's graphical output.
|
||||
|
||||
To fetch information in JSON format, use the following syntax:
|
||||
|
||||
$ curl wttr.in/Detroit?format=j1
|
||||
|
||||
This will fetch information on the Detroit region in JSON format. The j1 format code is used to allow for the use of other layouts for the JSON output.
|
||||
|
||||
The result will look something like the following:
|
||||
|
||||
{
|
||||
"current_condition": [
|
||||
{
|
||||
"FeelsLikeC": "25",
|
||||
"FeelsLikeF": "76",
|
||||
"cloudcover": "100",
|
||||
"humidity": "76",
|
||||
"observation_time": "04:08 PM",
|
||||
"precipMM": "0.2",
|
||||
"pressure": "1019",
|
||||
"temp_C": "22",
|
||||
"temp_F": "72",
|
||||
"uvIndex": 5,
|
||||
"visibility": "16",
|
||||
"weatherCode": "122",
|
||||
"weatherDesc": [
|
||||
{
|
||||
"value": "Overcast"
|
||||
}
|
||||
],
|
||||
"weatherIconUrl": [
|
||||
{
|
||||
"value": ""
|
||||
}
|
||||
],
|
||||
"winddir16Point": "NNE",
|
||||
"winddirDegree": "20",
|
||||
"windspeedKmph": "7",
|
||||
"windspeedMiles": "4"
|
||||
}
|
||||
],
|
||||
...
|
||||
|
||||
Most of these values are self-explanatory, aside from `weatherCode`. The `weatherCode` is an enumeration which you can find at either [the WorldWeatherOnline website](https://www.worldweatheronline.com/developer/api/docs/weather-icons.aspx) or [in the wttr.in source code](https://github.com/chubin/wttr.in/blob/master/lib/constants.py).
|
||||
|
||||
|
||||
## Moon phases
|
||||
|
||||
wttr.in can also be used to check the phase of the Moon. This example shows how to see the current Moon phase
|
||||
|
|
@ -446,11 +455,15 @@ If you want to get weather reports as PNG files, you'll also need to install:
|
|||
|
||||
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
|
||||
|
||||
If `virtualenv` is used:
|
||||
|
||||
$ virtualenv ve
|
||||
$ ve/bin/pip install -r requirements.txt
|
||||
$ ve/bin/python bin/srv.py
|
||||
$ 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/).
|
||||
|
|
|
|||
76
bin/proxy.py
|
|
@ -7,6 +7,10 @@ The proxy server acts as a backend for the wttr.in service.
|
|||
It caches the answers and handles various data sources transforming their
|
||||
answers into format supported by the wttr.in service.
|
||||
|
||||
If WTTRIN_TEST is specified, it works in a special test mode:
|
||||
it does not fetch and does not store the data in the cache,
|
||||
but is using the fake data from "test/proxy-data".
|
||||
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
|
|
@ -19,6 +23,7 @@ import sys
|
|||
import os
|
||||
import time
|
||||
import json
|
||||
import hashlib
|
||||
|
||||
import requests
|
||||
import cyrtranslit
|
||||
|
|
@ -35,7 +40,10 @@ from globals import PROXY_CACHEDIR, PROXY_HOST, PROXY_PORT
|
|||
from translations import PROXY_LANGS
|
||||
# pylint: enable=wrong-import-position
|
||||
|
||||
def is_testmode():
|
||||
"""Server is running in the wttr.in test mode"""
|
||||
|
||||
return "WTTRIN_TEST" in os.environ
|
||||
|
||||
def load_translations():
|
||||
"""
|
||||
|
|
@ -66,38 +74,56 @@ TRANSLATIONS = load_translations()
|
|||
def _find_srv_for_query(path, query): # pylint: disable=unused-argument
|
||||
return 'http://api.worldweatheronline.com'
|
||||
|
||||
def _cache_file(path, query):
|
||||
"""Return cache file name for specified `path` and `query`
|
||||
and for the current time.
|
||||
|
||||
Do smooth load on the server, expiration time
|
||||
is slightly varied basing on the path+query sha1 hash digest.
|
||||
"""
|
||||
|
||||
digest = hashlib.sha1(("%s %s" % (path, query)).encode("utf-8")).hexdigest()
|
||||
digest_number = ord(digest[0].upper())
|
||||
expiry_interval = 60*(digest_number+10)
|
||||
|
||||
timestamp = "%010d" % (int(time.time())//expiry_interval*expiry_interval)
|
||||
filename = os.path.join(PROXY_CACHEDIR, timestamp, path, query)
|
||||
|
||||
return filename
|
||||
|
||||
|
||||
def _load_content_and_headers(path, query):
|
||||
timestamp = time.strftime("%Y%m%d%H", time.localtime())
|
||||
cache_file = os.path.join(PROXY_CACHEDIR, timestamp, path, query)
|
||||
if is_testmode():
|
||||
cache_file = "test/proxy-data/data1"
|
||||
else:
|
||||
cache_file = _cache_file(path, query)
|
||||
try:
|
||||
return (open(cache_file, 'r').read(),
|
||||
json.loads(open(cache_file+".headers", 'r').read()))
|
||||
except IOError:
|
||||
return None, None
|
||||
|
||||
def _touch_empty_file(path, query, content, headers):
|
||||
timestamp = time.strftime("%Y%m%d%H", time.localtime())
|
||||
cache_file = os.path.join(PROXY_CACHEDIR + ".empty", timestamp, path, query)
|
||||
def _touch_empty_file(path, query):
|
||||
cache_file = _cache_file(path, query)
|
||||
cache_dir = os.path.dirname(cache_file)
|
||||
if not os.path.exists(cache_dir):
|
||||
os.makedirs(cache_dir)
|
||||
open(cache_file, 'w').write("")
|
||||
|
||||
def _save_content_and_headers(path, query, content, headers):
|
||||
timestamp = time.strftime("%Y%m%d%H", time.localtime())
|
||||
cache_file = os.path.join(PROXY_CACHEDIR, timestamp, path, query)
|
||||
cache_file = _cache_file(path, query)
|
||||
cache_dir = os.path.dirname(cache_file)
|
||||
if not os.path.exists(cache_dir):
|
||||
os.makedirs(cache_dir)
|
||||
open(cache_file + ".headers", 'w').write(json.dumps(headers))
|
||||
open(cache_file, 'w').write(content)
|
||||
open(cache_file, 'wb').write(content)
|
||||
|
||||
def translate(text, lang):
|
||||
"""
|
||||
Translate `text` into `lang`
|
||||
"""
|
||||
translated = TRANSLATIONS.get(lang, {}).get(text, text)
|
||||
if text.encode('utf-8') == translated:
|
||||
if text == translated:
|
||||
print("%s: %s" % (lang, text))
|
||||
return translated
|
||||
|
||||
|
|
@ -108,20 +134,25 @@ def cyr(to_translate):
|
|||
return cyrtranslit.to_cyrillic(to_translate)
|
||||
|
||||
def _patch_greek(original):
|
||||
return original.decode('utf-8').replace(u"Ηλιόλουστη/ο", u"Ηλιόλουστη").encode('utf-8')
|
||||
return original.replace(u"Ηλιόλουστη/ο", u"Ηλιόλουστη")
|
||||
|
||||
def add_translations(content, lang):
|
||||
"""
|
||||
Add `lang` translation to `content` (JSON)
|
||||
returned by the data source
|
||||
"""
|
||||
|
||||
if content is "{}":
|
||||
return {}
|
||||
|
||||
languages_to_translate = TRANSLATIONS.keys()
|
||||
try:
|
||||
d = json.loads(content) # pylint: disable=invalid-name
|
||||
except ValueError as exception:
|
||||
except (ValueError, TypeError) as exception:
|
||||
print("---")
|
||||
print(exception)
|
||||
print("---")
|
||||
return {}
|
||||
|
||||
try:
|
||||
weather_condition = d['data']['current_condition'][0]['weatherDesc'][0]['value']
|
||||
|
|
@ -132,16 +163,16 @@ def add_translations(content, lang):
|
|||
d['data']['current_condition'][0]['lang_%s' % lang] = \
|
||||
[{'value': cyr(
|
||||
d['data']['current_condition'][0]['lang_%s' % lang][0]['value']\
|
||||
.encode('utf-8'))}]
|
||||
)}]
|
||||
elif lang == 'el':
|
||||
d['data']['current_condition'][0]['lang_%s' % lang] = \
|
||||
[{'value': _patch_greek(
|
||||
d['data']['current_condition'][0]['lang_%s' % lang][0]['value']\
|
||||
.encode('utf-8'))}]
|
||||
)}]
|
||||
elif lang == 'sr-lat':
|
||||
d['data']['current_condition'][0]['lang_%s' % lang] = \
|
||||
[{'value':d['data']['current_condition'][0]['lang_sr'][0]['value']\
|
||||
.encode('utf-8')}]
|
||||
}]
|
||||
|
||||
fixed_weather = []
|
||||
for w in d['data']['weather']: # pylint: disable=invalid-name
|
||||
|
|
@ -153,13 +184,13 @@ def add_translations(content, lang):
|
|||
[{'value': translate(weather_condition, lang)}]
|
||||
elif lang == 'sr':
|
||||
h['lang_%s' % lang] = \
|
||||
[{'value': cyr(h['lang_%s' % lang][0]['value'].encode('utf-8'))}]
|
||||
[{'value': cyr(h['lang_%s' % lang][0]['value'])}]
|
||||
elif lang == 'el':
|
||||
h['lang_%s' % lang] = \
|
||||
[{'value': _patch_greek(h['lang_%s' % lang][0]['value'].encode('utf-8'))}]
|
||||
[{'value': _patch_greek(h['lang_%s' % lang][0]['value'])}]
|
||||
elif lang == 'sr-lat':
|
||||
h['lang_%s' % lang] = \
|
||||
[{'value': h['lang_sr'][0]['value'].encode('utf-8')}]
|
||||
[{'value': h['lang_sr'][0]['value']}]
|
||||
fixed_hourly.append(h)
|
||||
w['hourly'] = fixed_hourly
|
||||
fixed_weather.append(w)
|
||||
|
|
@ -177,7 +208,7 @@ def proxy(path):
|
|||
"""
|
||||
|
||||
lang = request.args.get('lang', 'en')
|
||||
query_string = request.query_string
|
||||
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')
|
||||
query_string += "&extra=localObsTime"
|
||||
|
|
@ -187,7 +218,6 @@ def proxy(path):
|
|||
if content is None:
|
||||
srv = _find_srv_for_query(path, query_string)
|
||||
url = '%s/%s?%s' % (srv, path, query_string)
|
||||
print(url)
|
||||
|
||||
attempts = 10
|
||||
response = None
|
||||
|
|
@ -203,14 +233,18 @@ def proxy(path):
|
|||
except ValueError:
|
||||
attempts -= 1
|
||||
|
||||
_touch_empty_file(path, query_string, content, headers)
|
||||
_touch_empty_file(path, query_string)
|
||||
if response:
|
||||
headers = {}
|
||||
headers['Content-Type'] = response.headers['content-type']
|
||||
_save_content_and_headers(path, query_string, response.content, headers)
|
||||
content = add_translations(response.content, lang)
|
||||
content = response.content
|
||||
else:
|
||||
content = "{}"
|
||||
else:
|
||||
print("cache found")
|
||||
|
||||
content = add_translations(content, lang)
|
||||
|
||||
return content, 200, headers
|
||||
|
||||
|
|
|
|||
145
cmd/srv.go
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/golang-lru"
|
||||
)
|
||||
|
||||
var lruCache *lru.Cache
|
||||
|
||||
type ResponseWithHeader struct {
|
||||
Body []byte
|
||||
Header http.Header
|
||||
StatusCode int // e.g. 200
|
||||
|
||||
}
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
lruCache, err = lru.New(12800)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
dialer := &net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
DualStack: true,
|
||||
}
|
||||
|
||||
http.DefaultTransport.(*http.Transport).DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
addr = "127.0.0.1:8002"
|
||||
return dialer.DialContext(ctx, network, addr)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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 {
|
||||
fmt.Printf("userip: %q is not IP:port\n", IPAddress)
|
||||
}
|
||||
}
|
||||
return IPAddress
|
||||
}
|
||||
|
||||
// implementation of the cache.get_signature of original wttr.in
|
||||
func findCacheDigest(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")
|
||||
|
||||
now := time.Now()
|
||||
secs := now.Unix()
|
||||
timestamp := secs / 1000
|
||||
|
||||
return fmt.Sprintf("%s:%s%s:%s:%s:%d", userAgent, queryHost, queryString, clientIpAddress, lang, timestamp)
|
||||
}
|
||||
|
||||
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 {
|
||||
// handle error
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
return ResponseWithHeader{
|
||||
Body: body,
|
||||
Header: res.Header,
|
||||
StatusCode: res.StatusCode,
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
var response ResponseWithHeader
|
||||
|
||||
cacheDigest := findCacheDigest(r)
|
||||
cacheBody, ok := lruCache.Get(cacheDigest)
|
||||
if ok {
|
||||
response = cacheBody.(ResponseWithHeader)
|
||||
} else {
|
||||
fmt.Println(cacheDigest)
|
||||
response = get(r)
|
||||
if response.StatusCode == 200 {
|
||||
lruCache.Add(cacheDigest, response)
|
||||
}
|
||||
}
|
||||
copyHeader(w.Header(), response.Header)
|
||||
w.WriteHeader(response.StatusCode)
|
||||
w.Write(response.Body)
|
||||
})
|
||||
|
||||
log.Fatal(http.ListenAndServe(":8081", nil))
|
||||
|
||||
}
|
||||
105
lib/cache.py
|
|
@ -5,17 +5,27 @@ LRU-Cache implementation for formatted (`format=`) answers
|
|||
import datetime
|
||||
import re
|
||||
import time
|
||||
import pylru
|
||||
import os
|
||||
import hashlib
|
||||
import random
|
||||
|
||||
import pytz
|
||||
import pylru
|
||||
|
||||
from globals import LRU_CACHE
|
||||
|
||||
CACHE_SIZE = 10000
|
||||
CACHE = pylru.lrucache(CACHE_SIZE)
|
||||
|
||||
# strings longer than this are stored not in ram
|
||||
# but in the file cache
|
||||
MIN_SIZE_FOR_FILECACHE = 80
|
||||
|
||||
def _update_answer(answer):
|
||||
def _now_in_tz(timezone):
|
||||
return datetime.datetime.now(pytz.timezone(timezone)).strftime("%H:%M:%S%z")
|
||||
|
||||
if "%{{NOW(" in answer:
|
||||
if isinstance(answer, str) and "%{{NOW(" in answer:
|
||||
answer = re.sub(r"%{{NOW\(([^}]*)\)}}", lambda x: _now_in_tz(x.group(1)), answer)
|
||||
|
||||
return answer
|
||||
|
|
@ -26,9 +36,9 @@ def get_signature(user_agent, query_string, client_ip_address, lang):
|
|||
`lang`, and `client_ip_address`
|
||||
"""
|
||||
|
||||
timestamp = int(time.time()) / 1000
|
||||
signature = "%s:%s:%s:%s:%s" % \
|
||||
(user_agent, query_string, client_ip_address, lang, timestamp)
|
||||
signature = "%s:%s:%s:%s" % \
|
||||
(user_agent, query_string, client_ip_address, lang)
|
||||
print(signature)
|
||||
return signature
|
||||
|
||||
def get(signature):
|
||||
|
|
@ -38,13 +48,92 @@ def get(signature):
|
|||
the `_update_answer` function.
|
||||
"""
|
||||
|
||||
if signature in CACHE:
|
||||
return _update_answer(CACHE[signature])
|
||||
value_record = CACHE.get(signature)
|
||||
if not value_record:
|
||||
return None
|
||||
|
||||
value = value_record["val"]
|
||||
expiry = value_record["expiry"]
|
||||
if value and time.time() < expiry:
|
||||
if value.startswith("file:") or value.startswith("bfile:"):
|
||||
value = _read_from_file(signature, sighash=value)
|
||||
if not value:
|
||||
return None
|
||||
return _update_answer(value)
|
||||
return None
|
||||
|
||||
def _randint(minimum, maximum):
|
||||
return random.randrange(maximum - minimum)
|
||||
|
||||
def store(signature, value):
|
||||
"""
|
||||
Store in cache `value` for `signature`
|
||||
"""
|
||||
CACHE[signature] = value
|
||||
|
||||
if len(value) >= MIN_SIZE_FOR_FILECACHE:
|
||||
value_to_store = _store_in_file(signature, value)
|
||||
else:
|
||||
value_to_store = value
|
||||
|
||||
value_record = {
|
||||
"val": value_to_store,
|
||||
"expiry": time.time() + _randint(1000, 2000),
|
||||
}
|
||||
|
||||
CACHE[signature] = value_record
|
||||
|
||||
return _update_answer(value)
|
||||
|
||||
def _hash(signature):
|
||||
return hashlib.md5(signature.encode("utf-8")).hexdigest()
|
||||
|
||||
def _store_in_file(signature, value):
|
||||
"""Store `value` for `signature` in cache file.
|
||||
Return file name (signature_hash) as the result.
|
||||
`value` can be string as well as bytes.
|
||||
Returned filename is prefixed with "file:" (for text files)
|
||||
or "bfile:" (for binary files).
|
||||
"""
|
||||
|
||||
signature_hash = _hash(signature)
|
||||
filename = os.path.join(LRU_CACHE, signature_hash)
|
||||
if not os.path.exists(LRU_CACHE):
|
||||
os.makedirs(LRU_CACHE)
|
||||
|
||||
if isinstance(value, bytes):
|
||||
mode = "wb"
|
||||
signature_hash = "bfile:%s" % signature_hash
|
||||
else:
|
||||
mode = "w"
|
||||
signature_hash = "file:%s" % signature_hash
|
||||
|
||||
with open(filename, mode) as f_cache:
|
||||
f_cache.write(value)
|
||||
return signature_hash
|
||||
|
||||
def _read_from_file(signature, sighash=None):
|
||||
"""Read value for `signature` from cache file,
|
||||
or return None if file is not found.
|
||||
If `sighash` is specified, do not calculate file name
|
||||
from signature, but use `sighash` instead.
|
||||
|
||||
`sigash` can be prefixed with "file:" (for text files)
|
||||
or "bfile:" (for binary files).
|
||||
"""
|
||||
|
||||
mode = "r"
|
||||
if sighash:
|
||||
if sighash.startswith("file:"):
|
||||
sighash = sighash[5:]
|
||||
elif sighash.startswith("bfile:"):
|
||||
sighash = sighash[6:]
|
||||
mode = "rb"
|
||||
else:
|
||||
sighash = _hash(signature)
|
||||
|
||||
filename = os.path.join(LRU_CACHE, sighash)
|
||||
if not os.path.exists(filename):
|
||||
return None
|
||||
|
||||
with open(filename, mode) as f_cache:
|
||||
return f_cache.read()
|
||||
|
|
|
|||
|
|
@ -52,55 +52,55 @@ WWO_CODE = {
|
|||
}
|
||||
|
||||
WEATHER_SYMBOL = {
|
||||
"Unknown": u"✨",
|
||||
"Cloudy": u"☁️",
|
||||
"Fog": u"🌫",
|
||||
"HeavyRain": u"🌧",
|
||||
"HeavyShowers": u"🌧",
|
||||
"HeavySnow": u"❄️",
|
||||
"HeavySnowShowers": u"❄️",
|
||||
"LightRain": u"🌦",
|
||||
"LightShowers": u"🌦",
|
||||
"LightSleet": u"🌧",
|
||||
"LightSleetShowers": u"🌧",
|
||||
"LightSnow": u"🌨",
|
||||
"LightSnowShowers": u"🌨",
|
||||
"PartlyCloudy": u"⛅️",
|
||||
"Sunny": u"☀️",
|
||||
"ThunderyHeavyRain": u"🌩",
|
||||
"ThunderyShowers": u"⛈",
|
||||
"ThunderySnowShowers": u"⛈",
|
||||
"VeryCloudy": u"☁️",
|
||||
"Unknown": "✨",
|
||||
"Cloudy": "☁️",
|
||||
"Fog": "🌫",
|
||||
"HeavyRain": "🌧",
|
||||
"HeavyShowers": "🌧",
|
||||
"HeavySnow": "❄️",
|
||||
"HeavySnowShowers": "❄️",
|
||||
"LightRain": "🌦",
|
||||
"LightShowers": "🌦",
|
||||
"LightSleet": "🌧",
|
||||
"LightSleetShowers": "🌧",
|
||||
"LightSnow": "🌨",
|
||||
"LightSnowShowers": "🌨",
|
||||
"PartlyCloudy": "⛅️",
|
||||
"Sunny": "☀️",
|
||||
"ThunderyHeavyRain": "🌩",
|
||||
"ThunderyShowers": "⛈",
|
||||
"ThunderySnowShowers": "⛈",
|
||||
"VeryCloudy": "☁️",
|
||||
}
|
||||
|
||||
WEATHER_SYMBOL_WIDTH_VTE = {
|
||||
u"✨": 2,
|
||||
u"☁️": 1,
|
||||
u"🌫": 1,
|
||||
u"🌧": 2,
|
||||
u"🌧": 2,
|
||||
u"❄️": 1,
|
||||
u"❄️": 1,
|
||||
u"🌦": 1,
|
||||
u"🌦": 1,
|
||||
u"🌧": 1,
|
||||
u"🌧": 1,
|
||||
u"🌨": 2,
|
||||
u"🌨": 2,
|
||||
u"⛅️": 2,
|
||||
u"☀️": 1,
|
||||
u"🌩": 2,
|
||||
u"⛈": 1,
|
||||
u"⛈": 1,
|
||||
u"☁️": 1,
|
||||
"✨": 2,
|
||||
"☁️": 1,
|
||||
"🌫": 2,
|
||||
"🌧": 2,
|
||||
"🌧": 2,
|
||||
"❄️": 1,
|
||||
"❄️": 1,
|
||||
"🌦": 1,
|
||||
"🌦": 1,
|
||||
"🌧": 1,
|
||||
"🌧": 1,
|
||||
"🌨": 2,
|
||||
"🌨": 2,
|
||||
"⛅️": 2,
|
||||
"☀️": 1,
|
||||
"🌩": 2,
|
||||
"⛈": 1,
|
||||
"⛈": 1,
|
||||
"☁️": 1,
|
||||
}
|
||||
|
||||
WIND_DIRECTION = [
|
||||
u"↓", u"↙", u"←", u"↖", u"↑", u"↗", u"→", u"↘",
|
||||
"↓", "↙", "←", "↖", "↑", "↗", "→", "↘",
|
||||
]
|
||||
|
||||
MOON_PHASES = (
|
||||
u"🌑", u"🌒", u"🌓", u"🌔", u"🌕", u"🌖", u"🌗", u"🌘"
|
||||
"🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"
|
||||
)
|
||||
|
||||
WEATHER_SYMBOL_WEGO = {
|
||||
|
|
|
|||
62
lib/extract_emoji.py
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
#!/usr/bin/env python
|
||||
#vim: fileencoding=utf-8
|
||||
|
||||
"""
|
||||
|
||||
At the moment, Pillow library does not support colorful emojis,
|
||||
that is why emojis must be extracted to external files first,
|
||||
and then they must be handled as usual graphical objects
|
||||
and not as text.
|
||||
|
||||
The files are extracted using Imagemagick.
|
||||
|
||||
Usage:
|
||||
|
||||
ve/bi/python lib/extract_emoji.py
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
|
||||
EMOJIS = [
|
||||
"✨",
|
||||
"☁️",
|
||||
"🌫",
|
||||
"🌧",
|
||||
"🌧",
|
||||
"❄️",
|
||||
"❄️",
|
||||
"🌦",
|
||||
"🌦",
|
||||
"🌧",
|
||||
"🌧",
|
||||
"🌨",
|
||||
"🌨",
|
||||
"⛅️",
|
||||
"☀️",
|
||||
"🌩",
|
||||
"⛈",
|
||||
"⛈",
|
||||
"☁️",
|
||||
"🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"
|
||||
]
|
||||
|
||||
def extract_emojis_to_directory(dirname):
|
||||
"""
|
||||
Extract emoji from an emoji font, to separate files.
|
||||
"""
|
||||
|
||||
emoji_font = "Noto Color Emoji"
|
||||
emoji_size = 30
|
||||
|
||||
for emoji in EMOJIS:
|
||||
filename = "%s/%s.png" % (dirname, emoji)
|
||||
convert_string = [
|
||||
"convert", "-background", "black", "-size", "%sx%s" % (emoji_size, emoji_size),
|
||||
"-set", "colorspace", "sRGB",
|
||||
"pango:<span font=\"%s\" size=\"20000\">%s</span>" % (emoji_font, emoji),
|
||||
filename
|
||||
]
|
||||
subprocess.Popen(convert_string)
|
||||
|
||||
if __name__ == '__main__':
|
||||
extract_emojis_to_directory("share/emoji")
|
||||
0
lib/fmt/__init__.py
Normal file
283
lib/fmt/png.py
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
#!/usr/bin/python
|
||||
#vim: encoding=utf-8
|
||||
# pylint: disable=wrong-import-position,wrong-import-order,redefined-builtin
|
||||
|
||||
"""
|
||||
This module is used to generate png-files for wttr.in queries.
|
||||
The only exported function is:
|
||||
|
||||
* render_ansi(png_file, text, options=None)
|
||||
|
||||
`render_ansi` is the main function of the module,
|
||||
which does rendering of stream into a PNG-file.
|
||||
|
||||
The module uses PIL for graphical tasks, and pyte for rendering
|
||||
of ANSI stream into terminal representation.
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import sys
|
||||
import io
|
||||
import os
|
||||
import glob
|
||||
|
||||
from PIL import Image, ImageFont, ImageDraw
|
||||
import pyte.screens
|
||||
import emoji
|
||||
import grapheme
|
||||
|
||||
from . import unicodedata2
|
||||
|
||||
sys.path.insert(0, "..")
|
||||
import constants
|
||||
import globals
|
||||
|
||||
COLS = 180
|
||||
ROWS = 100
|
||||
CHAR_WIDTH = 8
|
||||
CHAR_HEIGHT = 14
|
||||
FONT_SIZE = 13
|
||||
FONT_CAT = {
|
||||
'default': "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
|
||||
'Cyrillic': "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
|
||||
'Greek': "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
|
||||
'Arabic': "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
|
||||
'Hebrew': "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
|
||||
'Han': "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
|
||||
'Hiragana': "/usr/share/fonts/truetype/motoya-l-cedar/MTLc3m.ttf",
|
||||
'Katakana': "/usr/share/fonts/truetype/motoya-l-cedar/MTLc3m.ttf",
|
||||
'Hangul': "/usr/share/fonts/truetype/lexi/LexiGulim.ttf",
|
||||
'Braille': "/usr/share/fonts/truetype/ancient-scripts/Symbola_hint.ttf",
|
||||
'Emoji': "/usr/share/fonts/truetype/ancient-scripts/Symbola_hint.ttf",
|
||||
}
|
||||
|
||||
#
|
||||
# How to find font for non-standard scripts:
|
||||
#
|
||||
# $ fc-list :lang=ja
|
||||
#
|
||||
# GNU/Debian packages, that the fonts come from:
|
||||
#
|
||||
# * fonts-dejavu-core
|
||||
# * fonts-wqy-zenhei (Han)
|
||||
# * fonts-motoya-l-cedar (Hiragana/Katakana)
|
||||
# * fonts-lexi-gulim (Hangul)
|
||||
# * fonts-symbola (Braille/Emoji)
|
||||
#
|
||||
|
||||
def render_ansi(text, options=None):
|
||||
"""Render `text` (terminal sequence) in a PNG file
|
||||
paying attention to passed command line `options`.
|
||||
|
||||
Return: file content
|
||||
"""
|
||||
|
||||
screen = pyte.screens.Screen(COLS, ROWS)
|
||||
screen.set_mode(pyte.modes.LNM)
|
||||
stream = pyte.Stream(screen)
|
||||
|
||||
text, graphemes = _fix_graphemes(text)
|
||||
stream.feed(text)
|
||||
|
||||
buf = sorted(screen.buffer.items(), key=lambda x: x[0])
|
||||
buf = [[x[1] for x in sorted(line[1].items(), key=lambda x: x[0])] for line in buf]
|
||||
|
||||
return _gen_term(buf, graphemes, options=options)
|
||||
|
||||
def _color_mapping(color):
|
||||
"""Convert pyte color to PIL color
|
||||
|
||||
Return: tuple of color values (R,G,B)
|
||||
"""
|
||||
|
||||
if color == 'default':
|
||||
return 'lightgray'
|
||||
if color in ['green', 'black', 'cyan', 'blue', 'brown']:
|
||||
return color
|
||||
try:
|
||||
return (
|
||||
int(color[0:2], 16),
|
||||
int(color[2:4], 16),
|
||||
int(color[4:6], 16))
|
||||
except (ValueError, IndexError):
|
||||
# if we do not know this color and it can not be decoded as RGB,
|
||||
# print it and return it as it is (will be displayed as black)
|
||||
# print color
|
||||
return color
|
||||
return color
|
||||
|
||||
def _strip_buf(buf):
|
||||
"""Strips empty spaces from behind and from the right side.
|
||||
(from the right side is not yet implemented)
|
||||
"""
|
||||
|
||||
def empty_line(line):
|
||||
"Returns True if the line consists from spaces"
|
||||
return all(x.data == ' ' for x in line)
|
||||
|
||||
def line_len(line):
|
||||
"Returns len of the line excluding spaces from the right"
|
||||
|
||||
last_pos = len(line)
|
||||
while last_pos > 0 and line[last_pos-1].data == ' ':
|
||||
last_pos -= 1
|
||||
return last_pos
|
||||
|
||||
number_of_lines = 0
|
||||
for line in buf[::-1]:
|
||||
if not empty_line(line):
|
||||
break
|
||||
number_of_lines += 1
|
||||
|
||||
if number_of_lines:
|
||||
buf = buf[:-number_of_lines]
|
||||
|
||||
max_len = max(line_len(x) for x in buf)
|
||||
buf = [line[:max_len] for line in buf]
|
||||
|
||||
return buf
|
||||
|
||||
def _script_category(char):
|
||||
"""Returns category of a Unicode character
|
||||
|
||||
Possible values:
|
||||
default, Cyrillic, Greek, Han, Hiragana
|
||||
"""
|
||||
|
||||
if char in emoji.UNICODE_EMOJI:
|
||||
return "Emoji"
|
||||
|
||||
cat = unicodedata2.script_cat(char)[0]
|
||||
if char == u':':
|
||||
return 'Han'
|
||||
if cat in ['Latin', 'Common']:
|
||||
return 'default'
|
||||
return cat
|
||||
|
||||
def _load_emojilib():
|
||||
"""Load known emojis from a directory, and return dictionary
|
||||
of PIL Image objects correspodent to the loaded emojis.
|
||||
Each emoji is resized to the CHAR_HEIGHT size.
|
||||
"""
|
||||
|
||||
emojilib = {}
|
||||
for filename in glob.glob("share/emoji/*.png"):
|
||||
character = os.path.basename(filename)[:-4]
|
||||
emojilib[character] = \
|
||||
Image.open(filename).resize((CHAR_HEIGHT, CHAR_HEIGHT))
|
||||
return emojilib
|
||||
|
||||
# pylint: disable=too-many-locals,too-many-branches,too-many-statements
|
||||
def _gen_term(buf, graphemes, options=None):
|
||||
"""Renders rendered pyte buffer `buf` and list of workaround `graphemes`
|
||||
to a PNG file, and return its content
|
||||
"""
|
||||
|
||||
if not options:
|
||||
options = {}
|
||||
|
||||
current_grapheme = 0
|
||||
|
||||
buf = _strip_buf(buf)
|
||||
cols = max(len(x) for x in buf)
|
||||
rows = len(buf)
|
||||
|
||||
image = Image.new('RGB', (cols * CHAR_WIDTH, rows * CHAR_HEIGHT))
|
||||
|
||||
buf = buf[-ROWS:]
|
||||
|
||||
draw = ImageDraw.Draw(image)
|
||||
font = {}
|
||||
for cat in FONT_CAT:
|
||||
font[cat] = ImageFont.truetype(FONT_CAT[cat], FONT_SIZE)
|
||||
|
||||
emojilib = _load_emojilib()
|
||||
|
||||
x_pos = 0
|
||||
y_pos = 0
|
||||
for line in buf:
|
||||
x_pos = 0
|
||||
for char in line:
|
||||
current_color = _color_mapping(char.fg)
|
||||
if char.bg != 'default':
|
||||
draw.rectangle(
|
||||
((x_pos, y_pos),
|
||||
(x_pos+CHAR_WIDTH, y_pos+CHAR_HEIGHT)),
|
||||
fill=_color_mapping(char.bg))
|
||||
|
||||
if char.data == "!":
|
||||
try:
|
||||
data = graphemes[current_grapheme]
|
||||
except IndexError:
|
||||
pass
|
||||
current_grapheme += 1
|
||||
else:
|
||||
data = char.data
|
||||
|
||||
if data:
|
||||
cat = _script_category(data[0])
|
||||
if cat not in font:
|
||||
globals.log("Unknown font category: %s" % cat)
|
||||
if cat == 'Emoji' and emojilib.get(data):
|
||||
image.paste(emojilib.get(data), (x_pos, y_pos))
|
||||
else:
|
||||
draw.text(
|
||||
(x_pos, y_pos),
|
||||
data,
|
||||
font=font.get(cat, font.get('default')),
|
||||
fill=current_color)
|
||||
|
||||
x_pos += CHAR_WIDTH * constants.WEATHER_SYMBOL_WIDTH_VTE.get(data, 1)
|
||||
y_pos += CHAR_HEIGHT
|
||||
|
||||
if 'transparency' in options:
|
||||
transparency = options.get('transparency', '255')
|
||||
try:
|
||||
transparency = int(transparency)
|
||||
except ValueError:
|
||||
transparency = 255
|
||||
|
||||
if transparency < 0:
|
||||
transparency = 0
|
||||
|
||||
if transparency > 255:
|
||||
transparency = 255
|
||||
|
||||
image = image.convert("RGBA")
|
||||
datas = image.getdata()
|
||||
|
||||
new_data = []
|
||||
for item in datas:
|
||||
new_item = tuple(list(item[:3]) + [transparency])
|
||||
new_data.append(new_item)
|
||||
|
||||
image.putdata(new_data)
|
||||
|
||||
img_bytes = io.BytesIO()
|
||||
image.save(img_bytes, format="png")
|
||||
return img_bytes.getvalue()
|
||||
|
||||
def _fix_graphemes(text):
|
||||
"""
|
||||
Extract long graphemes sequences that can't be handled
|
||||
by pyte correctly because of the bug pyte#131.
|
||||
Graphemes are omited and replaced with placeholders,
|
||||
and returned as a list.
|
||||
|
||||
Return:
|
||||
text_without_graphemes, graphemes
|
||||
"""
|
||||
|
||||
output = ""
|
||||
graphemes = []
|
||||
|
||||
for gra in grapheme.graphemes(text):
|
||||
if len(gra) > 1:
|
||||
character = "!"
|
||||
graphemes.append(gra)
|
||||
else:
|
||||
character = gra
|
||||
output += character
|
||||
|
||||
return output, graphemes
|
||||
|
|
@ -1,3 +1,9 @@
|
|||
# downloaded from https://gist.github.com/2204527
|
||||
# described/recommended here:
|
||||
#
|
||||
# http://stackoverflow.com/questions/9868792/find-out-the-unicode-script-of-a-character
|
||||
#
|
||||
|
||||
from __future__ import print_function
|
||||
from unicodedata import *
|
||||
|
||||
|
|
@ -14,6 +14,7 @@ from __future__ import print_function
|
|||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
|
||||
MYDIR = os.path.abspath(os.path.dirname(os.path.dirname('__file__')))
|
||||
|
||||
|
|
@ -28,9 +29,9 @@ PYPHOON = "/home/igor/pyphoon/bin/pyphoon-lolcat"
|
|||
_DATADIR = "/wttr.in"
|
||||
_LOGDIR = "/wttr.in/log"
|
||||
|
||||
CACHEDIR = os.path.join(_DATADIR, "cache/wego/")
|
||||
IP2LCACHE = os.path.join(_DATADIR, "cache/ip2l/")
|
||||
PNG_CACHE = os.path.join(_DATADIR, "cache/png")
|
||||
LRU_CACHE = os.path.join(_DATADIR, "cache/lru")
|
||||
|
||||
LOG_FILE = os.path.join(_LOGDIR, 'main.log')
|
||||
|
||||
|
|
@ -41,7 +42,6 @@ BLACKLIST = os.path.join(MYDIR, "share/blacklist")
|
|||
HELP_FILE = os.path.join(MYDIR, 'share/help.txt')
|
||||
BASH_FUNCTION_FILE = os.path.join(MYDIR, 'share/bash-function.txt')
|
||||
TRANSLATION_FILE = os.path.join(MYDIR, 'share/translation.txt')
|
||||
TEST_FILE = os.path.join(MYDIR, 'share/test-NAME.txt')
|
||||
|
||||
IATA_CODES_FILE = os.path.join(MYDIR, 'share/list-of-iata-codes.txt')
|
||||
|
||||
|
|
@ -77,8 +77,8 @@ PLAIN_TEXT_AGENTS = [
|
|||
"lwp-request",
|
||||
"wget",
|
||||
"python-requests",
|
||||
"OpenBSD ftp",
|
||||
"PowerShell"
|
||||
"openbsd ftp",
|
||||
"powershell",
|
||||
]
|
||||
|
||||
PLAIN_TEXT_PAGES = [':help', ':bash.function', ':translation', ':iterm2']
|
||||
|
|
@ -123,3 +123,7 @@ def get_help_file(lang):
|
|||
if os.path.exists(help_file):
|
||||
return help_file
|
||||
return HELP_FILE
|
||||
|
||||
def remove_ansi(sometext):
|
||||
ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]')
|
||||
return ansi_escape.sub('', sometext)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ precise location description.
|
|||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import socket
|
||||
|
|
@ -35,12 +36,15 @@ def is_ip(ip_addr):
|
|||
Check if `ip_addr` looks like an IP Address
|
||||
"""
|
||||
|
||||
if sys.version_info[0] < 3:
|
||||
ip_addr = ip_addr.encode("utf-8")
|
||||
|
||||
try:
|
||||
socket.inet_pton(socket.AF_INET, ip_addr.encode("utf-8"))
|
||||
socket.inet_pton(socket.AF_INET, ip_addr)
|
||||
return True
|
||||
except socket.error:
|
||||
try:
|
||||
socket.inet_pton(socket.AF_INET6, ip_addr.encode("utf-8"))
|
||||
socket.inet_pton(socket.AF_INET6, ip_addr)
|
||||
return True
|
||||
except socket.error:
|
||||
return False
|
||||
|
|
@ -260,7 +264,7 @@ def location_processing(location, ip_addr):
|
|||
# if location is not None and location.upper() in IATA_CODES:
|
||||
# location = '~%s' % location
|
||||
|
||||
if location is not None and location.startswith('~'):
|
||||
if location is not None and not location.startswith("~-,") and location.startswith('~'):
|
||||
geolocation = geolocator(location_canonical_name(location[1:]))
|
||||
if geolocation is not None:
|
||||
if not override_location_name:
|
||||
|
|
|
|||
|
|
@ -1,3 +1,36 @@
|
|||
import re
|
||||
import json
|
||||
import zlib
|
||||
import base64
|
||||
|
||||
def serialize(parsed_query):
|
||||
return base64.b64encode(
|
||||
zlib.compress(
|
||||
json.dumps(parsed_query).encode("utf-8")),
|
||||
altchars=b"-_").decode("utf-8")
|
||||
|
||||
def deserialize(url):
|
||||
|
||||
string = url[2:]
|
||||
|
||||
extension = None
|
||||
if "." in string:
|
||||
string, extension = string.split(".", 1)
|
||||
|
||||
try:
|
||||
result = json.loads(
|
||||
zlib.decompress(
|
||||
base64.b64decode(string, altchars=b"-_")).decode("utf-8"))
|
||||
except zlib.error:
|
||||
return None
|
||||
|
||||
if extension == "png":
|
||||
result["png_filename"] = url
|
||||
result["html_output"] = False
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def metric_or_imperial(query, lang, us_ip=False):
|
||||
"""
|
||||
"""
|
||||
|
|
@ -29,7 +62,6 @@ def parse_query(args):
|
|||
result = {}
|
||||
|
||||
reserved_args = ["lang"]
|
||||
#q = "&".join(x for x in args.keys() if x not in reserved_args)
|
||||
|
||||
q = ""
|
||||
|
||||
|
|
@ -68,8 +100,6 @@ def parse_query(args):
|
|||
if days in q:
|
||||
result['days'] = days
|
||||
|
||||
result['no-caption'] = False
|
||||
result['no-city'] = False
|
||||
if 'q' in q:
|
||||
result['no-caption'] = True
|
||||
if 'Q' in q:
|
||||
|
|
@ -82,7 +112,61 @@ def parse_query(args):
|
|||
val = True
|
||||
if val == 'False':
|
||||
val = False
|
||||
if val:
|
||||
result[key] = val
|
||||
|
||||
# currently `view` is alias for `format`
|
||||
if "format" in result and not result.get("view"):
|
||||
result["view"] = result["format"]
|
||||
del result["format"]
|
||||
|
||||
return result
|
||||
|
||||
def parse_wttrin_png_name(name):
|
||||
"""
|
||||
Parse the PNG filename and return the result as a dictionary.
|
||||
For example:
|
||||
input = City_200x_lang=ru.png
|
||||
output = {
|
||||
"lang": "ru",
|
||||
"width": "200",
|
||||
"filetype": "png",
|
||||
"location": "City"
|
||||
}
|
||||
"""
|
||||
|
||||
parsed = {}
|
||||
to_be_parsed = {}
|
||||
|
||||
if name.lower()[-4:] == '.png':
|
||||
parsed['filetype'] = 'png'
|
||||
name = name[:-4]
|
||||
|
||||
parts = name.split('_')
|
||||
parsed['location'] = parts[0]
|
||||
|
||||
one_letter_options = ""
|
||||
for part in parts[1:]:
|
||||
if re.match('(?:[0-9]+)x', part):
|
||||
parsed['width'] = part[:-1]
|
||||
elif re.match('x(?:[0-9]+)', part):
|
||||
parsed['height'] = part[1:]
|
||||
elif re.match(part, '(?:[0-9]+)x(?:[0-9]+)'):
|
||||
parsed['width'], parsed['height'] = part.split('x', 1)
|
||||
elif '=' in part:
|
||||
arg, val = part.split('=', 1)
|
||||
to_be_parsed[arg] = val
|
||||
else:
|
||||
one_letter_options += part
|
||||
|
||||
for letter in one_letter_options:
|
||||
to_be_parsed[letter] = ''
|
||||
|
||||
parsed.update(parse_query(to_be_parsed))
|
||||
|
||||
# currently `view` is alias for `format`
|
||||
if "format" in parsed and not parsed.get("view"):
|
||||
parsed["view"] = parsed["format"]
|
||||
del parsed["format"]
|
||||
|
||||
return parsed
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ FULL_TRANSLATION = [
|
|||
|
||||
PARTIAL_TRANSLATION = [
|
||||
"az", "bg", "bs", "cy", "cs",
|
||||
"eo", "es", "fi", "ga", "hi", "hr",
|
||||
"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",
|
||||
|
|
@ -23,7 +23,7 @@ PARTIAL_TRANSLATION = [
|
|||
|
||||
PROXY_LANGS = [
|
||||
"af", "ar", "az", "be", "bs", "ca",
|
||||
"cy", "el", "eo", "et", "fa",
|
||||
"cy", "de", "el", "eo", "et", "eu", "fa", "fr",
|
||||
"fy", "he", "hr", "hu", "hy",
|
||||
"ia", "id", "is", "it", "ja", "kk",
|
||||
"lv", "mk", "nb", "nn", "ro",
|
||||
|
|
|
|||
0
lib/view/__init__.py
Normal file
|
|
@ -17,13 +17,17 @@ import sys
|
|||
import re
|
||||
import datetime
|
||||
import json
|
||||
try:
|
||||
from astral import Astral, Location
|
||||
except ImportError:
|
||||
pass
|
||||
from constants import WWO_CODE, WEATHER_SYMBOL, WIND_DIRECTION
|
||||
import requests
|
||||
|
||||
from astral import LocationInfo
|
||||
from astral import moon
|
||||
from astral.sun import sun
|
||||
|
||||
import pytz
|
||||
|
||||
from constants import WWO_CODE, WEATHER_SYMBOL, WIND_DIRECTION, WEATHER_SYMBOL_WIDTH_VTE
|
||||
from weather_data import get_weather_data
|
||||
import spark
|
||||
from . import v2
|
||||
|
||||
PRECONFIGURED_FORMAT = {
|
||||
'1': u'%c %t',
|
||||
|
|
@ -57,12 +61,13 @@ def render_temperature(data, query):
|
|||
return temperature
|
||||
|
||||
def render_condition(data, query):
|
||||
"""
|
||||
condition (c)
|
||||
"""Emoji encoded weather condition (c)
|
||||
"""
|
||||
|
||||
weather_condition = WEATHER_SYMBOL[WWO_CODE[data['weatherCode']]]
|
||||
return weather_condition
|
||||
spaces = " "*(WEATHER_SYMBOL_WIDTH_VTE.get(weather_condition) - 1)
|
||||
|
||||
return weather_condition + spaces
|
||||
|
||||
def render_condition_fullname(data, query):
|
||||
"""
|
||||
|
|
@ -140,18 +145,18 @@ def render_wind(data, query):
|
|||
degree = ""
|
||||
|
||||
if degree:
|
||||
wind_direction = WIND_DIRECTION[((degree+22)%360)/45]
|
||||
wind_direction = WIND_DIRECTION[((degree+22)%360)//45]
|
||||
else:
|
||||
wind_direction = ""
|
||||
|
||||
if query.get('use_ms_for_wind', False):
|
||||
unit = ' m/s'
|
||||
unit = 'm/s'
|
||||
wind = u'%s%.1f%s' % (wind_direction, float(data['windspeedKmph'])/36.0*10.0, unit)
|
||||
elif query.get('use_imperial', False):
|
||||
unit = ' mph'
|
||||
unit = 'mph'
|
||||
wind = u'%s%s%s' % (wind_direction, data['windspeedMiles'], unit)
|
||||
else:
|
||||
unit = ' km/h'
|
||||
unit = 'km/h'
|
||||
wind = u'%s%s%s' % (wind_direction, data['windspeedKmph'], unit)
|
||||
|
||||
return wind
|
||||
|
|
@ -161,35 +166,58 @@ def render_location(data, query):
|
|||
location (l)
|
||||
"""
|
||||
|
||||
return (data['override_location'] or data['location']) # .title()
|
||||
return (data['override_location'] or data['location'])
|
||||
|
||||
def render_moonphase(_, query):
|
||||
"""
|
||||
"""moonpahse(m)
|
||||
A symbol describing the phase of the moon
|
||||
"""
|
||||
astral = Astral()
|
||||
moon_index = int(
|
||||
int(32.0*astral.moon_phase(date=datetime.datetime.today())/28+2)%32/4
|
||||
)
|
||||
moon_phase = moon.phase(date=datetime.datetime.today())
|
||||
moon_index = int(int(32.0*moon_phase/28+2)%32/4)
|
||||
return MOON_PHASES[moon_index]
|
||||
|
||||
def render_moonday(_, query):
|
||||
"""
|
||||
"""moonday(M)
|
||||
An number describing the phase of the moon (days after the New Moon)
|
||||
"""
|
||||
astral = Astral()
|
||||
return str(int(astral.moon_phase(date=datetime.datetime.today())))
|
||||
moon_phase = moon.phase(date=datetime.datetime.today())
|
||||
return str(int(moon_phase))
|
||||
|
||||
def render_sunset(data, query):
|
||||
location = data['location']
|
||||
city_name = location
|
||||
astral = Astral()
|
||||
location = Location(('Nuremberg', 'Germany',
|
||||
49.453872, 11.077298, 'Europe/Berlin', 0))
|
||||
sun = location.sun(date=datetime.datetime.today(), local=True)
|
||||
##################################
|
||||
# this part should be rewritten
|
||||
# this is just a temporary solution
|
||||
|
||||
def get_geodata(location):
|
||||
text = requests.get("http://localhost:8004/%s" % location).text
|
||||
return json.loads(text)
|
||||
|
||||
|
||||
return str(sun['sunset'])
|
||||
def render_dawn(data, query, local_time_of):
|
||||
"""dawn (D)
|
||||
Local time of dawn"""
|
||||
return local_time_of("dawn")
|
||||
|
||||
def render_dusk(data, query, local_time_of):
|
||||
"""dusk (d)
|
||||
Local time of dusk"""
|
||||
return local_time_of("dusk")
|
||||
|
||||
def render_sunrise(data, query, local_time_of):
|
||||
"""sunrise (S)
|
||||
Local time of sunrise"""
|
||||
return local_time_of("sunrise")
|
||||
|
||||
def render_sunset(data, query, local_time_of):
|
||||
"""sunset (s)
|
||||
Local time of sunset"""
|
||||
return local_time_of("sunset")
|
||||
|
||||
def render_zenith(data, query, local_time_of):
|
||||
"""zenith (z)
|
||||
Local time of zenith"""
|
||||
return local_time_of("noon")
|
||||
|
||||
##################################
|
||||
|
||||
FORMAT_SYMBOL = {
|
||||
'c': render_condition,
|
||||
|
|
@ -200,17 +228,48 @@ FORMAT_SYMBOL = {
|
|||
'l': render_location,
|
||||
'm': render_moonphase,
|
||||
'M': render_moonday,
|
||||
's': render_sunset,
|
||||
'p': render_precipitation,
|
||||
'o': render_precipitation_chance,
|
||||
'P': render_pressure,
|
||||
}
|
||||
|
||||
FORMAT_SYMBOL_ASTRO = {
|
||||
'D': render_dawn,
|
||||
'd': render_dusk,
|
||||
'S': render_sunrise,
|
||||
's': render_sunset,
|
||||
'z': render_zenith,
|
||||
}
|
||||
|
||||
def render_line(line, data, query):
|
||||
"""
|
||||
Render format `line` using `data`
|
||||
"""
|
||||
|
||||
def get_local_time_of():
|
||||
|
||||
location = data["location"]
|
||||
geo_data = get_geodata(location)
|
||||
|
||||
city = LocationInfo()
|
||||
city.latitude = geo_data["latitude"]
|
||||
city.longitude = geo_data["longitude"]
|
||||
city.timezone = geo_data["timezone"]
|
||||
|
||||
timezone = city.timezone
|
||||
|
||||
local_tz = pytz.timezone(timezone)
|
||||
|
||||
datetime_day_start = datetime.datetime.now()\
|
||||
.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
current_sun = sun(city.observer, date=datetime_day_start)
|
||||
|
||||
local_time_of = lambda x: current_sun[x]\
|
||||
.replace(tzinfo=pytz.utc)\
|
||||
.astimezone(local_tz)\
|
||||
.strftime("%H:%M:%S")
|
||||
return local_time_of
|
||||
|
||||
def render_symbol(match):
|
||||
"""
|
||||
Render one format symbol from re `match`
|
||||
|
|
@ -220,13 +279,22 @@ def render_line(line, data, query):
|
|||
symbol_string = match.group(0)
|
||||
symbol = symbol_string[-1]
|
||||
|
||||
if symbol not in FORMAT_SYMBOL:
|
||||
return ''
|
||||
|
||||
if symbol in FORMAT_SYMBOL:
|
||||
render_function = FORMAT_SYMBOL[symbol]
|
||||
return render_function(data, query)
|
||||
if symbol in FORMAT_SYMBOL_ASTRO and local_time_of is not None:
|
||||
render_function = FORMAT_SYMBOL_ASTRO[symbol]
|
||||
return render_function(data, query, local_time_of)
|
||||
|
||||
return re.sub(r'%[^%]*[a-zA-Z]', render_symbol, line)
|
||||
return ''
|
||||
|
||||
template_regexp = r'%[^%]*[a-zA-Z]'
|
||||
for template_code in re.findall(template_regexp, line):
|
||||
if template_code.lstrip("%") in FORMAT_SYMBOL_ASTRO:
|
||||
local_time_of = get_local_time_of()
|
||||
break
|
||||
|
||||
return re.sub(template_regexp, render_symbol, line)
|
||||
|
||||
def render_json(data):
|
||||
output = json.dumps(data, indent=4, sort_keys=True)
|
||||
|
|
@ -237,47 +305,41 @@ def render_json(data):
|
|||
|
||||
return output
|
||||
|
||||
def format_weather_data(format_line, location, override_location, full_address, data, query):
|
||||
def format_weather_data(query, parsed_query, data):
|
||||
"""
|
||||
Format information about current weather `data` for `location`
|
||||
with specified in `format_line` format
|
||||
"""
|
||||
|
||||
if 'data' not in data:
|
||||
return 'Unknown location; please try ~%s' % location
|
||||
return 'Unknown location; please try ~%s' % parsed_query["location"]
|
||||
|
||||
format_line = parsed_query.get("view", "")
|
||||
if format_line in PRECONFIGURED_FORMAT:
|
||||
format_line = PRECONFIGURED_FORMAT[format_line]
|
||||
|
||||
if format_line == "j1":
|
||||
return render_json(data['data'])
|
||||
if format_line[:2] == "v2":
|
||||
return spark.main(location,
|
||||
override_location=override_location,
|
||||
full_address=full_address, data=data,
|
||||
view=format_line)
|
||||
return v2.main(query, parsed_query, data)
|
||||
|
||||
current_condition = data['data']['current_condition'][0]
|
||||
current_condition['location'] = location
|
||||
current_condition['override_location'] = override_location
|
||||
current_condition['location'] = parsed_query["location"]
|
||||
current_condition['override_location'] = parsed_query["override_location_name"]
|
||||
output = render_line(format_line, current_condition, query)
|
||||
return output
|
||||
|
||||
def wttr_line(location, override_location_name, full_address, query, lang, fmt):
|
||||
def wttr_line(query, parsed_query):
|
||||
"""
|
||||
Return 1line weather information for `location`
|
||||
in format `line_format`
|
||||
"""
|
||||
location = parsed_query['location']
|
||||
lang = parsed_query['lang']
|
||||
|
||||
format_line = query.get('format', fmt or '')
|
||||
|
||||
if format_line in PRECONFIGURED_FORMAT:
|
||||
format_line = PRECONFIGURED_FORMAT[format_line]
|
||||
|
||||
weather_data = get_weather_data(location, lang)
|
||||
|
||||
output = format_weather_data(
|
||||
format_line, location, override_location_name, full_address,
|
||||
weather_data, query)
|
||||
output = output.rstrip("\n")+"\n"
|
||||
return output
|
||||
data = get_weather_data(location, lang)
|
||||
output = format_weather_data(query, parsed_query, data)
|
||||
return output.rstrip("\n")+"\n"
|
||||
|
||||
def main():
|
||||
"""
|
||||
|
|
@ -288,8 +350,14 @@ def main():
|
|||
query = {
|
||||
'line': sys.argv[2],
|
||||
}
|
||||
parsed_query = {
|
||||
"location": location,
|
||||
"orig_location": location,
|
||||
"language": "en",
|
||||
"format": "v2",
|
||||
}
|
||||
|
||||
sys.stdout.write(wttr_line(location, location, None, query, 'en', "v1"))
|
||||
sys.stdout.write(wttr_line(query, parsed_query))
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
53
lib/view/moon.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import sys
|
||||
|
||||
import os
|
||||
import dateutil.parser
|
||||
|
||||
from gevent.subprocess import Popen, PIPE
|
||||
|
||||
sys.path.insert(0, "..")
|
||||
import constants
|
||||
import parse_query
|
||||
import globals
|
||||
|
||||
def get_moon(parsed_query):
|
||||
|
||||
location = parsed_query['orig_location']
|
||||
html = parsed_query['html_output']
|
||||
lang = parsed_query['lang']
|
||||
|
||||
date = None
|
||||
if '@' in location:
|
||||
date = location[location.index('@')+1:]
|
||||
location = location[:location.index('@')]
|
||||
|
||||
cmd = [globals.PYPHOON]
|
||||
if lang:
|
||||
cmd += ["-l", lang]
|
||||
|
||||
if date:
|
||||
try:
|
||||
dateutil.parser.parse(date)
|
||||
except Exception as e:
|
||||
print("ERROR: %s" % e)
|
||||
else:
|
||||
cmd += [date]
|
||||
|
||||
p = Popen(cmd, stdout=PIPE, stderr=PIPE)
|
||||
stdout = p.communicate()[0]
|
||||
stdout = stdout.decode("utf-8")
|
||||
|
||||
if parsed_query.get('no-terminal', False):
|
||||
stdout = globals.remove_ansi(stdout)
|
||||
|
||||
if html:
|
||||
p = Popen(
|
||||
["bash", globals.ANSI2HTML, "--palette=solarized", "--bg=dark"],
|
||||
stdin=PIPE, stdout=PIPE, stderr=PIPE)
|
||||
stdout, stderr = p.communicate(stdout.encode("utf-8"))
|
||||
stdout = stdout.decode("utf-8")
|
||||
stderr = stderr.decode("utf-8")
|
||||
if p.returncode != 0:
|
||||
globals.error(stdout + stderr)
|
||||
|
||||
return stdout
|
||||
|
|
@ -26,27 +26,23 @@ import re
|
|||
import math
|
||||
import json
|
||||
import datetime
|
||||
try:
|
||||
import StringIO
|
||||
except:
|
||||
import io as StringIO
|
||||
import io
|
||||
|
||||
import requests
|
||||
import diagram
|
||||
import pyjq
|
||||
import pytz
|
||||
import numpy as np
|
||||
try:
|
||||
from astral import Astral, Location
|
||||
except ImportError:
|
||||
pass
|
||||
from astral import LocationInfo
|
||||
from astral import moon, sun
|
||||
from scipy.interpolate import interp1d
|
||||
from babel.dates import format_datetime
|
||||
|
||||
from globals import WWO_KEY
|
||||
import constants
|
||||
import translations
|
||||
import wttr_line
|
||||
import parse_query
|
||||
from . import line as wttr_line
|
||||
|
||||
if not sys.version_info >= (3, 0):
|
||||
reload(sys) # noqa: F821
|
||||
|
|
@ -74,9 +70,11 @@ def interpolate_data(input_data, max_width):
|
|||
Resample `input_data` to number of `max_width` counts
|
||||
"""
|
||||
|
||||
x = list(range(len(input_data)))
|
||||
input_data = list(input_data)
|
||||
input_data_len = len(input_data)
|
||||
x = list(range(input_data_len))
|
||||
y = input_data
|
||||
xvals = np.linspace(0, len(input_data)-1, max_width)
|
||||
xvals = np.linspace(0, input_data_len-1, max_width)
|
||||
yinterp = interp1d(x, y, kind='cubic')
|
||||
return yinterp(xvals)
|
||||
|
||||
|
|
@ -86,12 +84,15 @@ def jq_query(query, data_parsed):
|
|||
"""
|
||||
|
||||
pyjq_data = pyjq.all(query, data_parsed)
|
||||
data = map(float, pyjq_data)
|
||||
data = list(map(float, pyjq_data))
|
||||
return data
|
||||
|
||||
# }}}
|
||||
# utils {{{
|
||||
def colorize(string, color_code):
|
||||
def colorize(string, color_code, html_output=False):
|
||||
if html_output:
|
||||
return "<font color='#777777'>%s</font>" % (string)
|
||||
else:
|
||||
return "\033[%sm%s\033[0m" % (color_code, string)
|
||||
# }}}
|
||||
# draw_spark {{{
|
||||
|
|
@ -140,11 +141,11 @@ def draw_spark(data, height, width, color_data):
|
|||
orig_max_line = max_line
|
||||
|
||||
# aligning it
|
||||
if len(max_line)/2 < j and len(max_line)/2 + j < width:
|
||||
spaces = " "*(j - len(max_line)/2)
|
||||
if len(max_line)//2 < j and len(max_line)//2 + j < width:
|
||||
spaces = " "*(j - len(max_line)//2)
|
||||
max_line = spaces + max_line # + spaces
|
||||
max_line = max_line + " "*(width - len(max_line))
|
||||
elif len(max_line)/2 + j >= width:
|
||||
elif len(max_line)//2 + j >= width:
|
||||
max_line = " "*(width - len(max_line)) + max_line
|
||||
|
||||
max_line = max_line.replace(orig_max_line, colorize(orig_max_line, "38;5;33"))
|
||||
|
|
@ -164,13 +165,13 @@ def draw_diagram(data, height, width):
|
|||
option.size = diagram.Point([width, height])
|
||||
option.mode = 'g'
|
||||
|
||||
stream = StringIO.StringIO()
|
||||
stream = io.BytesIO()
|
||||
gram = diagram.DGWrapper(
|
||||
data=[list(data), range(len(data))],
|
||||
dg_option=option,
|
||||
ostream=stream)
|
||||
gram.show()
|
||||
return stream.getvalue()
|
||||
return stream.getvalue().decode("utf-8")
|
||||
# }}}
|
||||
# draw_date {{{
|
||||
|
||||
|
|
@ -189,7 +190,7 @@ def draw_date(config, geo_data):
|
|||
datetime_ = datetime_day_start + datetime.timedelta(hours=24*day)
|
||||
date = format_datetime(datetime_, "EEE dd MMM", locale=locale, tzinfo=tzinfo)
|
||||
|
||||
spaces = ((24-len(date))/2)*" "
|
||||
spaces = ((24-len(date))//2)*" "
|
||||
date = spaces + date + spaces
|
||||
date = " "*(24-len(date)) + date
|
||||
answer += date
|
||||
|
|
@ -241,10 +242,7 @@ def draw_time(geo_data):
|
|||
def draw_astronomical(city_name, geo_data):
|
||||
datetime_day_start = datetime.datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
a = Astral()
|
||||
a.solar_depression = 'civil'
|
||||
|
||||
city = Location()
|
||||
city = LocationInfo()
|
||||
city.latitude = geo_data["latitude"]
|
||||
city.longitude = geo_data["longitude"]
|
||||
city.timezone = geo_data["timezone"]
|
||||
|
|
@ -256,33 +254,52 @@ def draw_astronomical(city_name, geo_data):
|
|||
current_date = (
|
||||
datetime_day_start
|
||||
+ datetime.timedelta(hours=1*time_interval)).replace(tzinfo=pytz.timezone(geo_data["timezone"]))
|
||||
sun = city.sun(date=current_date, local=False)
|
||||
|
||||
dawn = sun['dawn'] # .replace(tzinfo=None)
|
||||
dusk = sun['dusk'] # .replace(tzinfo=None)
|
||||
sunrise = sun['sunrise'] # .replace(tzinfo=None)
|
||||
sunset = sun['sunset'] # .replace(tzinfo=None)
|
||||
try:
|
||||
dawn = sun.dawn(city.observer, date=current_date)
|
||||
except ValueError:
|
||||
dawn = current_date
|
||||
|
||||
try:
|
||||
dusk = sun.dusk(city.observer, date=current_date)
|
||||
except ValueError:
|
||||
dusk = current_date + datetime.timedelta(hours=24)
|
||||
|
||||
try:
|
||||
sunrise = sun.sunrise(city.observer, date=current_date)
|
||||
except ValueError:
|
||||
sunrise = current_date
|
||||
|
||||
try:
|
||||
sunset = sun.sunset(city.observer, date=current_date)
|
||||
except ValueError:
|
||||
sunset = current_date + datetime.timedelta(hours=24)
|
||||
|
||||
char = "."
|
||||
if current_date < dawn:
|
||||
char = " "
|
||||
elif current_date > dusk:
|
||||
char = " "
|
||||
elif dawn < current_date and current_date < sunrise:
|
||||
elif dawn <= current_date and current_date <= sunrise:
|
||||
char = u"─"
|
||||
elif sunset < current_date and current_date < dusk:
|
||||
elif sunset <= current_date and current_date <= dusk:
|
||||
char = u"─"
|
||||
elif sunrise < current_date and current_date < sunset:
|
||||
elif sunrise <= current_date and current_date <= sunset:
|
||||
char = u"━"
|
||||
|
||||
answer += char
|
||||
|
||||
# moon
|
||||
if time_interval % 3 == 0:
|
||||
moon_phase = city.moon_phase(
|
||||
if time_interval in [0,23,47,69]: # time_interval % 3 == 0:
|
||||
moon_phase = moon.phase(
|
||||
date=datetime_day_start + datetime.timedelta(hours=time_interval))
|
||||
moon_phase_emoji = constants.MOON_PHASES[int(math.floor(moon_phase*1.0/28.0*8+0.5)) % len(constants.MOON_PHASES)]
|
||||
if time_interval in [0, 24, 48, 69]:
|
||||
moon_line += moon_phase_emoji + " "
|
||||
moon_phase_emoji = constants.MOON_PHASES[
|
||||
int(math.floor(moon_phase*1.0/28.0*8+0.5)) % len(constants.MOON_PHASES)]
|
||||
# if time_interval in [0, 24, 48, 69]:
|
||||
moon_line += moon_phase_emoji # + " "
|
||||
elif time_interval % 3 == 0:
|
||||
if time_interval not in [24,28]: #se:
|
||||
moon_line += " "
|
||||
else:
|
||||
moon_line += " "
|
||||
|
||||
|
|
@ -333,7 +350,7 @@ def draw_wind(data, color_data):
|
|||
|
||||
degree = int(degree)
|
||||
if degree:
|
||||
wind_direction = constants.WIND_DIRECTION[((degree+22)%360)/45]
|
||||
wind_direction = constants.WIND_DIRECTION[((degree+22)%360)//45]
|
||||
else:
|
||||
wind_direction = ""
|
||||
|
||||
|
|
@ -364,9 +381,9 @@ def add_frame(output, width, config):
|
|||
output = "\n".join(u"│"+(x or empty_line)+u"│" for x in output.splitlines()) + "\n"
|
||||
|
||||
weather_report = \
|
||||
translations.CAPTION[config["lang"]] \
|
||||
translations.CAPTION[config.get("lang") or "en"] \
|
||||
+ " " \
|
||||
+ (config["override_location"] or config["location"])
|
||||
+ (config["override_location_name"] or config["location"])
|
||||
|
||||
caption = u"┤ " + " " + weather_report + " " + u" ├"
|
||||
output = u"┌" + caption + u"─"*(width-len(caption)) + u"┐\n" \
|
||||
|
|
@ -429,7 +446,7 @@ def generate_panel(data_parsed, geo_data, config):
|
|||
|
||||
# }}}
|
||||
# textual information {{{
|
||||
def textual_information(data_parsed, geo_data, config):
|
||||
def textual_information(data_parsed, geo_data, config, html_output=False):
|
||||
"""
|
||||
Add textual information about current weather and
|
||||
astronomical conditions
|
||||
|
|
@ -453,8 +470,10 @@ def textual_information(data_parsed, geo_data, config):
|
|||
|
||||
return output
|
||||
|
||||
def _colorize(text, color):
|
||||
return colorize(text, color, html_output=html_output)
|
||||
|
||||
city = Location()
|
||||
city = LocationInfo()
|
||||
city.latitude = geo_data["latitude"]
|
||||
city.longitude = geo_data["longitude"]
|
||||
city.timezone = geo_data["timezone"]
|
||||
|
|
@ -464,7 +483,6 @@ def textual_information(data_parsed, geo_data, config):
|
|||
|
||||
datetime_day_start = datetime.datetime.now()\
|
||||
.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
sun = city.sun(date=datetime_day_start, local=True)
|
||||
|
||||
format_line = "%c %C, %t, %h, %w, %P"
|
||||
current_condition = data_parsed['data']['current_condition'][0]
|
||||
|
|
@ -474,22 +492,41 @@ def textual_information(data_parsed, geo_data, config):
|
|||
|
||||
output.append('Timezone: %s' % timezone)
|
||||
|
||||
tmp_output = []
|
||||
tmp_output.append(' Now: %%{{NOW(%s)}}' % timezone)
|
||||
tmp_output.append('Dawn: %s'
|
||||
% str(sun['dawn'].strftime("%H:%M:%S")))
|
||||
tmp_output.append('Sunrise: %s'
|
||||
% str(sun['sunrise'].strftime("%H:%M:%S")))
|
||||
tmp_output.append(' Zenith: %s'
|
||||
% str(sun['noon'].strftime("%H:%M:%S ")))
|
||||
tmp_output.append('Sunset: %s'
|
||||
% str(sun['sunset'].strftime("%H:%M:%S")))
|
||||
tmp_output.append('Dusk: %s'
|
||||
% str(sun['dusk'].strftime("%H:%M:%S")))
|
||||
local_tz = pytz.timezone(timezone)
|
||||
|
||||
def _get_local_time_of(what):
|
||||
_sun = {
|
||||
"dawn": sun.dawn,
|
||||
"sunrise": sun.sunrise,
|
||||
"noon": sun.noon,
|
||||
"sunset": sun.sunset,
|
||||
"dusk": sun.dusk,
|
||||
}[what]
|
||||
|
||||
current_time_of_what = _sun(city.observer, date=datetime_day_start)
|
||||
return current_time_of_what\
|
||||
.replace(tzinfo=pytz.utc)\
|
||||
.astimezone(local_tz)\
|
||||
.strftime("%H:%M:%S")
|
||||
|
||||
local_time_of = {}
|
||||
for what in ["dawn", "sunrise", "noon", "sunset", "dusk"]:
|
||||
try:
|
||||
local_time_of[what] = _get_local_time_of(what)
|
||||
except ValueError:
|
||||
local_time_of[what] = "-"*8
|
||||
|
||||
tmp_output = []
|
||||
|
||||
tmp_output.append(' Now: %%{{NOW(%s)}}' % timezone)
|
||||
tmp_output.append('Dawn: %s' % local_time_of["dawn"])
|
||||
tmp_output.append('Sunrise: %s' % local_time_of["sunrise"])
|
||||
tmp_output.append(' Zenith: %s ' % local_time_of["noon"])
|
||||
tmp_output.append('Sunset: %s' % local_time_of["sunset"])
|
||||
tmp_output.append('Dusk: %s' % local_time_of["dusk"])
|
||||
|
||||
color_code = "38;5;246"
|
||||
tmp_output = [
|
||||
re.sub("^([A-Za-z]*:)", lambda m: colorize(m.group(1), color_code), x)
|
||||
re.sub("^([A-Za-z]*:)", lambda m: _colorize(m.group(1), "2"), x)
|
||||
for x in tmp_output]
|
||||
|
||||
output.append(
|
||||
|
|
@ -517,9 +554,9 @@ def textual_information(data_parsed, geo_data, config):
|
|||
))
|
||||
|
||||
output = [
|
||||
re.sub("^( *[A-Za-z]*:)", lambda m: colorize(m.group(1), color_code),
|
||||
re.sub("^( +[A-Za-z]*:)", lambda m: colorize(m.group(1), color_code),
|
||||
re.sub(r"(\|)", lambda m: colorize(m.group(1), color_code), x)))
|
||||
re.sub("^( *[A-Za-z]*:)", lambda m: _colorize(m.group(1), "2"),
|
||||
re.sub("^( +[A-Za-z]*:)", lambda m: _colorize(m.group(1), "2"),
|
||||
re.sub(r"(\|)", lambda m: _colorize(m.group(1), "2"), x)))
|
||||
for x in output]
|
||||
|
||||
return "".join("%s\n" % x for x in output)
|
||||
|
|
@ -531,24 +568,42 @@ def get_geodata(location):
|
|||
return json.loads(text)
|
||||
# }}}
|
||||
|
||||
def main(location, override_location=None, data=None, full_address=None, view=None):
|
||||
config = {
|
||||
"lang": "en",
|
||||
"locale": "en_US",
|
||||
"location": location,
|
||||
"override_location": override_location,
|
||||
"full_address": full_address,
|
||||
"view": view,
|
||||
}
|
||||
def main(query, parsed_query, data):
|
||||
parsed_query["locale"] = "en_US"
|
||||
|
||||
location = parsed_query["location"]
|
||||
html_output = parsed_query["html_output"]
|
||||
|
||||
geo_data = get_geodata(location)
|
||||
if data is None:
|
||||
data_parsed = get_data(config)
|
||||
data_parsed = get_data(parsed_query)
|
||||
else:
|
||||
data_parsed = data
|
||||
|
||||
output = generate_panel(data_parsed, geo_data, config)
|
||||
output += textual_information(data_parsed, geo_data, config)
|
||||
if html_output:
|
||||
parsed_query["text"] = "no"
|
||||
filename = "b_" + parse_query.serialize(parsed_query) + ".png"
|
||||
output = """
|
||||
<html>
|
||||
<head>
|
||||
<title>Weather report for {orig_location}</title>
|
||||
<link rel="stylesheet" type="text/css" href="/files/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<img src="/{filename}" width="592" height="532"/>
|
||||
<pre>
|
||||
{textual_information}
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
||||
""".format(
|
||||
filename=filename, orig_location=parsed_query["orig_location"],
|
||||
textual_information=textual_information(
|
||||
data_parsed, geo_data, parsed_query, html_output=True))
|
||||
else:
|
||||
output = generate_panel(data_parsed, geo_data, parsed_query)
|
||||
if query.get("text") != "no" and parsed_query.get("text") != "no":
|
||||
output += textual_information(data_parsed, geo_data, parsed_query)
|
||||
return output
|
||||
|
||||
if __name__ == '__main__':
|
||||
166
lib/view/wttr.py
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
# vim: set encoding=utf-8
|
||||
# pylint: disable=wrong-import-position
|
||||
|
||||
"""
|
||||
Main view (wttr.in) implementation.
|
||||
The module is a wrapper for the modified Wego program.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import re
|
||||
|
||||
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
|
||||
|
||||
|
||||
def get_wetter(parsed_query):
|
||||
|
||||
location = parsed_query['location']
|
||||
html = parsed_query['html_output']
|
||||
lang = parsed_query['lang']
|
||||
|
||||
location_not_found = False
|
||||
if location == NOT_FOUND_LOCATION:
|
||||
location_not_found = True
|
||||
|
||||
stderr = ""
|
||||
returncode = 0
|
||||
if not location_not_found:
|
||||
stdout, stderr, returncode = _wego_wrapper(location, parsed_query)
|
||||
|
||||
if location_not_found or \
|
||||
(returncode != 0 \
|
||||
and ('Unable to find any matching weather'
|
||||
' location to the parsed_query submitted') in stderr):
|
||||
stdout, stderr, returncode = _wego_wrapper(NOT_FOUND_LOCATION, parsed_query)
|
||||
location_not_found = True
|
||||
stdout += get_message('NOT_FOUND_MESSAGE', lang)
|
||||
|
||||
first_line, stdout = _wego_postprocessing(location, parsed_query, stdout)
|
||||
|
||||
|
||||
if html:
|
||||
return _htmlize(stdout, first_line, parsed_query)
|
||||
return stdout
|
||||
|
||||
def _wego_wrapper(location, parsed_query):
|
||||
|
||||
lang = parsed_query['lang']
|
||||
location_name = parsed_query['override_location_name']
|
||||
|
||||
cmd = [WEGO, '--city=%s' % location]
|
||||
|
||||
if parsed_query.get('inverted_colors'):
|
||||
cmd += ['-inverse']
|
||||
|
||||
if parsed_query.get('use_ms_for_wind'):
|
||||
cmd += ['-wind_in_ms']
|
||||
|
||||
if parsed_query.get('narrow'):
|
||||
cmd += ['-narrow']
|
||||
|
||||
if lang and lang in SUPPORTED_LANGS:
|
||||
cmd += ['-lang=%s'%lang]
|
||||
|
||||
if parsed_query.get('use_imperial', False):
|
||||
cmd += ['-imperial']
|
||||
|
||||
if location_name:
|
||||
cmd += ['-location_name', location_name]
|
||||
|
||||
proc = Popen(cmd, stdout=PIPE, stderr=PIPE)
|
||||
stdout, stderr = proc.communicate()
|
||||
stdout = stdout.decode("utf-8")
|
||||
stderr = stderr.decode("utf-8")
|
||||
|
||||
return stdout, stderr, proc.returncode
|
||||
|
||||
def _wego_postprocessing(location, parsed_query, stdout):
|
||||
full_address = parsed_query['full_address']
|
||||
lang = parsed_query['lang']
|
||||
|
||||
if 'days' in parsed_query:
|
||||
if parsed_query['days'] == '0':
|
||||
stdout = "\n".join(stdout.splitlines()[:7]) + "\n"
|
||||
if parsed_query['days'] == '1':
|
||||
stdout = "\n".join(stdout.splitlines()[:17]) + "\n"
|
||||
if parsed_query['days'] == '2':
|
||||
stdout = "\n".join(stdout.splitlines()[:27]) + "\n"
|
||||
|
||||
|
||||
first = stdout.splitlines()[0]
|
||||
rest = stdout.splitlines()[1:]
|
||||
if parsed_query.get('no-caption', False):
|
||||
if ':' in first:
|
||||
first = first.split(":", 1)[1]
|
||||
stdout = "\n".join([first.strip()] + rest) + "\n"
|
||||
|
||||
if parsed_query.get('no-terminal', False):
|
||||
stdout = remove_ansi(stdout)
|
||||
|
||||
if parsed_query.get('no-city', False):
|
||||
stdout = "\n".join(stdout.splitlines()[2:]) + "\n"
|
||||
|
||||
if full_address \
|
||||
and parsed_query.get('format', 'txt') != 'png' \
|
||||
and (not parsed_query.get('no-city')
|
||||
and not parsed_query.get('no-caption')
|
||||
and not parsed_query.get('days') == '0'):
|
||||
line = "%s: %s [%s]\n" % (
|
||||
get_message('LOCATION', lang),
|
||||
full_address,
|
||||
location)
|
||||
stdout += line
|
||||
|
||||
if parsed_query.get('padding', False):
|
||||
lines = [x.rstrip() for x in stdout.splitlines()]
|
||||
max_l = max(len(remove_ansi(x)) for x in lines)
|
||||
last_line = " "*max_l + " .\n"
|
||||
stdout = " \n" + "\n".join(" %s " %x for x in lines) + "\n" + last_line
|
||||
|
||||
return first, stdout
|
||||
|
||||
def _htmlize(ansi_output, title, parsed_query):
|
||||
"""Return HTML representation of `ansi_output`.
|
||||
Use `title` as the title of the page.
|
||||
Format page according to query parameters from `parsed_query`."""
|
||||
|
||||
cmd = ["bash", ANSI2HTML, "--palette=solarized"]
|
||||
if not parsed_query.get('inverted_colors'):
|
||||
cmd += ["--bg=dark"]
|
||||
|
||||
proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
|
||||
stdout, stderr = proc.communicate(ansi_output.encode("utf-8"))
|
||||
stdout = stdout.decode("utf-8")
|
||||
stderr = stderr.decode("utf-8")
|
||||
if proc.returncode != 0:
|
||||
error(stdout + stderr)
|
||||
|
||||
if parsed_query.get('inverted_colors'):
|
||||
stdout = stdout.replace(
|
||||
'<body class="">', '<body class="" style="background:white;color:#777777">')
|
||||
|
||||
title = "<title>%s</title>" % title
|
||||
opengraph = _get_opengraph(parsed_query)
|
||||
stdout = re.sub("<head>", "<head>" + title + opengraph, stdout)
|
||||
return stdout
|
||||
|
||||
def _get_opengraph(parsed_query):
|
||||
"""Return OpenGraph data for `parsed_query`"""
|
||||
|
||||
url = parsed_query['request_url'] or ""
|
||||
pic_url = url.replace('?', '_')
|
||||
|
||||
return (
|
||||
'<meta property="og:image" content="%(pic_url)s_0pq.png" />'
|
||||
'<meta property="og:site_name" content="wttr.in" />'
|
||||
'<meta property="og:type" content="profile" />'
|
||||
'<meta property="og:url" content="%(url)s" />'
|
||||
) % {
|
||||
'pic_url': pic_url,
|
||||
'url': url,
|
||||
}
|
||||
247
lib/wttr.py
|
|
@ -1,247 +0,0 @@
|
|||
# vim: set encoding=utf-8
|
||||
|
||||
from __future__ import print_function
|
||||
import gevent
|
||||
from gevent.pywsgi import WSGIServer
|
||||
from gevent.queue import Queue
|
||||
from gevent.monkey import patch_all
|
||||
from gevent.subprocess import Popen, PIPE, STDOUT
|
||||
patch_all()
|
||||
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import dateutil.parser
|
||||
|
||||
from translations import get_message, FULL_TRANSLATION, PARTIAL_TRANSLATION, SUPPORTED_LANGS
|
||||
from globals import WEGO, PYPHOON, CACHEDIR, ANSI2HTML, \
|
||||
NOT_FOUND_LOCATION, DEFAULT_LOCATION, TEST_FILE, \
|
||||
log, error
|
||||
|
||||
def _is_invalid_location(location):
|
||||
if '.png' in location:
|
||||
return True
|
||||
|
||||
def remove_ansi(sometext):
|
||||
ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]')
|
||||
return ansi_escape.sub('', sometext)
|
||||
|
||||
def get_wetter(location, ip, html=False, lang=None, query=None, location_name=None, full_address=None, url=None):
|
||||
|
||||
local_url = url
|
||||
local_location = location
|
||||
|
||||
def get_opengraph():
|
||||
|
||||
if local_url is None:
|
||||
url = ""
|
||||
else:
|
||||
url = local_url.encode('utf-8')
|
||||
|
||||
if local_location is None:
|
||||
location = ""
|
||||
else:
|
||||
location = local_location.encode('utf-8')
|
||||
|
||||
pic_url = url.replace('?', '_')
|
||||
|
||||
return (
|
||||
'<meta property="og:image" content="%(pic_url)s_0pq.png" />'
|
||||
'<meta property="og:site_name" content="wttr.in" />'
|
||||
'<meta property="og:type" content="profile" />'
|
||||
'<meta property="og:url" content="%(url)s" />'
|
||||
) % {
|
||||
'pic_url': pic_url,
|
||||
'url': url,
|
||||
'location': location,
|
||||
}
|
||||
|
||||
# '<meta property="og:title" content="Weather report: %(location)s" />'
|
||||
# '<meta content="Partly cloudy // 6-8 °C // ↑ 9 km/h // 10 km // 0.4 mm" property="og:description" />'
|
||||
|
||||
def get_filename(location, lang=None, query=None, location_name=None):
|
||||
location = location.replace('/', '_')
|
||||
timestamp = time.strftime( "%Y%m%d%H", time.localtime() )
|
||||
|
||||
imperial_suffix = ''
|
||||
if query.get('use_imperial', False):
|
||||
imperial_suffix = '-imperial'
|
||||
|
||||
lang_suffix = ''
|
||||
if lang is not None:
|
||||
lang_suffix = '-lang_%s' % lang
|
||||
|
||||
if query != None:
|
||||
query_line = "_" + "_".join("%s=%s" % (key, value) for (key, value) in query.items())
|
||||
else:
|
||||
query_line = ""
|
||||
|
||||
if location_name is None:
|
||||
location_name = ""
|
||||
return "%s/%s/%s%s%s%s%s" % (CACHEDIR, location, timestamp, imperial_suffix, lang_suffix, query_line, location_name)
|
||||
|
||||
def save_weather_data(location, filename, lang=None, query=None, location_name=None, full_address=None):
|
||||
|
||||
if _is_invalid_location( location ):
|
||||
error("Invalid location: %s" % location)
|
||||
|
||||
NOT_FOUND_MESSAGE_HEADER = ""
|
||||
while True:
|
||||
location_not_found = False
|
||||
if location in [ "test-thunder" ]:
|
||||
test_name = location[5:]
|
||||
test_file = TEST_FILE.replace('NAME', test_name)
|
||||
stdout = open(test_file, 'r').read()
|
||||
stderr = ""
|
||||
break
|
||||
if location == NOT_FOUND_LOCATION:
|
||||
location_not_found = True
|
||||
location = DEFAULT_LOCATION
|
||||
|
||||
cmd = [WEGO, '--city=%s' % location]
|
||||
|
||||
if query.get('inverted_colors'):
|
||||
cmd += ['-inverse']
|
||||
|
||||
if query.get('use_ms_for_wind'):
|
||||
cmd += ['-wind_in_ms']
|
||||
|
||||
if query.get('narrow'):
|
||||
cmd += ['-narrow']
|
||||
|
||||
if lang and lang in SUPPORTED_LANGS:
|
||||
cmd += ['-lang=%s'%lang]
|
||||
|
||||
if query.get('use_imperial', False):
|
||||
cmd += ['-imperial']
|
||||
|
||||
if location_name:
|
||||
cmd += ['-location_name', location_name]
|
||||
|
||||
p = Popen(cmd, stdout=PIPE, stderr=PIPE)
|
||||
stdout, stderr = p.communicate()
|
||||
if p.returncode != 0:
|
||||
print("ERROR: location not found: %s" % location)
|
||||
if 'Unable to find any matching weather location to the query submitted' in stderr:
|
||||
if location != NOT_FOUND_LOCATION:
|
||||
NOT_FOUND_MESSAGE_HEADER = u"ERROR: %s: %s\n---\n\n" % (get_message('UNKNOWN_LOCATION', lang), location)
|
||||
location = NOT_FOUND_LOCATION
|
||||
continue
|
||||
error(stdout + stderr)
|
||||
break
|
||||
|
||||
dirname = os.path.dirname(filename)
|
||||
if not os.path.exists(dirname):
|
||||
os.makedirs(dirname)
|
||||
|
||||
if location_not_found:
|
||||
stdout += get_message('NOT_FOUND_MESSAGE', lang).encode('utf-8')
|
||||
stdout = NOT_FOUND_MESSAGE_HEADER.encode('utf-8') + stdout
|
||||
|
||||
if 'days' in query:
|
||||
if query['days'] == '0':
|
||||
stdout = "\n".join(stdout.splitlines()[:7]) + "\n"
|
||||
if query['days'] == '1':
|
||||
stdout = "\n".join(stdout.splitlines()[:17]) + "\n"
|
||||
if query['days'] == '2':
|
||||
stdout = "\n".join(stdout.splitlines()[:27]) + "\n"
|
||||
|
||||
first = stdout.splitlines()[0].decode('utf-8')
|
||||
rest = stdout.splitlines()[1:]
|
||||
if query.get('no-caption', False):
|
||||
|
||||
separator = None
|
||||
if ':' in first:
|
||||
separator = ':'
|
||||
if u':' in first:
|
||||
separator = u':'
|
||||
|
||||
if separator:
|
||||
first = first.split(separator,1)[1]
|
||||
stdout = "\n".join([first.strip().encode('utf-8')] + rest) + "\n"
|
||||
|
||||
if query.get('no-terminal', False):
|
||||
stdout = remove_ansi(stdout)
|
||||
|
||||
if query.get('no-city', False):
|
||||
stdout = "\n".join(stdout.splitlines()[2:]) + "\n"
|
||||
|
||||
if full_address \
|
||||
and query.get('format', 'txt') != 'png' \
|
||||
and (not query.get('no-city')
|
||||
and not query.get('no-caption')
|
||||
and not query.get('days') == '0'):
|
||||
line = "%s: %s [%s]\n" % (
|
||||
get_message('LOCATION', lang).encode('utf-8'),
|
||||
full_address.encode('utf-8'),
|
||||
location.encode('utf-8'))
|
||||
stdout += line
|
||||
|
||||
if query.get('padding', False):
|
||||
lines = [x.rstrip() for x in stdout.splitlines()]
|
||||
max_l = max(len(remove_ansi(x).decode('utf8')) for x in lines)
|
||||
last_line = " "*max_l + " .\n"
|
||||
stdout = " \n" + "\n".join(" %s " %x for x in lines) + "\n" + last_line
|
||||
|
||||
open(filename, 'w').write(stdout)
|
||||
|
||||
cmd = ["bash", ANSI2HTML, "--palette=solarized"]
|
||||
if not query.get('inverted_colors'):
|
||||
cmd += ["--bg=dark"]
|
||||
|
||||
p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE )
|
||||
stdout, stderr = p.communicate(stdout)
|
||||
if p.returncode != 0:
|
||||
error(stdout + stderr)
|
||||
|
||||
if query.get('inverted_colors'):
|
||||
stdout = stdout.replace('<body class="">', '<body class="" style="background:white;color:#777777">')
|
||||
|
||||
title = "<title>%s</title>" % first.encode('utf-8')
|
||||
opengraph = get_opengraph()
|
||||
stdout = re.sub("<head>", "<head>" + title + opengraph, stdout)
|
||||
open(filename+'.html', 'w').write(stdout)
|
||||
|
||||
filename = get_filename(location, lang=lang, query=query, location_name=location_name)
|
||||
if not os.path.exists(filename):
|
||||
save_weather_data(location, filename, lang=lang, query=query, location_name=location_name, full_address=full_address)
|
||||
if html:
|
||||
filename += '.html'
|
||||
|
||||
return open(filename).read()
|
||||
|
||||
def get_moon(location, html=False, lang=None, query=None):
|
||||
if query is None:
|
||||
query = {}
|
||||
|
||||
date = None
|
||||
if '@' in location:
|
||||
date = location[location.index('@')+1:]
|
||||
location = location[:location.index('@')]
|
||||
|
||||
cmd = [PYPHOON]
|
||||
if date:
|
||||
try:
|
||||
dateutil.parser.parse(date)
|
||||
except Exception as e:
|
||||
print("ERROR: %s" % e)
|
||||
else:
|
||||
cmd += [date]
|
||||
|
||||
env = os.environ.copy()
|
||||
if lang:
|
||||
env['LANG'] = lang
|
||||
p = Popen(cmd, stdout=PIPE, stderr=PIPE, env=env)
|
||||
stdout = p.communicate()[0]
|
||||
|
||||
if query.get('no-terminal', False):
|
||||
stdout = remove_ansi(stdout)
|
||||
|
||||
if html:
|
||||
p = Popen(["bash", ANSI2HTML, "--palette=solarized", "--bg=dark"], stdin=PIPE, stdout=PIPE, stderr=PIPE)
|
||||
stdout, stderr = p.communicate(stdout)
|
||||
if p.returncode != 0:
|
||||
error(stdout + stderr)
|
||||
|
||||
return stdout
|
||||
|
||||
343
lib/wttr_srv.py
|
|
@ -6,15 +6,18 @@ Main wttr.in rendering function implementation
|
|||
"""
|
||||
|
||||
import logging
|
||||
import io
|
||||
import os
|
||||
import time
|
||||
from gevent.threadpool import ThreadPool
|
||||
from flask import render_template, send_file, make_response
|
||||
|
||||
import wttrin_png
|
||||
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, log, \
|
||||
from globals import get_help_file, \
|
||||
BASH_FUNCTION_FILE, TRANSLATION_FILE, LOG_FILE, \
|
||||
NOT_FOUND_LOCATION, \
|
||||
MALFORMED_RESPONSE_HTML_PAGE, \
|
||||
|
|
@ -22,17 +25,20 @@ from globals import get_help_file, log, \
|
|||
MY_EXTERNAL_IP, QUERY_LIMITS
|
||||
from location import is_location_blocked, location_processing
|
||||
from limits import Limits
|
||||
from wttr import get_wetter, get_moon
|
||||
from wttr_line import wttr_line
|
||||
from view.wttr import get_wetter
|
||||
from view.moon import get_moon
|
||||
from view.line import wttr_line
|
||||
|
||||
import cache
|
||||
|
||||
if not os.path.exists(os.path.dirname(LOG_FILE)):
|
||||
os.makedirs(os.path.dirname(LOG_FILE))
|
||||
logging.basicConfig(filename=LOG_FILE, level=logging.DEBUG, format='%(asctime)s %(message)s')
|
||||
logging.basicConfig(filename=LOG_FILE, level=logging.INFO, format='%(asctime)s %(message)s')
|
||||
|
||||
LIMITS = Limits(whitelist=[MY_EXTERNAL_IP], limits=QUERY_LIMITS)
|
||||
|
||||
TASKS = ThreadPool(25)
|
||||
|
||||
def show_text_file(name, lang):
|
||||
"""
|
||||
show static file `name` for `lang`
|
||||
|
|
@ -51,12 +57,10 @@ def show_text_file(name, lang):
|
|||
text = text\
|
||||
.replace('NUMBER_OF_LANGUAGES', str(len(SUPPORTED_LANGS)))\
|
||||
.replace('SUPPORTED_LANGUAGES', ' '.join(SUPPORTED_LANGS))
|
||||
return text.decode('utf-8')
|
||||
return text
|
||||
|
||||
def client_ip_address(request):
|
||||
"""
|
||||
Return client ip address for `request`.
|
||||
Flask related
|
||||
def _client_ip_address(request):
|
||||
"""Return client ip address for flask `request`.
|
||||
"""
|
||||
|
||||
if request.headers.getlist("X-PNG-Query-For"):
|
||||
|
|
@ -112,55 +116,56 @@ def _parse_language_header(header):
|
|||
elif lang == 'en':
|
||||
yield None, lang_tuple[1]
|
||||
try:
|
||||
return max(supported_langs(), key=lambda lang_tuple:lang_tuple[1])[0]
|
||||
return max(supported_langs(), key=lambda lang_tuple: lang_tuple[1])[0]
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
return _find_supported_language(_parse_accept_language(header))
|
||||
|
||||
def get_answer_language_and_format(request):
|
||||
def get_answer_language_and_view(request):
|
||||
"""
|
||||
Return preferred answer language based on
|
||||
domain name, query arguments and headers
|
||||
"""
|
||||
|
||||
lang = None
|
||||
fmt = None
|
||||
view_name = None
|
||||
hostname = request.headers['Host']
|
||||
if hostname != 'wttr.in' and hostname.endswith('.wttr.in'):
|
||||
lang = hostname[:-8]
|
||||
if lang == "v2":
|
||||
fmt = "v2"
|
||||
if lang.startswith("v2"):
|
||||
view_name = lang
|
||||
lang = None
|
||||
|
||||
if 'lang' in request.args:
|
||||
lang = request.args.get('lang')
|
||||
if lang.lower() == 'none':
|
||||
lang = None
|
||||
|
||||
header_accept_language = request.headers.get('Accept-Language', '')
|
||||
if lang is None and header_accept_language:
|
||||
lang = _parse_language_header(header_accept_language)
|
||||
|
||||
return lang, fmt
|
||||
return lang, view_name
|
||||
|
||||
def get_output_format(request, query):
|
||||
def get_output_format(query, parsed_query):
|
||||
"""
|
||||
Return preferred output format: ansi, text, html or png
|
||||
based on arguments and headers in `request`.
|
||||
Return new location (can be rewritten)
|
||||
"""
|
||||
|
||||
if 'format' in query:
|
||||
return False
|
||||
# FIXME
|
||||
user_agent = request.headers.get('User-Agent', '').lower()
|
||||
if query.get('force-ansi'):
|
||||
if ('view' in query and not query["view"].startswith("v2")) \
|
||||
or parsed_query.get("png_filename") \
|
||||
or query.get('force-ansi'):
|
||||
return False
|
||||
|
||||
user_agent = parsed_query.get("user_agent", "").lower()
|
||||
html_output = not any(agent in user_agent for agent in PLAIN_TEXT_AGENTS)
|
||||
return html_output
|
||||
|
||||
def cyclic_location_selection(locations, period):
|
||||
"""
|
||||
Return one of `locations` (: separated list)
|
||||
def _cyclic_location_selection(locations, period):
|
||||
"""Return one of `locations` (: separated list)
|
||||
basing on the current time and query interval `period`
|
||||
"""
|
||||
|
||||
|
|
@ -173,24 +178,164 @@ def cyclic_location_selection(locations, period):
|
|||
except ValueError:
|
||||
period = 1
|
||||
|
||||
index = int(time.time())/period % len(locations)
|
||||
index = int(time.time()/period) % len(locations)
|
||||
return locations[index]
|
||||
|
||||
|
||||
def wttr(location, request):
|
||||
def _response(parsed_query, query, fast_mode=False):
|
||||
"""Create response text based on `parsed_query` and `query` data.
|
||||
If `fast_mode` is True, process only requests that can
|
||||
be handled very fast (cached and static files).
|
||||
"""
|
||||
Main rendering function, it processes incoming weather queries.
|
||||
Depending on user agent it returns output in HTML or ANSI format.
|
||||
|
||||
answer = None
|
||||
cache_signature = cache.get_signature(
|
||||
parsed_query["user_agent"],
|
||||
parsed_query["request_url"],
|
||||
parsed_query["ip_addr"],
|
||||
parsed_query["lang"])
|
||||
answer = cache.get(cache_signature)
|
||||
|
||||
if parsed_query['orig_location'] in PLAIN_TEXT_PAGES:
|
||||
answer = show_text_file(parsed_query['orig_location'], parsed_query['lang'])
|
||||
if parsed_query['html_output']:
|
||||
answer = render_template('index.html', body=answer)
|
||||
|
||||
if answer or fast_mode:
|
||||
return answer
|
||||
|
||||
# at this point, we could not handle the query fast,
|
||||
# so we handle it with all available logic
|
||||
loc = (parsed_query['orig_location'] or "").lower()
|
||||
if parsed_query.get("view"):
|
||||
output = wttr_line(query, parsed_query)
|
||||
elif loc == 'moon' or loc.startswith('moon@'):
|
||||
output = get_moon(parsed_query)
|
||||
else:
|
||||
output = get_wetter(parsed_query)
|
||||
|
||||
if parsed_query.get('png_filename'):
|
||||
# originally it was just a usual function call,
|
||||
# but it was a blocking call, so it was moved
|
||||
# to separate threads:
|
||||
#
|
||||
# output = fmt.png.render_ansi(
|
||||
# output, options=parsed_query)
|
||||
result = TASKS.spawn(fmt.png.render_ansi, output, options=parsed_query)
|
||||
output = result.get()
|
||||
else:
|
||||
if query.get('days', '3') != '0' \
|
||||
and not query.get('no-follow-line') \
|
||||
and ((parsed_query.get("view") or "v2")[:2] in ["v2"]):
|
||||
if parsed_query['html_output']:
|
||||
output = add_buttons(output)
|
||||
else:
|
||||
output += '\n' + get_message('FOLLOW_ME', parsed_query['lang']) + '\n'
|
||||
|
||||
return cache.store(cache_signature, output)
|
||||
|
||||
def parse_request(location, request, query, fast_mode=False):
|
||||
"""Parse request and provided extended information for the query,
|
||||
including location data, language, output format, view, etc.
|
||||
|
||||
Incoming data:
|
||||
request.args
|
||||
request.headers
|
||||
request.remote_addr
|
||||
request.referrer
|
||||
request.query_string
|
||||
|
||||
`location` location name extracted from the query url
|
||||
`request.args`
|
||||
`request.headers`
|
||||
`request.remote_addr`
|
||||
`request.referrer`
|
||||
`request.query_string`
|
||||
`query` parsed command line arguments
|
||||
|
||||
Parameters priorities (from low to high):
|
||||
|
||||
* HTTP-header
|
||||
* Domain name
|
||||
* URL
|
||||
* Filename
|
||||
|
||||
Return: dictionary with parsed parameters
|
||||
"""
|
||||
|
||||
def _wrap_response(response_text, html_output):
|
||||
if location and location.startswith("b_"):
|
||||
result = parse_query.deserialize(location)
|
||||
result["request_url"] = request.url
|
||||
if result:
|
||||
return result
|
||||
|
||||
png_filename = None
|
||||
if location is not None and location.lower().endswith(".png"):
|
||||
png_filename = location
|
||||
location = location[:-4]
|
||||
if location and ':' in location and location[0] != ":":
|
||||
location = _cyclic_location_selection(location, query.get('period', 1))
|
||||
|
||||
parsed_query = {
|
||||
'ip_addr': _client_ip_address(request),
|
||||
'user_agent': request.headers.get('User-Agent', '').lower(),
|
||||
'request_url': request.url,
|
||||
}
|
||||
|
||||
if png_filename:
|
||||
parsed_query["png_filename"] = png_filename
|
||||
parsed_query.update(parse_query.parse_wttrin_png_name(png_filename))
|
||||
|
||||
lang, _view = get_answer_language_and_view(request)
|
||||
|
||||
parsed_query["view"] = parsed_query.get("view", query.get("view", _view))
|
||||
parsed_query["location"] = parsed_query.get("location", location)
|
||||
parsed_query["orig_location"] = parsed_query["location"]
|
||||
parsed_query["lang"] = parsed_query.get("lang", lang)
|
||||
|
||||
parsed_query["html_output"] = get_output_format(query, parsed_query)
|
||||
|
||||
if not fast_mode: # not png_filename and not fast_mode:
|
||||
location, override_location_name, full_address, country, query_source_location = \
|
||||
location_processing(parsed_query["location"], parsed_query["ip_addr"])
|
||||
|
||||
us_ip = query_source_location[1] == 'United States' \
|
||||
and 'slack' not in parsed_query['user_agent']
|
||||
query = parse_query.metric_or_imperial(query, lang, us_ip=us_ip)
|
||||
|
||||
if country and location != NOT_FOUND_LOCATION:
|
||||
location = "%s,%s" % (location, country)
|
||||
|
||||
parsed_query.update({
|
||||
'location': location,
|
||||
'override_location_name': override_location_name,
|
||||
'full_address': full_address,
|
||||
'country': country,
|
||||
'query_source_location': query_source_location})
|
||||
|
||||
parsed_query.update(query)
|
||||
return parsed_query
|
||||
|
||||
|
||||
def wttr(location, request):
|
||||
"""Main rendering function, it processes incoming weather queries,
|
||||
and depending on the User-Agent string and other paramters of the query
|
||||
it returns output in HTML, ANSI or other format.
|
||||
"""
|
||||
|
||||
def _wrap_response(response_text, html_output, png_filename=None):
|
||||
if not isinstance(response_text, str) and \
|
||||
not isinstance(response_text, bytes):
|
||||
return response_text
|
||||
|
||||
if png_filename:
|
||||
response = make_response(send_file(
|
||||
io.BytesIO(response_text),
|
||||
attachment_filename=png_filename,
|
||||
mimetype='image/png'))
|
||||
|
||||
for key, value in {
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0',
|
||||
}.items():
|
||||
response.headers[key] = value
|
||||
else:
|
||||
response = make_response(response_text)
|
||||
response.mimetype = 'text/html' if html_output else 'text/plain'
|
||||
return response
|
||||
|
|
@ -198,119 +343,37 @@ def wttr(location, request):
|
|||
if is_location_blocked(location):
|
||||
return ""
|
||||
|
||||
ip_addr = client_ip_address(request)
|
||||
|
||||
try:
|
||||
LIMITS.check_ip(ip_addr)
|
||||
LIMITS.check_ip(_client_ip_address(request))
|
||||
except RuntimeError as exception:
|
||||
return str(exception)
|
||||
|
||||
png_filename = None
|
||||
if location is not None and location.lower().endswith(".png"):
|
||||
png_filename = location
|
||||
location = location[:-4]
|
||||
|
||||
lang, fmt = get_answer_language_and_format(request)
|
||||
query = parse_query.parse_query(request.args)
|
||||
html_output = get_output_format(request, query)
|
||||
user_agent = request.headers.get('User-Agent', '').lower()
|
||||
|
||||
# generating cache signature
|
||||
cache_signature = cache.get_signature(user_agent, request.url, ip_addr, lang)
|
||||
answer = cache.get(cache_signature)
|
||||
if answer:
|
||||
return _wrap_response(answer, html_output)
|
||||
|
||||
if location in PLAIN_TEXT_PAGES:
|
||||
help_ = show_text_file(location, lang)
|
||||
if html_output:
|
||||
return _wrap_response(render_template('index.html', body=help_), html_output)
|
||||
return _wrap_response(help_, html_output)
|
||||
|
||||
if location and ':' in location:
|
||||
location = cyclic_location_selection(location, query.get('period', 1))
|
||||
|
||||
orig_location = location
|
||||
|
||||
if not png_filename:
|
||||
location, override_location_name, full_address, country, query_source_location = \
|
||||
location_processing(location, ip_addr)
|
||||
|
||||
us_ip = query_source_location[1] == 'United States' and 'slack' not in user_agent
|
||||
query = parse_query.metric_or_imperial(query, lang, us_ip=us_ip)
|
||||
|
||||
# logging query
|
||||
orig_location_utf8 = (orig_location or "").encode('utf-8')
|
||||
location_utf8 = location.encode('utf-8')
|
||||
use_imperial = query.get('use_imperial', False)
|
||||
log(" ".join(map(str,
|
||||
[ip_addr, user_agent, orig_location_utf8, location_utf8, use_imperial, lang])))
|
||||
|
||||
if country and location != NOT_FOUND_LOCATION:
|
||||
location = "%s,%s" % (location, country)
|
||||
|
||||
# We are ready to return the answer
|
||||
# first, we try to process the query as fast as possible
|
||||
# (using the cache and static files),
|
||||
# and only if "fast_mode" was unsuccessful,
|
||||
# use the full track
|
||||
parsed_query = parse_request(location, request, query, fast_mode=True)
|
||||
response = _response(parsed_query, query, fast_mode=True)
|
||||
try:
|
||||
if fmt or 'format' in query:
|
||||
response_text = wttr_line(
|
||||
location, override_location_name, full_address, query, lang, fmt)
|
||||
fmt = fmt or query.get('format')
|
||||
response_text = cache.store(cache_signature, response_text)
|
||||
|
||||
return _wrap_response(
|
||||
response_text,
|
||||
html_output)
|
||||
|
||||
if png_filename:
|
||||
options = {
|
||||
'ip_addr': ip_addr,
|
||||
'lang': lang,
|
||||
'location': location}
|
||||
options.update(query)
|
||||
|
||||
cached_png_file = wttrin_png.make_wttr_in_png(png_filename, options=options)
|
||||
response = make_response(send_file(cached_png_file,
|
||||
attachment_filename=png_filename,
|
||||
mimetype='image/png'))
|
||||
for key, value in {
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0',
|
||||
}.items():
|
||||
response.headers[key] = value
|
||||
|
||||
# Trying to disable github caching
|
||||
return response
|
||||
|
||||
if orig_location and (orig_location.lower() == 'moon' or orig_location.lower().startswith('moon@')):
|
||||
output = get_moon(orig_location, html=html_output, lang=lang, query=query)
|
||||
else:
|
||||
output = get_wetter(location, ip_addr,
|
||||
html=html_output,
|
||||
lang=lang,
|
||||
query=query,
|
||||
location_name=override_location_name,
|
||||
full_address=full_address,
|
||||
url=request.url,
|
||||
)
|
||||
|
||||
if query.get('days', '3') != '0' and not query.get('no-follow-line'):
|
||||
if html_output:
|
||||
output = add_buttons(output)
|
||||
else:
|
||||
#output += '\n' + get_message('NEW_FEATURE', lang).encode('utf-8')
|
||||
output += '\n' + get_message('FOLLOW_ME', lang).encode('utf-8') + '\n'
|
||||
|
||||
return _wrap_response(output, html_output)
|
||||
|
||||
if not response:
|
||||
parsed_query = parse_request(location, request, query)
|
||||
response = _response(parsed_query, query)
|
||||
# pylint: disable=broad-except
|
||||
except Exception as exception:
|
||||
# if 'Malformed response' in str(exception) \
|
||||
# or 'API key has reached calls per day allowed limit' in str(exception):
|
||||
if html_output:
|
||||
return _wrap_response(MALFORMED_RESPONSE_HTML_PAGE, html_output)
|
||||
return _wrap_response(get_message('CAPACITY_LIMIT_REACHED', lang).encode('utf-8'), html_output)
|
||||
# logging.error("Exception has occured", exc_info=1)
|
||||
# return "ERROR"
|
||||
logging.error("Exception has occured", exc_info=1)
|
||||
if parsed_query['html_output']:
|
||||
response = MALFORMED_RESPONSE_HTML_PAGE
|
||||
else:
|
||||
response = get_message('CAPACITY_LIMIT_REACHED', parsed_query['lang'])
|
||||
|
||||
# if exception is occured, we return not a png file but text
|
||||
if "png_filename" in parsed_query:
|
||||
del parsed_query["png_filename"]
|
||||
return _wrap_response(
|
||||
response, parsed_query['html_output'],
|
||||
png_filename=parsed_query.get('png_filename'))
|
||||
|
||||
if __name__ == "__main__":
|
||||
import doctest
|
||||
|
|
|
|||
|
|
@ -1,323 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
#vim: encoding=utf-8
|
||||
|
||||
from __future__ import print_function
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
|
||||
"""
|
||||
This module is used to generate png-files for wttr.in queries.
|
||||
The only exported function is:
|
||||
|
||||
make_wttr_in_png(filename)
|
||||
|
||||
in filename (in the shortname) is coded the weather query.
|
||||
The function saves the weather report in the file and returns None.
|
||||
"""
|
||||
|
||||
import requests
|
||||
|
||||
from PIL import Image, ImageFont, ImageDraw
|
||||
import pyte.screens
|
||||
|
||||
# downloaded from https://gist.github.com/2204527
|
||||
# described/recommended here:
|
||||
#
|
||||
# http://stackoverflow.com/questions/9868792/find-out-the-unicode-script-of-a-character
|
||||
#
|
||||
import unicodedata2
|
||||
|
||||
MYDIR = os.path.abspath(os.path.dirname(os.path.dirname('__file__')))
|
||||
sys.path.append("%s/lib/" % MYDIR)
|
||||
import parse_query
|
||||
|
||||
from globals import PNG_CACHE, log
|
||||
|
||||
COLS = 180
|
||||
ROWS = 100
|
||||
|
||||
CHAR_WIDTH = 7
|
||||
CHAR_HEIGHT = 14
|
||||
|
||||
# How to find font for non-standard scripts:
|
||||
#
|
||||
# $ fc-list :lang=ja
|
||||
|
||||
FONT_SIZE = 12
|
||||
FONT_CAT = {
|
||||
'default': "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
|
||||
'Cyrillic': "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
|
||||
'Greek': "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
|
||||
'Arabic': "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
|
||||
'Hebrew': "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
|
||||
'Han': "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
|
||||
'Hiragana': "/usr/share/fonts/truetype/motoya-l-cedar/MTLc3m.ttf",
|
||||
'Katakana': "/usr/share/fonts/truetype/motoya-l-cedar/MTLc3m.ttf",
|
||||
'Hangul': "/usr/share/fonts/truetype/lexi/LexiGulim.ttf",
|
||||
}
|
||||
|
||||
def color_mapping(color):
|
||||
"""
|
||||
Convert pyte color to PIL color
|
||||
"""
|
||||
if color == 'default':
|
||||
return 'lightgray'
|
||||
if color in ['green', 'black', 'cyan', 'blue', 'brown']:
|
||||
return color
|
||||
try:
|
||||
return (int(color[0:2], 16), int(color[2:4], 16), int(color[4:6], 16))
|
||||
except:
|
||||
# if we do not know this color and it can not be decoded as RGB,
|
||||
# print it and return it as it is (will be displayed as black)
|
||||
# print color
|
||||
return color
|
||||
return color
|
||||
|
||||
def strip_buf(buf):
|
||||
"""
|
||||
Strips empty spaces from behind and from the right side.
|
||||
(from the right side is not yet implemented)
|
||||
"""
|
||||
def empty_line(line):
|
||||
"Returns True if the line consists from spaces"
|
||||
return all(x.data == ' ' for x in line)
|
||||
|
||||
def line_len(line):
|
||||
"Returns len of the line excluding spaces from the right"
|
||||
|
||||
last_pos = len(line)
|
||||
while last_pos > 0 and line[last_pos-1].data == ' ':
|
||||
last_pos -= 1
|
||||
return last_pos
|
||||
|
||||
number_of_lines = 0
|
||||
for line in buf[::-1]:
|
||||
if not empty_line(line):
|
||||
break
|
||||
number_of_lines += 1
|
||||
|
||||
if number_of_lines:
|
||||
buf = buf[:-number_of_lines]
|
||||
|
||||
max_len = max(line_len(x) for x in buf)
|
||||
buf = [line[:max_len] for line in buf]
|
||||
|
||||
return buf
|
||||
|
||||
def script_category(char):
|
||||
"""
|
||||
Returns category of a Unicode character
|
||||
Possible values:
|
||||
default, Cyrillic, Greek, Han, Hiragana
|
||||
"""
|
||||
cat = unicodedata2.script_cat(char)[0]
|
||||
if char == u':':
|
||||
return 'Han'
|
||||
if cat in ['Latin', 'Common']:
|
||||
return 'default'
|
||||
else:
|
||||
return cat
|
||||
|
||||
def gen_term(filename, buf, options=None):
|
||||
buf = strip_buf(buf)
|
||||
cols = max(len(x) for x in buf)
|
||||
rows = len(buf)
|
||||
|
||||
image = Image.new('RGB', (cols * CHAR_WIDTH, rows * CHAR_HEIGHT))
|
||||
|
||||
buf = buf[-ROWS:]
|
||||
|
||||
draw = ImageDraw.Draw(image)
|
||||
font = {}
|
||||
for cat in FONT_CAT:
|
||||
font[cat] = ImageFont.truetype(FONT_CAT[cat], FONT_SIZE)
|
||||
|
||||
x_pos = 0
|
||||
y_pos = 0
|
||||
for line in buf:
|
||||
x_pos = 0
|
||||
for char in line:
|
||||
current_color = color_mapping(char.fg)
|
||||
if char.bg != 'default':
|
||||
draw.rectangle(
|
||||
((x_pos, y_pos),
|
||||
(x_pos+CHAR_WIDTH, y_pos+CHAR_HEIGHT)),
|
||||
fill=color_mapping(char.bg))
|
||||
|
||||
if char.data:
|
||||
cat = script_category(char.data)
|
||||
if cat not in font:
|
||||
log("Unknown font category: %s" % cat)
|
||||
draw.text(
|
||||
(x_pos, y_pos),
|
||||
char.data,
|
||||
font=font.get(cat, font.get('default')),
|
||||
fill=current_color)
|
||||
#sys.stdout.write(c.data)
|
||||
|
||||
x_pos += CHAR_WIDTH
|
||||
y_pos += CHAR_HEIGHT
|
||||
#sys.stdout.write('\n')
|
||||
|
||||
if 'transparency' in options:
|
||||
transparency = options.get('transparency', '255')
|
||||
try:
|
||||
transparency = int(transparency)
|
||||
except:
|
||||
transparceny = 255
|
||||
|
||||
if transparency < 0:
|
||||
transparency = 0
|
||||
|
||||
if transparency > 255:
|
||||
transparency = 255
|
||||
|
||||
image = image.convert("RGBA")
|
||||
datas = image.getdata()
|
||||
|
||||
new_data = []
|
||||
for item in datas:
|
||||
new_item = tuple(list(item[:3]) + [transparency])
|
||||
new_data.append(new_item)
|
||||
|
||||
image.putdata(new_data)
|
||||
|
||||
|
||||
image.save(filename)
|
||||
|
||||
def typescript_to_one_frame(png_file, text, options=None):
|
||||
"""
|
||||
Render text (terminal sequence) in png_file
|
||||
"""
|
||||
|
||||
# fixing some broken characters because of bug #... in pyte 6.0
|
||||
text = text.replace('Н', 'H').replace('Ν', 'N')
|
||||
|
||||
screen = pyte.screens.Screen(COLS, ROWS)
|
||||
#screen.define_charset("B", "(")
|
||||
|
||||
stream = pyte.streams.ByteStream()
|
||||
stream.attach(screen)
|
||||
|
||||
stream.feed(text)
|
||||
|
||||
buf = sorted(screen.buffer.items(), key=lambda x: x[0])
|
||||
buf = [[x[1] for x in sorted(line[1].items(), key=lambda x: x[0])] for line in buf]
|
||||
|
||||
gen_term(png_file, buf, options=options)
|
||||
|
||||
#
|
||||
# wttr.in related functions
|
||||
#
|
||||
|
||||
def parse_wttrin_png_name(name):
|
||||
"""
|
||||
Parse the PNG filename and return the result as a dictionary.
|
||||
For example:
|
||||
input = City_200x_lang=ru.png
|
||||
output = {
|
||||
"lang": "ru",
|
||||
"width": "200",
|
||||
"filetype": "png",
|
||||
"location": "City"
|
||||
}
|
||||
"""
|
||||
|
||||
parsed = {}
|
||||
to_be_parsed = {}
|
||||
|
||||
if name.lower()[-4:] == '.png':
|
||||
parsed['filetype'] = 'png'
|
||||
name = name[:-4]
|
||||
|
||||
parts = name.split('_')
|
||||
parsed['location'] = parts[0]
|
||||
|
||||
for part in parts[1:]:
|
||||
if re.match('(?:[0-9]+)x', part):
|
||||
parsed['width'] = part[:-1]
|
||||
elif re.match('x(?:[0-9]+)', part):
|
||||
parsed['height'] = part[1:]
|
||||
elif re.match(part, '(?:[0-9]+)x(?:[0-9]+)'):
|
||||
parsed['width'], parsed['height'] = part.split('x', 1)
|
||||
elif '=' in part:
|
||||
arg, val = part.split('=', 1)
|
||||
to_be_parsed[arg] = val
|
||||
else:
|
||||
to_be_parsed[part] = ''
|
||||
|
||||
parsed.update(parse_query.parse_query(to_be_parsed))
|
||||
|
||||
return parsed
|
||||
|
||||
def make_wttrin_query(parsed):
|
||||
"""
|
||||
Convert parsed data into query name
|
||||
"""
|
||||
|
||||
for key in ['width', 'height', 'filetype']:
|
||||
if key in parsed:
|
||||
del parsed[key]
|
||||
|
||||
location = parsed['location']
|
||||
del parsed['location']
|
||||
|
||||
args = []
|
||||
if 'options' in parsed:
|
||||
args = [parsed['options']]
|
||||
del parsed['options']
|
||||
else:
|
||||
args = []
|
||||
|
||||
for key, val in parsed.items():
|
||||
args.append('%s=%s' % (key, val))
|
||||
|
||||
args.append('filetype=png')
|
||||
|
||||
url = "http://wttr.in/%s" % location
|
||||
if args != []:
|
||||
url += "?%s" % ("&".join(args))
|
||||
|
||||
return url
|
||||
|
||||
|
||||
def make_wttr_in_png(png_name, options=None):
|
||||
"""
|
||||
The function saves the weather report in the file and returns None.
|
||||
The weather query is coded in filename (in the shortname).
|
||||
"""
|
||||
|
||||
parsed = parse_wttrin_png_name(png_name)
|
||||
|
||||
# if location is MyLocation it should be overriden
|
||||
# with autodetected location (from options)
|
||||
if parsed.get('location', 'MyLocation') == 'MyLocation' or not parsed.get('location', ''):
|
||||
del parsed['location']
|
||||
|
||||
if options is not None:
|
||||
for key, val in options.items():
|
||||
if key not in parsed:
|
||||
parsed[key] = val
|
||||
url = make_wttrin_query(parsed)
|
||||
|
||||
timestamp = time.strftime("%Y%m%d%H", time.localtime())
|
||||
cached_basename = url[14:].replace('/','_')
|
||||
|
||||
cached_png_file = "%s/%s/%s.png" % (PNG_CACHE, timestamp, cached_basename)
|
||||
|
||||
dirname = os.path.dirname(cached_png_file)
|
||||
if not os.path.exists(dirname):
|
||||
os.makedirs(dirname)
|
||||
|
||||
if os.path.exists(cached_png_file):
|
||||
return cached_png_file
|
||||
|
||||
headers = {'X-PNG-Query-For': options.get('ip_addr', '1.1.1.1')}
|
||||
text = requests.get(url, headers=headers).text.replace('\n', '\r\n')
|
||||
curl_output = text.encode('utf-8')
|
||||
|
||||
typescript_to_one_frame(cached_png_file, curl_output, options=parsed)
|
||||
|
||||
return cached_png_file
|
||||
|
|
@ -8,14 +8,17 @@ pylint
|
|||
cyrtranslit
|
||||
astral
|
||||
timezonefinder==2.1.2
|
||||
Pillow
|
||||
pyte
|
||||
python-dateutil
|
||||
diagram
|
||||
pyjq
|
||||
scipy
|
||||
numpy
|
||||
pillow
|
||||
babel
|
||||
pylru
|
||||
pysocks
|
||||
supervisor
|
||||
numba
|
||||
emoji
|
||||
grapheme
|
||||
|
|
|
|||
BIN
share/emoji/☀️.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
share/emoji/☁️.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
share/emoji/⛅️.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
share/emoji/⛈.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
share/emoji/✨.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
share/emoji/❄️.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
share/emoji/🌑.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
share/emoji/🌒.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
share/emoji/🌓.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
share/emoji/🌔.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
share/emoji/🌕.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
share/emoji/🌖.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
share/emoji/🌗.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
share/emoji/🌘.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
share/emoji/🌦.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
share/emoji/🌧.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
share/emoji/🌨.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
share/emoji/🌩.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
share/emoji/🌫.png
Normal file
|
After Width: | Height: | Size: 725 B |
BIN
share/static/example-tmux-status-line.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
|
@ -10,6 +10,7 @@ Translated/improved/corrected by:
|
|||
Grigor Khachatryan @grigortw
|
||||
* Azerbaijani: Dmytro Nikitiuk, Elsevar Abbasov,
|
||||
Eldar Velibekov (@welibekov on github)
|
||||
* Basque: Iker Sagasti (@isagasti on github)
|
||||
* Belarusian: Igor Chubin, Anton Zhavoronkov @edogby (on github)
|
||||
* Bosnian: Ismar Kunc @ismarkunc
|
||||
* Bulgarian: Vladimir Vitkov @zeridon (on github)
|
||||
|
|
|
|||
|
|
@ -303,6 +303,7 @@ var (
|
|||
"da": "da_DK",
|
||||
"de": "de_DE",
|
||||
"el": "el_GR",
|
||||
"eu": "eu_ES",
|
||||
"eo": "eo",
|
||||
"es": "es_ES",
|
||||
"et": "et_EE",
|
||||
|
|
@ -423,7 +424,7 @@ var (
|
|||
|
||||
daytimeTranslation = map[string][]string{
|
||||
"af":{"Oggend","Middag", "Vroegaand", "Laatnag"},
|
||||
"ar":{"صباح", "ظهر", "مساء", "ليل" },
|
||||
"ar":{ "ﺎﻠﻠﻴﻟ", "ﺎﻠﻤﺳﺍﺀ", "ﺎﻠﻈﻫﺭ", "ﺎﻠﺼﺑﺎﺣ" },
|
||||
"az":{"Səhər", "Gün", "Axşam", "Gecə" },
|
||||
"be":{"Раніца", "Дзень", "Вечар", "Ноч" },
|
||||
"bg":{"Сутрин", "Обяд", "Вечер", "Нощ" },
|
||||
|
|
@ -915,6 +916,9 @@ func printDay(w weather) (ret []string) {
|
|||
if config.Lang == "ko" {
|
||||
dateName = lctime.Strftime("%b %d일 %a", d)
|
||||
}
|
||||
if config.Lang == "zh" || config.Lang == "zh-tw" || config.Lang == "zh-cn" {
|
||||
dateName = lctime.Strftime("%b%d日%A", d)
|
||||
}
|
||||
}
|
||||
// appendSide := 0
|
||||
// // for utf8.RuneCountInString(dateName) <= dateWidth {
|
||||
|
|
|
|||
1
test/proxy-data/data1
Normal file
1
test/proxy-data/data1.headers
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"Content-Type": "application/json"}
|
||||
71
test/query.sh
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
queries=(
|
||||
/
|
||||
/Kiev
|
||||
/Kiev.png
|
||||
/?T
|
||||
/Киев
|
||||
/Kiev?2
|
||||
"/Kiev?format=1"
|
||||
"/Kiev?format=2"
|
||||
"/Kiev?format=3"
|
||||
"/Kiev?format=4"
|
||||
"/Kiev?format=v2"
|
||||
"/Kiev?format=%s"
|
||||
"/Kiev?format=%S"
|
||||
"/Kiev?format=%D+%S+%z+%s+%d"
|
||||
"/:help"
|
||||
"/Kiev?T"
|
||||
"/Kiev?p"
|
||||
"/Kiev?q"
|
||||
"/Kiev?Q"
|
||||
"/Kiev_text=no_view=v2.png"
|
||||
"/Kiev.png?1nqF"
|
||||
"/Kiev_1nqF.png"
|
||||
)
|
||||
|
||||
options=$(cat <<EOF
|
||||
|
||||
-A firefox
|
||||
-H Accept-Language:ru
|
||||
-H X-Forwarded-For:1.1.1.1
|
||||
EOF
|
||||
)
|
||||
|
||||
server="http://127.0.0.1:8002"
|
||||
|
||||
if [ "$1" = update ]; then
|
||||
UPDATE=yes
|
||||
fi
|
||||
|
||||
if [[ $UPDATE = yes ]]; then
|
||||
true > test-data/signatures
|
||||
fi
|
||||
|
||||
result_tmp=$(mktemp wttrin-test-XXXXX)
|
||||
|
||||
while read -r -a args
|
||||
do
|
||||
for q in "${queries[@]}"; do
|
||||
signature=$(echo "${args[@]}" "$q" | sha1sum | awk '{print $1}')
|
||||
curl -ks "${args[@]}" "$server$q" > "$result_tmp"
|
||||
|
||||
result=$(sha1sum "$result_tmp" | awk '{print $1}')
|
||||
|
||||
# this must be moved to the server
|
||||
# but for the moment we just clean up
|
||||
# the cache after each call
|
||||
rm -rf "/wttr.in/cache"
|
||||
|
||||
if grep -Eq "(we are running out of queries|500 Internal Server Error)" "$result_tmp"; then
|
||||
echo "$q"
|
||||
fi
|
||||
|
||||
if [[ $UPDATE = yes ]]; then
|
||||
printf "%s %s %s\\n" "$signature" "$result" "${args[*]} $q" >> test-data/signatures
|
||||
elif ! grep -q "$signature $result" test-data/signatures; then
|
||||
echo "FAILED: curl -ks ${args[*]} $server$q"
|
||||
fi
|
||||
done
|
||||
done <<< "${options}"
|
||||
|
||||
rm "$result_tmp"
|
||||
88
test/test-data/signatures
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
8f27084b6294ddbe28dbcbf98f798730e8a79289 4ee2dd9cf8f5818902647ff832ef40d690096bf1 /
|
||||
ae537911bb7b0568f478073e661abee1cb4ff941 d123e570da22dee9798d353c4281cb5a2bdbaeac /Kiev
|
||||
4dc586807c16020b9f4dbb705326c698bea41665 a186d89e95061a7887c005ffa8bd1e29362de2da /Kiev.png
|
||||
3db1938bedc0ee0047bf3b043ddaf0aba1912f13 febab92af9526163bc9e502ecd7fa4225345e6f6 /?T
|
||||
2cc0ba7a57a6342e72fd7142ca18dbb0eae69416 ce7fb7a88cab697f5280ddabf344f0d397888956 /Киев
|
||||
928142e88da142ea8075cbfe09bfef349e72dbb1 0f86f59a45b4485fea1375ca945503d9abb9a96d /Kiev?2
|
||||
4f6f0a16ff415fad1c102c8023c5d8365ef63402 de3b9821d587753149eded5411ec397e7a2000e2 /Kiev?format=1
|
||||
c99903b86971ccccfcca4f13e6fca72776b4fbcf f64086e48d84ac6eb440ca080eff28de1470ec30 /Kiev?format=2
|
||||
2a0d6cd8d30a84328580611ca6dd6bed1d805a04 da7b79cdff330edc1a7a78fcf2f9c8bf7e432d40 /Kiev?format=3
|
||||
4e4e15eeb6a8b6ddb5d00591c4b5e9b74a13e6bc 6a61305ea631fbccb98090c62a96898e8ffa0d75 /Kiev?format=4
|
||||
a27d3e4ad7f820124ef57c9299715bc61cb71387 fed12e63dd5fb5e348ee30d94c7231112eca71cd /Kiev?format=v2
|
||||
cbe9fc56091b519e6aebcedd9a7541241f4c4cda 2d97c405f1557b822cc86b038aeea40c3eb79d7d /Kiev?format=%s
|
||||
84b79ec29670254c3570901e4c5db017516e088d 3f26bdbbd5faf7729f87a9cb964d407493e9a0d6 /Kiev?format=%S
|
||||
8a0111eb7a519adad1210661bbb49f960ba7f95f fe58cbd420cf36a910551a1037f5c4fa19b31074 /Kiev?format=%D+%S+%z+%s+%d
|
||||
83cc0ef08c24ad7ecc81d1a6cbd693bb06214ece 0ea998c1b53e452a373699ab953ab00e8a2870f0 /:help
|
||||
310b64f65fc9f66a5142bf6104f4f9b9d5eef0ea b0bd07f0c87aae9464c091ccb955f41ec6973098 /Kiev?T
|
||||
9bd1b460d4927df24724f45f69bd3132f3de8e04 d001bf6ab36b6c14f98f02fc4500706d7a9f05a8 /Kiev?p
|
||||
3ee1a25d436799804d7ebd8371d8022fa55a71d7 80f18be012d0471dce9fcd2b500f482bcd635347 /Kiev?q
|
||||
e0e8e7eca16bfac88503ac6d19a7a6c8b469c0fd d5d070c98237f0dffc82b176039f90a15f03a667 /Kiev?Q
|
||||
d08d1fa2546fee0717d1eb663cf63cd1505b8885 e380ef1a22f62a7fa1133d0e35d923c8587cb3ed /Kiev_text=no_view=v2.png
|
||||
b14e89e2c183139495bad5404748b3b6173063d7 ff70b13244929fa3a934e94f99f019c66501ad43 /Kiev.png?1nqF
|
||||
04e1a945bdde39d646e397e55c096ec46b53a92b ff70b13244929fa3a934e94f99f019c66501ad43 /Kiev_1nqF.png
|
||||
3e1be80e942a2ea5450c60e1c0ebfb154aca3da1 6a5bdefe64689f4d05128bd62a8118f4f2f52043 -A firefox /
|
||||
ee6bf0665c2719cda3ec1fbdb80413d821c99b8e 3f9c5091269ece259cce13fc842265019001ed54 -A firefox /Kiev
|
||||
98ef11678b7fd33425f97eeee70e00cd96206539 a186d89e95061a7887c005ffa8bd1e29362de2da -A firefox /Kiev.png
|
||||
ecbcf2cb9004a754c4559ce7e92fead68f71721a 2ea6d52a2108a481cbc0f44a881eb88642d68e80 -A firefox /?T
|
||||
74206d869128383dba2d840b848b90eb376fd851 7c6ce53ff25d91a5f46baa30077b69e0f09f2571 -A firefox /Киев
|
||||
91b89025b5acd56ca475924e0eb559a9734f3333 dbd49d93eff2b2cf82f7d266f90de950207a0561 -A firefox /Kiev?2
|
||||
e6cb82dab95e05167ffbcb90a10d6cb03cd02ec1 de3b9821d587753149eded5411ec397e7a2000e2 -A firefox /Kiev?format=1
|
||||
e65bc57e8d1df26c442a9ecf45afee390ff331a3 f64086e48d84ac6eb440ca080eff28de1470ec30 -A firefox /Kiev?format=2
|
||||
d743b331d5f4c81bbc8b168ce84a99ab22dc70cf da7b79cdff330edc1a7a78fcf2f9c8bf7e432d40 -A firefox /Kiev?format=3
|
||||
bf359ee92690c3a3061542dc6e78cb42ca837412 6a61305ea631fbccb98090c62a96898e8ffa0d75 -A firefox /Kiev?format=4
|
||||
cb875772a6610c991b95b3fbfa22fc7192e25843 5367fd6790d55639b1536ec71abf340e5c79ff45 -A firefox /Kiev?format=v2
|
||||
91c0076d8e6665c06aab7c7b2326b29718bfeb80 2d97c405f1557b822cc86b038aeea40c3eb79d7d -A firefox /Kiev?format=%s
|
||||
ae9d7b1ee27eb8201a0726a3e24fe195cf2ae9e4 3f26bdbbd5faf7729f87a9cb964d407493e9a0d6 -A firefox /Kiev?format=%S
|
||||
e94371697b70bf956b6f9352fad913d716e774e7 fe58cbd420cf36a910551a1037f5c4fa19b31074 -A firefox /Kiev?format=%D+%S+%z+%s+%d
|
||||
d520af45b491689d53024c696955db8b1e4eaa87 e916b140b1297cf5bea16d92a91260e9dc3e2bc9 -A firefox /:help
|
||||
6b80492b79a4cc510cb4a9654cb6ff085cdc1943 801d4c6d6837c9604944168a3930dfe05b0e9f5d -A firefox /Kiev?T
|
||||
e13d7449ce756e55ba3e84e4b7e601b36d0044b6 c5a87804710ab70b8798f56579d276f4ce1806bf -A firefox /Kiev?p
|
||||
3f6c192a6da5b79ea59ef94e99b9cbf4b0e7ede2 372aca50f441920ad623d62ee8fcde46d609f6f9 -A firefox /Kiev?q
|
||||
f28ca2a7a47f4859eac7d0307ec7ac67a40e0adf 1b4b66d58bd7e27abeeca45581e686f12fefe76a -A firefox /Kiev?Q
|
||||
62c5029cf297b1434c57228dc8c8cdfb5e68285d e380ef1a22f62a7fa1133d0e35d923c8587cb3ed -A firefox /Kiev_text=no_view=v2.png
|
||||
22aa5dc06076b086de776a3c601544b250fef07c ff70b13244929fa3a934e94f99f019c66501ad43 -A firefox /Kiev.png?1nqF
|
||||
3a00ecab00da83f70c2abf30379c5e79c791e383 ff70b13244929fa3a934e94f99f019c66501ad43 -A firefox /Kiev_1nqF.png
|
||||
ed573b89ca5522d6ab69dc1686b98b00391076bd 5ce7ea58bf02bff008baa3193b2db498268a244b -H Accept-Language:ru /
|
||||
b879673f66235bbf1913ff9abc58aff2fb8962d1 00a96a5d83608c2dad7921862bb3f244775f6b19 -H Accept-Language:ru /Kiev
|
||||
83d99896cf866ecbaa6d2c64c12bd31bc7b35068 92dc07acb93633974eaff19e8c1a99e590e140d9 -H Accept-Language:ru /Kiev.png
|
||||
9cbb6aa3e0b46e78229a32688db1cced9a44271d b368cc8f39e7a7ced04e3f4e6506e1eb4551e904 -H Accept-Language:ru /?T
|
||||
095d8d38c667923131801595b903e007b5f902f3 4ede3397f9def696adc7ecf3ffd46a59b8fb25cb -H Accept-Language:ru /Киев
|
||||
4e6cdfc38c9d9f2436438b345776c42cb8cab8a5 1b00c96a05f9daea8248a8e063d990797be933ad -H Accept-Language:ru /Kiev?2
|
||||
b7d8d0f0bb4c38aabac468c9a354bb4e2b401893 de3b9821d587753149eded5411ec397e7a2000e2 -H Accept-Language:ru /Kiev?format=1
|
||||
8f3bbfc9be6418e82edacecc54a9f3e9f26b7fbf f64086e48d84ac6eb440ca080eff28de1470ec30 -H Accept-Language:ru /Kiev?format=2
|
||||
f1d4178892fd3dc38e9f966112d317859acc9122 da7b79cdff330edc1a7a78fcf2f9c8bf7e432d40 -H Accept-Language:ru /Kiev?format=3
|
||||
cf44e154504d9bc2b9b6066bbb0f5d52fc12f13e 6a61305ea631fbccb98090c62a96898e8ffa0d75 -H Accept-Language:ru /Kiev?format=4
|
||||
4955c849f67da53203b8c96b15a0bf0a4a471bc6 e2d3509ef7f6c3fc84151f55e1c5eb2f28dfd155 -H Accept-Language:ru /Kiev?format=v2
|
||||
e23e33569bbe34de944dad3a647d2a7a525513b4 2d97c405f1557b822cc86b038aeea40c3eb79d7d -H Accept-Language:ru /Kiev?format=%s
|
||||
d3c44cb57a1ba487b9fe7ec37368d00eee5b4601 3f26bdbbd5faf7729f87a9cb964d407493e9a0d6 -H Accept-Language:ru /Kiev?format=%S
|
||||
db91cc89883050beedd2afac7c74276a4d2dcf42 fe58cbd420cf36a910551a1037f5c4fa19b31074 -H Accept-Language:ru /Kiev?format=%D+%S+%z+%s+%d
|
||||
3f69f4a605ce88643b4e0d62a588c92625d41aea 73b4142cd3af43472897989c61408d5765c2a6ef -H Accept-Language:ru /:help
|
||||
08553ca4bf71c738c4321fe7d84b4e6ff830956f 016fc03b18a8902f838719bbc171184603c08b60 -H Accept-Language:ru /Kiev?T
|
||||
b70f8b3fc8aee126c04b27b0d3b4c503b4292cbf b60b68a9e77275884812f7e52b06f6012ba5682a -H Accept-Language:ru /Kiev?p
|
||||
400efdba61125f8cb850d7c33caf4fc2739a960b 5ee4a043a91509ef57aec46a14a0c24f09e8ec47 -H Accept-Language:ru /Kiev?q
|
||||
b9fd454e73343f262a6d99dd80487495bd647c6f a10718896a07baadb87adf2bf0026b1f00252213 -H Accept-Language:ru /Kiev?Q
|
||||
8fed034e57624d0e0b33140673094e56e04087bc 83d236565782aff7416bf526b38636148d6ba15a -H Accept-Language:ru /Kiev_text=no_view=v2.png
|
||||
cf618118ddeff08355620e83a693c40239990545 37a4dd271b5a66513da4a6dc9caeab0ae15814db -H Accept-Language:ru /Kiev.png?1nqF
|
||||
0f106ea5605ecc852ab6f63eee348a9a954e137f 37a4dd271b5a66513da4a6dc9caeab0ae15814db -H Accept-Language:ru /Kiev_1nqF.png
|
||||
3ce3dd46413f236244410f142a4b44356a0cedf9 1c2eea391b35c8bfed2541435e1788307aa06bc1 -H X-Forwarded-For:1.1.1.1 /
|
||||
89be0a5787592298ce34f10b36da7ee87d1a1353 d123e570da22dee9798d353c4281cb5a2bdbaeac -H X-Forwarded-For:1.1.1.1 /Kiev
|
||||
a9977eadc628b1ede5d4f91ee103dfb740caa2b1 a186d89e95061a7887c005ffa8bd1e29362de2da -H X-Forwarded-For:1.1.1.1 /Kiev.png
|
||||
eec20c6be5e528967cddf6d0b72c84dbda553d43 9a39dbafa7e1550d374e38059c0f4b8f437e1739 -H X-Forwarded-For:1.1.1.1 /?T
|
||||
e304153f0e1e9b41781bf4eb6fb6c4a5b7513aec ce7fb7a88cab697f5280ddabf344f0d397888956 -H X-Forwarded-For:1.1.1.1 /Киев
|
||||
98f0b3a28863a861c6ac6d89ee5d49adb7f3f518 0f86f59a45b4485fea1375ca945503d9abb9a96d -H X-Forwarded-For:1.1.1.1 /Kiev?2
|
||||
cf04d7fe2cf36eba8d7fb4fa6def1c9015036456 de3b9821d587753149eded5411ec397e7a2000e2 -H X-Forwarded-For:1.1.1.1 /Kiev?format=1
|
||||
67fbe9168566709450eb35d36c60c27105335a7e f64086e48d84ac6eb440ca080eff28de1470ec30 -H X-Forwarded-For:1.1.1.1 /Kiev?format=2
|
||||
b2604348bf39774c85b7c18ae7b51f63a2c9f31a da7b79cdff330edc1a7a78fcf2f9c8bf7e432d40 -H X-Forwarded-For:1.1.1.1 /Kiev?format=3
|
||||
cf012400156c842e569b6a9f05b094e6b75348cd 6a61305ea631fbccb98090c62a96898e8ffa0d75 -H X-Forwarded-For:1.1.1.1 /Kiev?format=4
|
||||
1f4981348cab19df9846cd3b3923ee7a972ff9fa fed12e63dd5fb5e348ee30d94c7231112eca71cd -H X-Forwarded-For:1.1.1.1 /Kiev?format=v2
|
||||
e7d772042819bb62e6e259656a75bd7b0621d1da 2d97c405f1557b822cc86b038aeea40c3eb79d7d -H X-Forwarded-For:1.1.1.1 /Kiev?format=%s
|
||||
7db5a1daac653383c18aec25ad8583f2e5296845 3f26bdbbd5faf7729f87a9cb964d407493e9a0d6 -H X-Forwarded-For:1.1.1.1 /Kiev?format=%S
|
||||
a9331379fa4b5d61b5a87a8e4cd4412cdae970a1 fe58cbd420cf36a910551a1037f5c4fa19b31074 -H X-Forwarded-For:1.1.1.1 /Kiev?format=%D+%S+%z+%s+%d
|
||||
767a7407c14049fd77a6a2fedd1d8b35f6e47e0d 0ea998c1b53e452a373699ab953ab00e8a2870f0 -H X-Forwarded-For:1.1.1.1 /:help
|
||||
10631d55b42e7bc5ec15ffc5cddae712785eb354 b0bd07f0c87aae9464c091ccb955f41ec6973098 -H X-Forwarded-For:1.1.1.1 /Kiev?T
|
||||
031478f562663eb9f577b04032993e2f098146f6 d001bf6ab36b6c14f98f02fc4500706d7a9f05a8 -H X-Forwarded-For:1.1.1.1 /Kiev?p
|
||||
e106cd21a6b67196159c2baa023142e3a8859612 80f18be012d0471dce9fcd2b500f482bcd635347 -H X-Forwarded-For:1.1.1.1 /Kiev?q
|
||||
1a16c9b52ba90cb7ad3dd8902bf41b31a287d49e d5d070c98237f0dffc82b176039f90a15f03a667 -H X-Forwarded-For:1.1.1.1 /Kiev?Q
|
||||
83bd9cc6a646e44b75524474dd32f0fd1f5c5a39 e380ef1a22f62a7fa1133d0e35d923c8587cb3ed -H X-Forwarded-For:1.1.1.1 /Kiev_text=no_view=v2.png
|
||||
ac8dabb4d30cf6a6fae626e6fc29a4f2d9df0164 ff70b13244929fa3a934e94f99f019c66501ad43 -H X-Forwarded-For:1.1.1.1 /Kiev.png?1nqF
|
||||
f0b3c0851a03a9b182ab938fc7f14935bf7af1f0 ff70b13244929fa3a934e94f99f019c66501ad43 -H X-Forwarded-For:1.1.1.1 /Kiev_1nqF.png
|
||||