Full Code of chubin/wttr.in for AI

master 771d003db43b cached
222 files
1.0 MB
352.5k tokens
378 symbols
1 requests
Download .txt
Showing preview only (1,119K chars total). Download the full file or copy to clipboard to get everything.
Repository: chubin/wttr.in
Branch: master
Commit: 771d003db43b
Files: 222
Total size: 1.0 MB

Directory structure:
gitextract_3puwzenn/

├── .flake8
├── .github/
│   └── workflows/
│       └── makefile.yml
├── .gitignore
├── .golangci.yaml
├── .travis.yml
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── bin/
│   ├── geo-proxy.py
│   ├── proxy.py
│   └── srv.py
├── config/
│   └── services/
│       └── services.yaml
├── doc/
│   ├── integrations.md
│   └── terminal-images.md
├── go.mod
├── go.sum
├── internal/
│   ├── config/
│   │   └── config.go
│   ├── fmt/
│   │   └── png/
│   │       ├── colors.go
│   │       ├── go.mod
│   │       ├── go.sum
│   │       └── png.go
│   ├── geo/
│   │   ├── ip/
│   │   │   ├── convert.go
│   │   │   ├── ip.go
│   │   │   └── ip_test.go
│   │   └── location/
│   │       ├── cache.go
│   │       ├── convert.go
│   │       ├── location.go
│   │       ├── nominatim.go
│   │       ├── nominatim_locationiq.go
│   │       ├── nominatim_opencage.go
│   │       ├── response.go
│   │       └── search.go
│   ├── logging/
│   │   ├── logging.go
│   │   └── suppress.go
│   ├── options/
│   │   ├── options.go
│   │   ├── parse.go
│   │   └── processlog.go
│   ├── processor/
│   │   ├── j1.go
│   │   ├── peak.go
│   │   └── processor.go
│   ├── routing/
│   │   └── routing.go
│   ├── stats/
│   │   └── stats.go
│   ├── types/
│   │   ├── errors.go
│   │   └── types.go
│   ├── util/
│   │   ├── files.go
│   │   ├── http.go
│   │   └── yaml.go
│   └── view/
│       └── v1/
│           ├── api.go
│           ├── cmd.go
│           ├── format.go
│           ├── icons.go
│           ├── locale.go
│           └── view1.go
├── lib/
│   ├── airports.py
│   ├── buttons.py
│   ├── cache.py
│   ├── constants.py
│   ├── datasource/
│   │   └── README.md
│   ├── duplicate_translations.py
│   ├── extract_emoji.py
│   ├── fields.py
│   ├── fmt/
│   │   ├── __init__.py
│   │   ├── png.py
│   │   └── unicodedata2.py
│   ├── globals.py
│   ├── limits.py
│   ├── location.py
│   ├── metno.py
│   ├── parse_query.py
│   ├── proxy_log.py
│   ├── translations.py
│   ├── translations_v2.py
│   ├── view/
│   │   ├── __init__.py
│   │   ├── line.py
│   │   ├── moon.py
│   │   ├── prometheus.py
│   │   ├── v2.py
│   │   └── wttr.py
│   ├── weather_data.py
│   └── wttr_srv.py
├── requirements.txt
├── share/
│   ├── aliases
│   ├── ansi2html.sh
│   ├── bash-function.txt
│   ├── blacklist
│   ├── docker/
│   │   └── supervisord.conf
│   ├── help.txt
│   ├── iterm2.txt
│   ├── list-of-iata-codes.txt
│   ├── salt/
│   │   ├── README.md
│   │   ├── init.sls
│   │   ├── pillar.sls
│   │   ├── start.sh
│   │   ├── wegorc
│   │   └── wttr.service
│   ├── screenrc
│   ├── scripts/
│   │   ├── build-welang.sh
│   │   ├── clean-cache.sh
│   │   ├── log-space.sh
│   │   └── start-screen.sh
│   ├── static/
│   │   ├── malformed-response.html
│   │   └── style.css
│   ├── systemd/
│   │   ├── README.md
│   │   ├── wttrin.service
│   │   └── wttrin.sh
│   ├── templates/
│   │   └── index.html
│   ├── test-thunder.txt
│   ├── translation.txt
│   └── translations/
│       ├── af-help.txt
│       ├── af.txt
│       ├── am-help.txt
│       ├── am.txt
│       ├── ar-help.txt
│       ├── ar.txt
│       ├── az.txt
│       ├── be-help.txt
│       ├── be.txt
│       ├── bg-help.txt
│       ├── bn-help.txt
│       ├── bn.txt
│       ├── bs.txt
│       ├── ca-help.txt
│       ├── ca.txt
│       ├── cs-help.txt
│       ├── cs.txt
│       ├── cy.txt
│       ├── da-help.txt
│       ├── da.txt
│       ├── de-help.txt
│       ├── de.txt
│       ├── dk-help.txt
│       ├── el-help.txt
│       ├── el.txt
│       ├── en.txt
│       ├── eo.txt
│       ├── es-help.txt
│       ├── es.txt
│       ├── et-help.txt
│       ├── et.txt
│       ├── eu-help.txt
│       ├── eu.txt
│       ├── fa-help.txt
│       ├── fa.txt
│       ├── fr-help.txt
│       ├── fr.txt
│       ├── fy.txt
│       ├── ga.txt
│       ├── gl-help.txt
│       ├── gl.txt
│       ├── gu-help.txt
│       ├── gu.txt
│       ├── he.txt
│       ├── hi-help.txt
│       ├── hi.txt
│       ├── hr.txt
│       ├── hu-help.txt
│       ├── hu.txt
│       ├── hy.txt
│       ├── ia-help.txt
│       ├── ia.txt
│       ├── id-help.txt
│       ├── id.txt
│       ├── is.txt
│       ├── it-help.txt
│       ├── it.txt
│       ├── ja.txt
│       ├── kk-help.txt
│       ├── kk.txt
│       ├── lt-help.txt
│       ├── lt.txt
│       ├── lv-help.txt
│       ├── lv.txt
│       ├── messages/
│       │   ├── en.yaml
│       │   └── gu.yaml
│       ├── mg-help.txt
│       ├── mg.txt
│       ├── mk.txt
│       ├── mr-help.txt
│       ├── mr.txt
│       ├── nb-help.txt
│       ├── nb.txt
│       ├── nl-help.txt
│       ├── nl.txt
│       ├── nn.txt
│       ├── oc-help.txt
│       ├── oc.txt
│       ├── pl-help.txt
│       ├── pl.txt
│       ├── pt-br-help.txt
│       ├── pt-br.txt
│       ├── pt-help.txt
│       ├── pt.txt
│       ├── ro-help.txt
│       ├── ro.txt
│       ├── ru-help.txt
│       ├── ru.txt
│       ├── sl.txt
│       ├── ta-help.txt
│       ├── ta.txt
│       ├── te-help.txt
│       ├── te.txt
│       ├── th-help.txt
│       ├── th.txt
│       ├── tr-help.txt
│       ├── tr.txt
│       ├── uk-help.txt
│       ├── uk.txt
│       ├── ukr-help.txt
│       ├── uz.txt
│       ├── vi-help.txt
│       ├── vi.txt
│       ├── zh-cn-help.txt
│       ├── zh-cn.txt
│       ├── zh-tw-help.txt
│       └── zh-tw.txt
├── spec/
│   └── options/
│       └── options.yaml
├── srv.go
└── test/
    ├── proxy-data/
    │   ├── data1
    │   └── data1.headers
    ├── query.sh
    └── test-data/
        └── signatures

================================================
FILE CONTENTS
================================================

================================================
FILE: .flake8
================================================
[flake8]
ignore = E402,E501


================================================
FILE: .github/workflows/makefile.yml
================================================
name: Makefile CI

on:
  push:
    branches: [ "master" ]
  pull_request:
    branches: [ "master" ]

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

        # Set up Python environment
      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'  # Specify the Python version you need (3.11 required for pyjq)

      - name: Create and activate virtual environment
        run: |
          python -m venv venv
          source venv/bin/activate
          echo "Virtual environment created and activated."

      - name: Install Python dependencies
        run: |
          sudo apt-get install libjq-dev
          source venv/bin/activate
          pip install -r requirements.txt

      - name: Add Python virtual environment to PATH
        run: |
          echo "${{ github.workspace }}/venv/bin" >> $GITHUB_PATH

      # Set up Go environment
      - name: Set up Go
        uses: actions/setup-go@v4
        with:
          go-version: '1.24'  # Replace with the needed Go version

      - name: Install gofumpt
        run: |
          go install mvdan.cc/gofumpt@latest

      - name: Install goimports
        run: |
          go install golang.org/x/tools/cmd/goimports@latest

      - name: Install swagger
        run: |
          cd /tmp \
          && go install github.com/go-swagger/go-swagger/cmd/swagger@latest

      - name: Add Go bin to PATH
        run: |
          echo "${{ runner.temp }}/go/bin" >> $GITHUB_PATH


      - name: Install dependencies
        run: make

      - name: Run check
        run: make check


================================================
FILE: .gitignore
================================================
ve/
share/static/fonts/
*.pyc
data/
log/
.idea/
*.swp
*.mmdb
*.dat


================================================
FILE: .golangci.yaml
================================================
run:
  skip-dirs:
    - pkg/curlator
linters:
  enable-all: true
  disable:
    - wsl
    - wrapcheck
    - varnamelen
    - gci
    - exhaustivestruct
    - exhaustruct
    - gomnd
    - gofmt

    # to be fixed:
    - ireturn
    - gosec
    - noctx
    - interfacer

    # deprecated:
    - scopelint
    - deadcode
    - varcheck
    - maligned
    - ifshort
    - nosnakecase
    - structcheck
    - golint


================================================
FILE: .travis.yml
================================================
group: travis_latest
language: python
cache: pip
python:
    - 3.7
install:
    - pip install flake8 -r requirements.txt
before_script:
    # stop the build if there are Python syntax errors or undefined names
    - flake8 bin lib --count --select=E9,F63,F7,F82 --show-source --statistics
    # exit-zero treats all errors as warnings.  The GitHub editor is 127 chars wide
    - flake8 bin lib --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
script:
    - true  # pytest --capture=sys  # add other tests here
notifications:
    on_success: change
    on_failure: change  # `always` will be the setting once code changes slow down.


================================================
FILE: Dockerfile
================================================
# Build stage
FROM golang:1-alpine as builder

WORKDIR /app

COPY ./share/we-lang/ /app

RUN apk add --no-cache git

RUN go get -u github.com/mattn/go-colorable && \
    go get -u github.com/klauspost/lctime && \
    go get -u github.com/mattn/go-runewidth && \
    cd /app && CGO_ENABLED=0 go build .

# Application stage
FROM alpine:3.21.1

WORKDIR /app

COPY ./requirements.txt /app

ENV LLVM_CONFIG=/usr/bin/llvm11-config

RUN apk add --no-cache --virtual .build \
    autoconf \
    automake \
    g++ \
    gcc \
    jpeg-dev \
    llvm11-dev\
    make \
    zlib-dev \
    && apk add --no-cache \
    python3 \
    py3-pip \
    py3-scipy \
    py3-wheel \
    py3-gevent \
    zlib \
    jpeg \
    llvm11 \
    libtool \
    supervisor \
    py3-numpy-dev \
    python3-dev && \
    mkdir -p /app/cache && \
    mkdir -p /var/log/supervisor && \
    mkdir -p /etc/supervisor/conf.d && \
    chmod -R o+rw /var/log/supervisor && \
    chmod -R o+rw /var/run && \
    pip install -r requirements.txt --no-cache-dir && \
    apk del --no-cache -r .build

COPY --from=builder /app/wttr.in /app/bin/wttr.in
COPY ./bin /app/bin
COPY ./lib /app/lib
COPY ./share /app/share
COPY share/docker/supervisord.conf /etc/supervisor/supervisord.conf

ENV WTTR_MYDIR="/app"
ENV WTTR_GEOLITE="/app/GeoLite2-City.mmdb"
ENV WTTR_WEGO="/app/bin/wttr.in"
ENV WTTR_LISTEN_HOST="0.0.0.0"
ENV WTTR_LISTEN_PORT="8002"

EXPOSE 8002

CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]


================================================
FILE: LICENSE
================================================
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "{}"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright {yyyy} {name of copyright owner}

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.



================================================
FILE: Makefile
================================================
srv: srv.go internal/*/*.go internal/*/*/*.go
	#go build -o srv -ldflags '-w -linkmode external -extldflags "-static"' ./
	go build -o srv ./

check:
	true

go-test:
	go test ./...

lint:
	golangci-lint run ./...


================================================
FILE: README.md
================================================

*wttr.in — the right way to ~check~ `curl` the weather!*

wttr.in is a console-oriented weather forecast service that supports various information
representation methods like terminal-oriented ANSI-sequences for console HTTP clients
(curl, httpie, or wget), HTML for web browsers, or PNG for graphical viewers.

Originally started as a small project, a wrapper for [wego](https://github.com/schachmat/wego),
intended to demonstrate the power of the console-oriented services,
*wttr.in* became a popular weather reporting service, handling tens of millions[¹](#wttrin-usage-stats) of queries daily.

You can see it running here: [wttr.in](https://wttr.in).

[Documentation](https://wttr.in/:help) | [Usage](https://github.com/chubin/wttr.in#usage) | [One-line output](https://github.com/chubin/wttr.in#one-line-output) | [Data-rich output format](https://github.com/chubin/wttr.in#data-rich-output-format-v2) | [Map view](https://github.com/chubin/wttr.in#map-view-v3) | [Output formats](https://github.com/chubin/wttr.in#different-output-formats) | [Moon phases](https://github.com/chubin/wttr.in#moon-phases) | [Internationalization](https://github.com/chubin/wttr.in#internationalization-and-localization) | [Installation](https://github.com/chubin/wttr.in#installation)

## Usage

You can access the service from a shell or from a Web browser like this:

    $ curl wttr.in
    Weather for City: Paris, France

         \   /     Clear
          .-.      10 – 11 °C
       ― (   ) ―   ↑ 11 km/h
          `-’      10 km
         /   \     0.0 mm


Here is an example weather report:

![Weather Report](share/pics/San_Francisco.png)

Or in PowerShell:

```PowerShell
Invoke-RestMethod https://wttr.in
```

Want to get the weather information for a specific location? You can add the desired location to the URL in your
request like this:

    $ curl wttr.in/London
    $ curl wttr.in/Moscow
    $ curl wttr.in/Salt+Lake+City

If you omit the location name, you will get the report for your current location based on your IP address.

Use 3-letter airport codes in order to get the weather information at a certain airport:

    $ curl wttr.in/muc      # Weather for IATA: muc, Munich International Airport, Germany
    $ curl wttr.in/ham      # Weather for IATA: ham, Hamburg Airport, Germany

Let's say you'd like to get the weather for a geographical location other than a town or city - maybe an attraction
in a city, a mountain name, or some special location. Add the character `~` before the name to look up that special
location name before the weather is then retrieved:

	$ curl wttr.in/~Vostok+Station
	$ curl wttr.in/~Eiffel+Tower
	$ curl wttr.in/~Kilimanjaro

For these examples, you'll see a line below the weather forecast output that shows the geolocation
results of looking up the location:

	Location: Vostok Station, станция Восток, AAT, Antarctica [-78.4642714,106.8364678]
    Location: Tour Eiffel, 5, Avenue Anatole France, Gros-Caillou, 7e, Paris, Île-de-France, 75007, France [48.8582602,2.29449905432]
	Location: Kilimanjaro, Northern, Tanzania [-3.4762789,37.3872648]

You can also use IP-addresses (direct) or domain names (prefixed with `@`) to specify a location:

    $ curl wttr.in/@github.com
    $ curl wttr.in/@msu.ru

To get detailed information online, you can access the [/:help](https://wttr.in/:help) page:

    $ curl wttr.in/:help

### Weather Units

By default the USCS units are used for the queries from the USA and the metric system for the rest of the world.
You can override this behavior by adding `?u`, `?m` or `?M`   to a URL like this:

    $ curl wttr.in/Amsterdam?u  # USCS (used by default in US)
    $ curl wttr.in/Amsterdam?m  # metric (SI) (used by default everywhere except US)
    $ curl wttr.in/Amsterdam?M  # metric (SI), but show wind speed in m/s

If you have several options to pass, write them without delimiters in between for the one-letter options,
and use `&` as a delimiter for the long options with values:

    $ curl 'wttr.in/Amsterdam?m2&lang=nl'

It would be a rough equivalent of `-m2 --lang nl` for the GNU CLI syntax.

## Supported output formats and views

wttr.in currently supports five output formats:

* ANSI for the terminal;
* Plain-text for the terminal and scripts;
* HTML for the browser;
* PNG for the graphical viewers;
* JSON for scripts and APIs;
* Prometheus metrics for scripts and APIs.

The ANSI and HTML formats are selected based on the User-Agent string.

To force plain text, which disables colors:

    $ curl wttr.in/?T

To restrict output to glyphs available in standard console fonts (e.g. Consolas and Lucida Console):

    $ curl wttr.in/?d

The PNG format can be forced by adding `.png` to the end of the query:

    $ wget wttr.in/Paris.png

You can use all of the options with the PNG-format like in an URL, but you have
to separate them with `_` instead of `?` and `&`:

    $ wget wttr.in/Paris_0tqp_lang=fr.png

Useful options for the PNG format:

* `t` for transparency (`transparency=150`);
* transparency=0..255 for a custom transparency level.

Transparency is a useful feature when weather PNGs are used to add weather data to pictures:

    $ convert source.jpg <( curl wttr.in/Oymyakon_tqp0.png ) -geometry +50+50 -composite target.jpg

In this example:

* `source.jpg` - source file;
* `target.jpg` - target file;
* `Oymyakon` - name of the location;
* `tqp0` - options (recommended).

![Picture with weather data](https://pbs.twimg.com/media/C69-wsIW0AAcAD5.jpg)

You can embed a special wttr.in widget, that displays the weather condition for the current or a selected location, into a HTML page using the [wttr-switcher](https://github.com/midzer/wttr-switcher). That is how it looks like: [wttr-switcher-example](https://midzer.github.io/wttr-switcher/) or on a real world web site: https://feuerwehr-eisolzried.de/.

![Embedded wttr.in example at feuerwehr-eisolzried.de](https://user-images.githubusercontent.com/3875145/65265457-50eac180-db11-11e9-8f9b-2e1711dfc436.png)

## One-line output

One-line output format is convenient to be used to show weather info
in status bar of different programs, such as *tmux*, *weechat*, etc.

For one-line output format, specify additional URL parameter `format`:

```
$ curl wttr.in/Nuremberg?format=3
Nuremberg: 🌦 +11⁰C
```

Available preconfigured formats: 1, 2, 3, 4 and the custom format using the percent notation (see below).
* 1: Current weather at location: `🌦 +11⁰C`
* 2: Current weather at location with more details: `🌦   🌡️+11°C 🌬️↓4km/h`
* 3: Name of location and current weather at location: `Nuremberg: 🌦 +11⁰C`
* 4: Name of location and current weather at location with more details: `Nuremberg: 🌦   🌡️+11°C 🌬️↓4km/h`

You can specify multiple locations separated with `:` (for repeating queries):

```
$ curl wttr.in/Nuremberg:Hamburg:Berlin?format=3
Nuremberg: 🌦 +11⁰C
```
Or to process all this queries at once:

```
$ curl -s 'wttr.in/{Nuremberg,Hamburg,Berlin}?format=3'
Nuremberg: 🌦 +11⁰C
Hamburg: 🌦 +8⁰C
Berlin: 🌦 +8⁰C
```

To specify your own custom output format, use the special `%`-notation:

```
    c    Weather condition,
    C    Weather condition textual name,
    x    Weather condition, plain-text symbol,
    h    Humidity,
    t    Temperature (Actual),
    f    Temperature (Feels Like),
    w    Wind,
    l    Location,
    m    Moon phase 🌑🌒🌓🌔🌕🌖🌗🌘,
    M    Moon day,
    p    Precipitation (mm/3 hours),
    P    Pressure (hPa),
    e    Dew point,
    u    UV index (1-12),

    D    Dawn*,
    S    Sunrise*,
    z    Zenith*,
    s    Sunset*,
    d    Dusk*,
    T    Current time*,
    Z    Local timezone.

(*times are shown in the local timezone)
```

So, these two calls are the same:

```
    $ curl wttr.in/London?format=3
    London: ⛅️ +7⁰C
    $ curl wttr.in/London?format="%l:+%c+%t\n"
    London: ⛅️ +7⁰C
```

## Integrations

Thanks to the ease of integrating *wttr.in* into any program, there are a
plethora of popular integrations across various libraries, programming
languages, and systems.

*wttr.in* is compatible with:

* terminal managers,
* window managers,
* editors,
* chat clients,

and more, these integrations enhance workflow efficiency by embedding weather information directly into user interfaces.

See the full list of integrations here: [wttr.in integrations](doc/integrations.md)
and some of them below.

### tmux

When using in `tmux.conf`, you have to escape `%` with `%`, i.e. write there `%%` instead of `%`.

The output does not contain new line by default, when the %-notation is used, but it does contain it when preconfigured format (`1`,`2`,`3` etc.)
are used. To have the new line in the output when the %-notation is used, use '\n' and single quotes when doing a query from the shell.

In programs, that are querying the service automatically (such as tmux), it is better to use some reasonable update interval. In tmux, you can configure it with `status-interval`.

If several, `:` separated locations, are specified in the query, specify update period
as an additional query parameter `period=`:
```
set -g status-interval 60
WEATHER='#(curl -s wttr.in/London:Stockholm:Moscow\?format\="%%l:+%%c%%20%%t%%60%%w&period=60")'
set -g status-right "$WEATHER ..."
```
![wttr.in in tmux status bar](https://wttr.in/files/example-tmux-status-line.png)

### WeeChat

To embed in to an IRC ([WeeChat](https://github.com/weechat/weechat)) client's existing status bar:

```
/alias add wttr /exec -pipe "/mute /set plugins.var.wttr" url:wttr.in/Montreal?format=%l:+%c+%f+%h+%p+%P+%m+%w+%S+%s;/wait 3 /item refresh wttr
/trigger add wttr timer 60000;0;0 "" "" "/wttr"
/item add wttr "" "${plugins.var.wttr}"
/eval /set weechat.bar.status.items ${weechat.bar.status.items},spacer,wttr
/eval /set weechat.startup.command_after_plugins ${weechat.startup.command_after_plugins};/wttr
/wttr
```
![wttr.in in WeeChat status bar](https://i.imgur.com/XkYiRU7.png)


### conky

Conky usage example:

```
${texeci 1800 curl wttr.in/kyiv_0pq_lang=uk.png
  | convert - -transparent black $HOME/.config/conky/out.png}
${image $HOME/.config/conky/out.png -p 0,0}
```

![wttr.in in conky](https://user-images.githubusercontent.com/3875145/172178453-9e9ed9e3-9815-426a-9a21-afdd6e279fc8.png)


### IRC

IRC integration example:

* https://github.com/OpenSourceTreasure/Mirc-ASCII-weather-translate-pixel-editor

### Emojis support

To see emojis in terminal, you need:

1. Terminal support for emojis (was added to Cairo 1.15.8);
2. Font with emojis support.

For the emoji font, we recommend *Noto Color Emoji*, and a good alternative option would be the *Emoji One* font;
both of them support all necessary emoji glyphs.

Font configuration:

```xml
$ cat ~/.config/fontconfig/fonts.conf
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
  <alias>
    <family>serif</family>
    <prefer>
      <family>Noto Color Emoji</family>
    </prefer>
  </alias>
  <alias>
    <family>sans-serif</family>
    <prefer>
      <family>Noto Color Emoji</family>
    </prefer>
  </alias>
  <alias>
    <family>monospace</family>
    <prefer>
      <family>Noto Color Emoji</family>
    </prefer>
  </alias>
</fontconfig>
```

(to apply the configuration, run `fc-cache -f -v`).

In some cases, `tmux` and the terminal understanding of some emoji characters may differ, which may
cause strange effects similar to that described in #579.

### Squeak

To embed into the world main docking bar:

```smalltalk
wttr := (UpdatingStringMorph on: [(WebClient httpGet: 'https://wttr.in/?format=%20%20%l:%20%C+%t') content] selector: #value)
	stepTime: 60000;
	useStringFormat;
	yourself.
dockingBar := World mainDockingBars first.
dockingBar addMorph: wttr after: (dockingBar findA: ClockMorph).
```

![wttr.in integration in the Squeak world main docking bar](https://github.com/user-attachments/assets/4c2762b0-77ae-41a8-98db-3eb310d073bd)

## Data-rich output format (v2)

In the experimental data-rich output format, that is available under the view code `v2`,
a lot of additional weather and astronomical information is available:

* Temperature, and precipitation changes forecast throughout the days;
* Moonphase for today and the next three days;
* The current weather condition, temperature, humidity, wind speed and direction, pressure;
* Timezone;
* Dawn, sunrise, noon, sunset, dusk time for he selected location;
* Precise geographical coordinates for the selected location.

```
  $ curl v2.wttr.in/München
```

or

```
  $ curl wttr.in/München?format=v2
```

or, if you prefer Nerd Fonts instead of Emoji, `v2d` (day) or `v2n` (night):

```
  $ curl v2d.wttr.in/München
```


![data-rich output format](https://wttr.in/files/example-wttr-v2.png)

(The mode is experimental, and it has several limitations currently:

* It works only in terminal;
* Only English is supported).

Currently, you need some tweaks for some terminals, to get the best possible visualization.

### URXVT

Depending on your configuration you might be taking all steps, or only a few. URXVT currently doesn't support emoji related fonts, but we can get almost the same effect using *Font-Symbola*. So add to your `.Xresources` file the following line:
```
    xft:symbola:size=10:minspace=False
```
You can add it _after_ your preferred font and it will only show up when required.
Then, if you see or feel like you're having spacing issues, add this: `URxvt.letterSpace: 0`
For some reason URXVT sometimes stops deciding right the word spacing and we need to force it this way.

The result, should look like:

![URXVT Emoji line](https://user-images.githubusercontent.com/24360204/63842949-1d36d480-c975-11e9-81dd-998d1329bd8a.png)

## Map view (v3)

In the experimental map view, that is available under the view code `v3`,
weather information about a geographical region is available:

```
    $ curl v3.wttr.in/Bayern.sxl
```

![v3.wttr.in/Bayern](https://v3.wttr.in/Bayern.png)

or directly in browser:

*   https://v3.wttr.in/Bayern

The map view currently supports three formats:

* PNG (for browser and messengers);
* Sixel (terminal inline images support);
* IIP (terminal with iterm2 inline images protocol support).

Terminal with inline images protocols support:

⟶ *Detailed article: [Images in terminal](doc/terminal-images.md)*

| Terminal              | Environment    | Images support | Protocol |
| --------------------- | --------- | ------------- | --------- |
| uxterm                |   X11     |   yes         |   Sixel   |
| mlterm                |   X11     |   yes         |   Sixel   |
| kitty                 |   X11     |   yes         |   Kitty   |
| wezterm               |   X11     |   yes         |   IIP     |
| Darktile              |   X11     |   yes         |   Sixel   |
| Jexer                 |   X11     |   yes         |   Sixel   |
| GNOME Terminal        |   X11     |   [in-progress](https://gitlab.gnome.org/GNOME/vte/-/issues/253) |   Sixel   |
| alacritty             |   X11     |   [in-progress](https://github.com/alacritty/alacritty/issues/910) |  Sixel   |
| foot                  |  Wayland  |   yes         |   Sixel   |
| DomTerm               |   Web     |   yes         |   Sixel   |
| Yaft                  |   FB      |   yes         |   Sixel   |
| iTerm2                |   Mac OS X|   yes         |   IIP     |
| mintty                | Windows   |   yes         |   Sixel   |
| Windows Terminal  |   Windows     |   [in-progress](https://github.com/microsoft/terminal/issues/448) |   Sixel   |
| [RLogin](http://nanno.dip.jp/softlib/man/rlogin/) | Windows | yes         |   Sixel   |   |


## Different output formats

### 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:
```json
{
	"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).

A smaller version `format=j2` without hourly data is also availble. Can work well for microcontrollers with limited memory.

### Prometheus Metrics Output

The [Prometheus](https://github.com/prometheus/prometheus) Metrics format is a feature providing access to *wttr.in* data through an easy-to-parse format for monitoring systems, without requiring the user to create a complex script to reinterpret wttr.in's graphical output.

To fetch information in Prometheus format, use the following syntax:

    $ curl wttr.in/Detroit?format=p1

This will fetch information on the Detroit region in Prometheus Metrics format. The `p1` format code is used to allow for the use of other layouts for the Prometheus Metrics output.

A possible configuration for Prometheus could look like this:

```yaml
    - job_name: 'wttr_in_detroit'
        static_configs:
            - targets: ['wttr.in']
        metrics_path: '/Detroit'
        params:
            format: ['p1']
```

The result will look something like the following:


    # HELP temperature_feels_like_celsius Feels Like Temperature in Celsius
    temperature_feels_like_celsius{forecast="current"} 7
    # HELP temperature_feels_like_fahrenheit Feels Like Temperature in Fahrenheit
    temperature_feels_like_fahrenheit{forecast="current"} 45
    [truncated]
...


## 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
in the full-output mode:

    $ curl wttr.in/Moon

Get the moon phase for a particular date by adding `@YYYY-MM-DD`:

    $ curl wttr.in/Moon@2016-12-25

The moon phase information uses [pyphoon](https://github.com/chubin/pyphoon) as its backend.

To get the moon phase information in the online mode, use `%m`:

    $ curl wttr.in/London?format=%m
    🌖

Keep in mind that the Unicode representation of moon phases suffers 2 caveats:

- With some fonts, the representation `🌘` is ambiguous, for it either seem
  almost-shadowed or almost-lit, depending on whether your terminal is in
  light mode or dark mode. Relying on colored fonts like `noto-fonts` works
  around this problem.

- The representation `🌘` is also ambiguous, for it means "last quarter" in
  northern hemisphere, but "first quarter" in souther hemisphere. It also means
  nothing in tropical zones. This is a limitation that
  [Unicode](https://www.unicode.org/L2/L2017/17304-moon-var.pdf) is aware about.
  But it has not been worked around at `wttr.in` yet.

See #247, #364 for the corresponding tracking issues,
and [pyphoon#1](https://github.com/chubin/pyphoon/issues/1) for pyphoon. Any help is welcome.

## Internationalization and localization

wttr.in supports multilingual locations names that can be specified in any language in the world
(it may be surprising, but many locations in the world don't have an English name).

The query string should be specified in Unicode (hex-encoded or not). Spaces in the query string
must be replaced with `+`:

    $ curl wttr.in/станция+Восток
    Weather report: станция Восток

                   Overcast
          .--.     -65 – -47 °C
       .-(    ).   ↑ 23 km/h
      (___.__)__)  15 km
                   0.0 mm

The language used for the output (except the location name) does not depend on the input language
and it is either English (by default) or the preferred language of the browser (if the query
was issued from a browser) that is specified in the query headers (`Accept-Language`).

The language can be set explicitly when using console clients by using command-line options like this:

    curl -H "Accept-Language: fr" wttr.in
    http GET wttr.in Accept-Language:ru

The preferred language can be forced using the `lang` option:

    $ curl wttr.in/Berlin?lang=de

The third option is to choose the language using the DNS name used in the query:

    $ curl de.wttr.in/Berlin

wttr.in is currently translated into 54 languages, and the number of supported languages is constantly growing.

See [/:translation](https://wttr.in/:translation) to learn more about the translation process,
to see the list of supported languages and contributors, or to know how you can help to translate wttr.in
in your language.

![Queries to wttr.in in various languages](https://pbs.twimg.com/media/C7hShiDXQAES6z1.jpg)

## Installation

To install the application:

1. Install external dependencies
2. Install Python dependencies used by the service
3. Configure IP2Location (optional)
4. Get a WorldWeatherOnline API and configure wego
5. Configure wttr.in
6. Configure the HTTP-frontend service

### Install external dependencies

wttr.in has the following external dependencies:

* [golang](https://golang.org/doc/install), wego dependency
* [wego](https://github.com/schachmat/wego), weather client for terminal

After you install [golang](https://golang.org/doc/install), install `wego`:
```bash
go install github.com/schachmat/wego@latest
```

### Install Python dependencies

Python requirements:

* Flask
* geoip2
* geopy
* requests
* gevent

If you want to get weather reports as PNG files, you'll also need to install:

* PIL
* pyte (>=0.6)
* necessary fonts

You can install most of them using `pip`.

Some python package use LLVM, so install it first:

```bash
apt-get install llvm-7 llvm-7-dev
```
If `virtualenv` is used:
```bash
virtualenv -p python3 ve
ve/bin/pip3 install -r requirements.txt
ve/bin/python3 bin/srv.py
```

Also, you need to install the geoip2 database.
You can use a free database GeoLite2 that can be downloaded from (http://dev.maxmind.com/geoip/geoip2/geolite2/).

### Configure IP2Location (optional)

If you want to use the IP2location service for IP-addresses that are not covered by GeoLite2,
you have to obtain a API key of that service, and after that save into the `~/.ip2location.key` file:

```
$ echo 'YOUR_IP2LOCATION_KEY' > ~/.ip2location.key
```

If you don't have this file, the service will be silently skipped (it is not a big problem,
because the MaxMind database is pretty good).

### Installation with Docker

* Install Docker
* Build Docker Image
* These files should be mounted by the user at runtime:

```
/root/.wegorc
/root/.ip2location.key (optional)
/app/airports.dat
/app/GeoLite2-City.mmdb
```

### Get a WorldWeatherOnline key and configure wego

To get a WorldWeatherOnline API key, you must register here:

    https://developer.worldweatheronline.com/auth/register

After you have a WorldWeatherOnline key, you can save it into the
WWO key file: `~/.wwo.key`

Also, you have to specify the key in the `wego` configuration:

```json
$ cat ~/.wegorc
{
	"APIKey": "00XXXXXXXXXXXXXXXXXXXXXXXXXXX",
	"City": "London",
	"Numdays": 3,
	"Imperial": false,
	"Lang": "en"
}
```

The `City` parameter in `~/.wegorc` is ignored.

### Configure wttr.in

Configure the following environment variables that define the path to the local `wttr.in`
installation, to the GeoLite database, and to the `wego` installation. For example:

```bash
export WTTR_MYDIR="/home/igor/wttr.in"
export WTTR_GEOLITE="/home/igor/wttr.in/GeoLite2-City.mmdb"
export WTTR_WEGO="/home/igor/go/bin/wego"
export WTTR_LISTEN_HOST="0.0.0.0"
export WTTR_LISTEN_PORT="8002"
```


### Configure the HTTP-frontend service

It's recommended that you also configure the web server that will be used to access the service:

```nginx
server {
	listen [::]:80;
	server_name  wttr.in *.wttr.in;
	access_log  /var/log/nginx/wttr.in-access.log  main;
	error_log  /var/log/nginx/wttr.in-error.log;

	location / {
	    proxy_pass         http://127.0.0.1:8002;

	    proxy_set_header   Host             $host;
	    proxy_set_header   X-Real-IP        $remote_addr;
	    proxy_set_header   X-Forwarded-For  $remote_addr;

	    client_max_body_size       10m;
	    client_body_buffer_size    128k;

	    proxy_connect_timeout      90;
	    proxy_send_timeout         90;
	    proxy_read_timeout         90;

	    proxy_buffer_size          4k;
	    proxy_buffers              4 32k;
	    proxy_busy_buffers_size    64k;
	    proxy_temp_file_write_size 64k;

	    expires                    off;
	}
}
```

## wttr.in usage stats

As of March 2026, *wttr.in* handles 22-27 million queries per day from 300,000 to 320,000 users, according to the access logs.

![wttr.in usage stats](share/stats/stats.png)


================================================
FILE: bin/geo-proxy.py
================================================
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 sys
import os
import json

from flask import (
    Flask,
    request,
    render_template,
    send_from_directory,
    send_file,
    make_response,
    jsonify,
    Response,
)

app = Flask(__name__)

MYDIR = os.path.abspath(os.path.dirname("__file__"))
sys.path.append(os.path.join(MYDIR, "lib"))

CACHEDIR = os.path.join(MYDIR, "cache")

from geopy.geocoders import Nominatim  # , Mapzen

# geomapzen = Mapzen("mapzen-RBNbmcZ") # Nominatim()
geoosm = Nominatim(timeout=7, user_agent="wttrin-geo/0.0.2")

import airports

# from tzwhere import tzwhere
import timezonefinder

tf = timezonefinder.TimezoneFinder()


def load_cache(location_string):
    try:
        location_string = location_string.replace("/", "_")
        cachefile = os.path.join(CACHEDIR, location_string)

        return json.loads(open(cachefile, "r").read())
    except Exception:
        return None


def shorten_full_address(address):
    parts = address.split(",")
    if len(parts) > 6:
        parts = parts[:2] + [x for x in parts[-4:] if len(x) < 20]
        return ",".join(parts)
    return address


def save_cache(location_string, answer):
    location_string = location_string.replace("/", "_")
    cachefile = os.path.join(CACHEDIR, location_string)
    open(cachefile, "w").write(json.dumps(answer))


def query_osm(location_string):
    try:
        location = geoosm.geocode(location_string)
        return {
            "address": location.address,
            "latitude": location.latitude,
            "longitude": location.longitude,
        }

    except Exception as e:
        print(e)
        return None


def add_timezone_information(geo_data):
    # tzwhere_ = tzwhere.tzwhere()
    # timezone_str = tzwhere_.tzNameAt(geo_data["latitude"], geo_data["longitude"])
    timezone_str = tf.certain_timezone_at(
        lat=geo_data["latitude"], lng=geo_data["longitude"]
    )

    answer = geo_data.copy()
    answer["timezone"] = timezone_str

    return answer


@app.route("/<string:location>")
def find_location(location):

    airport_gps_location = airports.get_airport_gps_location(
        location.upper().lstrip("~")
    )
    is_airport = False
    if airport_gps_location is not None:
        location = airport_gps_location
        is_airport = True

    location = location.replace("+", " ")
    answer = load_cache(location)
    loaded_answer = None

    if answer is not None:
        loaded_answer = answer.copy()
        print("cache found: %s" % location)

    if answer is None:
        answer = query_osm(location)

    if is_airport:
        answer["address"] = shorten_full_address(answer["address"])

    if "timezone" not in answer:
        answer = add_timezone_information(answer)

    if answer is not None and loaded_answer != answer:
        save_cache(location, answer)

    if answer is None:
        return ""
    else:
        r = Response(json.dumps(answer))
        r.headers["Content-Type"] = "text/json; charset=utf-8"
        return r


app.config["JSON_AS_ASCII"] = False
server = WSGIServer(("127.0.0.1", 8004), app)
server.serve_forever()


================================================
FILE: bin/proxy.py
================================================
# vim: fileencoding=utf-8

"""

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

from gevent.pywsgi import WSGIServer
from gevent.monkey import patch_all

patch_all()

# pylint: disable=wrong-import-position,wrong-import-order
import sys
import os
import time
import json
import hashlib
import re
import logging


import requests
import cyrtranslit

from flask import Flask, request

APP = Flask(__name__)


MYDIR = os.path.abspath(os.path.dirname(os.path.dirname("__file__")))
sys.path.append("%s/lib/" % MYDIR)

import proxy_log
import globals
from globals import (
    PROXY_CACHEDIR,
    PROXY_PORT,
    USE_METNO,
    USER_AGENT,
    MISSING_TRANSLATION_LOG,
)
from metno import create_standard_json_from_metno, metno_request
from translations import PROXY_LANGS

# pylint: enable=wrong-import-position

proxy_logger = proxy_log.LoggerWWO(globals.PROXY_LOG_ACCESS, globals.PROXY_LOG_ERRORS)


def is_testmode():
    """Server is running in the wttr.in test mode"""

    return "WTTRIN_TEST" in os.environ


def load_translations():
    """
    load all translations
    """
    translations = {}

    for f_name in PROXY_LANGS:
        f_name = "share/translations/%s.txt" % f_name
        translation = {}
        lang = f_name.split("/")[-1].split(".", 1)[0]
        with open(f_name, "r") as f_file:
            for line in f_file:
                if ":" not in line:
                    continue
                if line.count(":") == 3:
                    _, trans, orig, _ = line.strip().split(":", 4)
                else:
                    _, trans, orig = line.strip().split(":", 3)
                trans = trans.strip()
                orig = orig.strip()

                translation[orig.lower()] = trans
        translations[lang] = translation
    return translations


TRANSLATIONS = load_translations()


def _is_metno():
    return USE_METNO


def _find_srv_for_query(path, query):  # pylint: disable=unused-argument
    if _is_metno():
        return "https://api.met.no"
    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())
    penalty = 0
    expiry_interval = 60 * (digest_number + penalty)

    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):
    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):
    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):
    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, "wb").write(content)


def translate(text, lang):
    """
    Translate `text` into `lang`.
    If `text` is comma-separated, translate each term independently.
    If no translation found, leave it untouched.
    """

    def _log_unknown_translation(lang, text):
        with open(MISSING_TRANSLATION_LOG % lang, "a") as f_missing_translation:
            f_missing_translation.write(text + "\n")

    if "," in text:
        terms = text.split(",")
        translated_terms = [translate(term.strip(), lang) for term in terms]
        return ", ".join(translated_terms)

    if lang not in TRANSLATIONS:
        _log_unknown_translation(lang, "UNKNOWN_LANGUAGE")
        return text

    if text.lower() not in TRANSLATIONS.get(lang, {}):
        _log_unknown_translation(lang, text)
        return text

    translated = TRANSLATIONS.get(lang, {}).get(text.lower(), text)
    return translated


def cyr(to_translate):
    """
    Transliterate `to_translate` from latin into cyrillic
    """
    return cyrtranslit.to_cyrillic(to_translate)


def _patch_greek(original):
    return original.replace("Ηλιόλουστη/ο", "Ηλιόλουστη")


def add_translations(content, lang):
    """
    Add `lang` translation to `content` (JSON)
    returned by the data source
    """

    if content == "{}":
        return {}

    languages_to_translate = TRANSLATIONS.keys()
    try:
        d = json.loads(content)  # pylint: disable=invalid-name
    except (ValueError, TypeError) as exception:
        print("---")
        print(exception)
        print("---")
        return {}

    if "current_condition" not in d["data"]:
        return content

    try:
        weather_condition = (
            d["data"]["current_condition"][0]["weatherDesc"][0]["value"]
            .capitalize()
            .strip()
        )
        d["data"]["current_condition"][0]["weatherDesc"][0]["value"] = weather_condition
        if lang in languages_to_translate:
            d["data"]["current_condition"][0]["lang_%s" % lang] = [
                {"value": translate(weather_condition, lang)}
            ]
        elif lang == "sr":
            d["data"]["current_condition"][0]["lang_%s" % lang] = [
                {
                    "value": cyr(
                        d["data"]["current_condition"][0]["lang_%s" % lang][0]["value"]
                    )
                }
            ]
        elif lang == "el":
            d["data"]["current_condition"][0]["lang_%s" % lang] = [
                {
                    "value": _patch_greek(
                        d["data"]["current_condition"][0]["lang_%s" % lang][0]["value"]
                    )
                }
            ]
        elif lang == "sr-lat":
            d["data"]["current_condition"][0]["lang_%s" % lang] = [
                {"value": d["data"]["current_condition"][0]["lang_sr"][0]["value"]}
            ]

        fixed_weather = []
        for w in d["data"]["weather"]:  # pylint: disable=invalid-name
            fixed_hourly = []
            for h in w["hourly"]:  # pylint: disable=invalid-name
                weather_condition = h["weatherDesc"][0]["value"].strip()
                if lang in languages_to_translate:
                    h["lang_%s" % lang] = [
                        {"value": translate(weather_condition, lang)}
                    ]
                elif lang == "sr":
                    h["lang_%s" % lang] = [
                        {"value": cyr(h["lang_%s" % lang][0]["value"])}
                    ]
                elif lang == "el":
                    h["lang_%s" % lang] = [
                        {"value": _patch_greek(h["lang_%s" % lang][0]["value"])}
                    ]
                elif lang == "sr-lat":
                    h["lang_%s" % lang] = [{"value": h["lang_sr"][0]["value"]}]
                fixed_hourly.append(h)
            w["hourly"] = fixed_hourly
            fixed_weather.append(w)
        d["data"]["weather"] = fixed_weather

        content = json.dumps(d)
    except (IndexError, ValueError) as exception:
        print(exception)
    return content


def _fetch_content_and_headers(path, query_string, **kwargs):
    content, headers = _load_content_and_headers(path, query_string)

    if content is None:
        srv = _find_srv_for_query(path, query_string)
        url = "%s/%s?%s" % (srv, path, query_string)

        attempts = 10
        response = None
        error = ""
        while attempts:
            try:
                response = requests.get(url, timeout=2, **kwargs)
            except requests.ReadTimeout:
                attempts -= 1
                continue
            try:
                data = json.loads(response.content)
                error = data.get("data", {}).get("error", "")
                if error:
                    try:
                        error = error[0]["msg"]
                    except (ValueError, IndexError):
                        error = "invalid error format: %s" % error
                break
            except ValueError:
                attempts -= 1
                error = "invalid response"

        proxy_logger.log(query_string, error)
        _touch_empty_file(path, query_string)
        if response:
            headers = {}
            headers["Content-Type"] = response.headers["content-type"]
            _save_content_and_headers(path, query_string, response.content, headers)
            content = response.content
        else:
            content = "{}"
    return content, headers


def _make_query(path, query_string):
    if _is_metno():
        path, query, days = metno_request(path, query_string)
        if USER_AGENT == "":
            raise ValueError(
                "User agent must be set to adhere to metno ToS: https://api.met.no/doc/TermsOfService"
            )
        content, headers = _fetch_content_and_headers(
            path, query, headers={"User-Agent": USER_AGENT}
        )
        content = create_standard_json_from_metno(content, days)
    else:
        # WWO tweaks
        query_string += "&extra=localObsTime"
        query_string += "&includelocation=yes"
        content, headers = _fetch_content_and_headers(path, query_string)

    return content, headers


def _normalize_query_string(query_string):
    # Normalized query string has the following fixes:
    # 1. Uses , for the coordinates separation
    # 2. Limits number of digits after .

    coord_match = re.search(r"q=[^&]*", query_string)
    coords_str = coord_match.group(0)
    coords = re.findall(r"[-0-9.]+", coords_str)
    if len(coords) != 2:
        return query_string

    lat = str(round(float(coords[0]), 2))
    lng = str(round(float(coords[1]), 2))
    query_string = re.sub(r"q=[^&]*", "q=" + lat + "," + lng, query_string)
    # print(query_string)

    # nqs = query_string.replace("%2C", ",")
    # lat, lng = nqs.split(",", 1)
    # nqs = f"{lat:.2f},{lng:.2f}"

    return query_string


@APP.route("/<path:path>")
def proxy(path):
    """
    Main proxy function. Handles incoming HTTP queries.
    """

    lang = request.args.get("lang", "en")
    query_string = request.query_string.decode("utf-8")
    query_string = _normalize_query_string(query_string)
    query_string = query_string.replace("sr-lat", "sr")
    query_string = query_string.replace("lang=None", "lang=en")
    content = ""
    headers = ""

    content, headers = _make_query(path, query_string)

    # _log_query(path, query_string, error)

    content = add_translations(content, lang)
    if "Unable to find any" in content:
        print(query_string)

    return content, 200, headers


if __name__ == "__main__":
    # app.run(host='0.0.0.0', port=5001, debug=False)
    # app.debug = True
    if len(sys.argv) == 1:
        bind_addr = "0.0.0.0"
        logging.getLogger('werkzeug').setLevel(logging.ERROR)  # Suppress Werkzeug logs
        SERVER = WSGIServer((bind_addr, PROXY_PORT), APP, log=None)
        SERVER.serve_forever()
    else:
        print("running single request from command line arg")
        APP.testing = True
        with APP.test_client() as c:
            resp = c.get(sys.argv[1])
            print("Status: " + resp.status)
            # print('Headers: ' + dumps(resp.headers))
            print(resp.data.decode("utf-8"))


================================================
FILE: bin/srv.py
================================================
#!/usr/bin/env python
# vim: set encoding=utf-8

from gevent.pywsgi import WSGIServer
from gevent.monkey import patch_all

patch_all()

# pylint: disable=wrong-import-position,wrong-import-order
import sys
import os
import jinja2

from flask import Flask, request, send_from_directory, send_file

APP = Flask(__name__)

MYDIR = os.path.abspath(os.path.dirname(os.path.dirname("__file__")))
sys.path.append("%s/lib/" % MYDIR)

import wttr_srv
from globals import TEMPLATES, STATIC, LISTEN_HOST, LISTEN_PORT

# pylint: enable=wrong-import-position,wrong-import-order

# from view.v3 import v3_file

MY_LOADER = jinja2.ChoiceLoader(
    [
        APP.jinja_loader,
        jinja2.FileSystemLoader(TEMPLATES),
    ]
)

APP.jinja_loader = MY_LOADER


# @APP.route("/v3/<string:location>")
# def send_v3(location):
#     filepath = v3_file(location)
#     if filepath.startswith("ERROR"):
#         return filepath.rstrip("\n") + "\n"
#     return send_file(filepath)


@APP.route("/files/<path:path>")
def send_static(path):
    "Send any static file located in /files/"
    return send_from_directory(STATIC, path)


@APP.route("/favicon.ico")
def send_favicon():
    "Send static file favicon.ico"
    return send_from_directory(STATIC, "favicon.ico")


@APP.route("/malformed-response.html")
def send_malformed():
    "Send static file malformed-response.html"
    return send_from_directory(STATIC, "malformed-response.html")


@APP.route("/")
@APP.route("/<string:location>")
def wttr(location=None):
    "Main function wrapper"
    return wttr_srv.wttr(location, request)


SERVER = WSGIServer(
    (LISTEN_HOST, int(os.environ.get("WTTRIN_SRV_PORT", LISTEN_PORT))), APP
)
SERVER.serve_forever()


================================================
FILE: config/services/services.yaml
================================================
services:
  - name: "main server"
    command: "while true; do sudo /wttr.in/bin/srv big-cache.yaml ; sleep 5; done"
    workdir: "$HOME"
    port: ...

  - name: "geo server"
    command: "while true; do /wttr.in/bin/srv geo.yaml; sleep 5; done"
    workdir: "$HOME"
    env:
      - NOMINATIM_OPENCAGE
    port: 8085

  - name: "proxy"
    command: ve/bin/python3 bin/proxy.py
    workdir: "/wttr.in/wttr.in-v2-v2"
    port: 5001

  - name: "format=j1"
    command: "WTTRIN_SRV_PORT=9003 ve/bin/python3 bin/srv.py"
    workdir: "/wttr.in/wttr.in-v2-v2"
    port: 9003

  - name: "format=line"
    command: "WTTRIN_SRV_PORT=9004 ve/bin/python3 bin/srv.py"
    workdir: "/wttr.in/wttr.in-v2-v2"
    port: 9004

  - name: "format=v1"
    command: "WTTRIN_SRV_PORT=9005 ve/bin/python3 bin/srv.py"
    workdir: "/wttr.in/wttr.in-v2-v2"
    port: 9005

  - name: "filetype=png"
    command: "WTTRIN_SRV_PORT=9006 ve/bin/python3 bin/srv.py"
    workdir: "/wttr.in/wttr.in-v2-v2"
    port: 9006


================================================
FILE: doc/integrations.md
================================================
## Integrations

Thanks to the ease of integrating *wttr.in* into any program, there are a
plethora of popular integrations across various libraries, programming
languages, and systems.

*wttr.in* is compatible with:

* terminal managers,
* window managers,
* editors,
* chat clients,

and more, these integrations enhance workflow efficiency by embedding weather information directly into user interfaces.

Below, we've compiled a list of some of these integrations. While not
exhaustive, it serves as a guide to help you use *wttr.in* within your favorite
application or add integration to a new one.


| Integration For                                    | Short Description                                                                                                | Repository                                                                                         |
|----------------------------------------------------|------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------|
| **Terminal Managers**                              |                                                                                                                  |                                                                                                    |
| [tmux](#tmux)                                      | Allows embedding weather data in the tmux status bar with customizable update intervals.                         | -                                                                                                  |
| **Window Managers Statusbars**                     |                                                                                                                  |                                                                                                    |
| [Waybar](#Waybar)                                  | A custom module in Rust for displaying weather in Waybar, with support for custom indicators and error handling. | [wttrbar](https://github.com/bjesus/wttrbar)                                                       |
| [Xmobar](#Xmobar)                                  | A Python script for integrating weather data into Xmobar.                                                        | [weather-xmobar-wttr.in](https://github.com/alexeygumirov/weather-xmobar-wttr.in)                  |
| [AwesomeWM](#AwesomeWM)                            | Displays weather in AwesomeWM with a focus on clean UI and tooltips.                                             | [wttr-widget](https://github.com/pivaldi/wttr-widget)                                              |
| **Editors**                                        |                                                                                                                  |                                                                                                    |
| [Emacs](#Emacs)                                    | An Emacs frontend for showing weather forecasts directly within Emacs.                                           | [emacs-wttrin](https://github.com/cjennings/emacs-wttrin)                                            |
| [Emacs](#Emacs)                                    | Displays weather in the Emacs mode line with emoji support, configurable for updates.                            | [display-wttr](https://git.sr.ht/~josegpt/display-wttr)                                            |
| **Chats**                                          |                                                                                                                  |                                                                                                    |
| [Conky](#Conky)                                    | Example script for displaying weather in Conky using transparent images.                                         | -                                                                                                  |
| [WeeChat](#WeeChat)                                | Script for embedding weather in the WeeChat IRC client's status bar.                                             | -                                                                                                  |
| [IRC](#IRC)                                        | Uses Qt-based mirc bot to show weather updates.                                                                  | [IRC integration](https://github.com/OpenSourceTreasure/Mirc-ASCII-weather-translate-pixel-editor) |
| **Other**                                          |                                                                                                                  |                                                                                                    |
| [ObsidianMD](#ObsidianMD)                          | Script for embedding weather in Squeak's world main docking bar.                                                 | -                                                                                                  |
| [RainMeter](#Rainmeter)                            |                                                                                                                  | -                                                                                                  |
| [Squeak](#Squeak)                                  |                                                                                                                  | -                                                                                                  |
| [Twitch](#Twitch)                                  |                                                                                                                  | -                                                                                                  |
| [GoogleSheets/Excel365](#google-sheets--excel-365) | Direct wttr.in access from Google Sheets / Excel 365                                                             | -                                                                                                  |
| [Raycast](#Raycast)                                | Weather extension for Raycast                                                                                    | -                                                                                                  |


## Terminal Managers

### tmux

When using in `tmux.conf`, you have to escape `%` with `%`, i.e. write there `%%` instead of `%`.

The output does not contain new line by default, when the %-notation is used, but it does contain it when preconfigured format (`1`,`2`,`3` etc.)
are used. To have the new line in the output when the %-notation is used, use '\n' and single quotes when doing a query from the shell.

In programs, that are querying the service automatically (such as tmux), it is better to use some reasonable update interval. In tmux, you can configure it with `status-interval`.

If several, `:` separated locations, are specified in the query, specify update period
as an additional query parameter `period=`:

```
set -g status-interval 60
WEATHER='#(curl -s wttr.in/London:Stockholm:Moscow\?format\="%%l:+%%c%%20%%t%%60%%w&period=60")'
set -g status-right "$WEATHER ..."
```

![wttr.in in tmux status bar](https://wttr.in/files/example-tmux-status-line.png)


## Window Managers Statusbars

### Waybar

[wttrbar](https://github.com/bjesus/wttrbar) by *bjesus*

A custom module for displaying weather in Waybar using wttr.in. It’s written in
Rust for reliability and supports features like custom indicators (e.g.,
temperature in Celsius or Fahrenheit), location specification, and date
formatting.

Example configuration:

```json
    "custom/weather": {
      "format": "{}°",
      "tooltip": true,
      "interval": 3600,
      "exec": "wttrbar --location=Paris --main-indicator=FeelsLikeC --date-format=%d.%m.%Y",
      "return-type": "json"
    }
```

Installation:

Compile using cargo build --release or download pre-built binaries. Requires a font supporting emojis (e.g., Noto Emoji) for weather icons.

Features: Supports custom styling based on weather conditions (e.g., sunny), handles wttr.in errors gracefully, and allows wind direction display with Unicode arrows.

<img src="https://user-images.githubusercontent.com/55081/232401699-b8345fe0-ffce-4353-b51b-615389153448.png" alt="wttrbar" width="300px">

### Xmobar

[weather-xmobar-wttr.in](https://github.com/alexeygumirov/weather-xmobar-wttr.in) by *alexeygumirov*

A Python-based script for displaying weather status in Xmobar, leveraging
wttr.in. It provides a lightweight solution for integrating weather data into
the Xmobar status bar.

![wttr-in-Xmodbar](https://raw.githubusercontent.com/alexeygumirov/weather-xmobar-wttr.in/refs/heads/main/screenshot/screenshot_day.png)

(displays weather conditions in a compact format suitable for Xmobar).

### AwesomeWM

[wttr-widget](https://github.com/pivaldi/wttr-widget) by *pivaldi*

A weather widget for AwesomeWM that uses wttr.in to display weather information. It’s designed to integrate seamlessly with the Awesome window manager, providing a tooltip with detailed weather data.

Displays weather conditions with a focus on a clean, customizable UI. The screenshot shows a tooltip with weather details like temperature and conditions.

![wttr-in-awesome](https://raw.githubusercontent.com/pivaldi/wttr-widget/refs/heads/master/screenshots/tooltip1.png)

(shows a tooltip with weather data integrated into AwesomeWM)

## Editors

### Emacs

#### emacs-wttrin by bcbcarl

[emacs-wttrin](https://github.com/bcbcarl/emacs-wttrin) by *bcbcarl*

An Emacs frontend for wttr.in, allowing users to view weather forecasts directly within Emacs. It’s designed for simplicity and integration into the Emacs workflow.

Users can configure it to fetch weather for specific locations and display it in a buffer.

#### display-wttr by josegpt

[display-wttr](https://git.sr.ht/~josegpt/display-wttr) by *josegpt*
    
Displays wttr.in weather data in the Emacs mode line with emoji support (requires Emacs 28 or later). It’s lightweight and configurable for periodic updates.

Configuration Example:

```elisp
(use-package display-wttr
  :config
  (display-wttr-mode))
```

Supports custom locations, update intervals, and emoji-based weather display. The repository has moved to https://git.sr.ht/~josegpt/display-wttr.

Screenshot: 

![display-wttr](https://raw.githubusercontent.com/josegpt/display-wttr/main/emoji-display-wttr.png)

(shows weather with emojis in the Emacs mode line).

## Chats

### conky

Conky usage example:

```
${texeci 1800 curl wttr.in/kyiv_0pq_lang=uk.png
  | convert - -transparent black $HOME/.config/conky/out.png}
${image $HOME/.config/conky/out.png -p 0,0}
```

![wttr.in in conky](https://user-images.githubusercontent.com/3875145/172178453-9e9ed9e3-9815-426a-9a21-afdd6e279fc8.png)

### WeeChat

To embed in to an IRC ([WeeChat](https://github.com/weechat/weechat)) client's existing status bar:

```
/alias add wttr /exec -pipe "/mute /set plugins.var.wttr" url:wttr.in/Montreal?format=%l:+%c+%f+%h+%p+%P+%m+%w+%S+%s;/wait 3 /item refresh wttr
/trigger add wttr timer 60000;0;0 "" "" "/wttr"
/item add wttr "" "${plugins.var.wttr}"
/eval /set weechat.bar.status.items ${weechat.bar.status.items},spacer,wttr
/eval /set weechat.startup.command_after_plugins ${weechat.startup.command_after_plugins};/wttr
/wttr
```

![wttr.in in WeeChat status bar](https://i.imgur.com/XkYiRU7.png)

### IRC

[IRC integration](https://github.com/OpenSourceTreasure/Mirc-ASCII-weather-translate-pixel-editor)

![wttr-in-irc](https://raw.githubusercontent.com/OpenSourceTreasure/Mirc-ASCII-weather-translate-pixel-editor/main/mirc-bot.png)

## Other

### ObsidianMD Integration

A script for ObsidianMD (a note-taking app) integrates wttr.in to embed weather data in daily notes using the Templater plugin. It fetches and displays weather details for a specified location only for the current day’s note.

*Usage:* Add a script to the Templater plugin to check if the note’s date matches today and fetch weather data. Example script:

```javascript
<%* if (tp.file.title === tp.date.now()) { %>
  <%* const weather = await requestUrl('https://wttr.in/Berlin?format=%l:+%c+%C+%t+feels+like+%f\nTime:+++++%T\nSunrise:++%S\nSunset:+++%s\nMoon:+++++%m\nWind:+++++%w\nRainfall:+%p\nHumidity:+%h') %>
  <%* tR += weather.text %>
<%* } else { %>
  Update the weather report!
<%* } %>
```

This displays weather details like temperature, sunrise/sunset, moon phase, wind, rainfall, and humidity in the note body.

*Features:* Updates only for the current day’s note, supports custom formats, and can be triggered manually via a hotkey.


### Rainmeter

Rainmeter, a desktop customization tool for Windows, can use wttr.in to display
weather data by parsing its JSON output. This is particularly useful for
creating custom desktop widgets.

Use a URL like `https://wttr.in/Alexandria,Virginia?format=j1` to fetch
JSON data, which includes detailed weather metrics (e.g., temperature,
humidity, wind speed, weather code). A Lua script can map weather codes to
local icons for visualization.

Parses JSON for detailed weather data, supports integration with
custom icons (referencing WorldWeatherOnline’s weather codes), and allows for
flexible widget design.

### Squeak

To embed into the world main docking bar:

```smalltalk
wttr := (UpdatingStringMorph on: [(WebClient httpGet: 'https://wttr.in/?format=%20%20%l:%20%C+%t') content] selector: #value)
	stepTime: 60000;
	useStringFormat;
	yourself.
dockingBar := World mainDockingBars first.
dockingBar addMorph: wttr after: (dockingBar findA: ClockMorph).
```

![wttr.in integration in the Squeak world main docking bar](https://github.com/user-attachments/assets/4c2762b0-77ae-41a8-98db-3eb310d073bd)

### Twitch

wttr.in is used to create a custom `!weather` command for Twitch streams, allowing viewers to query weather for a specific location (e.g., `!weather Toronto`).
The command formats wttr.in output for display in chat.

```bash
curl "https://wttr.in/Toronto?format=:+%c+%l+is+currently+%C+with+a+temperature+of+%t+(+%f).+The+wind+is+blowing+from+%w.+The+Humidity+is+currently+%h&u"
```

This outputs, e.g., : ⛅️ Toronto is currently Partly Cloudy with a temperature
of +7°C (+45°F). The wind is blowing from NE. The humidity is currently 65%. To
display Fahrenheit in parentheses, add `?u` to the URL for USCS units.

Features: Customizable output for Twitch chat, supports both Celsius and
Fahrenheit, and handles location-based queries dynamically.

Details: [wttr.in-on-twitch](https://www.reddit.com/r/commandline/comments/1eqoa0w/creating_a_weather_command_using_wttrin_service/)


### Google Sheets / Excel 365

It is possible to show the live weather in Google Sheets.

Assume you want the weather image for a specific location, and the location is either hardcoded or stored in a cell.
Example 1: Hardcoded Location (London):
In a cell (e.g., A1), enter:
excel

```
=IMAGE("https://wttr.in/London_0pq.png", 1)
```

This inserts a weather image for London, resized to fit the cell while maintaining the aspect ratio.
Example 2: Dynamic Location (from a cell):
If cell B1 contains the location (e.g., London), use:

```excel
=IMAGE("https://wttr.in/"&B1&"_0pq.png", 4, 100, 200)
```

This inserts the image with a custom size (100px height, 200px width). Adjust height and width as needed.

Customize the wttr.in URL (Optional):

* `_0pq`: Simple weather image (current conditions, no background).
* `_m`: Metric units (e.g., Celsius).
* `_u`: USCS units (e.g., Fahrenheit).
* `_t`: Transparent background.

Example with metric units and transparency:

```
=IMAGE("https://wttr.in/"&B1&"_0pqt_m.png", 1)
```

![Google Sheets Example](google-sheets.png)

### Raycast

* Raycast Store: https://www.raycast.com/tonka3000/weather
* Source code: [Github](https://github.com/raycast/extensions/tree/542ed079c2eb5a95df0835d83ab1f1c2b1970e44/extensions/weather/)
* Author: [tonka3000](https://github.com/tonka3000)

Raycast is a handy tool for Mac users that helps them work faster and more
efficiently.  It's popular among developers, designers, and tech enthusiasts who
want to streamline their workflows.
With Raycast, you can quickly launch apps,
search for files, and perform everyday tasks all from one central place,
boosting productivity and saving time.

With the "Weather" extension, you can quickly check information about the current weather.

![wttr.in in raycast](raycast.png)


================================================
FILE: doc/terminal-images.md
================================================

## Map view (v3)

In the experimental map view, that is available under the view code `v3`,
weather information about a geographical region is available:

```
    $ curl v3.wttr.in/Bayern.sxl
```

![v3.wttr.in/Bayern](https://v3.wttr.in/Bayern.png)

or directly in browser:

*   https://v3.wttr.in/Bayern

The map view currently supports three formats:

* PNG (for browser and messangers);
* Sixel (terminal inline images support);
* IIP (terminal with iterm2 inline images protocol support).

## Terminal with images support


| Terminal              | Environment    | Images support | Protocol |
| --------------------- | --------- | ------------- | --------- |
| uxterm                |   X11     |   yes         |   Sixel   |
| mlterm                |   X11     |   yes         |   Sixel   |
| kitty                 |   X11     |   yes         |   Kitty   |
| wezterm               |   X11     |   yes         |   IIP     |
| Darktile              |   X11     |   yes         |   Sixel   |
| Jexer                 |   X11     |   yes         |   Sixel   |
| GNOME Terminal        |   X11     |   [in-progress](https://gitlab.gnome.org/GNOME/vte/-/issues/253) |   Sixel   |
| alacritty             |   X11     |   [in-progress](https://github.com/alacritty/alacritty/issues/910) |  Sixel   |
| st                    |   X11     | [stixel](https://github.com/vizs/stixel) or [st-sixel](https://github.com/galatolofederico/st-sixel)     |   Sixel   |
| Konsole               |   X11     |   yes         |   Sixel   |
| DomTerm               |   Web     |   yes         |   Sixel   |
| Yaft                  |   FB      |   yes         |   Sixel   |
| iTerm2                |   Mac OS X|   yes         |   IIP     |
| mintty                | Windows   |   yes         |   Sixel   |
| Windows Terminal  |   Windows     |   [in-progress](https://github.com/microsoft/terminal/issues/448) |   Sixel   |
| [RLogin](http://nanno.dip.jp/softlib/man/rlogin/) | Windows | yes         |   Sixel   |   |

Support in all VTE-based terminals: termite, terminator, etc is more or less the same as in the GNOME Terminal

## Notes

### xterm/uxterm

To start xterm/uxterm with Sixel support:

```
uxterm -ti vt340
```

### Kitty

To view images in kitty:

```
curl -s v3.wttr.in/Tabasco.png | kitty icat --align=left
```

or even without `curl` at  all, because `icat` knows how to handle URLs:

```
kitty icat --align=left https://v3.wttr.in/Tabasco.png
```


================================================
FILE: go.mod
================================================
module github.com/chubin/wttr.in

go 1.16

require (
	github.com/alecthomas/kong v1.7.0
	github.com/denisenkom/go-mssqldb v0.0.0-20200910202707-1e08a3fab204 // indirect
	github.com/go-sql-driver/mysql v1.5.0 // indirect
	github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect
	github.com/hashicorp/golang-lru v1.0.2
	github.com/itchyny/gojq v0.12.11 // indirect
	github.com/klauspost/lctime v0.1.0
	github.com/lib/pq v1.8.0 // indirect
	github.com/mattn/go-colorable v0.1.14
	github.com/mattn/go-runewidth v0.0.16
	github.com/mattn/go-sqlite3 v1.14.24 // indirect
	github.com/rivo/uniseg v0.4.7 // indirect
	github.com/robfig/cron v1.2.0
	github.com/samonzeweb/godb v1.0.13
	github.com/sirupsen/logrus v1.9.3
	github.com/smartystreets/assertions v1.2.0 // indirect
	github.com/smartystreets/goconvey v1.6.4 // indirect
	github.com/stretchr/testify v1.8.1 // indirect
	github.com/zsefvlol/timezonemapper v1.0.0
	golang.org/x/crypto v0.17.0 // indirect
	golang.org/x/sys v0.30.0 // indirect
	google.golang.org/appengine v1.6.3 // indirect
	gopkg.in/yaml.v3 v3.0.1
)


================================================
FILE: go.sum
================================================
github.com/alecthomas/assert/v2 v2.1.0/go.mod h1:b/+1DI2Q6NckYi+3mXyH3wFb8qG37K/DuK80n7WefXA=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/kong v0.7.1 h1:azoTh0IOfwlAX3qN9sHWTxACE2oV8Bg2gAwBsMwDQY4=
github.com/alecthomas/kong v0.7.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U=
github.com/alecthomas/kong v1.7.0 h1:MnT8+5JxFDCvISeI6vgd/mFbAJwueJ/pqQNzZMsiqZE=
github.com/alecthomas/kong v1.7.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU=
github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.0.0-20190920000552-128d9f4ae1cd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/denisenkom/go-mssqldb v0.0.0-20200910202707-1e08a3fab204/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4=
github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/itchyny/gojq v0.12.11 h1:YhLueoHhHiN4mkfM+3AyJV6EPcCxKZsOnYf+aVSwaQw=
github.com/itchyny/gojq v0.12.11/go.mod h1:o3FT8Gkbg/geT4pLI0tF3hvip5F3Y/uskjRz9OYa38g=
github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE=
github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/klauspost/lctime v0.1.0 h1:nINsuFc860M9cyYhT6vfg6U1USh7kiVBj/s/2b04U70=
github.com/klauspost/lctime v0.1.0/go.mod h1:OwdMhr8tbQvusAsnilqkkgDQqivWlqyg0w5cfXkLiDk=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
github.com/samonzeweb/godb v1.0.8 h1:WRn6nq0FChYOzh+w8SgpXHUkEhL7W6ZqkCf5Ninx7Uc=
github.com/samonzeweb/godb v1.0.8/go.mod h1:LNDt3CakfBwpRY4AD0y/QPTbj+jB6O17tSxQES0p47o=
github.com/samonzeweb/godb v1.0.13 h1:dEWijZGizhSN7oOLFYq0+NquG54DCJ9WG55bEcH7GOA=
github.com/samonzeweb/godb v1.0.13/go.mod h1:Dcm9f9+aO6bQin4Ce4X3oOM2gvhGMt2naLIDLPQSaWQ=
github.com/samonzeweb/godb v1.0.15 h1:HyNb8o1w109as9KWE8ih1YIBe8jC4luJ22f1XNacW38=
github.com/samonzeweb/godb v1.0.15/go.mod h1:SxCHqyireDXNrZApknS9lGUEutA43x9eJF632ecbK5Q=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zsefvlol/timezonemapper v1.0.0 h1:HXqkOzf01gXYh2nDQcDSROikFgMaximnhE8BY9SyF6E=
github.com/zsefvlol/timezonemapper v1.0.0/go.mod h1:cVUCOLEmc/VvOMusEhpd2G/UBtadL26ZVz2syODXDoQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.3/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=


================================================
FILE: internal/config/config.go
================================================
package config

import (
	"log"
	"os"

	"gopkg.in/yaml.v3"

	"github.com/chubin/wttr.in/internal/types"
	"github.com/chubin/wttr.in/internal/util"
)

// Config of the program.
type Config struct {
	Cache
	Geo
	Logging
	Server
	Uplink
}

// Logging configuration.
type Logging struct {
	// AccessLog path.
	AccessLog string `yaml:"accessLog,omitempty"`

	// ErrorsLog path.
	ErrorsLog string `yaml:"errorsLog,omitempty"`

	// Interval between access log flushes, in seconds.
	Interval int `yaml:"interval,omitempty"`
}

// Server configuration.
type Server struct {
	// PortHTTP is port where HTTP server must listen.
	// If 0, HTTP is disabled.
	PortHTTP int `yaml:"portHttp,omitempty"`

	// PortHTTP is port where the HTTPS server must listen.
	// If 0, HTTPS is disabled.
	PortHTTPS int `yaml:"portHttps,omitempty"`

	// TLSCertFile contains path to cert file for TLS Server.
	TLSCertFile string `yaml:"tlsCertFile,omitempty"`

	// TLSCertFile contains path to key file for TLS Server.
	TLSKeyFile string `yaml:"tlsKeyFile,omitempty"`
}

// Uplink configuration.
type Uplink struct {
	// Address1 contains address of the uplink server in form IP:PORT
	// for format=j1 queries.
	Address1 string `yaml:"address1,omitempty"`

	// Address2 contains address of the uplink server in form IP:PORT
	// for format=* queries.
	Address2 string `yaml:"address2,omitempty"`

	// Address3 contains address of the uplink server in form IP:PORT
	// for all other queries.
	Address3 string `yaml:"address3,omitempty"`

	// Address4 contains address of the uplink server in form IP:PORT
	// for PNG queries.
	Address4 string `yaml:"address4,omitempty"`

	// Timeout for upstream queries.
	Timeout int `yaml:"timeout,omitempty"`

	// PrefetchInterval contains time (in milliseconds) indicating,
	// how long the prefetch procedure should take.
	PrefetchInterval int `yaml:"prefetchInterval,omitempty"`
}

// Cache configuration.
type Cache struct {
	// Size of the main cache.
	Size int `yaml:"size,omitempty"`
}

// Geo contains geolocation configuration.
type Geo struct {
	// IPCache contains the path to the IP Geodata cache.
	IPCache string `yaml:"ipCache,omitempty"`

	// IPCacheDB contains the path to the SQLite DB with the IP Geodata cache.
	IPCacheDB string `yaml:"ipCacheDb,omitempty"`

	IPCacheType types.CacheType `yaml:"ipCacheType,omitempty"`

	// LocationCache contains the path to the Location Geodata cache.
	LocationCache string `yaml:"locationCache,omitempty"`

	// LocationCacheDB contains the path to the SQLite DB with the Location Geodata cache.
	LocationCacheDB string `yaml:"locationCacheDb,omitempty"`

	LocationCacheType types.CacheType `yaml:"locationCacheType,omitempty"`

	Nominatim []Nominatim `yaml:"nominatim"`
}

type Nominatim struct {
	Name string

	// Type describes the type of the location service.
	// Supported types: iq.
	Type string

	URL string

	Token string
}

// Default contains the default configuration.
func Default() *Config {
	return &Config{
		Cache{
			Size: 12800,
		},
		Geo{
			IPCache:           "/wttr.in/cache/ip2l",
			IPCacheDB:         "/wttr.in/cache/geoip.db",
			IPCacheType:       types.CacheTypeDB,
			LocationCache:     "/wttr.in/cache/loc",
			LocationCacheDB:   "/wttr.in/cache/geoloc.db",
			LocationCacheType: types.CacheTypeDB,
			Nominatim: []Nominatim{
				{
					Name:  "locationiq",
					Type:  "iq",
					URL:   "https://eu1.locationiq.com/v1/search",
					Token: os.Getenv("NOMINATIM_LOCATIONIQ"),
				},
				{
					Name:  "opencage",
					Type:  "opencage",
					URL:   "https://api.opencagedata.com/geocode/v1/json",
					Token: os.Getenv("NOMINATIM_OPENCAGE"),
				},
			},
		},
		Logging{
			AccessLog: "/wttr.in/log/access.log",
			ErrorsLog: "/wttr.in/log/errors.log",
			Interval:  300,
		},
		Server{
			PortHTTP:    8083,
			PortHTTPS:   8084,
			TLSCertFile: "/wttr.in/etc/fullchain.pem",
			TLSKeyFile:  "/wttr.in/etc/privkey.pem",
		},
		Uplink{
			Address1:         "127.0.0.1:9002",
			Address2:         "127.0.0.1:9002",
			Address3:         "127.0.0.1:9002",
			Address4:         "127.0.0.1:9002",
			Timeout:          30,
			PrefetchInterval: 300,
		},
	}
}

// Load config from file.
func Load(filename string) (*Config, error) {
	var (
		config Config
		data   []byte
		err    error
	)

	data, err = os.ReadFile(filename)
	if err != nil {
		return nil, err
	}

	err = util.YamlUnmarshalStrict(data, &config)
	if err != nil {
		return nil, err
	}

	return &config, nil
}

func (c *Config) Dump() []byte {
	data, err := yaml.Marshal(c)
	if err != nil {
		// should never happen.
		log.Fatalln("config.Dump():", err)
	}

	return data
}


================================================
FILE: internal/fmt/png/colors.go
================================================
package main

// Source: https://www.ditig.com/downloads/256-colors.json

var ansiColorsDB = [][3]float64{
	{
		0, 0, 0,
	},
	{
		128, 0, 0,
	},
	{
		0, 128, 0,
	},
	{
		128, 128, 0,
	},
	{
		0, 0, 128,
	},
	{
		128, 0, 128,
	},
	{
		0, 128, 128,
	},
	{
		192, 192, 192,
	},
	{
		128, 128, 128,
	},
	{
		255, 0, 0,
	},
	{
		0, 255, 0,
	},
	{
		255, 255, 0,
	},
	{
		0, 0, 255,
	},
	{
		255, 0, 255,
	},
	{
		0, 255, 255,
	},
	{
		255, 255, 255,
	},
	{
		0, 0, 0,
	},
	{
		0, 0, 95,
	},
	{
		0, 0, 135,
	},
	{
		0, 0, 175,
	},
	{
		0, 0, 215,
	},
	{
		0, 0, 255,
	},
	{
		0, 95, 0,
	},
	{
		0, 95, 95,
	},
	{
		0, 95, 135,
	},
	{
		0, 95, 175,
	},
	{
		0, 95, 215,
	},
	{
		0, 95, 255,
	},
	{
		0, 135, 0,
	},
	{
		0, 135, 95,
	},
	{
		0, 135, 135,
	},
	{
		0, 135, 175,
	},
	{
		0, 135, 215,
	},
	{
		0, 135, 255,
	},
	{
		0, 175, 0,
	},
	{
		0, 175, 95,
	},
	{
		0, 175, 135,
	},
	{
		0, 175, 175,
	},
	{
		0, 175, 215,
	},
	{
		0, 175, 255,
	},
	{
		0, 215, 0,
	},
	{
		0, 215, 95,
	},
	{
		0, 215, 135,
	},
	{
		0, 215, 175,
	},
	{
		0, 215, 215,
	},
	{
		0, 215, 255,
	},
	{
		0, 255, 0,
	},
	{
		0, 255, 95,
	},
	{
		0, 255, 135,
	},
	{
		0, 255, 175,
	},
	{
		0, 255, 215,
	},
	{
		0, 255, 255,
	},
	{
		95, 0, 0,
	},
	{
		95, 0, 95,
	},
	{
		95, 0, 135,
	},
	{
		95, 0, 175,
	},
	{
		95, 0, 215,
	},
	{
		95, 0, 255,
	},
	{
		95, 95, 0,
	},
	{
		95, 95, 95,
	},
	{
		95, 95, 135,
	},
	{
		95, 95, 175,
	},
	{
		95, 95, 215,
	},
	{
		95, 95, 255,
	},
	{
		95, 135, 0,
	},
	{
		95, 135, 95,
	},
	{
		95, 135, 135,
	},
	{
		95, 135, 175,
	},
	{
		95, 135, 215,
	},
	{
		95, 135, 255,
	},
	{
		95, 175, 0,
	},
	{
		95, 175, 95,
	},
	{
		95, 175, 135,
	},
	{
		95, 175, 175,
	},
	{
		95, 175, 215,
	},
	{
		95, 175, 255,
	},
	{
		95, 215, 0,
	},
	{
		95, 215, 95,
	},
	{
		95, 215, 135,
	},
	{
		95, 215, 175,
	},
	{
		95, 215, 215,
	},
	{
		95, 215, 255,
	},
	{
		95, 255, 0,
	},
	{
		95, 255, 95,
	},
	{
		95, 255, 135,
	},
	{
		95, 255, 175,
	},
	{
		95, 255, 215,
	},
	{
		95, 255, 255,
	},
	{
		135, 0, 0,
	},
	{
		135, 0, 95,
	},
	{
		135, 0, 135,
	},
	{
		135, 0, 175,
	},
	{
		135, 0, 215,
	},
	{
		135, 0, 255,
	},
	{
		135, 95, 0,
	},
	{
		135, 95, 95,
	},
	{
		135, 95, 135,
	},
	{
		135, 95, 175,
	},
	{
		135, 95, 215,
	},
	{
		135, 95, 255,
	},
	{
		135, 135, 0,
	},
	{
		135, 135, 95,
	},
	{
		135, 135, 135,
	},
	{
		135, 135, 175,
	},
	{
		135, 135, 215,
	},
	{
		135, 135, 255,
	},
	{
		135, 175, 0,
	},
	{
		135, 175, 95,
	},
	{
		135, 175, 135,
	},
	{
		135, 175, 175,
	},
	{
		135, 175, 215,
	},
	{
		135, 175, 255,
	},
	{
		135, 215, 0,
	},
	{
		135, 215, 95,
	},
	{
		135, 215, 135,
	},
	{
		135, 215, 175,
	},
	{
		135, 215, 215,
	},
	{
		135, 215, 255,
	},
	{
		135, 255, 0,
	},
	{
		135, 255, 95,
	},
	{
		135, 255, 135,
	},
	{
		135, 255, 175,
	},
	{
		135, 255, 215,
	},
	{
		135, 255, 255,
	},
	{
		175, 0, 0,
	},
	{
		175, 0, 95,
	},
	{
		175, 0, 135,
	},
	{
		175, 0, 175,
	},
	{
		175, 0, 215,
	},
	{
		175, 0, 255,
	},
	{
		175, 95, 0,
	},
	{
		175, 95, 95,
	},
	{
		175, 95, 135,
	},
	{
		175, 95, 175,
	},
	{
		175, 95, 215,
	},
	{
		175, 95, 255,
	},
	{
		175, 135, 0,
	},
	{
		175, 135, 95,
	},
	{
		175, 135, 135,
	},
	{
		175, 135, 175,
	},
	{
		175, 135, 215,
	},
	{
		175, 135, 255,
	},
	{
		175, 175, 0,
	},
	{
		175, 175, 95,
	},
	{
		175, 175, 135,
	},
	{
		175, 175, 175,
	},
	{
		175, 175, 215,
	},
	{
		175, 175, 255,
	},
	{
		175, 215, 0,
	},
	{
		175, 215, 95,
	},
	{
		175, 215, 135,
	},
	{
		175, 215, 175,
	},
	{
		175, 215, 215,
	},
	{
		175, 215, 255,
	},
	{
		175, 255, 0,
	},
	{
		175, 255, 95,
	},
	{
		175, 255, 135,
	},
	{
		175, 255, 175,
	},
	{
		175, 255, 215,
	},
	{
		175, 255, 255,
	},
	{
		215, 0, 0,
	},
	{
		215, 0, 95,
	},
	{
		215, 0, 135,
	},
	{
		215, 0, 175,
	},
	{
		215, 0, 215,
	},
	{
		215, 0, 255,
	},
	{
		215, 95, 0,
	},
	{
		215, 95, 95,
	},
	{
		215, 95, 135,
	},
	{
		215, 95, 175,
	},
	{
		215, 95, 215,
	},
	{
		215, 95, 255,
	},
	{
		215, 135, 0,
	},
	{
		215, 135, 95,
	},
	{
		215, 135, 135,
	},
	{
		215, 135, 175,
	},
	{
		215, 135, 215,
	},
	{
		215, 135, 255,
	},
	{
		215, 175, 0,
	},
	{
		215, 175, 95,
	},
	{
		215, 175, 135,
	},
	{
		215, 175, 175,
	},
	{
		215, 175, 215,
	},
	{
		215, 175, 255,
	},
	{
		215, 215, 0,
	},
	{
		215, 215, 95,
	},
	{
		215, 215, 135,
	},
	{
		215, 215, 175,
	},
	{
		215, 215, 215,
	},
	{
		215, 215, 255,
	},
	{
		215, 255, 0,
	},
	{
		215, 255, 95,
	},
	{
		215, 255, 135,
	},
	{
		215, 255, 175,
	},
	{
		215, 255, 215,
	},
	{
		215, 255, 255,
	},
	{
		255, 0, 0,
	},
	{
		255, 0, 95,
	},
	{
		255, 0, 135,
	},
	{
		255, 0, 175,
	},
	{
		255, 0, 215,
	},
	{
		255, 0, 255,
	},
	{
		255, 95, 0,
	},
	{
		255, 95, 95,
	},
	{
		255, 95, 135,
	},
	{
		255, 95, 175,
	},
	{
		255, 95, 215,
	},
	{
		255, 95, 255,
	},
	{
		255, 135, 0,
	},
	{
		255, 135, 95,
	},
	{
		255, 135, 135,
	},
	{
		255, 135, 175,
	},
	{
		255, 135, 215,
	},
	{
		255, 135, 255,
	},
	{
		255, 175, 0,
	},
	{
		255, 175, 95,
	},
	{
		255, 175, 135,
	},
	{
		255, 175, 175,
	},
	{
		255, 175, 215,
	},
	{
		255, 175, 255,
	},
	{
		255, 215, 0,
	},
	{
		255, 215, 95,
	},
	{
		255, 215, 135,
	},
	{
		255, 215, 175,
	},
	{
		255, 215, 215,
	},
	{
		255, 215, 255,
	},
	{
		255, 255, 0,
	},
	{
		255, 255, 95,
	},
	{
		255, 255, 135,
	},
	{
		255, 255, 175,
	},
	{
		255, 255, 215,
	},
	{
		255, 255, 255,
	},
	{
		8, 8, 8,
	},
	{
		18, 18, 18,
	},
	{
		28, 28, 28,
	},
	{
		38, 38, 38,
	},
	{
		48, 48, 48,
	},
	{
		58, 58, 58,
	},
	{
		68, 68, 68,
	},
	{
		78, 78, 78,
	},
	{
		88, 88, 88,
	},
	{
		98, 98, 98,
	},
	{
		108, 108, 108,
	},
	{
		118, 118, 118,
	},
	{
		128, 128, 128,
	},
	{
		138, 138, 138,
	},
	{
		148, 148, 148,
	},
	{
		158, 158, 158,
	},
	{
		168, 168, 168,
	},
	{
		178, 178, 178,
	},
	{
		188, 188, 188,
	},
	{
		198, 198, 198,
	},
	{
		208, 208, 208,
	},
	{
		218, 218, 218,
	},
	{
		228, 228, 228,
	},
	{
		238, 238, 238,
	},
}


================================================
FILE: internal/fmt/png/go.mod
================================================
module example.com/m/v2

go 1.20

require (
	github.com/chubin/vt10x v0.0.0-20231112153020-ef4f56837bf1 // indirect
	github.com/fogleman/gg v1.3.0 // indirect
	github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
	golang.org/x/image v0.14.0 // indirect
)


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


================================================
FILE: internal/fmt/png/png.go
================================================
package main

import (
	"fmt"
	"log"
	"os"
	"strings"

	"github.com/chubin/vt10x"
	"github.com/fogleman/gg"
)

func StringSliceToRuneSlice(s string) [][]rune {
	strings := strings.Split(s, "\n")
	result := make([][]rune, len(strings))

	i := 0
	for _, str := range strings {
		if len(str) == 0 {
			continue
		}
		result[i] = []rune(str)
		i++
	}

	return result
}

func maxRowLength(rows [][]rune) int {
	maxLen := 0
	for _, row := range rows {
		if len(row) > maxLen {
			maxLen = len(row)
		}
	}
	return maxLen
}

func GeneratePng() {
	runes := StringSliceToRuneSlice(`
Weather report: Hochstadt an der Aisch, Germany

     \  /       Partly cloudy
   _ /"".-.     +5(2) °C
     \_(   ).   ↗ 9 km/h
     /(___(__)  10 km
                0.0 mm
                        ┌─────────────┐
┌───────────────────────┤  Sat 11 Nov ├───────────────────────┐
│             Noon      └──────┬──────┘      Night            │
├──────────────────────────────┼──────────────────────────────┤
│  _'/"".-.     Patchy rain po…│  _'/"".-.     Patchy rain po…│
│   ,\_(   ).   +6(3) °C       │   ,\_(   ).   +5(2) °C       │
│    /(___(__)  → 22-29 km/h   │    /(___(__)  ↗ 14-20 km/h   │
│      ‘ ‘ ‘ ‘  10 km          │      ‘ ‘ ‘ ‘  10 km          │
│     ‘ ‘ ‘ ‘   0.1 mm | 86%   │     ‘ ‘ ‘ ‘   0.0 mm | 89%   │
└──────────────────────────────┴──────────────────────────────┘
                        ┌─────────────┐
┌───────────────────────┤  Sun 12 Nov ├───────────────────────┐
│             Noon      └──────┬──────┘      Night            │
├──────────────────────────────┼──────────────────────────────┤
│    \  /       Partly cloudy  │      .-.      Light drizzle  │
│  _ /"".-.     +8(7) °C       │     (   ).    +5(2) °C       │
│    \_(   ).   ↑ 7-8 km/h     │    (___(__)   ↑ 13-18 km/h   │
│    /(___(__)  10 km          │     ‘ ‘ ‘ ‘   2 km           │
│               0.0 mm | 0%    │    ‘ ‘ ‘ ‘    0.3 mm | 76%   │
└──────────────────────────────┴──────────────────────────────┘
`)

	// Dimensions of each rune in pixels
	runeWidth := 8
	runeHeight := 14

	// Compute the width and height of the final image
	imageWidth := runeWidth * maxRowLength(runes)
	imageHeight := runeHeight * len(runes)

	// Create a new context with the computed dimensions
	dc := gg.NewContext(imageWidth, imageHeight)

	// fontPath := "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf"
	// fontPath := "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc"
	fontPath := "/usr/share/fonts/truetype/lexi/LexiGulim.ttf"

	err := dc.LoadFontFace(fontPath, 13)
	if err != nil {
		log.Fatal(err)
	}

	// Loop through each rune in the array and draw it on the context
	for i, row := range runes {
		for j, char := range row {
			// Compute the x and y coordinates for drawing the current rune
			x := float64(j*runeWidth + runeWidth/2)
			y := float64(i*runeHeight + runeHeight/2)

			// Set the appropriate color for the current rune
			if char == '#' {
				dc.SetRGB(0, 0, 0) // Black
			} else if char == '@' {
				dc.SetRGB(1, 0, 0) // Red
			} else {
				dc.SetRGB(1, 1, 1) // White
			}

			character := string(char)
			// if char == ' ' {
			// 	character = fmt.Sprint(j % 10)
			// }
			dc.DrawRectangle(x, y, x+float64(runeWidth), y+float64(runeHeight))
			dc.Fill()

			// Draw a rectangle with the rune's dimensions and color
			dc.DrawString(character, x, y) // Draw the character centered on the canvas
			// dc.DrawStringAnchored(character, x, y, 0.5, 0.5) // Draw the character centered on the canvas
		}
	}

	// Save the image to a PNG file
	err = dc.SavePNG("output.png")
	if err != nil {
		fmt.Println("Error saving PNG:", err)
		return
	}

	fmt.Println("PNG generated successfully")
}

func GeneratePngFromANSI(input []byte, outputFile string) error {
	// Dimensions of each rune in pixels
	runeWidth := 8
	runeHeight := 14
	fontSize := 13.0
	// fontPath := "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf"
	fontPath := "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc"

	imageCols := 80
	imageRows := 25

	// Compute the width and height of the final image
	imageWidth := runeWidth * imageCols
	imageHeight := runeHeight * imageRows

	// Create terminal and feed it with input.
	term := vt10x.New(vt10x.WithSize(imageCols, imageRows))
	_, err := term.Write([]byte("\033[20h"))
	if err != nil {
		return fmt.Errorf("virtual terminal write error: %w", err)
	}

	_, err = term.Write(input)
	if err != nil {
		return fmt.Errorf("virtual terminal write error: %w", err)
	}

	// Create a new context with the computed dimensions
	dc := gg.NewContext(imageWidth, imageHeight)

	err = dc.LoadFontFace(fontPath, fontSize) // Set font size to 96
	if err != nil {
		return fmt.Errorf("error loading font: %w", err)
	}

	// Loop through each rune in the array and draw it on the context
	for i := 0; i < imageRows; i++ {
		for j := 0; j < imageCols; j++ {
			// Compute the x and y coordinates for drawing the current rune
			x := float64(j * runeWidth)
			y := float64(i * runeHeight)

			cell := term.Cell(j, i)
			character := string(cell.Char)

			dc.DrawRectangle(x, y, float64(runeWidth), float64(runeHeight))
			bg := colorANSItoRGB(cell.BG)
			dc.SetRGB(bg[0], bg[1], bg[2])
			dc.Fill()

			fg := colorANSItoRGB(cell.FG)
			dc.SetRGB(fg[0], fg[1], fg[2])

			// Draw a rectangle with the rune's dimensions and color
			dc.DrawString(character, x, y+float64(runeHeight)-3) // Draw the character centered on the canvas
			// dc.DrawStringAnchored(character, x, y, 0.5, 0.5) // Draw the character centered on the canvas
		}
	}

	// Save the image to a PNG file
	err = dc.SavePNG(outputFile)
	if err != nil {
		return fmt.Errorf("error saving png: %w", err)
	}

	return nil
}

func colorANSItoRGB(colorANSI vt10x.Color) [3]float64 {
	defaultBG := vt10x.Color(0)
	defaultFG := vt10x.Color(8)

	if colorANSI == vt10x.DefaultFG {
		colorANSI = defaultFG
	}
	if colorANSI == vt10x.DefaultBG {
		colorANSI = defaultBG
	}

	if colorANSI > 255 {
		return [3]float64{127, 127, 127}
	}
	return ansiColorsDB[colorANSI]
}

func main() {
	data, err := os.ReadFile("zh-text.txt")
	if err != nil {
		log.Fatalln(err)
	}

	err = GeneratePngFromANSI(data, "output.png")
	if err != nil {
		log.Fatalln(err)
	}
}


================================================
FILE: internal/geo/ip/convert.go
================================================
package ip

import (
	"fmt"
	"log"
	"path/filepath"

	"github.com/samonzeweb/godb"
	"github.com/samonzeweb/godb/adapters/sqlite"

	"github.com/chubin/wttr.in/internal/util"
)

//nolint:cyclop
func (c *Cache) ConvertCache() error {
	dbfile := c.config.Geo.IPCacheDB

	err := util.RemoveFileIfExists(dbfile)
	if err != nil {
		return err
	}

	db, err := godb.Open(sqlite.Adapter, dbfile)
	if err != nil {
		return err
	}

	err = createTable(db, "Address")
	if err != nil {
		return err
	}

	log.Println("listing cache entries...")
	files, err := filepath.Glob(filepath.Join(c.config.Geo.IPCache, "*"))
	if err != nil {
		return err
	}

	log.Printf("going to convert %d entries\n", len(files))

	block := []Address{}
	for i, file := range files {
		ip := filepath.Base(file)
		loc, err := c.Read(ip)
		if err != nil {
			log.Println("invalid entry for", ip)

			continue
		}

		block = append(block, *loc)

		if i%1000 != 0 || i == 0 {
			continue
		}

		err = db.BulkInsert(&block).Do()
		if err != nil {
			return err
		}
		block = []Address{}
		log.Println("converted", i+1, "entries")
	}

	// inserting the rest.
	err = db.BulkInsert(&block).Do()
	if err != nil {
		return err
	}

	log.Println("converted", len(files), "entries")

	return nil
}

func createTable(db *godb.DB, tableName string) error {
	createTable := fmt.Sprintf(
		`create table %s (
	    name           text not null primary key,
        fullName       text not null,
        lat            text not null,
        long           text not null);
	`, tableName)

	_, err := db.CurrentDB().Exec(createTable)

	return err
}


================================================
FILE: internal/geo/ip/ip.go
================================================
package ip

import (
	"fmt"
	"log"
	"net/http"
	"os"
	"path"
	"regexp"
	"strconv"
	"strings"

	"github.com/samonzeweb/godb"
	"github.com/samonzeweb/godb/adapters/sqlite"

	"github.com/chubin/wttr.in/internal/config"
	"github.com/chubin/wttr.in/internal/routing"
	"github.com/chubin/wttr.in/internal/types"
	"github.com/chubin/wttr.in/internal/util"
)

// Address information.
type Address struct {
	IP          string  `db:"ip,key"`
	CountryCode string  `db:"countryCode"`
	Country     string  `db:"country"`
	Region      string  `db:"region"`
	City        string  `db:"city"`
	Latitude    float64 `db:"latitude"`
	Longitude   float64 `db:"longitude"`
}

func (l *Address) String() string {
	if l.Latitude == -1000 {
		return fmt.Sprintf(
			"%s;%s;%s;%s",
			l.CountryCode, l.Country, l.Region, l.City)
	}

	return fmt.Sprintf(
		"%s;%s;%s;%s;%v;%v",
		l.CountryCode, l.Country, l.Region, l.City, l.Latitude, l.Longitude)
}

// Cache provides access to the IP Geodata cache.
type Cache struct {
	config *config.Config
	db     *godb.DB
}

// NewCache returns new cache reader for the specified config.
func NewCache(config *config.Config) (*Cache, error) {
	db, err := godb.Open(sqlite.Adapter, config.Geo.IPCacheDB)
	if err != nil {
		return nil, err
	}

	// Needed for "upsert" implementation in Put()
	db.UseErrorParser()

	return &Cache{
		config: config,
		db:     db,
	}, nil
}

// Read returns location information from the cache, if found,
// or types.ErrNotFound if not found. If the entry is found, but its format
// is invalid, types.ErrInvalidCacheEntry is returned.
//
// Format:
//
//  [CountryCode];Country;Region;City;[Latitude];[Longitude]
//
// Example:
//
//  DE;Germany;Free and Hanseatic City of Hamburg;Hamburg;53.5736;9.9782
//

func (c *Cache) Read(addr string) (*Address, error) {
	if c.config.Geo.IPCacheType == types.CacheTypeDB {
		return c.readFromCacheDB(addr)
	}

	return c.readFromCacheFile(addr)
}

func (c *Cache) readFromCacheFile(addr string) (*Address, error) {
	bytes, err := os.ReadFile(c.cacheFile(addr))
	if err != nil {
		return nil, types.ErrNotFound
	}

	return NewAddressFromString(addr, string(bytes))
}

func (c *Cache) readFromCacheDB(addr string) (*Address, error) {
	result := Address{}
	err := c.db.Select(&result).
		Where("IP = ?", addr).
		Do()
	if err != nil {
		return nil, err
	}

	return &result, nil
}

func (c *Cache) Put(addr string, loc *Address) error {
	if c.config.Geo.IPCacheType == types.CacheTypeDB {
		return c.putToCacheDB(loc)
	}

	return c.putToCacheFile(addr, loc)
}

func (c *Cache) putToCacheDB(loc *Address) error {
	err := c.db.Insert(loc).Do()
	// it should work like this:
	//
	//   target := dberror.UniqueConstraint{}
	//   if errors.As(err, &target) {
	//
	// See: https://github.com/samonzeweb/godb/pull/23
	//
	// But for some reason it does not work,
	// so the dirty hack is used:
	if strings.Contains(fmt.Sprint(err), "UNIQUE constraint failed") {
		return c.db.Update(loc).Do()
	}

	return err
}

func (c *Cache) putToCacheFile(addr string, loc fmt.Stringer) error {
	return os.WriteFile(c.cacheFile(addr), []byte(loc.String()), 0o600)
}

// cacheFile returns path to the cache entry for addr.
func (c *Cache) cacheFile(addr string) string {
	return path.Join(c.config.Geo.IPCache, addr)
}

// NewAddressFromString parses the location cache entry s,
// and return location, or error, if the cache entry is invalid.
func NewAddressFromString(addr, s string) (*Address, error) {
	var (
		lat  float64 = -1000
		long float64 = -1000
		err  error
	)

	parts := strings.Split(s, ";")
	if len(parts) < 4 {
		return nil, types.ErrInvalidCacheEntry
	}

	if len(parts) >= 6 {
		lat, err = strconv.ParseFloat(parts[4], 64)
		if err != nil {
			return nil, types.ErrInvalidCacheEntry
		}

		long, err = strconv.ParseFloat(parts[5], 64)
		if err != nil {
			return nil, types.ErrInvalidCacheEntry
		}
	}

	return &Address{
		IP:          addr,
		CountryCode: parts[0],
		Country:     parts[1],
		Region:      parts[2],
		City:        parts[3],
		Latitude:    lat,
		Longitude:   long,
	}, nil
}

// Response provides routing interface to the geo cache.
//
// Temporary workaround to switch IP addresses handling to the Go server.
// Handles two queries:
//
// - /:geo-ip-put?ip=IP&value=VALUE
// - /:geo-ip-get?ip=IP
//
//nolint:cyclop
func (c *Cache) Response(r *http.Request) *routing.Cadre {
	var (
		respERR = &routing.Cadre{Body: []byte("ERR")}
		respOK  = &routing.Cadre{Body: []byte("OK")}
	)

	if ip := util.ReadUserIP(r); ip != "127.0.0.1" {
		log.Printf("geoIP access from %s rejected\n", ip)

		return nil
	}

	if r.URL.Path == "/:geo-ip-put" {
		ip := r.URL.Query().Get("ip")
		value := r.URL.Query().Get("value")
		if !validIP4(ip) || value == "" {
			log.Printf("invalid geoIP put query: ip='%s' value='%s'\n", ip, value)

			return respERR
		}

		location, err := NewAddressFromString(ip, value)
		if err != nil {
			return respERR
		}

		err = c.Put(ip, location)
		if err != nil {
			return respERR
		}

		return respOK
	}
	if r.URL.Path == "/:geo-ip-get" {
		ip := r.URL.Query().Get("ip")
		if !validIP4(ip) {
			return respERR
		}

		result, err := c.Read(ip)
		if result == nil || err != nil {
			return respERR
		}

		return &routing.Cadre{Body: []byte(result.String())}
	}

	return nil
}

func validIP4(ipAddress string) bool {
	re := regexp.MustCompile(
		`^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$`)

	return re.MatchString(strings.Trim(ipAddress, " "))
}


================================================
FILE: internal/geo/ip/ip_test.go
================================================
package ip_test

import (
	"testing"

	"github.com/stretchr/testify/require"

	. "github.com/chubin/wttr.in/internal/geo/ip"
	"github.com/chubin/wttr.in/internal/types"
)

//nolint:funlen
func TestParseCacheEntry(t *testing.T) {
	t.Parallel()
	tests := []struct {
		addr     string
		input    string
		expected Address
		err      error
	}{
		{
			"1.2.3.4",
			"DE;Germany;Free and Hanseatic City of Hamburg;Hamburg;53.5736;9.9782",
			Address{
				IP:          "1.2.3.4",
				CountryCode: "DE",
				Country:     "Germany",
				Region:      "Free and Hanseatic City of Hamburg",
				City:        "Hamburg",
				Latitude:    53.5736,
				Longitude:   9.9782,
			},
			nil,
		},

		{
			"1.2.3.4",
			"ES;Spain;Madrid, Comunidad de;Madrid;40.4165;-3.70256;28223;Orange Espagne SA;orange.es",
			Address{
				IP:          "1.2.3.4",
				CountryCode: "ES",
				Country:     "Spain",
				Region:      "Madrid, Comunidad de",
				City:        "Madrid",
				Latitude:    40.4165,
				Longitude:   -3.70256,
			},
			nil,
		},

		{
			"1.2.3.4",
			"US;United States of America;California;Mountain View",
			Address{
				IP:          "1.2.3.4",
				CountryCode: "US",
				Country:     "United States of America",
				Region:      "California",
				City:        "Mountain View",
				Latitude:    -1000,
				Longitude:   -1000,
			},
			nil,
		},

		// Invalid entries
		{
			"1.2.3.4",
			"DE;Germany;Free and Hanseatic City of Hamburg;Hamburg;53.5736;XXX",
			Address{},
			types.ErrInvalidCacheEntry,
		},
	}

	for _, tt := range tests {
		result, err := NewAddressFromString(tt.addr, tt.input)
		if tt.err == nil {
			require.NoError(t, err)
			require.Equal(t, *result, tt.expected)
		} else {
			require.ErrorIs(t, err, tt.err)
		}
	}
}


================================================
FILE: internal/geo/location/cache.go
================================================
package location

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path"
	"strconv"
	"strings"

	"github.com/samonzeweb/godb"
	"github.com/samonzeweb/godb/adapters/sqlite"
	log "github.com/sirupsen/logrus"
	"github.com/zsefvlol/timezonemapper"

	"github.com/chubin/wttr.in/internal/config"
	"github.com/chubin/wttr.in/internal/types"
)

// Cache is an implemenation of DB/file-based cache.
//
// At the moment, it is an implementation for the location cache,
// but it should be generalized to cache everything.
type Cache struct {
	config        *config.Config
	db            *godb.DB
	searcher      *Searcher
	indexField    string
	filesCacheDir string
}

// NewCache returns new cache reader for the specified config.
func NewCache(config *config.Config) (*Cache, error) {
	var (
		db  *godb.DB
		err error
	)

	if config.Geo.LocationCacheType == types.CacheTypeDB {
		log.Debugln("using db for location cache")
		db, err = godb.Open(sqlite.Adapter, config.Geo.LocationCacheDB)
		if err != nil {
			return nil, err
		}

		log.Debugln("db file:", config.Geo.LocationCacheDB)

		// Needed for "upsert" implementation in Put()
		db.UseErrorParser()
	}

	return &Cache{
		config:        config,
		db:            db,
		indexField:    "name",
		filesCacheDir: config.Geo.LocationCache,
		searcher:      NewSearcher(config),
	}, nil
}

// Resolve returns location information for specified location.
// If the information is found in the cache, it is returned.
// If it is not found, the external service is queried,
// and the result is stored in the cache.
func (c *Cache) Resolve(location string) (*Location, error) {
	location = normalizeLocationName(location)

	loc, err := c.Read(location)
	if !errors.Is(err, types.ErrNotFound) {
		return loc, err
	}

	log.Debugln("geo/location: not found in cache:", location)
	loc, err = c.searcher.Search(location)
	if err != nil {
		return nil, err
	}

	loc.Name = location
	loc.Timezone = latLngToTimezoneString(loc.Lat, loc.Lon)

	err = c.Put(location, loc)
	if err != nil {
		return nil, err
	}

	return loc, nil
}

// Read returns location information from the cache, if found,
// or types.ErrNotFound if not found. If the entry is found, but its format
// is invalid, types.ErrInvalidCacheEntry is returned.
func (c *Cache) Read(addr string) (*Location, error) {
	if c.config.Geo.LocationCacheType == types.CacheTypeFiles {
		return c.readFromCacheFile(addr)
	}

	return c.readFromCacheDB(addr)
}

func (c *Cache) readFromCacheFile(name string) (*Location, error) {
	var (
		fileLoc = struct {
			Latitude  float64 `json:"latitude"`
			Longitude float64 `json:"longitude"`
			Timezone  string  `json:"timezone"`
			Address   string  `json:"address"`
		}{}
		location Location
	)

	bytes, err := os.ReadFile(c.cacheFile(name))
	if err != nil {
		return nil, types.ErrNotFound
	}
	err = json.Unmarshal(bytes, &fileLoc)
	if err != nil {
		return nil, err
	}

	// normalize name
	name = strings.TrimSpace(
		strings.TrimRight(
			strings.TrimLeft(name, `"`), `"`))

	timezone := fileLoc.Timezone
	if timezone == "" {
		timezone = timezonemapper.LatLngToTimezoneString(fileLoc.Latitude, fileLoc.Longitude)
	}

	location = Location{
		Name:     name,
		Lat:      fmt.Sprint(fileLoc.Latitude),
		Lon:      fmt.Sprint(fileLoc.Longitude),
		Timezone: timezone,
		Fullname: fileLoc.Address,
	}

	return &location, nil
}

func (c *Cache) readFromCacheDB(addr string) (*Location, error) {
	result := Location{}
	err := c.db.Select(&result).
		Where(c.indexField+" = ?", addr).
		Do()

	if strings.Contains(fmt.Sprint(err), "no rows in result set") {
		return nil, types.ErrNotFound
	}

	if err != nil {
		return nil, fmt.Errorf("readFromCacheDB: %w", err)
	}

	return &result, nil
}

func (c *Cache) Put(addr string, loc *Location) error {
	log.Infoln("geo/location: storing in cache:", loc)
	if c.config.Geo.IPCacheType == types.CacheTypeDB {
		return c.putToCacheDB(loc)
	}

	return c.putToCacheFile(addr, loc)
}

func (c *Cache) putToCacheDB(loc *Location) error {
	err := c.db.Insert(loc).Do()
	if strings.Contains(fmt.Sprint(err), "UNIQUE constraint failed") {
		return c.db.Update(loc).Do()
	}

	return err
}

func (c *Cache) putToCacheFile(addr string, loc fmt.Stringer) error {
	return os.WriteFile(c.cacheFile(addr), []byte(loc.String()), 0o600)
}

// cacheFile returns path to the cache entry for addr.
func (c *Cache) cacheFile(item string) string {
	return path.Join(c.filesCacheDir, item)
}

// normalizeLocationName converts name into the standard location form
// with the following steps:
// - remove excessive spaces,
// - remove quotes,
// - convert to lover case.
func normalizeLocationName(name string) string {
	name = strings.ReplaceAll(name, `"`, " ")
	name = strings.ReplaceAll(name, `'`, " ")
	name = strings.TrimSpace(name)
	name = strings.Join(strings.Fields(name), " ")

	return strings.ToLower(name)
}

// latLngToTimezoneString returns timezone for lat, lon,
// or an empty string if they are invalid.
func latLngToTimezoneString(lat, lon string) string {
	latFloat, err := strconv.ParseFloat(lat, 64)
	if err != nil {
		log.Errorln("geoloc: latLngToTimezoneString:", err)

		return ""
	}
	lonFloat, err := strconv.ParseFloat(lon, 64)
	if err != nil {
		log.Errorln("geoloc: latLngToTimezoneString:", err)

		return ""
	}

	return timezonemapper.LatLngToTimezoneString(latFloat, lonFloat)
}


================================================
FILE: internal/geo/location/convert.go
================================================
package location

import (
	"database/sql"
	"errors"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"strings"

	"github.com/samonzeweb/godb"
	"github.com/samonzeweb/godb/adapters/sqlite"
)

// ConvertCache converts files-based cache into the DB-based cache.
// If reset is true, the DB cache is created from scratch.
//
//nolint:funlen,cyclop
func (c *Cache) ConvertCache(reset bool) error {
	var (
		dbfile     = c.config.Geo.LocationCacheDB
		tableName  = "Location"
		cacheFiles = c.filesCacheDir
		known      = map[string]bool{}
	)

	if reset {
		err := removeDBIfExists(dbfile)
		if err != nil {
			return err
		}
	}

	db, err := godb.Open(sqlite.Adapter, dbfile)
	if err != nil {
		return err
	}

	if reset {
		err = createTable(db, tableName)
		if err != nil {
			return err
		}
	}

	log.Println("listing cache entries...")
	files, err := filepath.Glob(filepath.Join(cacheFiles, "*"))
	if err != nil {
		return err
	}

	log.Printf("going to convert %d entries\n", len(files))

	block := []Location{}
	for i, file := range files {
		ip := filepath.Base(file)
		loc, err := c.Read(ip)
		if err != nil {
			log.Println("invalid entry for", ip)

			continue
		}

		// Skip too long location names.
		if len(loc.Name) > 25 {
			continue
		}

		// Skip duplicates.
		if known[loc.Name] {
			log.Println("skipping", loc.Name)

			continue
		}

		singleLocation := Location{}
		err = db.Select(&singleLocation).
			Where("name = ?", loc.Name).
			Do()
		if !errors.Is(err, sql.ErrNoRows) {
			log.Println("found in db:", loc.Name)

			continue
		}

		known[loc.Name] = true

		// Skip some invalid names.
		if strings.Contains(loc.Name, "\n") {
			continue
		}

		block = append(block, *loc)
		if i%1000 != 0 || i == 0 {
			continue
		}

		log.Println("going to insert new entries")
		err = db.BulkInsert(&block).Do()
		if err != nil {
			return err
		}
		block = []Location{}
		log.Println("converted", i+1, "entries")
	}

	// inserting the rest.
	err = db.BulkInsert(&block).Do()
	if err != nil {
		return err
	}

	log.Println("converted", len(files), "entries")

	return nil
}

func createTable(db *godb.DB, tableName string) error {
	createTable := fmt.Sprintf(
		`create table %s (
	    name           text not null primary key,
        displayName    text not null,
        lat            text not null,
        lon            text not null,
		timezone       text not null);
	`, tableName)

	_, err := db.CurrentDB().Exec(createTable)

	return err
}

func removeDBIfExists(filename string) error {
	_, err := os.Stat(filename)
	if err != nil {
		if !os.IsNotExist(err) {
			return err
		}
		// no db file
		return nil
	}

	return os.Remove(filename)
}


================================================
FILE: internal/geo/location/location.go
================================================
package location

import (
	"encoding/json"
	"log"
)

type Location struct {
	Name     string `db:"name,key" json:"name"`
	Lat      string `db:"lat" json:"latitude"`
	Lon      string `db:"lon" json:"longitude"`
	Timezone string `db:"timezone" json:"timezone"`
	Fullname string `db:"displayName" json:"address"`
}

// String returns string representation of location.
func (l *Location) String() string {
	bytes, err := json.Marshal(l)
	if err != nil {
		// should never happen
		log.Fatalln(err)
	}

	return string(bytes)
}


================================================
FILE: internal/geo/location/nominatim.go
================================================
package location

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"

	"github.com/chubin/wttr.in/internal/types"
	log "github.com/sirupsen/logrus"
)

type Nominatim struct {
	name  string
	url   string
	token string
	typ   string
}

type locationQuerier interface {
	Query(*Nominatim, string) (*Location, error)
}

func NewNominatim(name, typ, url, token string) *Nominatim {
	return &Nominatim{
		name:  name,
		url:   url,
		token: token,
		typ:   typ,
	}
}

func (n *Nominatim) Query(location string) (*Location, error) {
	var data locationQuerier

	switch n.typ {
	case "iq":
		data = &locationIQ{}
	case "opencage":
		data = &locationOpenCage{}
	default:
		return nil, fmt.Errorf("%s: %w", n.name, types.ErrUnknownLocationService)
	}

	return data.Query(n, location)
}

func makeQuery(url string, result interface{}) error {
	var errResponse struct {
		Error string
	}

	log.Debugln("nominatim:", url)
	resp, err := http.Get(url)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return err
	}

	err = json.Unmarshal(body, &errResponse)
	if err == nil && errResponse.Error != "" {
		return fmt.Errorf("%w: %s", types.ErrUpstream, errResponse.Error)
	}

	log.Debugln("nominatim: response: ", string(body))
	err = json.Unmarshal(body, &result)
	if err != nil {
		return err
	}

	return nil
}


================================================
FILE: internal/geo/location/nominatim_locationiq.go
================================================
package location

import (
	"fmt"
	"net/url"

	"github.com/chubin/wttr.in/internal/types"
)

type locationIQ []struct {
	Name string `db:"name,key"`
	Lat  string `db:"lat"`
	Lon  string `db:"lon"`
	//nolint:tagliatelle
	Fullname string `db:"displayName" json:"display_name"`
}

func (data *locationIQ) Query(n *Nominatim, location string) (*Location, error) {
	url := fmt.Sprintf(
		"%s?q=%s&format=json&language=native&limit=1&key=%s",
		n.url, url.QueryEscape(location), n.token)

	err := makeQuery(url, data)
	if err != nil {
		return nil, fmt.Errorf("%s: %w", n.name, err)
	}

	if len(*data) != 1 {
		return nil, fmt.Errorf("%w: %s: invalid response", types.ErrUpstream, n.name)
	}

	nl := &(*data)[0]

	return &Location{
		Lat:      nl.Lat,
		Lon:      nl.Lon,
		Fullname: nl.Fullname,
	}, nil
}


================================================
FILE: internal/geo/location/nominatim_opencage.go
================================================
package location

import (
	"fmt"
	"net/url"

	"github.com/chubin/wttr.in/internal/types"
)

type locationOpenCage struct {
	Results []struct {
		Name     string `db:"name,key"`
		Geometry struct {
			Lat float64 `db:"lat"`
			Lng float64 `db:"lng"`
		}
		Fullname string `json:"formatted"`
	} `json:"results"`
}

func (data *locationOpenCage) Query(n *Nominatim, location string) (*Location, error) {
	url := fmt.Sprintf(
		"%s?q=%s&language=native&limit=1&key=%s",
		n.url, url.QueryEscape(location), n.token)

	err := makeQuery(url, data)
	if err != nil {
		return nil, fmt.Errorf("%s: %w", n.name, err)
	}

	if len(data.Results) != 1 {
		return nil, fmt.Errorf("%w: %s: invalid response", types.ErrUpstream, n.name)
	}

	nl := data.Results[0]

	return &Location{
		Lat:      fmt.Sprint(nl.Geometry.Lat),
		Lon:      fmt.Sprint(nl.Geometry.Lng),
		Fullname: nl.Fullname,
	}, nil
}


================================================
FILE: internal/geo/location/response.go
================================================
package location

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"strings"

	"github.com/chubin/wttr.in/internal/routing"
)

// Response provides routing interface to the geo cache.
func (c *Cache) Response(r *http.Request) *routing.Cadre {
	var (
		locationName = r.URL.Query().Get("location")
		loc          *Location
		bytes        []byte
		err          error
	)

	if locationName == "" {
		return errorResponse("location is not specified")
	}

	if strings.Contains(locationName, ".html") || strings.Contains(locationName, ".txt") {
		return errorResponse("invalid location")
	}

	loc, err = c.Resolve(locationName)
	if err != nil {
		log.Println("geo/location error:", locationName, r.RemoteAddr)

		return errorResponse(fmt.Sprint(err))
	}

	bytes, err = json.Marshal(loc)
	if err != nil {
		return errorResponse(fmt.Sprint(err))
	}

	return &routing.Cadre{Body: bytes}
}

func errorResponse(s string) *routing.Cadre {
	return &routing.Cadre{Body: []byte(
		fmt.Sprintf(`{"error": %q}`, s),
	)}
}


================================================
FILE: internal/geo/location/search.go
================================================
package location

import "github.com/chubin/wttr.in/internal/config"

type Provider interface {
	Query(location string) (*Location, error)
}

type Searcher struct {
	providers []Provider
}

// NewSearcher returns a new Searcher for the specified config.
func NewSearcher(config *config.Config) *Searcher {
	providers := []Provider{}
	for _, p := range config.Geo.Nominatim {
		providers = append(providers, NewNominatim(p.Name, p.Type, p.URL, p.Token))
	}

	return &Searcher{
		providers: providers,
	}
}

// Search makes queries through all known providers,
// and returns response, as soon as it is not nil.
// If all responses were nil, the last response is returned.
func (s *Searcher) Search(location string) (*Location, error) {
	var (
		err    error
		result *Location
	)

	for _, p := range s.providers {
		result, err = p.Query(location)
		if result != nil && err == nil {
			return result, nil
		}
	}

	return result, err
}


================================================
FILE: internal/logging/logging.go
================================================
package logging

import (
	"fmt"
	"net/http"
	"os"
	"sync"
	"time"

	"github.com/chubin/wttr.in/internal/util"
)

// Logging request.
//

// RequestLogger logs all incoming HTTP requests.
type RequestLogger struct {
	buf      map[logEntry]int
	filename string
	m        sync.Mutex

	period    time.Duration
	lastFlush time.Time
}

type logEntry struct {
	Proto     string
	IP        string
	URI       string
	UserAgent string
}

// NewRequestLogger returns a new RequestLogger for the specified log file.
// Flush logging entries after period of time.
//
// If filename is empty, no log will be written, and all logging entries
// will be silently dropped.
func NewRequestLogger(filename string, period time.Duration) *RequestLogger {
	return &RequestLogger{
		buf:      map[logEntry]int{},
		filename: filename,
		m:        sync.Mutex{},
		period:   period,
	}
}

// Log logs information about a HTTP request.
func (rl *RequestLogger) Log(r *http.Request) error {
	le := logEntry{
		Proto:     "http",
		IP:        util.ReadUserIP(r),
		URI:       r.RequestURI,
		UserAgent: r.Header.Get("User-Agent"),
	}
	if r.TLS != nil {
		le.Proto = "https"
	}

	// Do not log 127.0.0.1 connections
	if le.IP == "127.0.0.1" {
		return nil
	}

	rl.m.Lock()
	rl.buf[le]++
	rl.m.Unlock()

	if time.Since(rl.lastFlush) > rl.period {
		return rl.flush()
	}

	return nil
}

// flush stores log data to disk, and flushes the buffer.
func (rl *RequestLogger) flush() error {
	rl.m.Lock()
	defer rl.m.Unlock()

	// It is possible, that while waiting the mutex,
	// the buffer was already flushed.
	if time.Since(rl.lastFlush) <= rl.period {
		return nil
	}

	if rl.filename != "" {
		// Generate log output.
		output := ""
		for k, hitsNumber := range rl.buf {
			output += fmt.Sprintf("%s %3d %s\n", time.Now().Format(time.RFC3339), hitsNumber, k.String())
		}

		// Open log file.
		//nolint:nosnakecase
		f, err := os.OpenFile(rl.filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o600)
		if err != nil {
			return err
		}
		defer f.Close()

		// Save output to log file.
		_, err = f.Write([]byte(output))
		if err != nil {
			return err
		}
	}

	// Flush buffer.
	rl.buf = map[logEntry]int{}
	rl.lastFlush = time.Now()

	return nil
}

// String returns string representation of logEntry.
func (e *logEntry) String() string {
	return fmt.Sprintf(
		"%s %s %s %s",
		e.Proto,
		e.IP,
		e.URI,
		e.UserAgent,
	)
}


================================================
FILE: internal/logging/suppress.go
================================================
package logging

import (
	"os"
	"strings"
	"sync"
)

// LogSuppressor provides io.Writer interface for logging
// with lines suppression. For usage with log.Logger.
type LogSuppressor struct {
	filename   string
	suppress   []string
	linePrefix string

	logFile *os.File
	m       sync.Mutex
}

// NewLogSuppressor creates a new LogSuppressor for specified
// filename and lines to be suppressed.
//
// If filename is empty, log entries will be printed to stderr.
func NewLogSuppressor(filename string, suppress []string, linePrefix string) *LogSuppressor {
	return &LogSuppressor{
		filename:   filename,
		suppress:   suppress,
		linePrefix: linePrefix,
	}
}

// Open opens log file.
func (ls *LogSuppressor) Open() error {
	var err error

	if ls.filename == "" {
		return nil
	}

	//nolint:nosnakecase
	ls.logFile, err = os.OpenFile(ls.filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o600)

	return err
}

// Close closes log file.
func (ls *LogSuppressor) Close() error {
	if ls.filename == "" {
		return nil
	}

	return ls.logFile.Close()
}

// Write writes p to log, and returns number f bytes written.
// Implements io.Writer interface.
func (ls *LogSuppressor) Write(p []byte) (int, error) {
	var output string

	if ls.filename == "" {
		return os.Stdin.Write(p)
	}

	ls.m.Lock()
	defer ls.m.Unlock()

	lines := strings.Split(string(p), ls.linePrefix)
	for _, line := range lines {
		if (func(line string) bool {
			for _, suppress := range ls.suppress {
				if strings.Contains(line, suppress) {
					return true
				}
			}

			return false
		})(line) {
			continue
		}
		output += line
	}

	return ls.logFile.Write([]byte(output))
}


================================================
FILE: internal/options/options.go
================================================
package options

import (
	"fmt"
	"io/ioutil"

	"gopkg.in/yaml.v3"
)

// WttrInOptions represents the configuration for wttr.in query options and format specifiers.
type WttrInOptions struct {
	QueryOptions     []QueryOption     `yaml:"query_options"`
	FormatSpecifiers []FormatSpecifier `yaml:"format_specifiers"`
}

// QueryOption defines a single query option for wttr.in, such as `lang` or `format`.
type QueryOption struct {
	// Name is the long name of the option (e.g., "lang", "current_only").
	Name string `yaml:"name"`

	// Short is the short name of the option (e.g., "m"), omitted if not present.
	Short string `yaml:"short,omitempty"`

	// Description of the option's purpose (e.g., "Specify output language").
	Description string `yaml:"description"`

	// Type is the data type of the option (e.g., "boolean", "string", "integer").
	Type string `yaml:"type"`

	// ValuesMap is the map of possible values to their descriptions (e.g., for `lang`, `format`).
	ValuesMap map[string]string `yaml:"values_map,omitempty"`

	// Values is the list of supported values (e.g., ["true", "false"] for booleans).
	Values []string `yaml:"values,omitempty"`

	// Range is the numeric range for integer options (e.g., min and max for `transparency`).
	Range *Range `yaml:"range,omitempty"`

	// Default is the default value of the option (e.g., "en" for `lang`, null for `background`).
	Default interface{} `yaml:"default"`

	// Validate contains validation conditions in function call style (e.g., "length 6" for `background`).
	Validate []string `yaml:"validate,omitempty"`

	// Active indicates if the option is implemented (true) or proposed (false).
	Active bool `yaml:"active"`

	// Note includes additional notes or caveats (e.g., "Proposed but not officially supported").
	Note string `yaml:"note,omitempty"`
}

// Range defines a numeric range for integer-type query options.
type Range struct {
	// Min is the minimum value of the range (e.g., 0 for `transparency`).
	Min int `yaml:"min"`

	// Max is the maximum value of the range, nullable for unbounded (e.g., null for `date`).
	Max *int `yaml:"max"`
}

// FormatSpecifier defines a format specifier for the `format` option (e.g., %c, %t).
type FormatSpecifier struct {
	// Specifier is the format specifier (e.g., "%c").
	Specifier string `yaml:"specifier"`

	// Description provides the output description of the specifier (e.g., "Weather condition").
	Description string `yaml:"description"`

	// Active indicates if the specifier is implemented (true) or proposed (false).
	Active bool `yaml:"active"`

	// Note contains additional notes, if any (e.g., "Proposed in issue #585").
	Note string `yaml:"note,omitempty"`
}

// NewFromFile reads a QueryOption from a YAML file and returns a pointer to it.
func NewFromFile(filename string) (*WttrInOptions, error) {
	// Read the YAML file
	data, err := ioutil.ReadFile(filename)
	if err != nil {
		return nil, fmt.Errorf("failed to read file: %w", err)
	}

	// Unmarshal the YAML content into a QueryOption struct
	var option WttrInOptions
	if err := yaml.Unmarshal(data, &option); err != nil {
		return nil, fmt.Errorf("failed to unmarshal YAML: %w", err)
	}

	return &option, nil
}


================================================
FILE: internal/options/parse.go
================================================
package options

import (
	"errors"
	"fmt"
	"net/url"
	"regexp"
	"strconv"
	"strings"
)

// Errors for query parsing and validation
var (
	ErrInvalidQueryString      = errors.New("ERR001: failed to parse query string")
	ErrMultipleValues          = errors.New("ERR002: option has multiple values")
	ErrUnknownOption           = errors.New("ERR003: unknown option")
	ErrOptionNotImplemented    = errors.New("ERR004: option is not implemented")
	ErrBooleanValueRequired    = errors.New("ERR005: option requires boolean value (true/false)")
	ErrInvalidBooleanValue     = errors.New("ERR006: invalid boolean value")
	ErrValueRequired           = errors.New("ERR007: option requires a value")
	ErrInvalidStringValue      = errors.New("ERR008: invalid string value")
	ErrInvalidValidationRule   = errors.New("ERR009: invalid validation rule")
	ErrInvalidLengthRule       = errors.New("ERR010: invalid length rule")
	ErrInvalidLength           = errors.New("ERR011: invalid length")
	ErrInvalidRegexpRule       = errors.New("ERR012: invalid regexp rule")
	ErrInvalidRegexp           = errors.New("ERR013: invalid regexp match")
	ErrUnsupportedRule         = errors.New("ERR014: unsupported validation rule")
	ErrInvalidInteger          = errors.New("ERR015: option requires an integer")
	ErrBelowMinimum            = errors.New("ERR016: value below minimum")
	ErrAboveMaximum            = errors.New("ERR017: value exceeds maximum")
	ErrUnsupportedType         = errors.New("ERR018: unsupported option type")
	ErrInactiveFormatSpecifier = errors.New("ERR019: format specifier is not implemented")
)

// ParseQueryString parses a wttr.in query string into a map of option names to values.
// It validates option names, types, content, and values based on the provided WttrInOptions.
// Returns the parsed map or an error if parsing or validation fails.
func ParseQueryString(query string, config *WttrInOptions) (map[string]string, error) {
	// Initialize result map and lookup tables
	result := make(map[string]string)
	shortToName, nameToOption := buildOptionLookups(config.QueryOptions)

	// Parse query string
	query = strings.ReplaceAll(
		strings.ReplaceAll(query, "%25", "%"),
		"%", "%25")

	parsed, err := url.ParseQuery(query)
	if err != nil {
		return nil, fmt.Errorf("%w: %v", ErrInvalidQueryString, err)
	}

	// Process query parameters
	for key, values := range parsed {
		if len(values) == 0 || (len(values) == 1 && values[0] == "") {
			if err := handleFlagOption(key, shortToName, nameToOption, result); err != nil {
				return nil, err
			}
			continue
		}
		if len(values) > 1 {
			return nil, fmt.Errorf("%w: %s: %v", ErrMultipleValues, key, values)
		}
		if err := handleValueOption(key, values[0], shortToName, nameToOption, result); err != nil {
			return nil, err
		}
	}

	// Validate format specifiers if format=custom
	if formatValue, exists := result["format"]; exists && formatValue == "custom" {
		for _, spec := range config.FormatSpecifiers {
			if !spec.Active {
				return nil, fmt.Errorf("%w: %s", ErrInactiveFormatSpecifier, spec.Specifier)
			}
		}
	}

	return result, nil
}

// buildOptionLookups creates lookup maps for short-to-long names and option details.
func buildOptionLookups(options []QueryOption) (map[string]string, map[string]QueryOption) {
	shortToName := make(map[string]string)
	nameToOption := make(map[string]QueryOption)
	for _, opt := range options {
		nameToOption[opt.Name] = opt
		if opt.Short != "" {
			shortToName[opt.Short] = opt.Name
		}
	}
	return shortToName, nameToOption
}

// handleFlagOption processes flag options (e.g., "0pq" or "m") without values.
func handleFlagOption(key string, shortToName map[string]string, nameToOption map[string]QueryOption, result map[string]string) error {
	// Handle bundled short options (e.g., "0pq")
	if len(key) > 1 && !strings.Contains(key, "=") {
		for _, short := range strings.Split(key, "") {
			if err := validateAndSetFlag(short, shortToName, nameToOption, result); err != nil {
				return err
			}
		}
		return nil
	}
	// Handle single flag
	return validateAndSetFlag(key, shortToName, nameToOption, result)
}

// validateAndSetFlag validates and sets a single flag option.
func validateAndSetFlag(key string, shortToName map[string]string, nameToOption map[string]QueryOption, result map[string]string) error {
	name, exists := shortToName[key]
	if !exists {
		name = key // Try long name
	}
	opt, exists := nameToOption[name]
	if !exists {
		return fmt.Errorf("%w: %s", ErrUnknownOption, key)
	}
	if !opt.Active {
		return fmt.Errorf("%w: %s", ErrOptionNotImplemented, name)
	}
	if opt.Type != "boolean" {
		return fmt.Errorf("%w: %s", ErrValueRequired, key)
	}
	result[name] = "true"
	return nil
}

// handleValueOption processes options with values (e.g., "lang=fr").
func handleValueOption(key, value string, shortToName map[string]string, nameToOption map[string]QueryOption, result map[string]string) error {
	// Resolve option name
	name, exists := shortToName[key]
	if !exists {
		name = key // Assume long name
	}
	opt, exists := nameToOption[name]
	if !exists {
		return fmt.Errorf("%w: %s", ErrUnknownOption, key)
	}
	if !opt.Active {
		return fmt.Errorf("%w: %s", ErrOptionNotImplemented, name)
	}

	// Validate based on type
	switch opt.Type {
	case "boolean":
		return validateBooleanOption(name, value, opt, result)
	case "string":
		return validateStringOption(name, value, opt, result)
	case "integer":
		return validateIntegerOption(name, value, opt, result)
	default:
		return fmt.Errorf("%w: %s: %s", ErrUnsupportedType, name, opt.Type)
	}
}

// validateBooleanOption validates and sets boolean option values.
func validateBooleanOption(name, value string, opt QueryOption, result map[string]string) error {
	if value != "true" && value != "false" {
		return fmt.Errorf("%w: %s: %s", ErrBooleanValueRequired, name, value)
	}
	if len(opt.Values) > 0 {
		for _, v := range opt.Values {
			if v == value {
				result[name] = value
				return nil
			}
		}
		return fmt.Errorf("%w: %s: %s, expected one of %v", ErrInvalidBooleanValue, name, value, opt.Values)
	}
	result[name] = value
	return nil
}

// validateStringOption validates and sets string option values, including background-specific rules.
func validateStringOption(name, value string, opt QueryOption, result map[string]string) error {
	if len(opt.ValuesMap) > 0 {
		if _, valid := opt.ValuesMap[value]; !valid {
			return fmt.Errorf("%w: %s: %s, expected one of %v", ErrInvalidStringValue, name, value, getMapKeys(opt.ValuesMap))
		}
	}
	if len(opt.Values) > 0 {
		for _, v := range opt.Values {
			if v == value {
				result[name] = value
				return nil
			}
		}
		return fmt.Errorf("%w: %s: %s, expected one of %v", ErrInvalidStringValue, name, value, opt.Values)
	}
	if name == "background" && len(opt.Validate) > 0 {
		for _, rule := range opt.Validate {
			if err := applyValidationRule(name, value, rule); err != nil {
				return err
			}
		}
	}
	result[name] = value
	return nil
}

// applyValidationRule applies a single validation rule (e.g., "length 6", "regexp [0-9a-fA-F]{6}").
func applyValidationRule(name, value, rule string) error {
	parts := strings.SplitN(rule, " ", 2)
	if len(parts) < 2 {
		return fmt.Errorf("%w: %s: %s", ErrInvalidValidationRule, name, rule)
	}
	switch parts[0] {
	case "length":
		length, err := strconv.Atoi(parts[1])
		if err != nil {
			return fmt.Errorf("%w: %s: %s", ErrInvalidLengthRule, name, rule)
		}
		if len(value) != length {
			return fmt.Errorf("%w: %s: %s, expected length %d", ErrInvalidLength, name, value, length)
		}
	case "regexp":
		re, err := regexp.Compile(parts[1])
		if err != nil {
			return fmt.Errorf("%w: %s: %s", ErrInvalidRegexpRule, name, rule)
		}
		if !re.MatchString(value) {
			return fmt.Errorf("%w: %s: %s, expected match for %s", ErrInvalidRegexp, name, value, parts[1])
		}
	default:
		return fmt.Errorf("%w: %s: %s", ErrUnsupportedRule, name, rule)
	}
	return nil
}

// validateIntegerOption validates and sets integer option values.
func validateIntegerOption(name, value string, opt QueryOption, result map[string]string) error {
	num, err := strconv.Atoi(value)
	if err != nil {
		return fmt.Errorf("%w: %s: %s", ErrInvalidInteger, name, value)
	}
	if opt.Range != nil {
		if num < opt.Range.Min {
			return fmt.Errorf("%w: %s: %d, minimum is %d", ErrBelowMinimum, name, num, opt.Range.Min)
		}
		if opt.Range.Max != nil && num > *opt.Range.Max {
			return fmt.Errorf("%w: %s: %d, maximum is %d", ErrAboveMaximum, name, num, *opt.Range.Max)
		}
	}
	result[name] = value
	return nil
}

// getMapKeys returns a sorted slice of keys from a map for error messages.
func getMapKeys(m map[string]string) []string {
	keys := make([]string, 0, len(m))
	for k := range m {
		keys = append(keys, k)
	}
	// Sort for consistent error messages
	for i := 0; i < len(keys)-1; i++ {
		for j := i + 1; j < len(keys); j++ {
			if keys[i] > keys[j] {
				keys[i], keys[j] = keys[j], keys[i]
			}
		}
	}
	return keys
}


================================================
FILE: internal/options/processlog.go
================================================
package options

import (
	"bufio"
	"fmt"
	"os"
	"strings"
)

// ProcessLogFile reads a wttr.in log file, parses queries, and writes invalid entries to an error file.
// Each log line is expected in the format: "timestamp request_id protocol ip query user_agent".
// Invalid queries and their error messages are written to errorFilePath.
func ProcessLogFile(logFilePath, errorFilePath string, config *WttrInOptions) error {
	logFile, err := openLogFile(logFilePath)
	if err != nil {
		return err
	}
	defer logFile.Close()

	errorFile, writer, err := openErrorFile(errorFilePath)
	if err != nil {
		return err
	}
	defer errorFile.Close()
	defer writer.Flush()

	return processLogLines(logFile, writer, config)
}

// openLogFile opens the log file for reading.
func openLogFile(logFilePath string) (*os.File, error) {
	logFile, err := os.Open(logFilePath)
	if err != nil {
		return nil, fmt.Errorf("failed to open log file: %w", err)
	}
	return logFile, nil
}

// openErrorFile creates and opens the error file for writing.
func openErrorFile(errorFilePath string) (*os.File, *bufio.Writer, error) {
	errorFile, err := os.Create(errorFilePath)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to create error file: %w", err)
	}
	writer := bufio.NewWriter(errorFile)
	return errorFile, writer, nil
}

// processLogLines reads and processes each line of the log file.
func processLogLines(logFile *os.File, writer *bufio.Writer, config *WttrInOptions) error {
	scanner := bufio.NewScanner(logFile)
	lineNumber := 0
	for scanner.Scan() {
		lineNumber++
		if err := processLogLine(scanner.Text(), lineNumber, writer, config); err != nil {
			return err
		}
	}
	if err := scanner.Err(); err != nil {
		return fmt.Errorf("failed to read log file: %w", err)
	}
	return nil
}

// processLogLine processes a single log line and writes errors to the error file.
func processLogLine(line string, lineNumber int, writer *bufio.Writer, config *WttrInOptions) error {
	query, err := extractQueryFromLine(line, lineNumber, writer)
	if err != nil {
		return err
	}

	_, err = ParseQueryString(query, config)
	if err != nil {
		_, writeErr := fmt.Fprintf(writer, "Line %d: Query: %s, Error: %v\n", lineNumber, query, err)
		if writeErr != nil {
			return fmt.Errorf("failed to write to error file at line %d: %w", lineNumber, writeErr)
		}
	}
	return nil
}

// extractQueryFromLine extracts the query from a log line and validates its format.
func extractQueryFromLine(line string, lineNumber int, writer *bufio.Writer) (string, error) {
	fields := strings.Fields(line)
	if len(fields) < 5 {
		_, err := fmt.Fprintf(writer, "Line %d: Invalid log format: %s\n", lineNumber, line)
		if err != nil {
			return "", fmt.Errorf("failed to write to error file at line %d: %w", lineNumber, err)
		}
		return "", nil
	}

	query := fields[4]
	if strings.Contains(query, "?") {
		parts := strings.SplitN(query, "?", 2)
		if len(parts) == 2 {
			return parts[1], nil
		}
		return "", nil
	}
	return "", nil
}


================================================
FILE: internal/processor/j1.go
================================================
package processor

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"strings"
	"time"
)

func getAny(req *http.Request, tr1, tr2, tr3, tr4 *http.Transport) (*ResponseWithHeader, error) {
	uri := strings.ReplaceAll(req.URL.RequestURI(), "%", "%25")

	u, err := url.Parse(uri)
	if err != nil {
		return nil, err
	}

	format := u.Query().Get("format")

	if format == "j1" {
		return getJ1(req, tr1)
	} else if format != "" {
		return getFormat(req, tr2)
	}

	// log.Println(req.URL.Query())
	// log.Println()

	if checkURLForPNG(req) {
		return getDefault(req, tr4)
	}

	return getDefault(req, tr3)
}

func getJ1(req *http.Request, transport *http.Transport) (*ResponseWithHeader, error) {
	return getUpstream(req, transport)
}

func getFormat(req *http.Request, transport *http.Transport) (*ResponseWithHeader, error) {
	return getUpstream(req, transport)
}

func getDefault(req *http.Request, transport *http.Transport) (*ResponseWithHeader, error) {
	return getUpstream(req, transport)
}

func getUpstream(req *http.Request, transport *http.Transport) (*ResponseWithHeader, error) {
	client := &http.Client{
		Transport: transport,
	}

	queryURL := fmt.Sprintf("http://%s%s", req.Host, req.RequestURI)

	proxyReq, err := http.NewRequest(req.Method, queryURL, req.Body)
	if err != nil {
		return nil, err
	}

	// proxyReq.Header.Set("Host", req.Host)
	// proxyReq.Header.Set("X-Forwarded-For", req.RemoteAddr)

	for header, values := range req.Header {
		for _, value := range values {
			proxyReq.Header.Add(header, value)
		}
	}

	if proxyReq.Header.Get("X-Forwarded-For") == "" {
		proxyReq.Header.Set("X-Forwarded-For", ipFromAddr(req.RemoteAddr))
	}

	res, err := client.Do(proxyReq)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()

	body, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return nil, err
	}

	return &ResponseWithHeader{
		InProgress: false,
		Expires:    time.Now().Add(time.Duration(randInt(1000, 1500)) * time.Second),
		Body:       body,
		Header:     res.Header,
		StatusCode: res.StatusCode,
	}, nil
}

func checkURLForPNG(r *http.Request) bool {
	url := r.URL.String()
	return strings.Contains(url, ".png") && !strings.Contains(url, "/files/")
}


================================================
FILE: internal/processor/peak.go
================================================
package processor

import (
	"log"
	"net/http"
	"sync"
	"time"

	"github.com/robfig/cron"
)

func (rp *RequestProcessor) startPeakHandling() error {
	var err error

	c := cron.New()
	// cronTime := fmt.Sprintf("%d,%d * * * *", 30-prefetchInterval/60, 60-prefetchInterval/60)
	err = c.AddFunc(
		"24 * * * *",
		func() { rp.prefetchPeakRequests(&rp.peakRequest30) },
	)
	if err != nil {
		return err
	}

	err = c.AddFunc(
		"54 * * * *",
		func() { rp.prefetchPeakRequests(&rp.peakRequest60) },
	)
	if err != nil {
		return err
	}

	c.Start()

	return nil
}

// registerPeakRequest registers requests coming in the peak time.
// Such requests can be prefetched afterwards just before the peak time comes.
func (rp *RequestProcessor) savePeakRequest(cacheDigest string, r *http.Request) {
	if _, min, _ := time.Now().Clock(); min == 30 {
		rp.peakRequest30.Store(cacheDigest, *r)
	} else if min == 0 {
		rp.peakRequest60.Store(cacheDigest, *r)
	}
}

func (rp *RequestProcessor) prefetchRequest(r *http.Request) error {
	_, err := rp.ProcessRequest(r)

	return err
}

func syncMapLen(sm *sync.Map) int {
	count := 0
	f := func(key, value interface{}) bool {
		// Not really certain about this part, don't know for sure
		// if this is a good check for an entry's existence
		if key == "" {
			return false
		}
		count++

		return true
	}

	sm.Range(f)

	return count
}

func (rp *RequestProcessor) prefetchPeakRequests(peakRequestMap *sync.Map) {
	peakRequestLen := syncMapLen(peakRequestMap)
	if peakRequestLen == 0 {
		return
	}
	log.Printf("PREFETCH: Prefetching %d requests\n", peakRequestLen)
	sleepBetweenRequests := time.Duration(rp.config.Uplink.PrefetchInterval*1000/peakRequestLen) * time.Millisecond
	peakRequestMap.Range(func(key interface{}, value interface{}) bool {
		req, ok := value.(http.Request)
		if !ok {
			log.Println("missing value for:", key)

			return true
		}

		go func(r http.Request) {
			err := rp.prefetchRequest(&r)
			if err != nil {
				log.Println("prefetch request:", err)
			}
		}(req)
		peakRequestMap.Delete(key)
		time.Sleep(sleepBetweenRequests)

		return true
	})
}


================================================
FILE: internal/processor/processor.go
================================================
package processor

import (
	"context"
	"fmt"
	"log"
	"math/rand"
	"net"
	"net/http"
	"strings"
	"sync"
	"time"

	lru "github.com/hashicorp/golang-lru"

	"github.com/chubin/wttr.in/internal/config"
	geoip "github.com/chubin/wttr.in/internal/geo/ip"
	geoloc "github.com/chubin/wttr.in/internal/geo/location"
	"github.com/chubin/wttr.in/internal/routing"
	"github.com/chubin/wttr.in/internal/stats"
	"github.com/chubin/wttr.in/internal/util"
)

// plainTextAgents contains signatures of the plain-text agents.
func plainTextAgents() []string {
	return []string{
		"curl",
		"httpie",
		"lwp-request",
		"wget",
		"python-httpx",
		"python-requests",
		"openbsd ftp",
		"powershell",
		"fetch",
		"aiohttp",
		"http_get",
		"xh",
		"nushell",
		"zig",
		"node",
	}
}

type ResponseWithHeader struct {
	InProgress bool      // true if the request is being processed
	Expires    time.Time // expiration time of the cache entry

	Body       []byte
	Header     http.Header
	StatusCode int // e.g. 200
}

// RequestProcessor handles incoming requests.
type RequestProcessor struct {
	peakRequest30      sync.Map
	peakRequest60      sync.Map
	lruCache           *lru.Cache
	stats              *stats.Stats
	router             routing.Router
	upstreamTransport1 *http.Transport
	upstreamTransport2 *http.Transport
	upstreamTransport3 *http.Transport
	upstreamTransport4 *http.Transport
	config             *config.Config
	geoIPCache         *geoip.Cache
	geoLocation        *geoloc.Cache
}

// NewRequestProcessor returns new RequestProcessor.
func NewRequestProcessor(config *config.Config) (*RequestProcessor, error) {
	lruCache, err := lru.New(config.Cache.Size)
	if err != nil {
		return nil, err
	}

	dialer := &net.Dialer{
		Timeout:   time.Duration(config.Uplink.Timeout) * time.Second,
		KeepAlive: time.Duration(config.Uplink.Timeout) * time.Second,
		DualStack: true,
	}

	transport1 := &http.Transport{
		DialContext: func(ctx context.Context, network, _ string) (net.Conn, error) {
			return dialer.DialContext(ctx, network, config.Uplink.Address1)
		},
	}
	transport2 := &http.Transport{
		DialContext: func(ctx context.Context, network, _ string) (net.Conn, error) {
			return dialer.DialContext(ctx, network, config.Uplink.Address2)
		},
	}
	transport3 := &http.Transport{
		DialContext: func(ctx context.Context, network, _ string) (net.Conn, error) {
			return dialer.DialContext(ctx, network, config.Uplink.Address3)
		},
	}
	transport4 := &http.Transport{
		DialContext: func(ctx context.Context, network, _ string) (net.Conn, error) {
			return dialer.DialContext(ctx, network, config.Uplink.Address4)
		},
	}

	geoCache, err := geoip.NewCache(config)
	if err != nil {
		return nil, err
	}

	geoLocation, err := geoloc.NewCache(config)
	if err != nil {
		return nil, err
	}

	rp := &RequestProcessor{
		lruCache:           lruCache,
		stats:              stats.New(),
		upstreamTransport1: transport1,
		upstreamTransport2: transport2,
		upstreamTransport3: transport3,
		upstreamTransport4: transport4,
		config:             config,
		geoIPCache:         geoCache,
		geoLocation:        geoLocation,
	}

	// Initialize routes.
	rp.router.AddPath("/:stats", rp.stats)
	rp.router.AddPath("/:geo-ip-get", rp.geoIPCache)
	rp.router.AddPath("/:geo-ip-put", rp.geoIPCache)
	rp.router.AddPath("/:geo-location", rp.geoLocation)

	return rp, nil
}

// Start starts async request processor jobs, such as peak handling.
func (rp *RequestProcessor) Start() error {
	return rp.startPeakHandling()
}

func (rp *RequestProcessor) ProcessRequest(r *http.Request) (*ResponseWithHeader, error) {
	var (
		response *ResponseWithHeader
		ip       = util.ReadUserIP(r)
	)

	if ip != "127.0.0.1" {
		rp.stats.Inc("total")
	}

	// Main routing logic.
	if rh := rp.router.Route(r); rh != nil {
		result := rh.Response(r)
		if result != nil {
			return fromCadre(result), nil
		}
	}

	if resp, ok := redirectInsecure(r); ok {
		rp.stats.Inc("redirects")

		return resp, nil
	}

	if dontCache(r) {
		rp.stats.Inc("uncached")

		return getAny(r, rp.upstreamTransport1, rp.upstreamTransport2, rp.upstreamTransport3, rp.upstreamTransport4)
	}

	// processing cached request
	cacheDigest := getCacheDigest(r)

	rp.savePeakRequest(cacheDigest, r)

	response = rp.processRequestFromCache(r)
	if response != nil {
		return response, nil
	}

	return rp.processUncachedRequest(r)
}

// processRequestFromCache processes requests using the cache.
// If no entry in cache found, nil is returned.
func (rp *RequestProcessor) processRequestFromCache(r *http.Request) *ResponseWithHeader {
	var (
		cacheEntry  ResponseWithHeader
		cacheDigest = getCacheDigest(r)
		ok          bool
	)

	cacheBody, _ := rp.lruCache.Get(cacheDigest)
	cacheEntry, ok = cacheBody.(ResponseWithHeader)
	if !ok {
		return nil
	}

	// If after all attempts we still have no answer,
	// respond with an error message.
	// (WAS: we try to make the query on our own)
	for attempts := 0; attempts < 300; attempts++ {
		if !ok || !cacheEntry.InProgress {
			break
		}
		time.Sleep(30 * time.Millisecond)
		cacheBody, _ = rp.lruCache.Get(cacheDigest)
		v, ok := cacheBody.(ResponseWithHeader)
		if ok {
			cacheEntry = v
		}
	}
	if cacheEntry.InProgress {
		// log.Printf("TIMEOUT: %s\n", cacheDigest)
		return &ResponseWithHeader{
			InProgress: false,
			Expires:    time.Now().Add(time.Duration(randInt(1000, 1500)) * time.Second),
			Body:       []byte("This query is already being processed"),
			StatusCode: 200,
		}
	}
	if ok && !cacheEntry.InProgress && cacheEntry.Expires.After(time.Now()) {
		rp.stats.Inc("cache1")

		return &cacheEntry
	}

	return nil
}

// processUncachedRequest processes requests that were not found in the cache.
func (rp *RequestProcessor) processUncachedRequest(r *http.Request) (*ResponseWithHeader, error) {
	var (
		cacheDigest = getCacheDigest(r)
		ip          = util.ReadUserIP(r)
		response    *ResponseWithHeader
		err         error
	)

	// Indicate, that the request is being handled.
	rp.lruCache.Add(cacheDigest, ResponseWithHeader{InProgress: true})

	// Response was not found in cache.
	// Starting real handling.
	format := r.URL.Query().Get("format")
	if len(format) != 0 {
		rp.stats.Inc("format")
		if format == "j1" {
			rp.stats.Inc("format=j1")
		}
	}

	// Count, how many IP addresses are known.
	_, err = rp.geoIPCache.Read(ip)
	if err == nil {
		rp.stats.Inc("geoip")
	}

	response, err = getAny(r, rp.upstreamTransport1, rp.upstreamTransport2, rp.upstreamTransport3, rp.upstreamTransport4)
	if err != nil {
		return nil, err
	}
	if response.StatusCode == 200 || response.StatusCode == 304 || response.StatusCode == 404 {
		rp.lruCache.Add(cacheDigest, *response)
	} else {
		log.Printf("REMOVE: %d response for %s from cache\n", response.StatusCode, cacheDigest)
		rp.lruCache.Remove(cacheDigest)
	}

	return response, nil
}

// getCacheDigest is an implementation of the cache.get_signature of original wttr.in.
func getCacheDigest(req *http.Request) string {
	userAgent := req.Header.Get("User-Agent")

	queryHost := req.Host
	queryString := req.RequestURI

	clientIPAddress := util.ReadUserIP(req)

	lang := req.Header.Get("Accept-Language")

	return fmt.Sprintf("%s:%s%s:%s:%s", userAgent, queryHost, queryString, clientIPAddress, lang)
}

// dontCache returns true if req should not be cached.
func dontCache(req *http.Request) bool {
	// dont cache cyclic requests
	loc := strings.Split(req.RequestURI, "?")[0]

	return strings.Contains(loc, ":")
}

// redirectInsecure returns redirection response, and bool value, if redirection was needed,
// if the query comes from a browser, and it is insecure.
//
// Insecure queries are marked by the frontend web server
// with X-Forwarded-Proto header:
// `proxy_set_header   X-Forwarded-Proto $scheme;`.
func redirectInsecure(req *http.Request) (*ResponseWithHeader, bool) {
	if isPlainTextAgent(req.Header.Get("User-Agent")) {
		return nil, false
	}

	if req.TLS != nil || strings.ToLower(req.Header.Get("X-Forwarded-Proto")) == "https" {
		return nil, false
	}

	target := "https://" + req.Host + req.URL.Path
	if len(req.URL.RawQuery) > 0 {
		target += "?" + req.URL.RawQuery
	}

	body := []byte(fmt.Sprintf(`<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="%s">here</A>.
</BODY></HTML>
`, target))

	return &ResponseWithHeader{
		InProgress: false,
		Expires:    time.Now().Add(time.Duration(randInt(1000, 1500)) * time.Second),
		Body:       body,
		Header:     http.Header{"Location": []string{target}},
		StatusCode: 301,
	}, true
}

// isPlainTextAgent returns true if userAgent is a plain-text agent.
func isPlainTextAgent(userAgent string) bool {
	userAgentLower := strings.ToLower(userAgent)
	for _, signature := range plainTextAgents() {
		if strings.Contains(userAgentLower, signature) {
			return true
		}
	}

	return false
}

func randInt(min int, max int) int {
	return min + rand.Intn(max-min)
}

// ipFromAddr returns IP address from a ADDR:PORT pair.
func ipFromAddr(s string) string {
	pos := strings.LastIndex(s, ":")
	if pos == -1 {
		return s
	}

	return s[:pos]
}

// fromCadre converts Cadre into a responseWithHeader.
func fromCadre(cadre *routing.Cadre) *ResponseWithHeader {
	return &ResponseWithHeader{
		Body:       cadre.Body,
		Expires:    cadre.Expires,
		StatusCode: 200,
		InProgress: false,
	}
}


================================================
FILE: internal/routing/routing.go
================================================
package routing

import (
	"net/http"
	"time"
)

// CadreFormat specifies how the shot data is formatted.
type CadreFormat int

const (
	// CadreFormatANSI represents Terminal ANSI format.
	CadreFormatANSI = iota

	// CadreFormatHTML represents HTML.
	CadreFormatHTML

	// CadreFormatPNG represents PNG.
	CadreFormatPNG
)

// Cadre contains result of a query execution.
type Cadre struct {
	// Body contains the data of Cadre, formatted as Format.
	Body []byte

	// Format of the shot.
	Format CadreFormat

	// Expires contains the time of the Cadre expiration,
	// or 0 if it does not expire.
	Expires time.Time
}

// Handler can handle queries and return views.
type Handler interface {
	Response(*http.Request) *Cadre
}

type routeFunc func(*http.Request) bool

type route struct {
	routeFunc
	Handler
}

// Router keeps a routing table, and finds queries handlers, based on its rules.
type Router struct {
	rt []route
}

// Route returns a query handler based on its content.
func (r *Router) Route(req *http.Request) Handler {
	for _, re := range r.rt {
		if re.routeFunc(req) {
			return re.Handler
		}
	}

	return nil
}

// AddPath adds route for a static path.
func (r *Router) AddPath(path string, handler Handler) {
	r.rt = append(r.rt, route{routePath(path), handler})
}

func routePath(path string) routeFunc {
	return routeFunc(func(req *http.Request) bool {
		return req.URL.Path == path
	})
}


================================================
FILE: internal/stats/stats.go
================================================
package stats

import (
	"bytes"
	"fmt"
	"net/http"
	"sync"
	"time"

	"github.com/chubin/wttr.in/internal/routing"
)

// Stats holds processed requests statistics.
type Stats struct {
	m         sync.Mutex
	v         map[string]int
	startTime time.Time
}

// New returns new Stats.
func New() *Stats {
	return &Stats{
		v:         map[string]int{},
		startTime: time.Now(),
	}
}

// Inc key by one.
func (c *Stats) Inc(key string) {
	c.m.Lock()
	c.v[key]++
	c.m.Unlock()
}

// Get current key counter value.
func (c *Stats) Get(key string) int {
	c.m.Lock()
	defer c.m.Unlock()

	return c.v[key]
}

// Reset key counter.
func (c *Stats) Reset(key string) int {
	c.m.Lock()
	defer c.m.Unlock()
	result := c.v[key]
	c.v[key] = 0

	return result
}

// Show returns current statistics formatted as []byte.
func (c *Stats) Show() []byte {
	var b bytes.Buffer

	c.m.Lock()
	defer c.m.Unlock()

	uptime := time.Since(c.startTime) / time.Second

	fmt.Fprintf(&b, "%-20s: %v\n", "Running since", c.startTime.Format(time.RFC3339))
	fmt.Fprintf(&b, "%-20s: %d\n", "Uptime (min)", uptime/60)

	fmt.Fprintf(&b, "%-20s: %d\n", "Total queries", c.v["total"])

	if uptime != 0 {
		fmt.Fprintf(&b, "%-20s: %d\n", "Throughput (QpM)", c.v["total"]*60/int(uptime))
	}

	fmt.Fprintf(&b, "%-20s: %d\n", "Cache L1 queries", c.v["cache1"])

	if c.v["total"] != 0 {
		fmt.Fprintf(&b, "%-20s: %d\n", "Cache L1 queries (%)", (100*c.v["cache1"])/c.v["total"])
	}

	fmt.Fprintf(&b, "%-20s: %d\n", "Upstream queries", c.v["total"]-c.v["cache1"])
	fmt.Fprintf(&b, "%-20s: %d\n", "Queries with format", c.v["format"])
	fmt.Fprintf(&b, "%-20s: %d\n", "Queries with format=j1", c.v["format=j1"])
	fmt.Fprintf(&b, "%-20s: %d\n", "Queries with known IP", c.v["geoip"])

	return b.Bytes()
}

func (c *Stats) Response(*http.Request) *routing.Cadre {
	return &routing.Cadre{
		Body: c.Show(),
	}
}


================================================
FILE: internal/types/errors.go
================================================
package types

import "errors"

var (
	ErrNotFound          = errors.New("cache entry not found")
	ErrInvalidCacheEntry = errors.New("invalid cache entry format")
	ErrUpstream          = errors.New("upstream error")

	// ErrNoServersConfigured means that there are no servers to run.
	ErrNoServersConfigured = errors.New("no servers configured")

	ErrUnknownLocationService = errors.New("unknown location service")
)


================================================
FILE: internal/types/types.go
================================================
package types

type CacheType string

const (
	CacheTypeDB    = "db"
	CacheTypeFiles = "files"
)


================================================
FILE: internal/util/files.go
================================================
package util

import "os"

// RemoveFileIfExists removes filename if exists, or does nothing if the file
// is not there. Returns an error, if it occurred during deletion.
func RemoveFileIfExists(filename string) error {
	_, err := os.Stat(filename)
	if err != nil {
		if !os.IsNotExist(err) {
			return err
		}
		// no db file
		return nil
	}

	return os.Remove(filename)
}


================================================
FILE: internal/util/http.go
================================================
package util

import (
	"log"
	"net"
	"net/http"
)

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

	return IPAddress
}


================================================
FILE: internal/util/yaml.go
================================================
package util

import (
	"bytes"

	"gopkg.in/yaml.v3"
)

// YamlUnmarshalStrict unmarshals YAML data with an error when unknown fields are present.
func YamlUnmarshalStrict(in []byte, out interface{}) error {
	dec := yaml.NewDecoder(bytes.NewReader(in))
	dec.KnownFields(true)

	return dec.Decode(out)
}


================================================
FILE: internal/view/v1/api.go
================================================
//nolint:forbidigo,funlen,nestif,goerr113,gocognit,cyclop
package v1

import (
	"bytes"
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"os"
	"strconv"
	"strings"
)

//nolint:tagliatelle
type cond struct {
	ChanceOfRain   string  `json:"chanceofrain"`
	FeelsLikeC     int     `json:",string"`
	PrecipMM       float32 `json:"precipMM,string"`
	TempC          int     `json:"tempC,string"`
	TempC2         int     `json:"temp_C,string"`
	Time           int     `json:"time,string"`
	VisibleDistKM  int     `json:"visibility,string"`
	WeatherCode    int     `json:"weatherCode,string"`
	WeatherDesc    []struct{ Value string }
	WindGustKmph   int `json:",string"`
	Winddir16Point string
	WindspeedKmph  int `json:"windspeedKmph,string"`
}

type astro struct {
	Moonrise string
	Moonset  string
	Sunrise  string
	Sunset   string
}

type weather struct {
	Astronomy []astro
	Date      string
	Hourly    []cond
	MaxtempC  int `json:"maxtempC,string"`
	MintempC  int `json:"mintempC,string"`
}

type loc struct {
	Query string `json:"query"`
	Type  string `json:"type"`
}

//nolint:tagliatelle
type resp struct {
	Data struct {
		Cur     []cond                 `json:"current_condition"`
		Err     []struct{ Msg string } `json:"error"`
		Req     []loc                  `json:"request"`
		Weather []weather              `json:"weather"`
	} `json:"data"`
}

func (g *global) getDataFromAPI() (*resp, error) {
	var (
		ret    resp
		params []string
	)

	if len(g.config.APIKey) == 0 {
		return nil, fmt.Errorf("no API key specified. Setup instructions are in the README")
	}
	params = append(params, "key="+g.config.APIKey)

	// non-flag shortcut arguments will overwrite possible flag arguments
	for _, arg := range flag.Args() {
		if v, err := strconv.Atoi(arg); err == nil && len(arg) == 1 {
			g.config.Numdays = v
		} else {
			g.config.City = arg
		}
	}

	if len(g.config.City) > 0 {
		params = append(params, "q="+url.QueryEscape(g.config.City))
	}
	params = append(params, "format=json", "num_of_days="+strconv.Itoa(g.config.Numdays), "tp=3")
	if g.config.Lang != "" {
		params = append(params, "lang="+g.config.Lang)
	}

	if g.debug {
		fmt.Fprintln(os.Stderr, params)
	}

	res, err := http.Get(wuri + strings.Join(params, "&"))
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	body, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return nil, err
	}

	if g.debug {
		var out bytes.Buffer

		err := json.Indent(&out, body, "", "  ")
		if err != nil {
			return nil, err
		}

		_, err = out.WriteTo(os.Stderr)
		if err != nil {
			return nil, err
		}

		fmt.Print("\n\n")
	}

	if g.config.Lang == "" {
		if err = json.Unmarshal(body, &ret); err != nil {
			return nil, err
		}
	} else {
		if err = g.unmarshalLang(body, &ret); err != nil {
			return nil, err
		}
	}

	return &ret, nil
}

func (g *global) unmarshalLang(body []byte, r *resp) error {
	var rv map[string]interface{}
	if err := json.Unmarshal(body, &rv); err != nil {
		return err
	}
	if data, ok := rv["data"].(map[string]interface{}); ok {
		if ccs, ok := data["current_condition"].([]interface{}); ok {
			for _, cci := range ccs {
				cc, ok := cci.(map[string]interface{})
				if !ok {
					continue
				}
				langs, ok := cc["lang_"+g.config.Lang].([]interface{})
				if !ok || len(langs) == 0 {
					continue
				}
				weatherDesc, ok := cc["weatherDesc"].([]interface{})
				if !ok || len(weatherDesc) == 0 {
					continue
				}
				weatherDesc[0] = langs[0]
			}
		}
		if ws, ok := data["weather"].([]interface{}); ok {
			for _, wi := range ws {
				w, ok := wi.(map[string]interface{})
				if !ok {
					continue
				}
				if hs, ok := w["hourly"].([]interface{}); ok {
					for _, hi := range hs {
						h, ok := hi.(map[string]interface{})
						if !ok {
							continue
						}
						langs, ok := h["lang_"+g.config.Lang].([]interface{})
						if !ok || len(langs) == 0 {
							continue
						}
						weatherDesc, ok := h["weatherDesc"].([]interface{})
						if !ok || len(weatherDesc) == 0 {
							continue
						}
						weatherDesc[0] = langs[0]
					}
				}
			}
		}
	}
	var buf bytes.Buffer
	if err := json.NewEncoder(&buf).Encode(rv); err != nil {
		return err
	}
	if err := json.NewDecoder(&buf).Decode(r); err != nil {
		return err
	}

	return nil
}


================================================
FILE: internal/view/v1/cmd.go
================================================
// This code represents wttr.in view v1.
// It is based on wego (github.com/schachmat/wego) from which it diverged back in 2016.

//nolint:forbidigo,funlen,gocognit,cyclop
package v1

import (
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"os/user"
	"path"
	"regexp"
	"strings"

	"github.com/mattn/go-colorable"
	"github.com/mattn/go-runewidth"
)

type Configuration struct {
	APIKey       string
	City         string
	Numdays      int
	Imperial     bool
	WindUnit     bool
	Inverse      bool
	Lang         string
	Narrow       bool
	LocationName string
	WindMS       bool
	RightToLeft  bool
}

type global struct {
	ansiEsc    *regexp.Regexp
	config     Configuration
	configpath string
	debug      bool
}

const (
	wuri      = "http://127.0.0.1:5001/premium/v1/weather.ashx?"
	suri      = "http://127.0.0.1:5001/premium/v1/search.ashx?"
	slotcount = 4
)

func (g *global) configload() error {
	b, err := ioutil.ReadFile(g.configpath)
	if err == nil {
		return json.Unmarshal(b, &g.config)
	}

	return err
}

func (g *global) configsave() error {
	j, err := json.MarshalIndent(g.config, "", "\t")
	if err == nil {
		return ioutil.WriteFile(g.configpath, j, 0o600)
	}

	return err
}

func (g *global) init() {
	flag.IntVar(&g.config.Numdays, "days", 3, "Number of days of weather forecast to be displayed")
	flag.StringVar(&g.config.Lang, "lang", "en", "Language of the report")
	flag.StringVar(&g.config.City, "city", "New York", "City to be queried")
	flag.BoolVar(&g.debug, "debug", false, "Print out raw json response for debugging purposes")
	flag.BoolVar(&g.config.Imperial, "imperial", false, "Use imperial units")
	flag.BoolVar(&g.config.Inverse, "inverse", false, "Use inverted colors")
	flag.BoolVar(&g.config.Narrow, "narrow", false, "Narrow output (two columns)")
	flag.StringVar(&g.config.LocationName, "location_name", "", "Location name (used in the caption)")
	flag.BoolVar(&g.config.WindMS, "wind_in_ms", false, "Show wind speed in m/s")
	flag.BoolVar(&g.config.RightToLeft, "right_to_left", false, "Right to left script")
	g.configpath = os.Getenv("WEGORC")
	if g.configpath == "" {
		usr, err := user.Current()
		if err != nil {
			log.Fatalf("%v\nYou can set the environment variable WEGORC to point to your config file as a workaround.", err)
		}
		g.configpath = path.Join(usr.HomeDir, ".wegorc")
	}
	g.config.APIKey = ""
	g.config.Imperial = false
	g.config.Lang = "en"
	err := g.configload()
	var pathError *os.PathError
	if errors.Is(err, pathError) {
		log.Printf("No config file found. Creating %s ...", g.configpath)
		if err2 := g.configsave(); err2 != nil {
			log.Fatal(err2)
		}
	} else if err != nil {
		log.Fatalf("could not parse %v: %v", g.configpath, err)
	}

	g.ansiEsc = regexp.MustCompile("\033.*?m")
}

func Cmd() error {
	g := global{}
	g.init()

	flag.Parse()

	r, err := g.getDataFromAPI()
	if err != nil {
		return err
	}

	if r.Data.Req == nil || len(r.Data.Req) < 1 {
		if r.Data.Err != nil && len(r.Data.Err) >= 1 {
			log.Fatal(r.Data.Err[0].Msg)
		}
		log.Fatal("Malformed response.")
	}
	locationName := r.Data.Req[0].Query
	if g.config.LocationName != "" {
		locationName = g.config.LocationName
	}
	if g.config.Lang == "he" || g.config.Lang == "ar" || g.config.Lang == "fa" {
		g.config.RightToLeft = true
	}
	if caption, ok := localizedCaption()[g.config.Lang]; !ok {
		fmt.Printf("Weather report: %s\n\n", locationName)
	} else {
		if g.config.RightToLeft {
			caption = locationName + " " + caption
			space := strings.Repeat(" ", 125-runewidth.StringWidth(caption))
			fmt.Printf("%s%s\n\n", space, caption)
		} else {
			fmt.Printf("%s %s\n\n", caption, locationName)
		}
	}
	stdout := colorable.NewColorableStdout()

	if r.Data.Cur == nil || len(r.Data.Cur) < 1 {
		log.Fatal("No weather data available.")
	}
	out := g.formatCond(make([]string, 5), r.Data.Cur[0], true)
	for _, val := range out {
		if g.config.RightToLeft {
			fmt.Fprint(stdout, strings.Repeat(" ", 94))
		} else {
			fmt.Fprint(stdout, " ")
		}
		fmt.Fprintln(stdout, val)
	}

	if g.config.Numdays == 0 {
		return nil
	}
	if r.Data.Weather == nil {
		log.Fatal("No detailed weather forecast available.")
	}
	for _, d := range r.Data.Weather {
		lines, err := g.printDay(d)
		if err != nil {
			return err
		}
		for _, val := range lines {
			fmt.Fprintln(stdout, val)
		}
	}

	return nil
}


================================================
FILE: internal/view/v1/format.go
================================================
//nolint:funlen,nestif,cyclop,gocognit,gocyclo
package v1

import (
	"fmt"
	"strings"
	"unicode/utf8"

	"github.com/mattn/go-runewidth"
)

func windDir() map[string]string {
	return map[string]string{
		"N":   "\033[1m↓\033[0m",
		"NNE": "\033[1m↓\033[0m",
		"NE":  "\033[1m↙\033[0m",
		"ENE": "\033[1m↙\033[0m",
		"E":   "\033[1m←\033[0m",
		"ESE": "\033[1m←\033[0m",
		"SE":  "\033[1m↖\033[0m",
		"SSE": "\033[1m↖\033[0m",
		"S":   "\033[1m↑\033[0m",
		"SSW": "\033[1m↑\033[0m",
		"SW":  "\033[1m↗\033[0m",
		"WSW": "\033[1m↗\033[0m",
		"W":   "\033[1m→\033[0m",
		"WNW": "\033[1m→\033[0m",
		"NW":  "\033[1m↘\033[0m",
		"NNW": "\033[1m↘\033[0m",
	}
}

func (g *global) formatTemp(c cond) string {
	color := func(temp int, explicitPlus bool) string {
		var col int
		//nolint:dupl
		if !g.config.Inverse {
			// Extremely cold temperature must be shown with violet
			// because dark blue is too dark
			col = 165
			switch temp {
			case -15, -14, -13:
				col = 171
			case -12, -11, -10:
				col = 33
			case -9, -8, -7:
				col = 39
			case -6, -5, -4:
				col = 45
			case -3, -2, -1:
				col = 51
			case 0, 1:
				col = 50
			case 2, 3:
				col = 49
			case 4, 5:
				col = 48
			case 6, 7:
				col = 47
			case 8, 9:
				col = 46
			case 10, 11, 12:
				col = 82
			case 13, 14, 15:
				col = 118
			case 16, 17, 18:
				col = 154
			case 19, 20, 21:
				col = 190
			case 22, 23, 24:
				col = 226
			case 25, 26, 27:
				col = 220
			case 28, 29, 30:
				col = 214
			case 31, 32, 33:
				col = 208
			case 34, 35, 36:
				col = 202
			default:
				if temp > 0 {
					col = 196
				}
			}
		} else {
			col = 16
			switch temp {
			case -15, -14, -13:
				col = 17
			case -12, -11, -10:
				col = 18
			case -9, -8, -7:
				col = 19
			case -6, -5, -4:
				col = 20
			case -3, -2, -1:
				col = 21
			case 0, 1:
				col = 30
			case 2, 3:
				col = 28
			case 4, 5:
				col = 29
			case 6, 7:
				col = 30
			case 8, 9:
				col = 34
			case 10, 11, 12:
				col = 35
			case 13, 14, 15:
				col = 36
			case 16, 17, 18:
				col = 40
			case 19, 20, 21:
				col = 59
			case 22, 23, 24:
				col = 100
			case 25, 26, 27:
				col = 101
			case 28, 29, 30:
				col = 94
			case 31, 32, 33:
				col = 166
			case 34, 35, 36:
				col = 52
			default:
				if temp > 0 {
					col = 196
				}
			}
		}
		if g.config.Imperial {
			temp = (temp*18 + 320) / 10
		}
		if explicitPlus {
			return fmt.Sprintf("\033[38;5;%03dm+%d\033[0m", col, temp)
		}

		return fmt.Sprintf("\033[38;5;%03dm%d\033[0m", col, temp)
	}
	t := c.TempC
	if t == 0 {
		t = c.TempC2
	}

	// hyphen := " - "

	// if (config.Lang == "sl") {
	//     hyphen = "-"
	// }

	// hyphen = ".."

	explicitPlus1 := false
	explicitPlus2 := false
	if c.FeelsLikeC != t {
		if t > 0 {
			explicitPlus1 = true
		}
		if c.FeelsLikeC > 0 {
			explicitPlus2 = true
		}
		if explicitPlus1 {
			explicitPlus2 = false
		}

		return g.pad(
			fmt.Sprintf("%s(%s) °%s",
				color(t, explicitPlus1),
				color(c.FeelsLikeC, explicitPlus2),
				unitTemp()[g.config.Imperial]),
			15)
	}

	return g.pad(fmt.Sprintf("%s °%s", color(c.FeelsLikeC, false), unitTemp()[g.config.Imperial]), 15)
}

func (g *global) formatWind(c cond) string {
	unitWindString := unitWind(0, g.config.Lang)
	if g.config.WindMS {
		unitWindString = unitWind(2, g.config.Lang)
	} else if g.config.Imperial {
		unitWindString = unitWind(1, g.config.Lang)
	}

	hyphen := "-"

	cWindGustKmph := speedToColor(c.WindGustKmph, windInRightUnits(c.WindGustKmph, g.config.WindMS, g.config.Imperial))
	cWindspeedKmph := speedToColor(c.WindspeedKmph, windInRightUnits(c.WindspeedKmph, g.config.WindMS, g.config.Imperial))
	if windInRightUnits(c.WindGustKmph, g.config.WindMS, g.config.Imperial) >
		windInRightUnits(c.WindspeedKmph, g.config.WindMS, g.config.Imperial) {
		return g.pad(
			fmt.Sprintf("%s %s%s%s %s", windDir()[c.Winddir16Point], cWindspeedKmph, hyphen, cWindGustKmph, unitWindString),
			15)
	}

	return g.pad(fmt.Sprintf("%s %s %s", windDir()[c.Winddir16Point], cWindspeedKmph, unitWindString), 15)
}

func windInRightUnits(spd int, windMS, imperial bool) int {
	if windMS {
		spd = (spd * 1000) / 3600
	} else if imperial {
		spd = (spd * 1000) / 1609
	}

	return spd
}

func speedToColor(spd, spdConverted int) string {
	col := 46
	switch spd {
	case 1, 2, 3:
		col = 82
	case 4, 5, 6:
		col = 118
	case 7, 8, 9:
		col = 154
	case 10, 11, 12:
		col = 190
	case 13, 14, 15:
		col = 226
	case 16, 17, 18, 19:
		col = 220
	case 20, 21, 22, 23:
		col = 214
	case 24, 25, 26, 27:
		col = 208
	case 28, 29, 30, 31:
		col = 202
	default:
		if spd > 0 {
			col = 196
		}
	}

	return fmt.Sprintf("\033[38;5;%03dm%d\033[0m", col, spdConverted)
}

func (g *global) formatVisibility(c cond) string {
	if g.config.Imperial {
		c.VisibleDistKM = (c.VisibleDistKM * 621) / 1000
	}

	return g.pad(fmt.Sprintf("%d %s", c.VisibleDistKM, unitVis(g.config.Imperial, g.config.Lang)), 15)
}

func (g *global) formatRain(c cond) string {
	rainUnit := c.PrecipMM
	if g.config.Imperial {
		rainUnit = c.PrecipMM * 0.039
	}
	if c.ChanceOfRain != "" {
		return g.pad(fmt.Sprintf(
			"%.1f %s | %s%%",
			rainUnit,
			unitRain(g.config.Imperial, g.config.Lang),
			c.ChanceOfRain), 15)
	}

	return g.pad(fmt.Sprintf("%.1f %s", rainUnit, unitRain(g.config.Imperial, g.config.Lang)), 15)
}

func (g *global) formatCond(cur []string, c cond, current bool) []string {
	var (
		ret  []string
		icon []string
	)

	if i, ok := codes()[c.WeatherCode]; !ok {
		icon = getIcon("iconUnknown")
	} else {
		icon = i
	}
	if g.config.Inverse {
		// inverting colors
		for i := range icon {
			icon[i] = strings.ReplaceAll(icon[i], "38;5;226", "38;5;94")
			icon[i] = strings.ReplaceAll(icon[i], "38;5;250", "38;5;243")
			icon[i] = strings.ReplaceAll(icon[i], "38;5;21", "38;5;18")
			icon[i] = strings.ReplaceAll(icon[i], "38;5;255", "38;5;245")
			icon[i] = strings.ReplaceAll(icon[i], "38;5;111", "38;5;63")
			icon[i] = strings.ReplaceAll(icon[i], "38;5;251", "38;5;238")
		}
	}
	// desc := fmt.Sprintf("%-15.15v", c.WeatherDesc[0].Value)
	desc := c.WeatherDesc[0].Value
	if g.config.RightToLeft {
		for runewidth.StringWidth(desc) < 15 {
			desc = " " + desc
		}
		for runewidth.StringWidth(desc) > 15 {
			_, size := utf8.DecodeLastRuneInString(desc)
			desc = desc[size:]
		}
	} else {
		for runewidth.StringWidth(desc) < 15 {
			desc += " "
		}
		for runewidth.StringWidth(desc) > 15 {
			_, size := utf8.DecodeLastRuneInString(desc)
			desc = desc[:len(desc)-size]
		}
	}
	if current {
		if g.config.RightToLeft {
			desc = c.WeatherDesc[0].Value
			if runewidth.StringWidth(desc) < 15 {
				desc = strings.Repeat(" ", 15-runewidth.StringWidth(desc)) + desc
			}
		} else {
			desc = c.WeatherDesc[0].Value
		}
	} else {
		if g.config.RightToLeft {
			if frstRune, size := utf8.DecodeRuneInString(desc); frstRune != ' ' {
				desc = "…" + desc[size:]
				for runewidth.StringWidth(desc) < 15 {
					desc = " " + desc
				}
			}
		} else {
			if lastRune, size := utf8.DecodeLastRuneInString(desc); lastRune != ' ' {
				desc = desc[:len(desc)-size] + "…"
				// for numberOfSpaces < runewidth.StringWidth(fmt.Sprintf("%c", lastRune)) - 1 {
				for runewidth.StringWidth(desc) < 15 {
					desc += " "
				}
			}
		}
	}
	if g.config.RightToLeft {
		ret = append(
			ret,
			fmt.Sprintf("%v %v %v", cur[0], desc, icon[0]),
			fmt.Sprintf("%v %v %v", cur[1], g.formatTemp(c), icon[1]),
			fmt.Sprintf("%v %v %v", cur[2], g.formatWind(c), icon[2]),
			fmt.Sprintf("%v %v %v", cur[3], g.formatVisibility(c), icon[3]),
			fmt.Sprintf("%v %v %v", cur[4], g.formatRain(c), icon[4]))
	} else {
		ret = append(
			ret,
			fmt.Sprintf("%v %v %v", cur[0], icon[0], desc),
			fmt.Sprintf("%v %v %v", cur[1], icon[1], g.formatTemp(c)),
			fmt.Sprintf("%v %v %v", cur[2], icon[2], g.formatWind(c)),
			fmt.Sprintf("%v %v %v", cur[3], icon[3], g.formatVisibility(c)),
			fmt.Sprintf("%v %v %v", cur[4], icon[4], g.formatRain(c)))
	}

	return ret
}

func justifyCenter(s string, width int) string {
	appendSide := 0
	for runewidth.StringWidth(s) <= width {
		if appendSide == 1 {
			s += " "
			appendSide = 0
		} else {
			s = " " + s
			appendSide = 1
		}
	}

	return s
}

func reverse(s string) string {
	r := []rune(s)
	for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
		r[i], r[j] = r[j], r[i]
	}

	return string(r)
}

func (g *global) pad(s string, mustLen int) string {
	var ret string
	ret = s
	realLen := utf8.RuneCountInString(g.ansiEsc.ReplaceAllLiteralString(s, ""))
	delta := mustLen - realLen
	if delta > 0 {
		if g.config.RightToLeft {
			ret = strings.Repeat(" ", delta) + ret + "\033[0m"
		} else {
			ret += "\033[0m" + strings.Repeat(" ", delta)
		}
	} else if delta < 0 {
		toks := g.ansiEsc.Split(s, 2)
		tokLen := utf8.RuneCountInString(toks[0])
		esc := g.ansiEsc.FindString(s)
		if tokLen > mustLen {
			ret = fmt.Sprintf("%.*s\033[0m", mustLen, toks[0])
		} else {
			ret = fmt.Sprintf("%s%s%s", toks[0], esc, g.pad(toks[1], mustLen-tokLen))
		}
	}

	return ret
}


================================================
FILE: internal/view/v1/icons.go
================================================
package v1

//nolint:funlen
func getIcon(name string) []string {
	icon := map[string][]string{
		"iconUnknown": {
			"    .-.      ",
			"     __)     ",
			"    (        ",
			"     `-’     ",
			"      •      ",
		},

		"iconSunny": {
			"\033[38;5;226m    \\   /    \033[0m",
			"\033[38;5;226m     .-.     \033[0m",
			"\033[38;5;226m  ― (   ) ―  \033[0m",
			"\033[38;5;226m     `-’     \033[0m",
			"\033[38;5;226m    /   \\    \033[0m",
		},

		"iconPartlyCloudy": {
			"\033[38;5;226m   \\  /\033[0m      ",
			"\033[38;5;226m _ /\"\"\033[38;5;250m.-.    \033[0m",
			"\033[38;5;226m   \\_\033[38;5;250m(   ).  \033[0m",
			"\033[38;5;226m   /\033[38;5;250m(___(__) \033[0m",
			"             ",
		},

		"iconCloudy": {
			"             ",
			"\033[38;5;250m     .--.    \033[0m",
			"\033[38;5;250m  .-(    ).  \033[0m",
			"\033[38;5;250m (___.__)__) \033[0m",
			"             ",
		},

		"iconVeryCloudy": {
			"             ",
			"\033[38;5;240;1m     .--.    \033[0m",
			"\033[38;5;240;1m  .-(    ).  \033[0m",
			"\033[38;5;240;1m (___.__)__) \033[0m",
			"             ",
		},

		"iconLightShowers": {
			"\033[38;5;226m _`/\"\"\033[38;5;250m.-.    \033[0m",
			"\033[38;5;226m  ,\\_\033[38;5;250m(   ).  \033[0m",
			"\033[38;5;226m   /\033[38;5;250m(___(__) \033[0m",
			"\033[38;5;111m     ‘ ‘ ‘ ‘ \033[0m",
			"\033[38;5;111m    ‘ ‘ ‘ ‘  \033[0m",
		},

		"iconHeavyShowers": {
			"\033[38;5;226m _`/\"\"\033[38;5;240;1m.-.    \033[0m",
			"\033[38;5;226m  ,\\_\033[38;5;240;1m(   ).  \033[0m",
			"\033[38;5;226m   /\033[38;5;240;1m(___(__) \033[0m",
			"\033[38;5;21;1m   ‚‘‚‘‚‘‚‘  \033[0m",
			"\033[38;5;21;1m   ‚’‚’‚’‚’  \033[0m",
		},

		"iconLightSnowShowers": {
			"\033[38;5;226m _`/\"\"\033[38;5;250m.-.    \033[0m",
			"\033[38;5;226m  ,\\_\033[38;5;250m(   ).  \033[0m",
			"\033[38;5;226m   /\033[38;5;250m(___(__) \033[0m",
			"\033[38;5;255m     *  *  * \033[0m",
			"\033[38;5;255m    *  *  *  \033[0m",
		},

		"iconHeavySnowShowers": {
			"\033[38;5;226m _`/\"\"\033[38;5;240;1m.-.    \033[0m",
			"\033[38;5;226m  ,\\_\033[38;5;240;1m(   ).  \033[0m",
			"\033[38;5;226m   /\033[38;5;240;1m(___(__) \033[0m",
			"\033[38;5;255;1m    * * * *  \033[0m",
			"\033[38;5;255;1m   * * * *   \033[0m",
		},

		"iconLightSleetShowers": {
			"\033[38;5;226m _`/\"\"\033[38;5;250m.-.    \033[0m",
			"\033[38;5;226m  ,\\_\033[38;5;250m(   ).  \033[0m",
			"\033[38;5;226m   /\033[38;5;250m(___(__) \033[0m",
			"\033[38;5;111m     ‘ \033[38;5;255m*\033[38;5;111m ‘ \033[38;5;255m* \033[0m",
			"\033[38;5;255m    *\033[38;5;111m ‘ \033[38;5;255m*\033[38;5;111m ‘  \033[0m",
		},

		"iconThunderyShowers": {
			"\033[38;5;226m _`/\"\"\033[38;5;250m.-.    \033[0m",
			"\033[38;5;226m  ,\\_\033[38;5;250m(   ).  \033[0m",
			"\033[38;5;226m   /\033[38;5;250m(___(__) \033[0m",
			"\033[38;5;228;5m    ⚡\033[38;5;111;25m‘‘\033[38;5;228;5m⚡\033[38;5;111;25m‘‘ \033[0m",
			"\033[38;5;111m    ‘ ‘ ‘ ‘  \033[0m",
		},

		"iconThunderyHeavyRain": {
			"\033[38;5;240;1m     .-.     \033[0m",
			"\033[38;5;240;1m    (   ).   \033[0m",
			"\033[38;5;240;1m   (___(__)  \033[0m",
			"\033[38;5;21;1m  ‚‘\033[38;5;228;5m⚡\033[38;5;21;25m‘‚\033[38;5;228;5m⚡\033[38;5;21;25m‚‘ \033[0m",
			"\033[38;5;21;1m  ‚’‚’\033[38;5;228;5m⚡\033[38;5;21;25m’‚’  \033[0m",
		},

		"iconThunderySnowShowers": {
			"\033[38;5;226m _`/\"\"\033[38;5;250m.-.    \033[0m",
			"\033[38;5;226m  ,\\_\033[38;5;250m(   ).  \033[0m",
			"\033[38;5;226m   /\033[38;5;250m(___(__) \033[0m",
			"\033[38;5;255m     *\033[38;5;228;5m⚡\033[38;5;255;25m*\033[38;5;228;5m⚡\033[38;5;255;25m* \033[0m",
			"\033[38;5;255m    *  *  *  \033[0m",
		},

		"iconLightRain": {
			"\033[38;5;250m     .-.     \033[0m",
			"\033[38;5;250m    (   ).   \033[0m",
			"\033[38;5;250m   (___(__)  \033[0m",
			"\033[38;5;111m    ‘ ‘ ‘ ‘  \033[0m",
			"\033[38;5;111m   ‘ ‘ ‘ ‘   \033[0m",
		},

	
Download .txt
gitextract_3puwzenn/

├── .flake8
├── .github/
│   └── workflows/
│       └── makefile.yml
├── .gitignore
├── .golangci.yaml
├── .travis.yml
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── bin/
│   ├── geo-proxy.py
│   ├── proxy.py
│   └── srv.py
├── config/
│   └── services/
│       └── services.yaml
├── doc/
│   ├── integrations.md
│   └── terminal-images.md
├── go.mod
├── go.sum
├── internal/
│   ├── config/
│   │   └── config.go
│   ├── fmt/
│   │   └── png/
│   │       ├── colors.go
│   │       ├── go.mod
│   │       ├── go.sum
│   │       └── png.go
│   ├── geo/
│   │   ├── ip/
│   │   │   ├── convert.go
│   │   │   ├── ip.go
│   │   │   └── ip_test.go
│   │   └── location/
│   │       ├── cache.go
│   │       ├── convert.go
│   │       ├── location.go
│   │       ├── nominatim.go
│   │       ├── nominatim_locationiq.go
│   │       ├── nominatim_opencage.go
│   │       ├── response.go
│   │       └── search.go
│   ├── logging/
│   │   ├── logging.go
│   │   └── suppress.go
│   ├── options/
│   │   ├── options.go
│   │   ├── parse.go
│   │   └── processlog.go
│   ├── processor/
│   │   ├── j1.go
│   │   ├── peak.go
│   │   └── processor.go
│   ├── routing/
│   │   └── routing.go
│   ├── stats/
│   │   └── stats.go
│   ├── types/
│   │   ├── errors.go
│   │   └── types.go
│   ├── util/
│   │   ├── files.go
│   │   ├── http.go
│   │   └── yaml.go
│   └── view/
│       └── v1/
│           ├── api.go
│           ├── cmd.go
│           ├── format.go
│           ├── icons.go
│           ├── locale.go
│           └── view1.go
├── lib/
│   ├── airports.py
│   ├── buttons.py
│   ├── cache.py
│   ├── constants.py
│   ├── datasource/
│   │   └── README.md
│   ├── duplicate_translations.py
│   ├── extract_emoji.py
│   ├── fields.py
│   ├── fmt/
│   │   ├── __init__.py
│   │   ├── png.py
│   │   └── unicodedata2.py
│   ├── globals.py
│   ├── limits.py
│   ├── location.py
│   ├── metno.py
│   ├── parse_query.py
│   ├── proxy_log.py
│   ├── translations.py
│   ├── translations_v2.py
│   ├── view/
│   │   ├── __init__.py
│   │   ├── line.py
│   │   ├── moon.py
│   │   ├── prometheus.py
│   │   ├── v2.py
│   │   └── wttr.py
│   ├── weather_data.py
│   └── wttr_srv.py
├── requirements.txt
├── share/
│   ├── aliases
│   ├── ansi2html.sh
│   ├── bash-function.txt
│   ├── blacklist
│   ├── docker/
│   │   └── supervisord.conf
│   ├── help.txt
│   ├── iterm2.txt
│   ├── list-of-iata-codes.txt
│   ├── salt/
│   │   ├── README.md
│   │   ├── init.sls
│   │   ├── pillar.sls
│   │   ├── start.sh
│   │   ├── wegorc
│   │   └── wttr.service
│   ├── screenrc
│   ├── scripts/
│   │   ├── build-welang.sh
│   │   ├── clean-cache.sh
│   │   ├── log-space.sh
│   │   └── start-screen.sh
│   ├── static/
│   │   ├── malformed-response.html
│   │   └── style.css
│   ├── systemd/
│   │   ├── README.md
│   │   ├── wttrin.service
│   │   └── wttrin.sh
│   ├── templates/
│   │   └── index.html
│   ├── test-thunder.txt
│   ├── translation.txt
│   └── translations/
│       ├── af-help.txt
│       ├── af.txt
│       ├── am-help.txt
│       ├── am.txt
│       ├── ar-help.txt
│       ├── ar.txt
│       ├── az.txt
│       ├── be-help.txt
│       ├── be.txt
│       ├── bg-help.txt
│       ├── bn-help.txt
│       ├── bn.txt
│       ├── bs.txt
│       ├── ca-help.txt
│       ├── ca.txt
│       ├── cs-help.txt
│       ├── cs.txt
│       ├── cy.txt
│       ├── da-help.txt
│       ├── da.txt
│       ├── de-help.txt
│       ├── de.txt
│       ├── dk-help.txt
│       ├── el-help.txt
│       ├── el.txt
│       ├── en.txt
│       ├── eo.txt
│       ├── es-help.txt
│       ├── es.txt
│       ├── et-help.txt
│       ├── et.txt
│       ├── eu-help.txt
│       ├── eu.txt
│       ├── fa-help.txt
│       ├── fa.txt
│       ├── fr-help.txt
│       ├── fr.txt
│       ├── fy.txt
│       ├── ga.txt
│       ├── gl-help.txt
│       ├── gl.txt
│       ├── gu-help.txt
│       ├── gu.txt
│       ├── he.txt
│       ├── hi-help.txt
│       ├── hi.txt
│       ├── hr.txt
│       ├── hu-help.txt
│       ├── hu.txt
│       ├── hy.txt
│       ├── ia-help.txt
│       ├── ia.txt
│       ├── id-help.txt
│       ├── id.txt
│       ├── is.txt
│       ├── it-help.txt
│       ├── it.txt
│       ├── ja.txt
│       ├── kk-help.txt
│       ├── kk.txt
│       ├── lt-help.txt
│       ├── lt.txt
│       ├── lv-help.txt
│       ├── lv.txt
│       ├── messages/
│       │   ├── en.yaml
│       │   └── gu.yaml
│       ├── mg-help.txt
│       ├── mg.txt
│       ├── mk.txt
│       ├── mr-help.txt
│       ├── mr.txt
│       ├── nb-help.txt
│       ├── nb.txt
│       ├── nl-help.txt
│       ├── nl.txt
│       ├── nn.txt
│       ├── oc-help.txt
│       ├── oc.txt
│       ├── pl-help.txt
│       ├── pl.txt
│       ├── pt-br-help.txt
│       ├── pt-br.txt
│       ├── pt-help.txt
│       ├── pt.txt
│       ├── ro-help.txt
│       ├── ro.txt
│       ├── ru-help.txt
│       ├── ru.txt
│       ├── sl.txt
│       ├── ta-help.txt
│       ├── ta.txt
│       ├── te-help.txt
│       ├── te.txt
│       ├── th-help.txt
│       ├── th.txt
│       ├── tr-help.txt
│       ├── tr.txt
│       ├── uk-help.txt
│       ├── uk.txt
│       ├── ukr-help.txt
│       ├── uz.txt
│       ├── vi-help.txt
│       ├── vi.txt
│       ├── zh-cn-help.txt
│       ├── zh-cn.txt
│       ├── zh-tw-help.txt
│       └── zh-tw.txt
├── spec/
│   └── options/
│       └── options.yaml
├── srv.go
└── test/
    ├── proxy-data/
    │   ├── data1
    │   └── data1.headers
    ├── query.sh
    └── test-data/
        └── signatures
Download .txt
SYMBOL INDEX (378 symbols across 58 files)

FILE: bin/geo-proxy.py
  function load_cache (line 44) | def load_cache(location_string):
  function shorten_full_address (line 54) | def shorten_full_address(address):
  function save_cache (line 62) | def save_cache(location_string, answer):
  function query_osm (line 68) | def query_osm(location_string):
  function add_timezone_information (line 82) | def add_timezone_information(geo_data):
  function find_location (line 96) | def find_location(location):

FILE: bin/proxy.py
  function is_testmode (line 60) | def is_testmode():
  function load_translations (line 66) | def load_translations():
  function _is_metno (line 95) | def _is_metno():
  function _find_srv_for_query (line 99) | def _find_srv_for_query(path, query):  # pylint: disable=unused-argument
  function _cache_file (line 105) | def _cache_file(path, query):
  function _load_content_and_headers (line 124) | def _load_content_and_headers(path, query):
  function _touch_empty_file (line 138) | def _touch_empty_file(path, query):
  function _save_content_and_headers (line 146) | def _save_content_and_headers(path, query, content, headers):
  function translate (line 155) | def translate(text, lang):
  function cyr (line 183) | def cyr(to_translate):
  function _patch_greek (line 190) | def _patch_greek(original):
  function add_translations (line 194) | def add_translations(content, lang):
  function _fetch_content_and_headers (line 277) | def _fetch_content_and_headers(path, query_string, **kwargs):
  function _make_query (line 318) | def _make_query(path, query_string):
  function _normalize_query_string (line 338) | def _normalize_query_string(query_string):
  function proxy (line 362) | def proxy(path):

FILE: bin/srv.py
  function send_static (line 47) | def send_static(path):
  function send_favicon (line 53) | def send_favicon():
  function send_malformed (line 59) | def send_malformed():
  function wttr (line 66) | def wttr(location=None):

FILE: internal/config/config.go
  type Config (line 14) | type Config struct
    method Dump (line 187) | func (c *Config) Dump() []byte {
  type Logging (line 23) | type Logging struct
  type Server (line 35) | type Server struct
  type Uplink (line 52) | type Uplink struct
  type Cache (line 78) | type Cache struct
  type Geo (line 84) | type Geo struct
  type Nominatim (line 104) | type Nominatim struct
  function Default (line 117) | func Default() *Config {
  function Load (line 167) | func Load(filename string) (*Config, error) {

FILE: internal/fmt/png/png.go
  function StringSliceToRuneSlice (line 13) | func StringSliceToRuneSlice(s string) [][]rune {
  function maxRowLength (line 29) | func maxRowLength(rows [][]rune) int {
  function GeneratePng (line 39) | func GeneratePng() {
  function GeneratePngFromANSI (line 129) | func GeneratePngFromANSI(input []byte, outputFile string) error {
  function colorANSItoRGB (line 197) | func colorANSItoRGB(colorANSI vt10x.Color) [3]float64 {
  function main (line 214) | func main() {

FILE: internal/geo/ip/convert.go
  method ConvertCache (line 15) | func (c *Cache) ConvertCache() error {
  function createTable (line 76) | func createTable(db *godb.DB, tableName string) error {

FILE: internal/geo/ip/ip.go
  type Address (line 23) | type Address struct
    method String (line 33) | func (l *Address) String() string {
  type Cache (line 46) | type Cache struct
    method Read (line 80) | func (c *Cache) Read(addr string) (*Address, error) {
    method readFromCacheFile (line 88) | func (c *Cache) readFromCacheFile(addr string) (*Address, error) {
    method readFromCacheDB (line 97) | func (c *Cache) readFromCacheDB(addr string) (*Address, error) {
    method Put (line 109) | func (c *Cache) Put(addr string, loc *Address) error {
    method putToCacheDB (line 117) | func (c *Cache) putToCacheDB(loc *Address) error {
    method putToCacheFile (line 135) | func (c *Cache) putToCacheFile(addr string, loc fmt.Stringer) error {
    method cacheFile (line 140) | func (c *Cache) cacheFile(addr string) string {
    method Response (line 190) | func (c *Cache) Response(r *http.Request) *routing.Cadre {
  function NewCache (line 52) | func NewCache(config *config.Config) (*Cache, error) {
  function NewAddressFromString (line 146) | func NewAddressFromString(addr, s string) (*Address, error) {
  function validIP4 (line 240) | func validIP4(ipAddress string) bool {

FILE: internal/geo/ip/ip_test.go
  function TestParseCacheEntry (line 13) | func TestParseCacheEntry(t *testing.T) {

FILE: internal/geo/location/cache.go
  type Cache (line 25) | type Cache struct
    method Resolve (line 66) | func (c *Cache) Resolve(location string) (*Location, error) {
    method Read (line 94) | func (c *Cache) Read(addr string) (*Location, error) {
    method readFromCacheFile (line 102) | func (c *Cache) readFromCacheFile(name string) (*Location, error) {
    method readFromCacheDB (line 143) | func (c *Cache) readFromCacheDB(addr string) (*Location, error) {
    method Put (line 160) | func (c *Cache) Put(addr string, loc *Location) error {
    method putToCacheDB (line 169) | func (c *Cache) putToCacheDB(loc *Location) error {
    method putToCacheFile (line 178) | func (c *Cache) putToCacheFile(addr string, loc fmt.Stringer) error {
    method cacheFile (line 183) | func (c *Cache) cacheFile(item string) string {
  function NewCache (line 34) | func NewCache(config *config.Config) (*Cache, error) {
  function normalizeLocationName (line 192) | func normalizeLocationName(name string) string {
  function latLngToTimezoneString (line 203) | func latLngToTimezoneString(lat, lon string) string {

FILE: internal/geo/location/convert.go
  method ConvertCache (line 20) | func (c *Cache) ConvertCache(reset bool) error {
  function createTable (line 119) | func createTable(db *godb.DB, tableName string) error {
  function removeDBIfExists (line 134) | func removeDBIfExists(filename string) error {

FILE: internal/geo/location/location.go
  type Location (line 8) | type Location struct
    method String (line 17) | func (l *Location) String() string {

FILE: internal/geo/location/nominatim.go
  type Nominatim (line 13) | type Nominatim struct
    method Query (line 33) | func (n *Nominatim) Query(location string) (*Location, error) {
  type locationQuerier (line 20) | type locationQuerier interface
  function NewNominatim (line 24) | func NewNominatim(name, typ, url, token string) *Nominatim {
  function makeQuery (line 48) | func makeQuery(url string, result interface{}) error {

FILE: internal/geo/location/nominatim_locationiq.go
  type locationIQ (line 10) | type locationIQ
    method Query (line 18) | func (data *locationIQ) Query(n *Nominatim, location string) (*Locatio...

FILE: internal/geo/location/nominatim_opencage.go
  type locationOpenCage (line 10) | type locationOpenCage struct
    method Query (line 21) | func (data *locationOpenCage) Query(n *Nominatim, location string) (*L...

FILE: internal/geo/location/response.go
  method Response (line 14) | func (c *Cache) Response(r *http.Request) *routing.Cadre {
  function errorResponse (line 45) | func errorResponse(s string) *routing.Cadre {

FILE: internal/geo/location/search.go
  type Provider (line 5) | type Provider interface
  type Searcher (line 9) | type Searcher struct
    method Search (line 28) | func (s *Searcher) Search(location string) (*Location, error) {
  function NewSearcher (line 14) | func NewSearcher(config *config.Config) *Searcher {

FILE: internal/logging/logging.go
  type RequestLogger (line 17) | type RequestLogger struct
    method Log (line 48) | func (rl *RequestLogger) Log(r *http.Request) error {
    method flush (line 76) | func (rl *RequestLogger) flush() error {
  type logEntry (line 26) | type logEntry struct
    method String (line 116) | func (e *logEntry) String() string {
  function NewRequestLogger (line 38) | func NewRequestLogger(filename string, period time.Duration) *RequestLog...

FILE: internal/logging/suppress.go
  type LogSuppressor (line 11) | type LogSuppressor struct
    method Open (line 33) | func (ls *LogSuppressor) Open() error {
    method Close (line 47) | func (ls *LogSuppressor) Close() error {
    method Write (line 57) | func (ls *LogSuppressor) Write(p []byte) (int, error) {
  function NewLogSuppressor (line 24) | func NewLogSuppressor(filename string, suppress []string, linePrefix str...

FILE: internal/options/options.go
  type WttrInOptions (line 11) | type WttrInOptions struct
  type QueryOption (line 17) | type QueryOption struct
  type Range (line 53) | type Range struct
  type FormatSpecifier (line 62) | type FormatSpecifier struct
  function NewFromFile (line 77) | func NewFromFile(filename string) (*WttrInOptions, error) {

FILE: internal/options/parse.go
  function ParseQueryString (line 38) | func ParseQueryString(query string, config *WttrInOptions) (map[string]s...
  function buildOptionLookups (line 82) | func buildOptionLookups(options []QueryOption) (map[string]string, map[s...
  function handleFlagOption (line 95) | func handleFlagOption(key string, shortToName map[string]string, nameToO...
  function validateAndSetFlag (line 110) | func validateAndSetFlag(key string, shortToName map[string]string, nameT...
  function handleValueOption (line 130) | func handleValueOption(key, value string, shortToName map[string]string,...
  function validateBooleanOption (line 158) | func validateBooleanOption(name, value string, opt QueryOption, result m...
  function validateStringOption (line 176) | func validateStringOption(name, value string, opt QueryOption, result ma...
  function applyValidationRule (line 203) | func applyValidationRule(name, value, rule string) error {
  function validateIntegerOption (line 232) | func validateIntegerOption(name, value string, opt QueryOption, result m...
  function getMapKeys (line 250) | func getMapKeys(m map[string]string) []string {

FILE: internal/options/processlog.go
  function ProcessLogFile (line 13) | func ProcessLogFile(logFilePath, errorFilePath string, config *WttrInOpt...
  function openLogFile (line 31) | func openLogFile(logFilePath string) (*os.File, error) {
  function openErrorFile (line 40) | func openErrorFile(errorFilePath string) (*os.File, *bufio.Writer, error) {
  function processLogLines (line 50) | func processLogLines(logFile *os.File, writer *bufio.Writer, config *Wtt...
  function processLogLine (line 66) | func processLogLine(line string, lineNumber int, writer *bufio.Writer, c...
  function extractQueryFromLine (line 83) | func extractQueryFromLine(line string, lineNumber int, writer *bufio.Wri...

FILE: internal/processor/j1.go
  function getAny (line 12) | func getAny(req *http.Request, tr1, tr2, tr3, tr4 *http.Transport) (*Res...
  function getJ1 (line 38) | func getJ1(req *http.Request, transport *http.Transport) (*ResponseWithH...
  function getFormat (line 42) | func getFormat(req *http.Request, transport *http.Transport) (*ResponseW...
  function getDefault (line 46) | func getDefault(req *http.Request, transport *http.Transport) (*Response...
  function getUpstream (line 50) | func getUpstream(req *http.Request, transport *http.Transport) (*Respons...
  function checkURLForPNG (line 95) | func checkURLForPNG(r *http.Request) bool {

FILE: internal/processor/peak.go
  method startPeakHandling (line 12) | func (rp *RequestProcessor) startPeakHandling() error {
  method savePeakRequest (line 40) | func (rp *RequestProcessor) savePeakRequest(cacheDigest string, r *http....
  method prefetchRequest (line 48) | func (rp *RequestProcessor) prefetchRequest(r *http.Request) error {
  function syncMapLen (line 54) | func syncMapLen(sm *sync.Map) int {
  method prefetchPeakRequests (line 72) | func (rp *RequestProcessor) prefetchPeakRequests(peakRequestMap *sync.Ma...

FILE: internal/processor/processor.go
  function plainTextAgents (line 25) | func plainTextAgents() []string {
  type ResponseWithHeader (line 45) | type ResponseWithHeader struct
  type RequestProcessor (line 55) | type RequestProcessor struct
    method Start (line 136) | func (rp *RequestProcessor) Start() error {
    method ProcessRequest (line 140) | func (rp *RequestProcessor) ProcessRequest(r *http.Request) (*Response...
    method processRequestFromCache (line 185) | func (rp *RequestProcessor) processRequestFromCache(r *http.Request) *...
    method processUncachedRequest (line 231) | func (rp *RequestProcessor) processUncachedRequest(r *http.Request) (*...
  function NewRequestProcessor (line 71) | func NewRequestProcessor(config *config.Config) (*RequestProcessor, erro...
  function getCacheDigest (line 273) | func getCacheDigest(req *http.Request) string {
  function dontCache (line 287) | func dontCache(req *http.Request) bool {
  function redirectInsecure (line 300) | func redirectInsecure(req *http.Request) (*ResponseWithHeader, bool) {
  function isPlainTextAgent (line 332) | func isPlainTextAgent(userAgent string) bool {
  function randInt (line 343) | func randInt(min int, max int) int {
  function ipFromAddr (line 348) | func ipFromAddr(s string) string {
  function fromCadre (line 358) | func fromCadre(cadre *routing.Cadre) *ResponseWithHeader {

FILE: internal/routing/routing.go
  type CadreFormat (line 9) | type CadreFormat
  constant CadreFormatANSI (line 13) | CadreFormatANSI = iota
  constant CadreFormatHTML (line 16) | CadreFormatHTML
  constant CadreFormatPNG (line 19) | CadreFormatPNG
  type Cadre (line 23) | type Cadre struct
  type Handler (line 36) | type Handler interface
  type routeFunc (line 40) | type routeFunc
  type route (line 42) | type route struct
  type Router (line 48) | type Router struct
    method Route (line 53) | func (r *Router) Route(req *http.Request) Handler {
    method AddPath (line 64) | func (r *Router) AddPath(path string, handler Handler) {
  function routePath (line 68) | func routePath(path string) routeFunc {

FILE: internal/stats/stats.go
  type Stats (line 14) | type Stats struct
    method Inc (line 29) | func (c *Stats) Inc(key string) {
    method Get (line 36) | func (c *Stats) Get(key string) int {
    method Reset (line 44) | func (c *Stats) Reset(key string) int {
    method Show (line 54) | func (c *Stats) Show() []byte {
    method Response (line 85) | func (c *Stats) Response(*http.Request) *routing.Cadre {
  function New (line 21) | func New() *Stats {

FILE: internal/types/types.go
  type CacheType (line 3) | type CacheType
  constant CacheTypeDB (line 6) | CacheTypeDB    = "db"
  constant CacheTypeFiles (line 7) | CacheTypeFiles = "files"

FILE: internal/util/files.go
  function RemoveFileIfExists (line 7) | func RemoveFileIfExists(filename string) error {

FILE: internal/util/http.go
  function ReadUserIP (line 11) | func ReadUserIP(r *http.Request) string {

FILE: internal/util/yaml.go
  function YamlUnmarshalStrict (line 10) | func YamlUnmarshalStrict(in []byte, out interface{}) error {

FILE: internal/view/v1/api.go
  type cond (line 18) | type cond struct
  type astro (line 33) | type astro struct
  type weather (line 40) | type weather struct
  type loc (line 48) | type loc struct
  type resp (line 54) | type resp struct
  method getDataFromAPI (line 63) | func (g *global) getDataFromAPI() (*resp, error) {
  method unmarshalLang (line 134) | func (g *global) unmarshalLang(body []byte, r *resp) error {

FILE: internal/view/v1/cmd.go
  type Configuration (line 24) | type Configuration struct
  type global (line 38) | type global struct
    method configload (line 51) | func (g *global) configload() error {
    method configsave (line 60) | func (g *global) configsave() error {
    method init (line 69) | func (g *global) init() {
  constant wuri (line 46) | wuri      = "http://127.0.0.1:5001/premium/v1/weather.ashx?"
  constant suri (line 47) | suri      = "http://127.0.0.1:5001/premium/v1/search.ashx?"
  constant slotcount (line 48) | slotcount = 4
  function Cmd (line 105) | func Cmd() error {

FILE: internal/view/v1/format.go
  function windDir (line 12) | func windDir() map[string]string {
  method formatTemp (line 33) | func (g *global) formatTemp(c cond) string {
  method formatWind (line 178) | func (g *global) formatWind(c cond) string {
  function windInRightUnits (line 200) | func windInRightUnits(spd int, windMS, imperial bool) int {
  function speedToColor (line 210) | func speedToColor(spd, spdConverted int) string {
  method formatVisibility (line 240) | func (g *global) formatVisibility(c cond) string {
  method formatRain (line 248) | func (g *global) formatRain(c cond) string {
  method formatCond (line 264) | func (g *global) formatCond(cur []string, c cond, current bool) []string {
  function justifyCenter (line 353) | func justifyCenter(s string, width int) string {
  function reverse (line 368) | func reverse(s string) string {
  method pad (line 377) | func (g *global) pad(s string, mustLen int) string {

FILE: internal/view/v1/icons.go
  function getIcon (line 4) | func getIcon(name string) []string {
  function codes (line 162) | func codes() map[int][]string {

FILE: internal/view/v1/locale.go
  function locale (line 4) | func locale() map[string]string {
  function localizedCaption (line 80) | func localizedCaption() map[string]string {
  function daytimeTranslation (line 157) | func daytimeTranslation() map[string][]string {
  function unitTemp (line 234) | func unitTemp() map[bool]string {
  function localizedRain (line 241) | func localizedRain() map[string]map[bool]string {
  function localizedVis (line 262) | func localizedVis() map[string]map[bool]string {
  function localizedWind (line 283) | func localizedWind() map[string]map[int]string {
  function unitWind (line 313) | func unitWind(unit int, lang string) string {
  function unitVis (line 322) | func unitVis(unit bool, lang string) string {
  function unitRain (line 331) | func unitRain(unit bool, lang string) string {

FILE: internal/view/v1/view1.go
  function slotTimes (line 10) | func slotTimes() []int {
  method printDay (line 15) | func (g *global) printDay(w weather) ([]string, error) {

FILE: lib/airports.py
  function load_aiports_index (line 6) | def load_aiports_index():
  function get_airport_gps_location (line 20) | def get_airport_gps_location(iata_code):

FILE: lib/buttons.py
  function add_buttons (line 25) | def add_buttons(output):

FILE: lib/cache.py
  function _update_answer (line 25) | def _update_answer(answer):
  function get_signature (line 37) | def get_signature(user_agent, query_string, client_ip_address, lang):
  function get (line 60) | def get(signature):
  function _randint (line 85) | def _randint(minimum, maximum):
  function store (line 89) | def store(signature, value):
  function _hash (line 111) | def _hash(signature):
  function _store_in_file (line 115) | def _store_in_file(signature, value):
  function _read_from_file (line 140) | def _read_from_file(signature, sighash=None):

FILE: lib/duplicate_translations.py
  function remove_colon_and_strip_from_str (line 4) | def remove_colon_and_strip_from_str(line):
  function print_result_for_file (line 11) | def print_result_for_file(file_path, file_name, duplicate_entries):
  function find_duplicates (line 29) | def find_duplicates(directory, debug=False):

FILE: lib/extract_emoji.py
  function extract_emojis_to_directory (line 51) | def extract_emojis_to_directory(dirname):

FILE: lib/fmt/png.py
  function render_ansi (line 70) | def render_ansi(text, options=None):
  function _color_mapping (line 90) | def _color_mapping(color, inverse=False):
  function _strip_buf (line 113) | def _strip_buf(buf):
  function _script_category (line 145) | def _script_category(char):
  function _load_emojilib (line 163) | def _load_emojilib():
  function _gen_term (line 177) | def _gen_term(buf, graphemes, options=None):
  function _fix_graphemes (line 272) | def _fix_graphemes(text):

FILE: lib/fmt/unicodedata2.py
  function script_cat (line 1784) | def script_cat(chr):
  function script (line 1803) | def script(chr):
  function category (line 1808) | def category(chr):
  function _compile_scripts_txt (line 1813) | def _compile_scripts_txt():

FILE: lib/globals.py
  function error (line 137) | def error(text):
  function log (line 146) | def log(text):
  function debug_log (line 154) | def debug_log(text):
  function get_help_file (line 163) | def get_help_file(lang):
  function remove_ansi (line 172) | def remove_ansi(sometext):

FILE: lib/limits.py
  function _time_caps (line 25) | def _time_caps(minutes, hours, days):
  class Limits (line 33) | class Limits(object):
    method __init__ (line 42) | def __init__(self, whitelist=None, limits=None):
    method _log_visit (line 66) | def _log_visit(self, interval, ip_address):
    method _limit_exceeded (line 71) | def _limit_exceeded(self, interval, ip_address):
    method _get_limit (line 76) | def _get_limit(self, interval):
    method _report_excessive_visits (line 79) | def _report_excessive_visits(self, interval, ip_address):
    method check_ip (line 84) | def check_ip(self, ip_address):
    method reset (line 102) | def reset(self):
    method _clear_counters_if_needed (line 109) | def _clear_counters_if_needed(self):

FILE: lib/location.py
  function _debug_log (line 66) | def _debug_log(s):
  function _is_ip (line 72) | def _is_ip(ip_addr):
  function _location_normalize (line 91) | def _location_normalize(location):
  function _geolocator (line 105) | def _geolocator(location):
  function _ipcachewrite (line 137) | def _ipcachewrite(ip_addr, location):
  function _ipcache (line 162) | def _ipcache(ip_addr):
  function _ip2location (line 191) | def _ip2location(ip_addr):
  function _ipinfo (line 223) | def _ipinfo(ip_addr):
  function _geoip (line 246) | def _geoip(ip_addr):
  function _country_name_workaround (line 278) | def _country_name_workaround(country):
  function _get_location (line 285) | def _get_location(ip_addr):
  function _location_canonical_name (line 328) | def _location_canonical_name(location):
  function _load_aliases (line 337) | def _load_aliases(aliases_filename):
  function _load_iata_codes (line 353) | def _load_iata_codes(iata_codes_filename):
  function is_location_blocked (line 369) | def is_location_blocked(location):
  function _get_hemisphere (line 377) | def _get_hemisphere(location):
  function _fully_qualified_location (line 393) | def _fully_qualified_location(location, region, country):
  function location_processing (line 422) | def location_processing(location, ip_addr):
  function _main_ (line 521) | def _main_():
  function _trace_ip (line 541) | def _trace_ip():

FILE: lib/metno.py
  function metno_request (line 19) | def metno_request(path, query_string):
  function celsius_to_f (line 60) | def celsius_to_f(celsius):
  function to_weather_code (line 64) | def to_weather_code(symbol_code):
  function to_description (line 123) | def to_description(symbol_code):
  function to_16_point (line 129) | def to_16_point(degrees):
  function meters_to_miles (line 165) | def meters_to_miles(meters):
  function mm_to_inches (line 169) | def mm_to_inches(mm):
  function hpa_to_mb (line 173) | def hpa_to_mb(hpa):
  function hpa_to_in (line 177) | def hpa_to_in(hpa):
  function hpa_to_mmHg (line 181) | def hpa_to_mmHg(hpa):
  function group_hours_to_days (line 185) | def group_hours_to_days(lat, lng, hourlies, days_to_return):
  function _convert_hour (line 246) | def _convert_hour(hour):
  function _convert_hourly (line 367) | def _convert_hourly(hours):
  function create_standard_json_from_metno (line 376) | def create_standard_json_from_metno(content, days_to_return):

FILE: lib/parse_query.py
  function serialize (line 7) | def serialize(parsed_query):
  function deserialize (line 13) | def deserialize(url):
  function metric_or_imperial (line 35) | def metric_or_imperial(query, lang, us_ip=False):
  function parse_query (line 62) | def parse_query(args):
  function parse_wttrin_png_name (line 130) | def parse_wttrin_png_name(name):

FILE: lib/proxy_log.py
  class Logger (line 11) | class Logger:
    method __init__ (line 18) | def __init__(self, filename_access, filename_errors):
    method _shorten_query (line 25) | def _shorten_query(self, query):
    method log (line 28) | def log(self, query, error):
  class LoggerWWO (line 45) | class LoggerWWO(Logger):
    method _shorten_query (line 50) | def _shorten_query(self, query):

FILE: lib/translations.py
  function get_message (line 1063) | def get_message(message_name, lang):

FILE: lib/view/line.py
  function convert_to_fahrenheit (line 54) | def convert_to_fahrenheit(temp):
  function render_temperature (line 60) | def render_temperature(data, query):
  function render_feel_like_temperature (line 76) | def render_feel_like_temperature(data, query):
  function render_condition (line 92) | def render_condition(data, query):
  function render_condition_fullname (line 114) | def render_condition_fullname(data, query):
  function render_condition_plain (line 135) | def render_condition_plain(data, query):
  function render_condition_int (line 143) | def render_condition_int(data, query):
  function render_humidity (line 148) | def render_humidity(data, query):
  function render_precipitation (line 159) | def render_precipitation(data, query):
  function render_precipitation_chance (line 170) | def render_precipitation_chance(data, query):
  function render_pressure (line 181) | def render_pressure(data, query):
  function render_dewpoint (line 192) | def render_dewpoint(data, query):
  function render_uv_index (line 219) | def render_uv_index(data, query):
  function render_wind (line 228) | def render_wind(data, query):
  function render_location (line 268) | def render_location(data, query):
  function render_moonphase (line 276) | def render_moonphase(_, query):
  function render_moonday (line 285) | def render_moonday(_, query):
  function get_geodata (line 298) | def get_geodata(location):
  function render_dawn (line 306) | def render_dawn(data, query, local_time_of):
  function render_dusk (line 312) | def render_dusk(data, query, local_time_of):
  function render_sunrise (line 318) | def render_sunrise(data, query, local_time_of):
  function render_sunset (line 324) | def render_sunset(data, query, local_time_of):
  function render_zenith (line 330) | def render_zenith(data, query, local_time_of):
  function render_local_time (line 336) | def render_local_time(data, query, local_time_of):
  function render_local_timezone (line 342) | def render_local_timezone(data, query, local_time_of):
  function render_line (line 380) | def render_line(line, data, query):
  function render_json (line 441) | def render_json(data):
  function format_weather_data (line 457) | def format_weather_data(query, parsed_query, data):
  function wttr_line (line 491) | def wttr_line(query, parsed_query):
  function main (line 504) | def main():

FILE: lib/view/moon.py
  function get_moon (line 14) | def get_moon(parsed_query):

FILE: lib/view/prometheus.py
  function _render_current (line 11) | def _render_current(data, for_day="current", already_seen=[]):
  function _convert_time_to_minutes (line 54) | def _convert_time_to_minutes(time_str):
  function render_prometheus (line 67) | def render_prometheus(data):

FILE: lib/view/v2.py
  function get_data (line 54) | def get_data(config):
  function interpolate_data (line 70) | def interpolate_data(input_data, max_width):
  function jq_query (line 84) | def jq_query(query, data_parsed):
  function colorize (line 96) | def colorize(string, color_code, html_output=False):
  function draw_spark (line 107) | def draw_spark(data, height, width, color_data):
  function draw_diagram (line 175) | def draw_diagram(data, height, width):
  function draw_date (line 193) | def draw_date(config, geo_data):
  function draw_time (line 221) | def draw_time(geo_data):
  function draw_astronomical (line 257) | def draw_astronomical(city_name, geo_data, config):
  function draw_emoji (line 338) | def draw_emoji(data, config):
  function draw_wind (line 360) | def draw_wind(data, color_data, config):
  function add_frame (line 418) | def add_frame(output, width, config):
  function generate_panel (line 449) | def generate_panel(data_parsed, geo_data, config):
  function textual_information (line 510) | def textual_information(data_parsed, geo_data, config, html_output=False):
  function get_geodata (line 640) | def get_geodata(location):
  function main (line 650) | def main(query, parsed_query, data):

FILE: lib/view/wttr.py
  function get_wetter (line 27) | def get_wetter(parsed_query):
  function _wego_wrapper (line 91) | def _wego_wrapper(location, parsed_query):
  function _wego_postprocessing (line 127) | def _wego_postprocessing(location, parsed_query, stdout):
  function _htmlize (line 180) | def _htmlize(ansi_output, title, parsed_query):
  function _get_opengraph (line 207) | def _get_opengraph(parsed_query):

FILE: lib/weather_data.py
  function get_weather_data (line 10) | def get_weather_data(location, lang):

FILE: lib/wttr_srv.py
  function show_text_file (line 58) | def show_text_file(name, lang):
  function _client_ip_address (line 79) | def _client_ip_address(request):
  function _parse_language_header (line 96) | def _parse_language_header(header):
  function get_answer_language_and_view (line 143) | def get_answer_language_and_view(request):
  function get_output_format (line 170) | def get_output_format(query, parsed_query):
  function _cyclic_location_selection (line 193) | def _cyclic_location_selection(locations, period):
  function _response (line 211) | def _response(parsed_query, query, fast_mode=False):
  function parse_request (line 278) | def parse_request(location, request, query, fast_mode=False):
  function wttr (line 369) | def wttr(location, request):

FILE: srv.go
  constant logLineStart (line 40) | logLineStart = "LOG_LINE_START "
  function suppressMessages (line 42) | func suppressMessages() []string {
  function copyHeader (line 51) | func copyHeader(dst, src http.Header) {
  function serveHTTP (line 59) | func serveHTTP(mux *http.ServeMux, port int, logFile io.Writer, errs cha...
  function serveHTTPS (line 71) | func serveHTTPS(mux *http.ServeMux, port int, certFile, keyFile string, ...
  function serve (line 93) | func serve(conf *config.Config) error {
  function mainHandler (line 152) | func mainHandler(
  function main (line 183) | func main() {
  function convertGeoIPCache (line 241) | func convertGeoIPCache(conf *config.Config) error {
  function convertGeoLocationCache (line 250) | func convertGeoLocationCache(conf *config.Config) error {
  function setLogLevel (line 259) | func setLogLevel(logLevel string) error {
Condensed preview — 222 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,156K chars).
[
  {
    "path": ".flake8",
    "chars": 28,
    "preview": "[flake8]\nignore = E402,E501\n"
  },
  {
    "path": ".github/workflows/makefile.yml",
    "chars": 1635,
    "preview": "name: Makefile CI\n\non:\n  push:\n    branches: [ \"master\" ]\n  pull_request:\n    branches: [ \"master\" ]\n\njobs:\n  build:\n\n  "
  },
  {
    "path": ".gitignore",
    "chars": 67,
    "preview": "ve/\nshare/static/fonts/\n*.pyc\ndata/\nlog/\n.idea/\n*.swp\n*.mmdb\n*.dat\n"
  },
  {
    "path": ".golangci.yaml",
    "chars": 412,
    "preview": "run:\n  skip-dirs:\n    - pkg/curlator\nlinters:\n  enable-all: true\n  disable:\n    - wsl\n    - wrapcheck\n    - varnamelen\n "
  },
  {
    "path": ".travis.yml",
    "chars": 658,
    "preview": "group: travis_latest\nlanguage: python\ncache: pip\npython:\n    - 3.7\ninstall:\n    - pip install flake8 -r requirements.txt"
  },
  {
    "path": "Dockerfile",
    "chars": 1486,
    "preview": "# Build stage\nFROM golang:1-alpine as builder\n\nWORKDIR /app\n\nCOPY ./share/we-lang/ /app\n\nRUN apk add --no-cache git\n\nRUN"
  },
  {
    "path": "LICENSE",
    "chars": 11358,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "Makefile",
    "chars": 213,
    "preview": "srv: srv.go internal/*/*.go internal/*/*/*.go\n\t#go build -o srv -ldflags '-w -linkmode external -extldflags \"-static\"' ."
  },
  {
    "path": "README.md",
    "chars": 25376,
    "preview": "\n*wttr.in — the right way to ~check~ `curl` the weather!*\n\nwttr.in is a console-oriented weather forecast service that s"
  },
  {
    "path": "bin/geo-proxy.py",
    "chars": 3271,
    "preview": "import gevent\nfrom gevent.pywsgi import WSGIServer\nfrom gevent.queue import Queue\nfrom gevent.monkey import patch_all\nfr"
  },
  {
    "path": "bin/proxy.py",
    "chars": 12178,
    "preview": "# vim: fileencoding=utf-8\n\n\"\"\"\n\nThe proxy server acts as a backend for the wttr.in service.\n\nIt caches the answers and h"
  },
  {
    "path": "bin/srv.py",
    "chars": 1697,
    "preview": "#!/usr/bin/env python\n# vim: set encoding=utf-8\n\nfrom gevent.pywsgi import WSGIServer\nfrom gevent.monkey import patch_al"
  },
  {
    "path": "config/services/services.yaml",
    "chars": 989,
    "preview": "services:\n  - name: \"main server\"\n    command: \"while true; do sudo /wttr.in/bin/srv big-cache.yaml ; sleep 5; done\"\n   "
  },
  {
    "path": "doc/integrations.md",
    "chars": 16759,
    "preview": "## Integrations\n\nThanks to the ease of integrating *wttr.in* into any program, there are a\nplethora of popular integrati"
  },
  {
    "path": "doc/terminal-images.md",
    "chars": 2446,
    "preview": "\n## Map view (v3)\n\nIn the experimental map view, that is available under the view code `v3`,\nweather information about a"
  },
  {
    "path": "go.mod",
    "chars": 1085,
    "preview": "module github.com/chubin/wttr.in\n\ngo 1.16\n\nrequire (\n\tgithub.com/alecthomas/kong v1.7.0\n\tgithub.com/denisenkom/go-mssqld"
  },
  {
    "path": "go.sum",
    "chars": 14200,
    "preview": "github.com/alecthomas/assert/v2 v2.1.0/go.mod h1:b/+1DI2Q6NckYi+3mXyH3wFb8qG37K/DuK80n7WefXA=\ngithub.com/alecthomas/asse"
  },
  {
    "path": "internal/config/config.go",
    "chars": 4618,
    "preview": "package config\n\nimport (\n\t\"log\"\n\t\"os\"\n\n\t\"gopkg.in/yaml.v3\"\n\n\t\"github.com/chubin/wttr.in/internal/types\"\n\t\"github.com/chu"
  },
  {
    "path": "internal/fmt/png/colors.go",
    "chars": 5854,
    "preview": "package main\n\n// Source: https://www.ditig.com/downloads/256-colors.json\n\nvar ansiColorsDB = [][3]float64{\n\t{\n\t\t0, 0, 0,"
  },
  {
    "path": "internal/fmt/png/go.mod",
    "chars": 276,
    "preview": "module example.com/m/v2\n\ngo 1.20\n\nrequire (\n\tgithub.com/chubin/vt10x v0.0.0-20231112153020-ef4f56837bf1 // indirect\n\tgit"
  },
  {
    "path": "internal/fmt/png/go.sum",
    "chars": 768,
    "preview": "github.com/chubin/vt10x v0.0.0-20231112153020-ef4f56837bf1 h1:CHg5BTAJZmCjBaAAQrD92s248JHH3JTsLlaC6QBJo/Y=\ngithub.com/ch"
  },
  {
    "path": "internal/fmt/png/png.go",
    "chars": 6176,
    "preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/chubin/vt10x\"\n\t\"github.com/fogleman/gg\"\n)\n\nfunc Stri"
  },
  {
    "path": "internal/geo/ip/convert.go",
    "chars": 1590,
    "preview": "package ip\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"path/filepath\"\n\n\t\"github.com/samonzeweb/godb\"\n\t\"github.com/samonzeweb/godb/adapters"
  },
  {
    "path": "internal/geo/ip/ip.go",
    "chars": 5520,
    "preview": "package ip\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"path\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/samonzeweb/go"
  },
  {
    "path": "internal/geo/ip/ip_test.go",
    "chars": 1733,
    "preview": "package ip_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t. \"github.com/chubin/wttr.in/internal/geo"
  },
  {
    "path": "internal/geo/location/cache.go",
    "chars": 5357,
    "preview": "package location\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/samonzewe"
  },
  {
    "path": "internal/geo/location/convert.go",
    "chars": 2648,
    "preview": "package location\n\nimport (\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/samon"
  },
  {
    "path": "internal/geo/location/location.go",
    "chars": 524,
    "preview": "package location\n\nimport (\n\t\"encoding/json\"\n\t\"log\"\n)\n\ntype Location struct {\n\tName     string `db:\"name,key\" json:\"name\""
  },
  {
    "path": "internal/geo/location/nominatim.go",
    "chars": 1371,
    "preview": "package location\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\n\t\"github.com/chubin/wttr.in/internal/types\""
  },
  {
    "path": "internal/geo/location/nominatim_locationiq.go",
    "chars": 801,
    "preview": "package location\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\n\t\"github.com/chubin/wttr.in/internal/types\"\n)\n\ntype locationIQ []struct {\n"
  },
  {
    "path": "internal/geo/location/nominatim_opencage.go",
    "chars": 884,
    "preview": "package location\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\n\t\"github.com/chubin/wttr.in/internal/types\"\n)\n\ntype locationOpenCage struc"
  },
  {
    "path": "internal/geo/location/response.go",
    "chars": 1008,
    "preview": "package location\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/chubin/wttr.in/internal/r"
  },
  {
    "path": "internal/geo/location/search.go",
    "chars": 934,
    "preview": "package location\n\nimport \"github.com/chubin/wttr.in/internal/config\"\n\ntype Provider interface {\n\tQuery(location string) "
  },
  {
    "path": "internal/logging/logging.go",
    "chars": 2393,
    "preview": "package logging\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/chubin/wttr.in/internal/util\"\n)\n\n// Log"
  },
  {
    "path": "internal/logging/suppress.go",
    "chars": 1643,
    "preview": "package logging\n\nimport (\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n)\n\n// LogSuppressor provides io.Writer interface for logging\n// with "
  },
  {
    "path": "internal/options/options.go",
    "chars": 3193,
    "preview": "package options\n\nimport (\n\t\"fmt\"\n\t\"io/ioutil\"\n\n\t\"gopkg.in/yaml.v3\"\n)\n\n// WttrInOptions represents the configuration for "
  },
  {
    "path": "internal/options/parse.go",
    "chars": 8970,
    "preview": "package options\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n)\n\n// Errors for query parsing and"
  },
  {
    "path": "internal/options/processlog.go",
    "chars": 2984,
    "preview": "package options\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n)\n\n// ProcessLogFile reads a wttr.in log file, parses queries"
  },
  {
    "path": "internal/processor/j1.go",
    "chars": 2194,
    "preview": "package processor\n\nimport (\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n)\n\nfunc getAny(req *http.Reque"
  },
  {
    "path": "internal/processor/peak.go",
    "chars": 2107,
    "preview": "package processor\n\nimport (\n\t\"log\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/robfig/cron\"\n)\n\nfunc (rp *RequestProcessor)"
  },
  {
    "path": "internal/processor/processor.go",
    "chars": 9403,
    "preview": "package processor\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"math/rand\"\n\t\"net\"\n\t\"net/http\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\tlru \"g"
  },
  {
    "path": "internal/routing/routing.go",
    "chars": 1408,
    "preview": "package routing\n\nimport (\n\t\"net/http\"\n\t\"time\"\n)\n\n// CadreFormat specifies how the shot data is formatted.\ntype CadreForm"
  },
  {
    "path": "internal/stats/stats.go",
    "chars": 1859,
    "preview": "package stats\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/chubin/wttr.in/internal/routing\"\n)\n\n//"
  },
  {
    "path": "internal/types/errors.go",
    "chars": 417,
    "preview": "package types\n\nimport \"errors\"\n\nvar (\n\tErrNotFound          = errors.New(\"cache entry not found\")\n\tErrInvalidCacheEntry "
  },
  {
    "path": "internal/types/types.go",
    "chars": 97,
    "preview": "package types\n\ntype CacheType string\n\nconst (\n\tCacheTypeDB    = \"db\"\n\tCacheTypeFiles = \"files\"\n)\n"
  },
  {
    "path": "internal/util/files.go",
    "chars": 375,
    "preview": "package util\n\nimport \"os\"\n\n// RemoveFileIfExists removes filename if exists, or does nothing if the file\n// is not there"
  },
  {
    "path": "internal/util/http.go",
    "chars": 537,
    "preview": "package util\n\nimport (\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n)\n\n// ReadUserIP returns IP address of the client from http.Request,\n//"
  },
  {
    "path": "internal/util/yaml.go",
    "chars": 303,
    "preview": "package util\n\nimport (\n\t\"bytes\"\n\n\t\"gopkg.in/yaml.v3\"\n)\n\n// YamlUnmarshalStrict unmarshals YAML data with an error when u"
  },
  {
    "path": "internal/view/v1/api.go",
    "chars": 4272,
    "preview": "//nolint:forbidigo,funlen,nestif,goerr113,gocognit,cyclop\npackage v1\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n"
  },
  {
    "path": "internal/view/v1/cmd.go",
    "chars": 4341,
    "preview": "// This code represents wttr.in view v1.\n// It is based on wego (github.com/schachmat/wego) from which it diverged back "
  },
  {
    "path": "internal/view/v1/format.go",
    "chars": 8979,
    "preview": "//nolint:funlen,nestif,cyclop,gocognit,gocyclo\npackage v1\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"unicode/utf8\"\n\n\t\"github.com/matt"
  },
  {
    "path": "internal/view/v1/icons.go",
    "chars": 6863,
    "preview": "package v1\n\n//nolint:funlen\nfunc getIcon(name string) []string {\n\ticon := map[string][]string{\n\t\t\"iconUnknown\": {\n\t\t\t\"  "
  },
  {
    "path": "internal/view/v1/locale.go",
    "chars": 9271,
    "preview": "package v1\n\n//nolint:funlen\nfunc locale() map[string]string {\n\treturn map[string]string{\n\t\t\"af\":     \"af_ZA\",\n\t\t\"am\":   "
  },
  {
    "path": "internal/view/v1/view1.go",
    "chars": 3481,
    "preview": "package v1\n\nimport (\n\t\"math\"\n\t\"time\"\n\n\t\"github.com/klauspost/lctime\"\n)\n\nfunc slotTimes() []int {\n\treturn []int{9 * 60, 1"
  },
  {
    "path": "lib/airports.py",
    "chars": 541,
    "preview": "import csv\n\nAIRPORTS_DAT_FILE = \"/home/igor/wttrin-geo/share/airports.dat\"\n\n\ndef load_aiports_index():\n    file_ = open("
  },
  {
    "path": "lib/buttons.py",
    "chars": 1926,
    "preview": "TWITTER_BUTTON = \"\"\"\n<a href=\"https://twitter.com/igor_chubin?ref_src=twsrc%5Etfw\" class=\"twitter-follow-button\" data-sh"
  },
  {
    "path": "lib/cache.py",
    "chars": 4191,
    "preview": "\"\"\"\nLRU-Cache implementation for formatted (`format=`) answers\n\"\"\"\n\nimport datetime\nimport re\nimport time\nimport os\nimpo"
  },
  {
    "path": "lib/constants.py",
    "chars": 10851,
    "preview": "# vim: fileencoding=utf-8\n\nWWO_CODE = {\n    \"113\": \"Sunny\",\n    \"116\": \"PartlyCloudy\",\n    \"119\": \"Cloudy\",\n    \"122\": \""
  },
  {
    "path": "lib/datasource/README.md",
    "chars": 946,
    "preview": "\nCurrently wttr.in uses just one data source, but more data sources must be added.\nHaving more data sources will increas"
  },
  {
    "path": "lib/duplicate_translations.py",
    "chars": 3047,
    "preview": "import os\n\n\ndef remove_colon_and_strip_from_str(line):\n    \"\"\"\n    Removes the colon from the line and strips the line.\n"
  },
  {
    "path": "lib/extract_emoji.py",
    "chars": 1345,
    "preview": "#!/usr/bin/env python\n# vim: fileencoding=utf-8\n\n\"\"\"\n\nAt the moment, Pillow library does not support colorful emojis,\nth"
  },
  {
    "path": "lib/fields.py",
    "chars": 2872,
    "preview": "\"\"\"\nHuman readable description of the available data fields\ndescribing current weather, weather forecast, and astronomic"
  },
  {
    "path": "lib/fmt/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "lib/fmt/png.py",
    "chars": 8123,
    "preview": "#!/usr/bin/python\n# vim: encoding=utf-8\n# pylint: disable=wrong-import-position,wrong-import-order,redefined-builtin\n\n\"\""
  },
  {
    "path": "lib/fmt/unicodedata2.py",
    "chars": 58217,
    "preview": "# downloaded from https://gist.github.com/2204527\n# described/recommended here:\n#\n#   http://stackoverflow.com/questions"
  },
  {
    "path": "lib/globals.py",
    "chars": 4588,
    "preview": "\"\"\"\nglobal configuration of the project\n\nExternal environment variables:\n\n    WTTR_MYDIR\n    WTTR_GEOLITE\n    WTTR_WEGO\n"
  },
  {
    "path": "lib/limits.py",
    "chars": 3211,
    "preview": "\"\"\"\nConnection limitation.\n\nNumber of connections from one IP is limited.\nWe have nothing against scripting and automate"
  },
  {
    "path": "lib/location.py",
    "chars": 16854,
    "preview": "\"\"\"\nAll location related functions and converters.\n\nThe main entry point is `location_processing` which gets `location` "
  },
  {
    "path": "lib/metno.py",
    "chars": 17444,
    "preview": "#!/bin/env python\n# vim: fileencoding=utf-8\nfrom datetime import datetime, timedelta\nimport json\nimport logging\nimport o"
  },
  {
    "path": "lib/parse_query.py",
    "chars": 4395,
    "preview": "import re\nimport json\nimport zlib\nimport base64\n\n\ndef serialize(parsed_query):\n    return base64.b64encode(\n        zlib"
  },
  {
    "path": "lib/proxy_log.py",
    "chars": 1248,
    "preview": "\"\"\"\nLogger of proxy queries\n\n\"\"\"\n\n# pylint: disable=consider-using-with,too-few-public-methods\n\nimport datetime\n\n\nclass "
  },
  {
    "path": "lib/translations.py",
    "chars": 48210,
    "preview": "# vim: fileencoding=utf-8\n\n\"\"\"\nTranslation of almost everything.\n\"\"\"\n\nFULL_TRANSLATION = [\n    \"am\",\n    \"ar\",\n    \"af\","
  },
  {
    "path": "lib/translations_v2.py",
    "chars": 9784,
    "preview": "# vim: fileencoding=utf-8\n\n\"\"\"\nTranslation of v2\n\"\"\"\n\n# pylint: disable=line-too-long,bad-whitespace\nV2_TRANSLATION = {\n"
  },
  {
    "path": "lib/view/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "lib/view/line.py",
    "chars": 12300,
    "preview": "# vim: fileencoding=utf-8\n\n\"\"\"\nOne-line output mode.\n\nInitial implementation of one-line output mode.\n\n[ ] forecast\n[ ] "
  },
  {
    "path": "lib/view/moon.py",
    "chars": 1535,
    "preview": "import sys\n\nimport os\nimport dateutil.parser\n\nfrom gevent.subprocess import Popen, PIPE\n\nsys.path.insert(0, \"..\")\nimport"
  },
  {
    "path": "lib/view/prometheus.py",
    "chars": 2048,
    "preview": "\"\"\"\nRendering weather data in the Prometheus format.\n\n\"\"\"\n\nfrom datetime import datetime\n\nfrom fields import DESCRIPTION"
  },
  {
    "path": "lib/view/v2.py",
    "chars": 19230,
    "preview": "# vim: fileencoding=utf-8\n# vim: foldmethod=marker foldenable:\n\n\"\"\"\n[X] emoji\n[ ] wego icon\n[ ] v2.wttr.in\n[X] astronomi"
  },
  {
    "path": "lib/view/wttr.py",
    "chars": 6438,
    "preview": "# vim: set encoding=utf-8\n# pylint: disable=wrong-import-position\n\n\"\"\"\nMain view (wttr.in) implementation.\nThe module is"
  },
  {
    "path": "lib/weather_data.py",
    "chars": 541,
    "preview": "\"\"\"\nWeather data source\n\"\"\"\n\nimport json\nimport requests\nfrom globals import WWO_KEY\n\n\ndef get_weather_data(location, la"
  },
  {
    "path": "lib/wttr_srv.py",
    "chars": 15701,
    "preview": "#!/usr/bin/env python\n# vim: set encoding=utf-8\n\n\"\"\"\nMain wttr.in rendering function implementation\n\"\"\"\n\nimport logging\n"
  },
  {
    "path": "requirements.txt",
    "chars": 347,
    "preview": "flask\ngeoip2\ngeopy\nrequests\ngevent\ndnspython\npylint\ncyrtranslit\nastral>=2.0,<=2.2\ntimezonefinder\npytz\npyte\npython-dateut"
  },
  {
    "path": "share/aliases",
    "chars": 1526,
    "preview": "MDW         : MDW Chicago\nmdw         : MDW Chicago\nMsk         : Moscow\nMoskva      : Moscow\nMoskau      : Moscow\nKyiv "
  },
  {
    "path": "share/ansi2html.sh",
    "chars": 16185,
    "preview": "#!/bin/sh\n\n# Convert ANSI (terminal) colours and attributes to HTML\n\n# Licence: LGPLv2\n# Author:\n#    http://www.pixelbe"
  },
  {
    "path": "share/bash-function.txt",
    "chars": 891,
    "preview": "#! /usr/bin/env bash\n# If you source this file, it will set WTTR_PARAMS as well as show weather.\n\n# WTTR_PARAMS is space"
  },
  {
    "path": "share/blacklist",
    "chars": 106,
    "preview": "NOT_FOUND\napple-touch-icon.png\napple-touch-icon-precomposed.png\napple-touch-icon-152x152-precomposed.png\n\n"
  },
  {
    "path": "share/docker/supervisord.conf",
    "chars": 622,
    "preview": "[supervisord]\nnodaemon=true\nlogfile=/var/log/supervisor/supervisord.log\npidfile=/var/run/supervisord.pid\n\n[program:srv]\n"
  },
  {
    "path": "share/help.txt",
    "chars": 2568,
    "preview": "Usage:\n\n    $ curl wttr.in          # current location\n    $ curl wttr.in/muc      # weather in the Munich airport\n\nSupp"
  },
  {
    "path": "share/iterm2.txt",
    "chars": 61696,
    "preview": "\u001b]1337;File=name=TnVyZW1iZXJnLnBuZw==;size=46226;inline=1:iVBORw0KGgoAAAANSUhEUgAAA2sAAAIUCAIAAAAL1CNnAAC0WUlEQVR4nOzdeV"
  },
  {
    "path": "share/list-of-iata-codes.txt",
    "chars": 22700,
    "preview": "GKA\nMAG\nHGU\nLAE\nPOM\nWWK\nUAK\nGOH\nSFJ\nTHU\nAEY\nEGS\nHFN\nHZK\nIFJ\nKEF\nPFJ\nRKV\nSIJ\nVEY\nYAM\nYAV\nYAW\nYAY\nYAZ\nYBB\nYBC\nYBG\nYBK\nYBL\n"
  },
  {
    "path": "share/salt/README.md",
    "chars": 705,
    "preview": "# Opinionated example of deployment via Salt Stack\n\n## Assumptions:\n  * user & group srv:srv exist, this is used as a ge"
  },
  {
    "path": "share/salt/init.sls",
    "chars": 2554,
    "preview": "wttr:\n  service.running:\n    - enable: True\n    - watch:\n      - file: /srv/ephemeral/start.sh\n      - git: wttr-repo\n  "
  },
  {
    "path": "share/salt/pillar.sls",
    "chars": 75,
    "preview": "wttr:\n  apikey: insert-api-key-here-and-make-this-pillar-available-to-salt\n"
  },
  {
    "path": "share/salt/start.sh",
    "chars": 311,
    "preview": "#!/bin/sh\nexport WEGORC=\"/srv/ephemeral/.wegorc\"\nexport GOPATH=\"/srv/ephemeral\"\n\nexport WTTR_MYDIR=\"/srv/ephemeral/wttr."
  },
  {
    "path": "share/salt/wegorc",
    "chars": 1676,
    "preview": "# wego configuration\n# \n# This config has https://github.com/schachmat/ingo syntax.\n# Empty lines or lines starting with"
  },
  {
    "path": "share/salt/wttr.service",
    "chars": 163,
    "preview": "[Unit]\nDescription=Wttr weather service\n\n[Service]\nExecStart=/usr/bin/authbind --deep /srv/ephemeral/start.sh\nRestart=al"
  },
  {
    "path": "share/screenrc",
    "chars": 154,
    "preview": "screen -t srv.py bash -c \"cd ~/wttr.in; ve/bin/python bin/srv.py; bash -i\"\nscreen -t proxy.py bash -c \"cd ~/wttr.in; ve/"
  },
  {
    "path": "share/scripts/build-welang.sh",
    "chars": 979,
    "preview": "#!/usr/bin/env bash\n\n# The scipr is used to build a standalone we-lang binary from\n# the wttr.in Go source code. The scr"
  },
  {
    "path": "share/scripts/clean-cache.sh",
    "chars": 569,
    "preview": "#!/bin/bash\n\nLOGFILE=/tmp/clean-cache.log\n\n_log() {\n  echo \"$(date +\"[%Y-%m-%d %H:%M:%S]\") $*\" >> \"$LOGFILE\"\n}\n\n_log_pip"
  },
  {
    "path": "share/scripts/log-space.sh",
    "chars": 242,
    "preview": "#!/usr/bin/env bash\n\nLOG_DIR=\"/wttr.in/log\"\nLOG_FILE=\"$LOG_DIR/diskspace.log\"\n\nDISK=/wttr.in\n\nlog() {\n  mkdir -p \"$LOG_D"
  },
  {
    "path": "share/scripts/start-screen.sh",
    "chars": 134,
    "preview": "#!/bin/bash\n\nSESSION_NAME=wttr.in\nSCREENRC_PATH=$(dirname $(dirname \"$0\"))/screenrc\n\nscreen -dmS \"$SESSION_NAME\" -c \"$SC"
  },
  {
    "path": "share/static/malformed-response.html",
    "chars": 2057,
    "preview": "<html>\n<title>wttr.in</title>\n<head>\n<script async src=\"https://platform.twitter.com/widgets.js\" charset=\"utf-8\"></scrip"
  },
  {
    "path": "share/static/style.css",
    "chars": 711,
    "preview": "body {\n    background: black;\n    color: #bbbbbb;\n}\n\n/* Switch to light mode if the user prefers it */\n/*\n@media (prefer"
  },
  {
    "path": "share/systemd/README.md",
    "chars": 1259,
    "preview": "To add **wttr.in** to systemd as a service, do the following steps.\n\n\n1. **Create a systemd service file**: You’ll need "
  },
  {
    "path": "share/systemd/wttrin.service",
    "chars": 229,
    "preview": "[Unit]\nDescription=wttr.in services\n\n[Service]\nType=oneshot\nRemainAfterExit=yes\nExecStart=/home/igor/src/wttr.in/share/s"
  },
  {
    "path": "share/systemd/wttrin.sh",
    "chars": 1192,
    "preview": "#!/usr/bin/env bash\n\nSESSION_NAME=\"\"\nSRC_DIR=/home/igor/src/wttr.in\nSERVICES_FILE=config/services/services.yaml\n\nstart_s"
  },
  {
    "path": "share/templates/index.html",
    "chars": 296,
    "preview": "<html>\n<head>\n<meta name=\"color-scheme\" content=\"dark light\">\n<link rel=\"stylesheet\" type=\"text/css\" href=\"https://adobe"
  },
  {
    "path": "share/test-thunder.txt",
    "chars": 6230,
    "preview": "Prévisions météo pour: Test-Milon, France\n\n \u001b[38;5;226m _`/\"\"\u001b[38;5;250m.-.    \u001b[0m Averse de pluie légère\n \u001b[38;5;226m "
  },
  {
    "path": "share/translation.txt",
    "chars": 4885,
    "preview": "wttr.in is translated in NUMBER_OF_LANGUAGES languages:\n\n    SUPPORTED_LANGUAGES\n\nTranslated/improved/corrected by:\n\n   "
  },
  {
    "path": "share/translations/af-help.txt",
    "chars": 2342,
    "preview": "Gebruik:\n\n    $ curl wttr.in          # huidige ligging\n    $ cur wttr.in/muc       # weerberig in München se lughawe\n\nO"
  },
  {
    "path": "share/translations/af.txt",
    "chars": 3045,
    "preview": "113: Helder                                 : Clear\n113: Sonnig                                 : Sunny\n116: Gedeeltelik"
  },
  {
    "path": "share/translations/am-help.txt",
    "chars": 2481,
    "preview": "አጠቃቀም:\n\n    $ curl wttr.in          # አሁን ያሉበት አካባቢ\n    $ curl wttr.in/add      # የአየር ሁነታ በቦሌ አዲስ አበባ አለምአቀፍ ኤርፖርት \n\nተቀ"
  },
  {
    "path": "share/translations/am.txt",
    "chars": 2730,
    "preview": "113: ጥርት ያለ                           : Clear\n113: ፀሐያማ                             : Sunny\n116: ከፊል ደመናማ               "
  },
  {
    "path": "share/translations/ar-help.txt",
    "chars": 2410,
    "preview": "الإستخدام:\n\n    $ curl wttr.in          # الموقع الحالي\n    $ curl wttr.in/muc      # الطقس في مطار ميونخ\n\nأنواع الأماكن"
  },
  {
    "path": "share/translations/ar.txt",
    "chars": 3672,
    "preview": "113: صاف                                                   : Clear \n113: مشمس                                           "
  },
  {
    "path": "share/translations/az.txt",
    "chars": 4729,
    "preview": "113: Aydınlı                            : Clear                             : Ясно                         \n113: Günəşli"
  },
  {
    "path": "share/translations/be-help.txt",
    "chars": 2472,
    "preview": "Выкарыстанне:\n\n    $ curl wttr.in          # надвор'е ў вашым месцы\n    $ curl wttr.in/msq      # надвор'е ў аэрапорце М"
  },
  {
    "path": "share/translations/be.txt",
    "chars": 4183,
    "preview": "113: Ясна                                       : Clear                               : \n113: Сонечна                   "
  },
  {
    "path": "share/translations/bg-help.txt",
    "chars": 2461,
    "preview": "Употреба:\n\n    $ curl wttr.in          # текущо местоположение\n    $ curl wttr.in/sof      # времето на Софийското летищ"
  },
  {
    "path": "share/translations/bn-help.txt",
    "chars": 2412,
    "preview": "ব্যবহার:\n\n    $ curl wttr.in          # এখন যেখানে আছ\n    $ curl wttr.in/cdg      # প্যারিস - চার্লস ডি গল বিমানবন্দরে আ"
  },
  {
    "path": "share/translations/bn.txt",
    "chars": 5664,
    "preview": ": বজ্রঝড়ের সঙ্গে ভারী বৃষ্টি ও শিলাবৃষ্টি                            : Heavy rain and hail with thunderstorm\n: বজ্রঝড়ে"
  },
  {
    "path": "share/translations/bs.txt",
    "chars": 3139,
    "preview": "113 : Vedro                                   : Clear\n113 : Sunčano                                 : Sunny\n116 : Djelom"
  },
  {
    "path": "share/translations/ca-help.txt",
    "chars": 2555,
    "preview": "Instruccions:\n\n    $ curl wttr.in          # el clima de la ubicació actual\n    $ curl wttr.in/muc      # el clima de l'"
  },
  {
    "path": "share/translations/ca.txt",
    "chars": 3045,
    "preview": "113 : Clar                                  : Clear\n113 : Assolellat                            : Sunny\n116 : Parcialmen"
  },
  {
    "path": "share/translations/cs-help.txt",
    "chars": 2532,
    "preview": "Použití:\n\n    $ curl wttr.in          # lokální informace\n    $ curl wttr.in/muc      # počasí v letšiti v Mnichově\n\nPod"
  },
  {
    "path": "share/translations/cs.txt",
    "chars": 3941,
    "preview": ": Unášený sníh                                      : Low drifting snow\n: Částečná mlha                                 "
  },
  {
    "path": "share/translations/cy.txt",
    "chars": 2934,
    "preview": "113: Glir                               : Clear\n113: Heulog                             : Sunny\n116: Rhannol gymylog    "
  },
  {
    "path": "share/translations/da-help.txt",
    "chars": 2248,
    "preview": "Brugsanvisning:\n\n    $ curl wttr.in          # Nuværende lokation\n    $ curl wttr.in/aarhus   # Vejret i Aarhus\n\nUnderst"
  },
  {
    "path": "share/translations/da.txt",
    "chars": 3045,
    "preview": "113: Skyfrit                                : Clear\n113: Sol                                    : Sunny\n116: Delvist sky"
  },
  {
    "path": "share/translations/de-help.txt",
    "chars": 2400,
    "preview": "Benutzung:\n\n    $ curl wttr.in          # aktuelle Position\n    $ curl wttr.in/muc      # Wetter, Flughafen München\n\nUnt"
  },
  {
    "path": "share/translations/de.txt",
    "chars": 17969,
    "preview": ": Dunst in der Nähe                                         : Haze in vicinity\n: Starker Regen- und Schneeschauer       "
  },
  {
    "path": "share/translations/dk-help.txt",
    "chars": 2233,
    "preview": "Brugsanvisning:\n\n    $ curl wttr.in          # nuværende lokation\n    $ curl wttr.in/osl      # vejret på Gardermoen fly"
  },
  {
    "path": "share/translations/el-help.txt",
    "chars": 3028,
    "preview": "Χρήση:\r\n\r\n    $ curl wttr.in          # καιρός τρέχουσας τοποθεσίας (κατά προσέγγιση, βάσει IP)\r\n    $ curl wttr.in/ath "
  },
  {
    "path": "share/translations/el.txt",
    "chars": 2904,
    "preview": "113: Καθαρός                            : Clear\r\n113: Λιακάδα                            : Sunny\r\n116: Αραιή συννεφιά   "
  },
  {
    "path": "share/translations/en.txt",
    "chars": 2857,
    "preview": "113:                                    : Clear\n113:                                    : Sunny\n116:                    "
  },
  {
    "path": "share/translations/eo.txt",
    "chars": 3045,
    "preview": "113: Klara                                  : Clear\n113: Suna                                   : Sunny\n116: Parte nuba "
  },
  {
    "path": "share/translations/es-help.txt",
    "chars": 2843,
    "preview": "Instrucciones:\n\n    $ curl wttr.in          # El clima en su ubicación actual\n    $ curl wttr.in/muc      # El clima en "
  },
  {
    "path": "share/translations/es.txt",
    "chars": 22013,
    "preview": "114: Despejado                                   : Clear                                              :\n113: Soleado    "
  },
  {
    "path": "share/translations/et-help.txt",
    "chars": 2283,
    "preview": "Kasutus:\n\n    $ curl wttr.in          # praegune asukoht\n    $ curl wttr.in/tll      # ilmaprognoos Tallinna Lennujaamas"
  },
  {
    "path": "share/translations/et.txt",
    "chars": 3331,
    "preview": "113: Selge                              : Clear\n113: Päikeseline                        : Sunny\n116: Vahelduv pilvisus  "
  },
  {
    "path": "share/translations/eu-help.txt",
    "chars": 2491,
    "preview": "Argibideak:\n\n    $ curl wttr.in          # eguraldia zure kokapenean\n    $ curl wttr.in/bio      # eguraldia Bilboko air"
  },
  {
    "path": "share/translations/eu.txt",
    "chars": 2113,
    "preview": "114: Oskarbia: Clear\n113: Eguzkitsu: Sunny\n116: Neurri batean hodeitsu: Partly cloudy\n119: Hodeitsu: Cloudy\n122: Iluna: "
  },
  {
    "path": "share/translations/fa-help.txt",
    "chars": 2828,
    "preview": "                                                                          :نحوه استفاده\n\n    $ curl wttr.in          # م"
  },
  {
    "path": "share/translations/fa.txt",
    "chars": 3796,
    "preview": "113: صاف                                                    : Clear\n113: آفتابی                                         "
  },
  {
    "path": "share/translations/fr-help.txt",
    "chars": 2580,
    "preview": "Usage:\n\n    $ curl wttr.in          # emplacement actuel\n    $ curl wttr.in/cdg      # météo à l'aéroport de Paris - Cha"
  },
  {
    "path": "share/translations/fr.txt",
    "chars": 21771,
    "preview": "113: Ensoleillé                                  : Sunny                                              :\n114: Temps clair"
  },
  {
    "path": "share/translations/fy.txt",
    "chars": 2625,
    "preview": "113: Helder                       : Clear                             \n113: Sinnich                      : Sunny\n116: By"
  },
  {
    "path": "share/translations/ga.txt",
    "chars": 2880,
    "preview": "113: Geal                               : Clear\n113: Grianmhar                          : Sunny\n116: Breacscamallach    "
  },
  {
    "path": "share/translations/gl-help.txt",
    "chars": 2541,
    "preview": "Instrucións:\n\n    $ curl wttr.in          # o tempo na sua localización actual\n    $ curl wttr.in/muc      # o tempo no "
  },
  {
    "path": "share/translations/gl.txt",
    "chars": 2857,
    "preview": "113: Despexado                          : Clear\n113: Solleiro                           : Sunny\n116: Parcialmente Nubrad"
  },
  {
    "path": "share/translations/gu-help.txt",
    "chars": 2490,
    "preview": "Usage:\n\n    $ curl wttr.in          # current location\n    $ curl wttr.in/muc      # weather in the Munich airport\n\nSupp"
  },
  {
    "path": "share/translations/gu.txt",
    "chars": 5636,
    "preview": ": Fortes pluies et orages de grêle                 : Heavy rain and hail with thunderstorm\n: Fortes pluies orageuses    "
  },
  {
    "path": "share/translations/he.txt",
    "chars": 3691,
    "preview": "113 :                                     בהיר: Clear\n113 :                                     שמשי: Sunny\n116 :       "
  },
  {
    "path": "share/translations/hi-help.txt",
    "chars": 2597,
    "preview": "उपयोग:\n\n    $ curl wttr.in          # वर्तमान स्थान के मौसम की जानकारी \n    $ curl wttr.in/muc      # म्यूनिख हवाई अड्डे"
  },
  {
    "path": "share/translations/hi.txt",
    "chars": 6389,
    "preview": ": तेज बारिश और गरज के साथ ओलावृष्टि                 \t\t   : Heavy rain and hail with thunderstorm\n: गरज के साथ तेज बारिश "
  },
  {
    "path": "share/translations/hr.txt",
    "chars": 21821,
    "preview": "   :     Kiša                                    : Rain                                               :\n113 : Vedro     "
  },
  {
    "path": "share/translations/hu-help.txt",
    "chars": 2545,
    "preview": "Használat:\n\n    $ curl wttr.in          # jelenlegi tartózkodási hely\n    $ curl wttr.in/muc      # időjárás a müncheni "
  },
  {
    "path": "share/translations/hu.txt",
    "chars": 3336,
    "preview": "113: Derült                              : Clear\n113: Napos                               : Sunny\n116: Közepesen felhős "
  },
  {
    "path": "share/translations/hy.txt",
    "chars": 4728,
    "preview": "113: Պարզ                               : Clear                             : Ясно                         \n113: Արևոտ  "
  },
  {
    "path": "share/translations/ia-help.txt",
    "chars": 2550,
    "preview": "Instructiones:\n\n    $ curl wttr.in          # le tempore a vostre location actual\n    $ curl wttr.in/muc      # le tempo"
  },
  {
    "path": "share/translations/ia.txt",
    "chars": 2856,
    "preview": "113: Clar                               : Clear\n113: Allegre                            : Sunny\n116: Pauc Nubilose      "
  },
  {
    "path": "share/translations/id-help.txt",
    "chars": 2306,
    "preview": "Cara penggunaan:\n\n    $ curl wttr.in          # lokasi saat ini\n    $ curl wttr.in/muc      # cuaca di bandara Munich\n\nD"
  },
  {
    "path": "share/translations/id.txt",
    "chars": 3046,
    "preview": "113: Langit bersih                          : Clear \n113: Cerah                                  : Sunny\n116: Sebagian b"
  },
  {
    "path": "share/translations/is.txt",
    "chars": 3327,
    "preview": "113 : Heiðskýrt                                   : Clear\n113 : Sól                                         : Sunny\n116 "
  },
  {
    "path": "share/translations/it-help.txt",
    "chars": 2332,
    "preview": "Istruzioni:\n\n\t$ curl wttr.in\t\t\t\t# Il tempo nella tua posizione attuale\n\t$ curl wttr.in/muc\t\t\t# Meteo all'aeroporto di Mo"
  },
  {
    "path": "share/translations/it.txt",
    "chars": 7335,
    "preview": ": Pioggia intensa e grandine con temporali                             : Heavy rain and hail with thunderstorm\n: Pioggia"
  },
  {
    "path": "share/translations/ja.txt",
    "chars": 2668,
    "preview": "113: 快晴                               : Clear\n113: 晴れ                               : Sunny\n116: 所により曇り                 "
  },
  {
    "path": "share/translations/kk-help.txt",
    "chars": 2403,
    "preview": "Использование:\n\n    $ curl wttr.in          # текущее местоположение\n    $ curl wttr.in/svo      # погода в аэропорту Ше"
  },
  {
    "path": "share/translations/kk.txt",
    "chars": 4746,
    "preview": "113: Ашық                               : Clear                             : Ясно                         \n113: Шуақ кү"
  },
  {
    "path": "share/translations/lt-help.txt",
    "chars": 2595,
    "preview": "Naudojimas:\n\n    $ curl wttr.in          # dabartinė vietovė\n    $ curl wttr.in/plq      # oras Palangos oro uoste\n\nPala"
  },
  {
    "path": "share/translations/lt.txt",
    "chars": 3995,
    "preview": "113: Giedra                                  : Clear                               :\n113: Saulėta                       "
  },
  {
    "path": "share/translations/lv-help.txt",
    "chars": 2744,
    "preview": "Izmantošana:\n\n    $ curl wttr.in          # pašreizējā atrašanās vieta\n    $ curl wttr.in/rix      # laikapstākļi Rīgas "
  },
  {
    "path": "share/translations/lv.txt",
    "chars": 3475,
    "preview": "   : Tuvumā īslaicīgs lietus                      : Patchy rain nearby\n   : Tuvumā pērkona negaiss                      "
  },
  {
    "path": "share/translations/messages/en.yaml",
    "chars": 1530,
    "preview": "lang:\n    name: English\n    code: \"en\"\n    issue: \"\"\n    translators:\n        - \"Igor Chubin @igor_chubin\"\n    locale: \""
  },
  {
    "path": "share/translations/messages/gu.yaml",
    "chars": 1527,
    "preview": "lang:\n    name: Gujarati\n    code: gu\n    issue: \"774\"\n    translators:\n        - @wylited (on GitHub)\n    locale: gu_IN"
  },
  {
    "path": "share/translations/mg-help.txt",
    "chars": 2786,
    "preview": "Fampiasana azy:\n\n    $ curl wttr.in                # toetr'andro eo amin'ny toerana misy anao\n    $ curl wttr.in/antanan"
  },
  {
    "path": "share/translations/mg.txt",
    "chars": 6561,
    "preview": ": Oram-be manavandra sy oram-baratra                            : Heavy rain and hail with thunderstorm\n: Oram-be sy ora"
  },
  {
    "path": "share/translations/mk.txt",
    "chars": 2429,
    "preview": "113: Ведро: Clear \n113: Сончево: Sunny\n116: Делумно Облачно: Partly cloudy \n119: Облачно: Cloudy \n122: Облачно: Overcast"
  },
  {
    "path": "share/translations/mr-help.txt",
    "chars": 2606,
    "preview": "वापर:\n\n    $ curl wttr.in          # वर्तमान स्थळाचे हवामान\n    $ curl wttr.in/muc      # म्युनिक विमानतळावरील हवामान\n\nउ"
  },
  {
    "path": "share/translations/mr.txt",
    "chars": 4509,
    "preview": "    : मुसळधार पाऊस, गारा व झंझावात      : Heavy rain and hail with thunderstorm\n    : मुसळधार पाऊस व झंझावात          : "
  },
  {
    "path": "share/translations/nb-help.txt",
    "chars": 2188,
    "preview": "Bruk:\n\n    $ curl wttr.in          # nåværende lokasjon\n    $ curl wttr.in/osl      # været på Gardermoen flyplass\n\nStøt"
  },
  {
    "path": "share/translations/nb.txt",
    "chars": 10901,
    "preview": "113: Sol                                    : Sunny\n114: Klart                                  : Clear\n116: Delvis skye"
  },
  {
    "path": "share/translations/nl-help.txt",
    "chars": 2600,
    "preview": "Gebruik:\n\n    $ curl wttr.in              # huidige locatie\n    $ curl wttr.in/muc          # weer op de luchthaven van "
  },
  {
    "path": "share/translations/nl.txt",
    "chars": 5338,
    "preview": ": Zware regen en hagel met onweer                      : Heavy rain and hail with thunderstorm\n: Zware regen met onweer "
  },
  {
    "path": "share/translations/nn.txt",
    "chars": 3045,
    "preview": "113: Klart                                  : Clear\n113: Sol                                    : Sunny\n116: Til dels sk"
  },
  {
    "path": "share/translations/oc-help.txt",
    "chars": 2566,
    "preview": "Usatge:\n\n    $ curl wttr.in          # emplaçament actual\n    $ curl wttr.in/cdg      # metèo a l'aeropòrt de Paris - Ch"
  },
  {
    "path": "share/translations/oc.txt",
    "chars": 3574,
    "preview": ": Pluèja                                           : Rain\n: Plugeta, Raissas                                 : Light Rai"
  },
  {
    "path": "share/translations/pl-help.txt",
    "chars": 2580,
    "preview": "Użycie:\n\n    $ curl wttr.in          # aktualna lokalizacja\n    $ curl wttr.in/waw      # wybrana lokalizacja (WAW - Lot"
  },
  {
    "path": "share/translations/pl.txt",
    "chars": 3891,
    "preview": "113: Bezchmurnie                                              : Clear\n113: Słonecznie                                   "
  },
  {
    "path": "share/translations/pt-br-help.txt",
    "chars": 2906,
    "preview": "Uso:\n\n    $ curl wttr.in          # apresenta o clima na sua localização atual\n    $ curl wttr.in/muc      # apresenta o"
  },
  {
    "path": "share/translations/pt-br.txt",
    "chars": 3626,
    "preview": "113: Limpo                                        :  Clear\n113: Ensolarado                                   :  Sunny\n11"
  },
  {
    "path": "share/translations/pt-help.txt",
    "chars": 2885,
    "preview": "Utilização:\n\n    $ curl wttr.in          # o clima na sua localização atual\n    $ curl wttr.in/muc      # o clima no aer"
  },
  {
    "path": "share/translations/pt.txt",
    "chars": 3722,
    "preview": "113: Limpo                                        :  Clear\n113: Ensolarado                                   :  Sunny\n11"
  },
  {
    "path": "share/translations/ro-help.txt",
    "chars": 2373,
    "preview": "Utilizare:\n\n    $ curl wttr.in          # localizare curentă\n    $ curl wttr.in/otp      # Aeroportul Internațional Henr"
  },
  {
    "path": "share/translations/ro.txt",
    "chars": 3468,
    "preview": "113: Senin                                          :  Clear\n113: Însorit                                        :  Sunn"
  },
  {
    "path": "share/translations/ru-help.txt",
    "chars": 2403,
    "preview": "Использование:\n\n    $ curl wttr.in          # текущее местоположение\n    $ curl wttr.in/svo      # погода в аэропорту Ше"
  },
  {
    "path": "share/translations/ru.txt",
    "chars": 29187,
    "preview": "   : Мгла в окрестностях                                               : Haze in vicinity                               "
  },
  {
    "path": "share/translations/sl.txt",
    "chars": 3232,
    "preview": "113: Jasno                                      : Clear\n113: Sončno                                     : Sunny\n116: Del"
  },
  {
    "path": "share/translations/ta-help.txt",
    "chars": 2600,
    "preview": "பயன்பாடு:\n\n    $ curl wttr.in          # தற்போதைய இடம்\n    $ curl wttr.in/cdg      # பாரிஸ் - சார்லஸ் டி கோல் விமான நில"
  },
  {
    "path": "share/translations/ta.txt",
    "chars": 5751,
    "preview": ": இடியுடன் கூடிய கனமழை மற்றும் ஆலங்கட்டி மழை          : Heavy rain and hail with thunderstorm\n: இடியுடன் கூடிய கனமழை    "
  }
]

// ... and 22 more files (download for full content)

About this extraction

This page contains the full source code of the chubin/wttr.in GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 222 files (1.0 MB), approximately 352.5k tokens, and a symbol index with 378 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!