├── static ├── css │ ├── menu.css │ ├── search.css │ ├── dark_blur_beta.css │ ├── settings-style.css │ ├── gentoo_lavender.css │ ├── light.css │ ├── catppuccin_mocha_blue.css │ ├── catppuccin_mocha_green.css │ ├── dark.css │ ├── darker.css │ ├── classic.css │ ├── github_night.css │ ├── night.css │ ├── dark_default.css │ ├── dark_blur.css │ ├── red.css │ └── ancient.css ├── favicon.ico ├── favicon.png ├── preview1.webp ├── preview2.webp ├── preview3.webp ├── searchicon.png ├── imagesearch.png ├── themepreview │ ├── preview1.webp │ ├── preview10.webp │ ├── preview11.webp │ ├── preview12.webp │ ├── preview13.webp │ ├── preview2.webp │ ├── preview3.webp │ ├── preview4.webp │ ├── preview5.webp │ ├── preview6.webp │ ├── preview7.webp │ ├── preview8.webp │ └── preview9.webp ├── fonts │ ├── inter-v12-latin-300.woff │ ├── inter-v12-latin-300.woff2 │ ├── inter-v12-latin-700.woff │ ├── inter-v12-latin-700.woff2 │ ├── inter-v12-latin-regular.woff │ ├── inter-v12-latin-regular.woff2 │ └── material-icons-round-v108-latin-regular.woff2 ├── sheng-l-q2dUSl9S4Xg-unsplash.webp ├── opensearch.xml.example ├── lang │ ├── README.md │ ├── mandarin_chinese.json │ ├── japanese.json │ ├── korean.json │ ├── english.json │ ├── danish.json │ ├── swedish.json │ ├── norwegian.json │ ├── turkish.json │ ├── russian.json │ ├── dutch.json │ ├── ukrainian.json │ ├── romanian.json │ ├── italian.json │ ├── german.json │ ├── polish.json │ ├── portuguese.json │ ├── spanish.json │ ├── french_canadian.json │ ├── french.json │ └── greek.json ├── torrentSort.js ├── cookies-settings-version.js ├── uxlang.js ├── menu.js ├── cookies.js ├── calculator.js └── script.js ├── .dockerignore ├── .gitignore ├── SECURITY.md ├── requirements.txt ├── scripts ├── docker-cmd.sh ├── generate-opensearch.sh └── generate-pyconfig.py ├── src ├── text_engines │ ├── objects │ │ ├── textResult.py │ │ ├── wikiSnippet.py │ │ └── fullEngineResults.py │ ├── mullvad.py │ ├── qwant.py │ └── google.py ├── torrent_sites │ ├── nyaa.py │ ├── rutor.py │ ├── thepiratebay.py │ └── torrentgalaxy.py ├── torrents.py ├── video.py ├── images.py ├── textResults.py └── helpers.py ├── compose.yaml ├── .env ├── alpine-based.dockerfile ├── ubuntu-based.dockerfile ├── templates ├── videos.html ├── preresults_layout.html ├── discover.html ├── torrents.html ├── images.html ├── search.html └── settings.html ├── resources └── _config.py.gen.template ├── README.md └── _config.py /static/css/menu.css: -------------------------------------------------------------------------------- 1 | .search-menu { 2 | margin-top: -50px !important; 3 | } -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | *.dockerfile 2 | .* 3 | !.git 4 | compose.yaml 5 | *.md 6 | LICENSE 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | **/__pycache__ 3 | venv 4 | **/opensearch.xml 5 | .vscode 6 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/HEAD/static/favicon.png -------------------------------------------------------------------------------- /static/preview1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/HEAD/static/preview1.webp -------------------------------------------------------------------------------- /static/preview2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/HEAD/static/preview2.webp -------------------------------------------------------------------------------- /static/preview3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/HEAD/static/preview3.webp -------------------------------------------------------------------------------- /static/searchicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/HEAD/static/searchicon.png -------------------------------------------------------------------------------- /static/imagesearch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/HEAD/static/imagesearch.png -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | Please report any potential security issues to security@extravi.dev 3 | -------------------------------------------------------------------------------- /static/themepreview/preview1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/HEAD/static/themepreview/preview1.webp -------------------------------------------------------------------------------- /static/themepreview/preview10.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/HEAD/static/themepreview/preview10.webp -------------------------------------------------------------------------------- /static/themepreview/preview11.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/HEAD/static/themepreview/preview11.webp -------------------------------------------------------------------------------- /static/themepreview/preview12.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/HEAD/static/themepreview/preview12.webp -------------------------------------------------------------------------------- /static/themepreview/preview13.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/HEAD/static/themepreview/preview13.webp -------------------------------------------------------------------------------- /static/themepreview/preview2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/HEAD/static/themepreview/preview2.webp -------------------------------------------------------------------------------- /static/themepreview/preview3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/HEAD/static/themepreview/preview3.webp -------------------------------------------------------------------------------- /static/themepreview/preview4.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/HEAD/static/themepreview/preview4.webp -------------------------------------------------------------------------------- /static/themepreview/preview5.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/HEAD/static/themepreview/preview5.webp -------------------------------------------------------------------------------- /static/themepreview/preview6.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/HEAD/static/themepreview/preview6.webp -------------------------------------------------------------------------------- /static/themepreview/preview7.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/HEAD/static/themepreview/preview7.webp -------------------------------------------------------------------------------- /static/themepreview/preview8.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/HEAD/static/themepreview/preview8.webp -------------------------------------------------------------------------------- /static/themepreview/preview9.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/HEAD/static/themepreview/preview9.webp -------------------------------------------------------------------------------- /static/fonts/inter-v12-latin-300.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/HEAD/static/fonts/inter-v12-latin-300.woff -------------------------------------------------------------------------------- /static/fonts/inter-v12-latin-300.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/HEAD/static/fonts/inter-v12-latin-300.woff2 -------------------------------------------------------------------------------- /static/fonts/inter-v12-latin-700.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/HEAD/static/fonts/inter-v12-latin-700.woff -------------------------------------------------------------------------------- /static/fonts/inter-v12-latin-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/HEAD/static/fonts/inter-v12-latin-700.woff2 -------------------------------------------------------------------------------- /static/fonts/inter-v12-latin-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/HEAD/static/fonts/inter-v12-latin-regular.woff -------------------------------------------------------------------------------- /static/sheng-l-q2dUSl9S4Xg-unsplash.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/HEAD/static/sheng-l-q2dUSl9S4Xg-unsplash.webp -------------------------------------------------------------------------------- /static/fonts/inter-v12-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/HEAD/static/fonts/inter-v12-latin-regular.woff2 -------------------------------------------------------------------------------- /static/css/search.css: -------------------------------------------------------------------------------- 1 | html { 2 | overflow: hidden; 3 | height: 100%; 4 | } 5 | 6 | body { 7 | min-height: 100%; 8 | overflow-x: hidden; 9 | } -------------------------------------------------------------------------------- /static/fonts/material-icons-round-v108-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/HEAD/static/fonts/material-icons-round-v108-latin-regular.woff2 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | lxml 3 | bs4 4 | gunicorn 5 | requests 6 | thefuzz 7 | httpx[http2] 8 | trio 9 | werkzeug>=3.0.3 # not directly required, pinned by Snyk to avoid a vulnerability 10 | urllib3>=2.2.2 # not directly required, pinned by Snyk to avoid a vulnerability 11 | anyio>=4.4.0 # not directly required, pinned by Snyk to avoid a vulnerability 12 | -------------------------------------------------------------------------------- /scripts/docker-cmd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # This script is meant to be ran solely in a Docker container. 3 | # Do not run it manually, it's not meant for that. 4 | 5 | sh scripts/generate-opensearch.sh || exit $? 6 | python3 scripts/generate-pyconfig.py || exit $? 7 | 8 | [ "$WORKERS" ] || WORKERS=2 9 | [ "$THREADS" ] || THREADS=8 10 | [ "$PORT" ] || PORT=8000 11 | 12 | exec gunicorn --workers $WORKERS --threads $THREADS --bind="0.0.0.0:$PORT" __init__:app 13 | -------------------------------------------------------------------------------- /src/text_engines/objects/textResult.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | # Provided as a 'blueprint' for a singular result from the text engine. 4 | @dataclass 5 | class TextResult: 6 | title: str 7 | desc: str 8 | url: str 9 | sublinks: list 10 | 11 | def asDICT(self): 12 | return { 13 | "title": self.title, 14 | "desc": self.desc, 15 | "url": self.url, 16 | "sublinks": self.sublinks, 17 | } 18 | -------------------------------------------------------------------------------- /compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | araa-search: 3 | container_name: Araa 4 | image: docker.io/temthelem/araa-search:latest 5 | env_file: 6 | - .env # May be redundant. Who cares ¯\_(ツ)_/¯ 7 | ports: 8 | - "${PORT}:${PORT}" 9 | watchtower: # Not required. Keeps containers up-to-date. 10 | container_name: watchtower 11 | image: docker.io/containrrr/watchtower 12 | volumes: 13 | - /var/run/docker.sock:/var/run/docker.sock 14 | command: --interval 60 15 | -------------------------------------------------------------------------------- /static/opensearch.xml.example: -------------------------------------------------------------------------------- 1 | 2 | 3 | Araa 4 | 5 | 6 | UTF-8 7 | UTF-8 8 | en-us 9 | 10 | -------------------------------------------------------------------------------- /static/lang/README.md: -------------------------------------------------------------------------------- 1 | The file should be saved to a file in `static/lang/` and should be titled with the english name of the language in all lower case, followed by '.json'. 2 | 3 | Make sure to add an entry to `UX_LANGUAGES` in _config.py 4 | 5 | It should be formatted like this: 6 | 7 | {'lang_lower': 'french', 'lang_fancy': 'French (Français)'}, 8 | 9 | `lang_lower` should match the first part of the json file (e.g. 'english' for 'english.json'). 10 | `lang_fancy` should be the name of the language in english with the first letter capitalized, followed by the name of the language in its own language in brackets. 11 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # Essential server variables 2 | #DOMAIN= 3 | PORT=8000 4 | WORKERS=8 5 | THREADS=2 6 | 7 | # Some basic customization. 8 | SHEBANG=! 9 | DEFAULT_THEME=dark_blur 10 | DEFAULT_METHOD=GET 11 | DONATE_URL=https://github.com/sponsors/Extravi 12 | ENABLE_API=False 13 | DEFAULT_LANG=english 14 | 15 | # Piped (an alternative yt frontend & proxy) config 16 | PIPED_INSTANCE=yt.extravi.dev 17 | PIPED_API=ytapi.extravi.dev 18 | PIPED_PROXY=ytproxy.extravi.dev 19 | 20 | # Torrenting configuration. 21 | ENABLE_TORRENTS=True 22 | TORRENT_SITES=[ 'nyaa', 'torrentgalaxy', 'tpb', 'rutor' ] 23 | TORRENTGALAXY_DOMAIN=torrentgalaxy.to 24 | NYAA_DOMAIN=nyaa.si 25 | APIBAY_DOMAIN=apibay.org 26 | RUTOR_DOMAIN=rutor.info 27 | -------------------------------------------------------------------------------- /alpine-based.dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:alpine 2 | 3 | # LABEL can be used to attach metadata to the container. 4 | LABEL title="Araa Search" \ 5 | description="A privacy-respecting, ad-free, self-hosted Google metasearch engine with strong security and full API support." \ 6 | git_repo="https://github.com/TEMtheLEM/araa-search" \ 7 | authors="https://github.com/Extravi/araa-search/contributors" \ 8 | maintainer="TEMtheLEM " \ 9 | image="https://hub.docker.com/r/temthelem/araa-search" 10 | 11 | WORKDIR /app 12 | 13 | COPY requirements.txt /app/ 14 | 15 | # We will only be running our own python app in a container, 16 | # so this shouldn't be terrible. 17 | RUN pip install --break-system-packages -r requirements.txt 18 | 19 | COPY . . 20 | 21 | ENV ORIGIN_REPO=https://github.com/TEMtheLEM/araa-search 22 | 23 | CMD [ "sh", "scripts/docker-cmd.sh" ] 24 | -------------------------------------------------------------------------------- /scripts/generate-opensearch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Generates the opensearch.xml file based off of a $DOMAIN env. var. 3 | # Fails if $DOMAIN is blank or not set. 4 | 5 | if [ $DOMAIN ]; then 6 | echo " 7 | 8 | Araa 9 | 10 | 11 | UTF-8 12 | UTF-8 13 | en-us 14 | " > ./static/opensearch.xml; 15 | else 16 | echo "Make a DOMAIN env. variable & set it to your domain! 17 | (Ex; DOMAIN=www.yourdomain.com)"; 18 | exit 1; 19 | fi 20 | -------------------------------------------------------------------------------- /ubuntu-based.dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:latest 2 | 3 | # LABEL can be used to attach metadata to the container. 4 | LABEL title="Araa Search" \ 5 | description="A privacy-respecting, ad-free, self-hosted Google metasearch engine with strong security and full API support." \ 6 | git_repo="https://github.com/TEMtheLEM/araa-search" \ 7 | authors="https://github.com/Extravi/araa-search/contributors" \ 8 | maintainer="TEMtheLEM " \ 9 | image="https://hub.docker.com/r/temthelem/araa-search" 10 | 11 | WORKDIR /app 12 | 13 | COPY requirements.txt /app/ 14 | 15 | RUN apt update && apt upgrade -y 16 | RUN apt install python3 python3-venv python3-pip --no-install-recommends -y 17 | 18 | # We will only be running our own python app in a container, 19 | # so this shouldn't be terrible. 20 | RUN pip install --break-system-packages -r requirements.txt 21 | 22 | COPY . . 23 | 24 | ENV ORIGIN_REPO=https://github.com/TEMtheLEM/araa-search 25 | 26 | CMD [ "sh", "scripts/docker-cmd.sh" ] 27 | -------------------------------------------------------------------------------- /src/text_engines/objects/wikiSnippet.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | 3 | @dataclass 4 | class WikiSnippet: 5 | title: str 6 | desc: str 7 | link: str 8 | image: str | None = None 9 | wiki_thumb_proxy_link: str | None = None 10 | known_for: str | None = None 11 | info: dict = field(default_factory = dict) 12 | 13 | def asDICT(self): 14 | # self.info is a dict[str, Tag] => `Tag`s are used because of links. 15 | # we just want a dict[str, str] => we don't want to perserve links, just text. 16 | info_cleaned = {} 17 | for info in self.info.items(): 18 | keypoint = info[0][:len(info[0]) - 2] # remove a trailing ": " 19 | info_cleaned[keypoint] = info[1].get_text() 20 | 21 | return { 22 | "title": self.title, 23 | "image": self.image, 24 | "desc": self.desc, 25 | "link": self.link, 26 | "wiki_thumb_proxy_link": self.wiki_thumb_proxy_link, 27 | "known_for": self.known_for, 28 | "info": info_cleaned, 29 | } 30 | -------------------------------------------------------------------------------- /static/css/dark_blur_beta.css: -------------------------------------------------------------------------------- 1 | html { 2 | overflow: hidden; 3 | height: 100%; 4 | } 5 | 6 | body { 7 | min-height: 100%; 8 | overflow-x: hidden; 9 | } 10 | 11 | html::before { 12 | content: ""; 13 | position: absolute; 14 | top: -10px; 15 | left: -10px; 16 | width: calc(100% + 20px); 17 | height: calc(100% + 20px); 18 | background: url("/sheng-l-q2dUSl9S4Xg-unsplash.webp") no-repeat center; 19 | background-size: cover; 20 | filter: blur(5px) brightness(67%); 21 | z-index: -1; 22 | } 23 | 24 | .search-container input { 25 | background: rgba(21, 21, 21, 0); 26 | } 27 | 28 | .wrapper { 29 | background: rgba(21, 21, 21, 0.7); 30 | -webkit-backdrop-filter: blur(20px); 31 | backdrop-filter: blur(20px); 32 | box-shadow: 0 4px 8px rgba(0, 0, 0, .2); 33 | --search-bg-input-border: rgba(60, 64, 67, 0.7); 34 | } 35 | 36 | .autocomplete ul li:hover { 37 | background: rgba(255, 255, 255, 0.2); 38 | } 39 | 40 | .selected { 41 | background: rgba(255, 255, 255, 0.2); 42 | } 43 | 44 | .search-button-wrapper button { 45 | background: rgba(21, 21, 21, 0.7); 46 | -webkit-backdrop-filter: blur(20px); 47 | backdrop-filter: blur(20px); 48 | box-shadow: 0 4px 8px rgba(0, 0, 0, .2); 49 | } -------------------------------------------------------------------------------- /src/text_engines/objects/fullEngineResults.py: -------------------------------------------------------------------------------- 1 | from src.text_engines.objects.textResult import TextResult 2 | from src.text_engines.objects.wikiSnippet import WikiSnippet 3 | from dataclasses import dataclass, field 4 | 5 | @dataclass 6 | class FullEngineResults: 7 | engine: str 8 | search_type: str 9 | ok: bool 10 | code: int 11 | results: list[TextResult] = field(default_factory = list) 12 | wiki: WikiSnippet | None = None 13 | featured: str | None = None 14 | correction: str | None = None 15 | top_result_sublinks: list[TextResult] = field(default_factory = list) 16 | 17 | def asDICT(self): 18 | results_asdict = [result.asDICT() for result in self.results] 19 | 20 | return { 21 | "engine": self.engine, 22 | "type": self.search_type, 23 | "ok": self.ok, 24 | "code": self.code, 25 | "results": results_asdict, 26 | "results.len": len(results_asdict), 27 | "wiki": self.wiki.asDICT() if self.wiki != None else None, 28 | "featured": self.featured, 29 | "correction": self.correction, 30 | "sublinks": self.top_result_sublinks, 31 | "sublinks.len": len(self.top_result_sublinks) 32 | } 33 | 34 | -------------------------------------------------------------------------------- /templates/videos.html: -------------------------------------------------------------------------------- 1 | {% extends "results_layout.html" %} 2 | 3 | {% block body %} 4 |

{{ lang_data.results.results }} {{ fetched }} {{ lang_data.results.seconds }}

5 | {% if results %} 6 | {% for result in results %} 7 |
8 | 13 |
14 |

{{ result[1] }}

15 |

{{ result[3] }} • {{ result[2] }}

16 |

{{ result[5] }} | {{ result[4] }}

17 |
18 |
19 |
20 | {% endfor %} 21 | 22 | {% else %} 23 |
24 | Your search '{{ q }}' came back with no results.
25 | Try rephrasing your search term and/or recorrect any spelling mistakes. 26 |
27 | {% endif %} 28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /static/torrentSort.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @source: ./torrentSort.js 3 | * 4 | * @licstart The following is the entire license notice for the 5 | * JavaScript code in this page. 6 | * 7 | * Copyright (C) 2024 Extravi 8 | * 9 | * The JavaScript code in this page is free software: you can 10 | * redistribute it and/or modify it under the terms of the GNU Affero 11 | * General Public License as published by the Free Software Foundation, 12 | * either version 3 of the License, or (at your option) any later version. 13 | * 14 | * The code is distributed WITHOUT ANY WARRANTY; without even the 15 | * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 16 | * See the GNU Affero General Public License for more details. 17 | * 18 | * As additional permission under GNU Affero General Public License 19 | * section 7, you may distribute non-source (e.g., minimized or compacted) 20 | * forms of that code without the copy of the GNU Affero General Public 21 | * License normally required by section 4, provided you include this 22 | * license notice and a URL through which recipients can access the 23 | * Corresponding Source. 24 | * 25 | * @licend The above is the entire license notice 26 | * for the JavaScript code in this page. 27 | */ 28 | 29 | let options = document.querySelector(".torrent-settings"); 30 | options.addEventListener("change", () => { 31 | options.form.submit(); 32 | }) 33 | -------------------------------------------------------------------------------- /static/lang/mandarin_chinese.json: -------------------------------------------------------------------------------- 1 | { 2 | "buttons": { 3 | "text": "网页", 4 | "image": "图片", 5 | "video": "视频", 6 | "reddit": "Reddit", 7 | "torrent": "种子", 8 | "next": "下一步", 9 | "previous": "上一步" 10 | }, 11 | "results": { 12 | "results": "结果获取耗时", 13 | "seconds": "秒" 14 | }, 15 | "footer": { 16 | "settings": "设置", 17 | "source_code": "源代码", 18 | "commit": "提交", 19 | "donate": "捐赠" 20 | }, 21 | "search_buttons": { 22 | "search_text": "使用{araa_name}搜索", 23 | "search_images": "使用{araa_name}搜索图片" 24 | }, 25 | "settings": { 26 | "theme": "主题", 27 | "user_theme": "用户主题", 28 | "all_settings": "所有设置", 29 | "settings_header": "设置", 30 | "discover_themes": "浏览社区创建的主题", 31 | "discover_themes_button": "发现主题", 32 | "preferred_language": "搜索结果的首选语言", 33 | "google_domain": "谷歌域名", 34 | "safe_search": "安全搜索", 35 | "open_links_new_tab": "在新标签页中打开链接", 36 | "disable_javascript": "禁用JavaScript(不建议)会影响网站功能,禁用自动完成并减慢某些功能。 启用JavaScript不会泄露隐私,因为请求通过服务器端点处理。", 37 | "save_settings": "保存设置", 38 | "return_to_settings": "返回设置", 39 | "preferredux_language": "{araa_name}的标题、按钮和其他文本的首选语言", 40 | "on": "开", 41 | "off": "关", 42 | "any_language": "任何语言" 43 | }, 44 | "api_links": { 45 | "api_link": "API", 46 | "suggestions_api_link": "建议API" 47 | } 48 | } -------------------------------------------------------------------------------- /static/css/settings-style.css: -------------------------------------------------------------------------------- 1 | .logomobile { 2 | top: 13px !important; 3 | font-size: 23px; 4 | } 5 | 6 | .no-decoration { 7 | color: var(--fg) !important; 8 | transition: all .3s ease; 9 | font-weight: 700; 10 | text-decoration: none; 11 | } 12 | 13 | .no-decoration:hover { 14 | color: var(--blue) !important; 15 | text-decoration: none; 16 | } 17 | 18 | .theme-settings { 19 | width: 100% !important; 20 | } 21 | 22 | html { 23 | height: 100%; 24 | } 25 | 26 | body { 27 | min-height: 100%; 28 | } 29 | 30 | .themes-settings-menu>div { 31 | width: calc(32%) !important; 32 | margin: 5px; 33 | } 34 | 35 | p { 36 | max-width: 620px !important; 37 | } 38 | 39 | #discoverButton { 40 | border-radius: 4px; 41 | padding: 6px; 42 | font-size: 15px; 43 | border: 1px solid var(--border); 44 | color: var(--font-fg); 45 | width: 160px; 46 | background: var(--button); 47 | float: right; 48 | transition: all .3s ease; 49 | } 50 | 51 | #discoverButton:hover { 52 | border: 1px solid #5f6368; 53 | cursor: pointer; 54 | } 55 | 56 | @media only screen and (max-width: 950px) { 57 | .themes-settings-menu>div { 58 | width: calc(20% - -60px) !important; 59 | margin: 5px; 60 | } 61 | } 62 | 63 | @media only screen and (max-width: 750px) { 64 | .themes-settings-menu>div { 65 | width: calc(50% - 10px) !important; 66 | margin: 5px; 67 | } 68 | 69 | .logomobile { 70 | top: -5px !important; 71 | } 72 | } -------------------------------------------------------------------------------- /src/torrent_sites/nyaa.py: -------------------------------------------------------------------------------- 1 | from _config import * 2 | from src import helpers 3 | from urllib.parse import quote 4 | 5 | def name(): 6 | return "nyaa" 7 | 8 | def get_catagory_code(cat): 9 | match cat: 10 | case "all": 11 | return "" 12 | case "anime": 13 | return "&c=1_0" 14 | case "music": 15 | return "&c=2_0" 16 | case "game": 17 | return "&c=6_2" 18 | case "software": 19 | return "&c=6_1" 20 | case _: 21 | return "ignore" 22 | 23 | 24 | def search(query, catagory="all"): 25 | catagory = get_catagory_code(catagory) 26 | if catagory == "ignore": 27 | return [] 28 | 29 | soup, response_code = helpers.makeHTMLRequest(f"https://{NYAA_DOMAIN}/?f=0&q={quote(query)}{catagory}") 30 | results = [] 31 | for torrent in soup.select(".default, .success, .danger"): 32 | list_of_tds = torrent.find_all("td") 33 | byte_size = helpers.string_to_bytes(list_of_tds[3].get_text().strip()) 34 | 35 | results.append({ 36 | "href": NYAA_DOMAIN, 37 | "title": list_of_tds[1].find_all("a")[-1].get_text(), 38 | "magnet": helpers.apply_trackers(list_of_tds[2].find_all("a")[-1]["href"]), 39 | "bytes": byte_size, 40 | "size": helpers.bytes_to_string(byte_size), 41 | "views": None, 42 | "seeders": int(list_of_tds[5].get_text().strip()), 43 | "leechers": int(list_of_tds[6].get_text().strip()) 44 | }) 45 | return results 46 | -------------------------------------------------------------------------------- /static/lang/japanese.json: -------------------------------------------------------------------------------- 1 | { 2 | "buttons": { 3 | "text": "ウェブ", 4 | "image": "画像", 5 | "video": "ビデオ", 6 | "reddit": "Reddit", 7 | "torrent": "トレント", 8 | "next": "次へ", 9 | "previous": "前へ" 10 | }, 11 | "results": { 12 | "results": "結果を取得しました", 13 | "seconds": "秒" 14 | }, 15 | "footer": { 16 | "settings": "設定", 17 | "source_code": "ソースコード", 18 | "commit": "コミット", 19 | "donate": "寄付" 20 | }, 21 | "search_buttons": { 22 | "search_text": "{araa_name}で検索", 23 | "search_images": "{araa_name}で画像検索" 24 | }, 25 | "settings": { 26 | "theme": "テーマ", 27 | "user_theme": "ユーザーテーマ", 28 | "all_settings": "すべての設定", 29 | "settings_header": "設定", 30 | "discover_themes": "コミュニティが作成したテーマを閲覧", 31 | "discover_themes_button": "テーマを発見", 32 | "preferred_language": "検索結果のための優先言語", 33 | "google_domain": "Googleドメイン", 34 | "safe_search": "安全検索", 35 | "open_links_new_tab": "リンクを新しいタブで開く", 36 | "disable_javascript": "JavaScriptを無効にする(お勧めしません)はウェブサイトの機能に影響を与え、自動入力を無効にし、一部の機能を遅くします。 JavaScriptを有効にしてもプライバシーは侵害されません。リクエストはサーバーサイドのエンドポイントを介して処理されます。", 37 | "save_settings": "設定を保存", 38 | "return_to_settings": "設定に戻る", 39 | "preferredux_language": "{araa_name}からの見出し、ボタン、およびその他のテキストの優先言語", 40 | "on": "オン", 41 | "off": "オフ", 42 | "any_language": "任意の言語" 43 | }, 44 | "api_links": { 45 | "api_link": "API", 46 | "suggestions_api_link": "提案API" 47 | } 48 | } -------------------------------------------------------------------------------- /static/lang/korean.json: -------------------------------------------------------------------------------- 1 | { 2 | "buttons": { 3 | "text": "웹", 4 | "image": "이미지", 5 | "video": "비디오", 6 | "reddit": "Reddit", 7 | "torrent": "토렌트", 8 | "next": "다음", 9 | "previous": "이전" 10 | }, 11 | "results": { 12 | "results": "결과를 가져온 시간", 13 | "seconds": "초" 14 | }, 15 | "footer": { 16 | "settings": "설정", 17 | "source_code": "소스 코드", 18 | "commit": "확인", 19 | "donate": "기부" 20 | }, 21 | "search_buttons": { 22 | "search_text": "{araa_name}로 검색", 23 | "search_images": "{araa_name}로 이미지 검색" 24 | }, 25 | "settings": { 26 | "theme": "테마", 27 | "user_theme": "사용자 테마", 28 | "all_settings": "모든 설정", 29 | "settings_header": "설정", 30 | "discover_themes": "커뮤니티가 만든 테마 검색", 31 | "discover_themes_button": "테마 찾기", 32 | "preferred_language": "검색 결과의 우선 언어", 33 | "google_domain": "Google 도메인", 34 | "safe_search": "안전 검색", 35 | "open_links_new_tab": "새 탭에서 링크 열기", 36 | "disable_javascript": "JavaScript 비활성화(권장하지 않음)는 웹 사이트 기능에 영향을 미치며 자동 완성을 비활성화하고 일부 기능을 늦춥니다. JavaScript를 사용 중지해도 개인 정보 유출이 발생하지 않으며 요청은 서버 측 엔드포인트를 통해 처리됩니다.", 37 | "save_settings": "설정 저장", 38 | "return_to_settings": "설정으로 돌아가기", 39 | "preferredux_language": "{araa_name}의 제목, 버튼 및 기타 텍스트에 대한 기본 언어", 40 | "on": "켜짐", 41 | "off": "꺼짐", 42 | "any_language": "아무 언어" 43 | }, 44 | "api_links": { 45 | "api_link": "API", 46 | "suggestions_api_link": "제안 API" 47 | } 48 | } -------------------------------------------------------------------------------- /static/css/gentoo_lavender.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --html-bg: #dddaec; 3 | --font-fg: #000000; 4 | --fg: #202124; 5 | 6 | --search-bg: #ffffff; 7 | --search-bg-input: #f6f6f6; 8 | --search-bg-input-border: #dadce0; 9 | --search-select: #eeeeee; 10 | 11 | --border: #dadce0; 12 | 13 | --link: #1a0dab; 14 | --link-visited: #681da8; 15 | 16 | --snip-border: #dadce0; 17 | --snip-background: #ffffff; 18 | --snip-text: #000000; 19 | 20 | --settings-border: #5f6368; 21 | --button: #f6f6f6; 22 | 23 | --footer-bg: #e1e1e1; 24 | --footer-font: #4c416e; 25 | 26 | --highlight: #202124; 27 | 28 | --blue: #4c416e; 29 | 30 | --green: #31b06e; 31 | 32 | --image-view: #ffffff; 33 | --image-view-titlebar: #ffffff; 34 | --view-image-color: #f1f3f4; 35 | --image-select: #f6f6f6; 36 | --fff: #fff; 37 | 38 | --publish-info: #7f869e; 39 | 40 | --search-button: #202124; 41 | } 42 | 43 | .wrapper-results:hover, 44 | .wrapper-results:focus-within, 45 | .wrapper:hover, 46 | .wrapper:focus-within { 47 | box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08) !important; 48 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1) !important; 49 | } 50 | 51 | .check p { 52 | color: var(--highlight) !important; 53 | } 54 | 55 | .image_view { 56 | box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08) !important; 57 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1) !important; 58 | } 59 | 60 | .view-image-search { 61 | box-shadow: none; 62 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1) !important; 63 | } 64 | 65 | .view-image-search:hover { 66 | box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08) !important; 67 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1) !important; 68 | } 69 | -------------------------------------------------------------------------------- /src/torrent_sites/rutor.py: -------------------------------------------------------------------------------- 1 | from _config import * 2 | from src import helpers 3 | from urllib.parse import quote 4 | 5 | def name(): 6 | return "rutor" 7 | 8 | def get_catagory_code(cat): 9 | match cat: 10 | case "all": 11 | return "" 12 | case "audiobook": 13 | return "ignore" 14 | case "movie": 15 | return "&category=1" 16 | case "tv": 17 | return "&category=6" 18 | case "games": 19 | return "&category=8" 20 | case "software": 21 | return "&category=9" 22 | case "anime": 23 | return "&category=10" 24 | case "music": 25 | return "&category=2" 26 | case "xxx": 27 | return "ignore" 28 | case _: 29 | return "" 30 | 31 | def search(query, catagory="all"): 32 | catagory = get_catagory_code(catagory) 33 | if catagory == "ignore": 34 | return [] 35 | 36 | 37 | url = f"https://{RUTOR_DOMAIN}/search/{quote(query)}{catagory}" 38 | html, response_code = helpers.makeHTMLRequest(url) 39 | results = [] 40 | 41 | for torrent in html.select(".gai, .tum"): 42 | tds = torrent.find_all("td") 43 | spans = torrent.find_all("span") 44 | byte_size = helpers.string_to_bytes(tds[-2].get_text()) 45 | 46 | results.append({ 47 | "href": RUTOR_DOMAIN, 48 | "title": tds[1].get_text(), 49 | "magnet": helpers.apply_trackers(tds[1].find_all("a")[1]["href"]), 50 | "bytes": byte_size, 51 | "size": helpers.bytes_to_string(byte_size), 52 | "views": None, 53 | "seeders": int(spans[0].get_text()), 54 | "leechers": int(spans[1].get_text()), 55 | }) 56 | 57 | return results 58 | -------------------------------------------------------------------------------- /static/css/light.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --html-bg: #ffffff; 3 | --font-fg: #000000; 4 | --fg: #202124; 5 | 6 | --search-bg: #ffffff; 7 | --search-bg-input: #f6f6f6; 8 | --search-bg-input-border: #dadce0; 9 | --search-select: #eeeeee; 10 | 11 | --border: #dadce0; 12 | 13 | --link: #1a0dab; 14 | --link-visited: #681da8; 15 | 16 | --snip-border: #dadce0; 17 | --snip-background: #ffffff; 18 | --snip-text: #000000; 19 | 20 | --settings-border: #5f6368; 21 | --button: #f6f6f6; 22 | 23 | --footer-bg: #f6f6f6; 24 | --footer-font: #353535; 25 | 26 | --highlight: #202124; 27 | 28 | --blue: #4285f4; 29 | 30 | --green: #202124; 31 | 32 | --image-view: #ffffff; 33 | --image-view-titlebar: #ffffff; 34 | --view-image-color: #f1f3f4; 35 | --image-select: #f6f6f6; 36 | --fff: #fff; 37 | 38 | --publish-info: #202124; 39 | 40 | --search-button: #202124; 41 | } 42 | 43 | .wrapper-results:hover, 44 | .wrapper-results:focus-within, 45 | .wrapper:hover, 46 | .wrapper:focus-within { 47 | box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08) !important; 48 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1) !important; 49 | } 50 | 51 | .check p { 52 | color: var(--highlight) !important; 53 | } 54 | 55 | .image_view { 56 | box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08) !important; 57 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1) !important; 58 | } 59 | 60 | .search-menu { 61 | box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08) !important; 62 | } 63 | 64 | .view-image-search { 65 | box-shadow: none; 66 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1) !important; 67 | } 68 | 69 | .view-image-search:hover { 70 | box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08) !important; 71 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1) !important; 72 | } 73 | -------------------------------------------------------------------------------- /src/torrent_sites/thepiratebay.py: -------------------------------------------------------------------------------- 1 | from _config import * 2 | from src import helpers 3 | from urllib.parse import quote 4 | 5 | def name(): 6 | return "tpb" 7 | 8 | def get_catagory_code(cat): 9 | match cat: 10 | case "all": 11 | return "" 12 | case "audiobook": 13 | return "102" 14 | case "movie": 15 | return "201" 16 | case "tv": 17 | return "205" 18 | case "games": 19 | return "400" 20 | case "software": 21 | return "300" 22 | case "anime": 23 | # TPB has no anime catagory. 24 | return "ignore" 25 | case "music": 26 | return "100" 27 | case "xxx": 28 | safesearch = (request.cookies.get("safe", "active") == "active") 29 | if safesearch: 30 | return "ignore" 31 | return "500" 32 | case _: 33 | return "" 34 | 35 | def search(query, catagory="all"): 36 | catagory = get_catagory_code(catagory) 37 | if catagory == "ignore": 38 | return [] 39 | 40 | url = f"https://{API_BAY_DOMAIN}/q.php?q={quote(query)}&cat={catagory}" 41 | torrent_data = helpers.makeJSONRequest(url) 42 | results = [] 43 | 44 | for torrent in torrent_data: 45 | byte_size = int(torrent["size"]) 46 | results.append({ 47 | "href": "thepiratebay.org", 48 | "title": torrent["name"], 49 | "magnet": helpers.apply_trackers( 50 | torrent["info_hash"], 51 | name=torrent["name"], 52 | magnet=False 53 | ), 54 | "bytes": byte_size, 55 | "size": helpers.bytes_to_string(byte_size), 56 | "views": None, 57 | "seeders": int(torrent["seeders"]), 58 | "leechers": int(torrent["leechers"]) 59 | }) 60 | 61 | return results 62 | -------------------------------------------------------------------------------- /static/lang/english.json: -------------------------------------------------------------------------------- 1 | { 2 | "buttons": { 3 | "text": "Web", 4 | "image": "Images", 5 | "video": "Videos", 6 | "reddit": "Reddit", 7 | "torrent": "Torrents", 8 | "next": "Next", 9 | "previous": "Previous" 10 | }, 11 | "results": { 12 | "results": "Fetched the results in", 13 | "seconds": "seconds" 14 | }, 15 | "footer": { 16 | "settings": "Settings", 17 | "source_code": "Source code", 18 | "commit": "Commit", 19 | "donate": "Donate" 20 | }, 21 | "search_buttons": { 22 | "search_text": "Search with {araa_name}", 23 | "search_images": "Search images with {araa_name}" 24 | }, 25 | "settings": { 26 | "theme": "Theme", 27 | "user_theme": "User Theme", 28 | "all_settings": "All settings", 29 | "settings_header": "Settings", 30 | "discover_themes": "Browse themes made by the community", 31 | "discover_themes_button": "Discover Themes", 32 | "preferred_language": "Preferred language for your search results", 33 | "google_domain": "Google domain", 34 | "safe_search": "SafeSearch", 35 | "open_links_new_tab": "Open links in a new tab", 36 | "disable_javascript": "Disabling JavaScript (not recommended) affects webpage features, disables autocomplete, and slows certain functions. Keeping JavaScript enabled won't compromise your privacy, as requests are processed through server-side endpoints.", 37 | "save_settings": "Save your settings", 38 | "return_to_settings": "Return to settings", 39 | "preferredux_language": "Preferred language for headlines, buttons, and other text from {araa_name}", 40 | "on": "on", 41 | "off": "off", 42 | "any_language": "Any Language" 43 | }, 44 | "api_links": { 45 | "api_link": "API", 46 | "suggestions_api_link": "Suggestions API" 47 | } 48 | } -------------------------------------------------------------------------------- /static/css/catppuccin_mocha_blue.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --html-bg: #1e1e2e; 3 | --font-fg: #cdd6f4; 4 | --fg: #cdd6f4; 5 | 6 | --search-bg: #11111b; 7 | --search-bg-input: #1e1e2e; 8 | --search-bg-input-border: #303030; 9 | --search-select: #11111b; 10 | 11 | --border: #303030; 12 | 13 | --link: #89b4fa; 14 | --link-visited: #cba6f7; 15 | 16 | --snip-border: #303030; 17 | --snip-background: #11111b; 18 | --snip-text: #cdd6f4; 19 | 20 | --settings-border: #303030; 21 | --button: #11111b; 22 | 23 | --footer-bg: #11111b; 24 | --footer-font: #cdd6f4; 25 | 26 | --highlight: #ofofof; 27 | 28 | --blue: #89b4fa; 29 | 30 | --green: #31b06e; 31 | 32 | --image-view: #11111b; 33 | --image-view-titlebar: #11111b; 34 | --view-image-color: #000000; 35 | --image-select: #303030; 36 | --fff: #fff; 37 | 38 | --search-button: #cdd6f4; 39 | 40 | --publish-info: #7f869e; 41 | 42 | color-scheme: dark; 43 | } 44 | 45 | .calc-btn:hover { 46 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); 47 | } 48 | 49 | .calc-btn-2:hover { 50 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); 51 | } 52 | 53 | .calc-btn-2 { 54 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 55 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1); 56 | } 57 | 58 | .calc-btn { 59 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 60 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1); 61 | } 62 | 63 | .calc { 64 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); 65 | } 66 | 67 | .view-image-search { 68 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 69 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1); 70 | } 71 | 72 | .view-image-search:hover { 73 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); 74 | } 75 | -------------------------------------------------------------------------------- /static/css/catppuccin_mocha_green.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --html-bg: #1e1e2e; 3 | --font-fg: #cdd6f4; 4 | --fg: #cdd6f4; 5 | 6 | --search-bg: #11111b; 7 | --search-bg-input: #1e1e2e; 8 | --search-bg-input-border: #303030; 9 | --search-select: #11111b; 10 | 11 | --border: #303030; 12 | 13 | --link: #a6e3a1; 14 | --link-visited: #cba6f7; 15 | 16 | --snip-border: #303030; 17 | --snip-background: #11111b; 18 | --snip-text: #cdd6f4; 19 | 20 | --settings-border: #303030; 21 | --button: #11111b; 22 | 23 | --footer-bg: #11111b; 24 | --footer-font: #cdd6f4; 25 | 26 | --highlight: #ofofof; 27 | 28 | --blue: #a6e3a1; 29 | 30 | --green: #31b06e; 31 | 32 | --image-view: #11111b; 33 | --image-view-titlebar: #11111b; 34 | --view-image-color: #000000; 35 | --image-select: #303030; 36 | --fff: #fff; 37 | 38 | --search-button: #cdd6f4; 39 | 40 | --publish-info: #7f869e; 41 | 42 | color-scheme: dark; 43 | } 44 | 45 | .calc-btn:hover { 46 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); 47 | } 48 | 49 | .calc-btn-2:hover { 50 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); 51 | } 52 | 53 | .calc-btn-2 { 54 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 55 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1); 56 | } 57 | 58 | .calc-btn { 59 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 60 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1); 61 | } 62 | 63 | .calc { 64 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); 65 | } 66 | 67 | .view-image-search { 68 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 69 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1); 70 | } 71 | 72 | .view-image-search:hover { 73 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); 74 | } 75 | -------------------------------------------------------------------------------- /static/lang/danish.json: -------------------------------------------------------------------------------- 1 | { 2 | "buttons": { 3 | "text": "Web", 4 | "image": "Billeder", 5 | "video": "Videoer", 6 | "reddit": "Reddit", 7 | "torrent": "Torrents", 8 | "next": "Næste", 9 | "previous": "Forrige" 10 | }, 11 | "results": { 12 | "results": "Resultater hentet på", 13 | "seconds": "sekunder" 14 | }, 15 | "footer": { 16 | "settings": "Indstillinger", 17 | "source_code": "Kildekode", 18 | "commit": "Godkendelse", 19 | "donate": "Doner" 20 | }, 21 | "search_buttons": { 22 | "search_text": "Søg med {araa_name}", 23 | "search_images": "Søg billeder med {araa_name}" 24 | }, 25 | "settings": { 26 | "theme": "Tema", 27 | "user_theme": "Brugertema", 28 | "all_settings": "Alle indstillinger", 29 | "settings_header": "Indstillinger", 30 | "discover_themes": "Gennemse temaer lavet af fællesskabet", 31 | "discover_themes_button": "Find temaer", 32 | "preferred_language": "Foretrukken sprog til dine søgeresultater", 33 | "google_domain": "Google domæne", 34 | "safe_search": "Sikker søgning", 35 | "open_links_new_tab": "Åbn links i en ny fane", 36 | "disable_javascript": "Deaktivering af JavaScript (ikke anbefalet) påvirker websidefunktioner, deaktiverer autoudfyldning og bremser visse funktioner. At holde JavaScript aktiveret vil ikke kompromittere dit privatliv, da anmodninger behandles gennem serverbaserede slutpunkter.", 37 | "save_settings": "Gem dine indstillinger", 38 | "return_to_settings": "Tilbage til indstillinger", 39 | "preferredux_language": "Foretrukken Sprog til Overskrifter, Knapper og Anden Tekst fra {araa_name}", 40 | "on": "på", 41 | "off": "fra", 42 | "any_language": "Enhver sprog" 43 | }, 44 | "api_links": { 45 | "api_link": "API", 46 | "suggestions_api_link": "Forslag API" 47 | } 48 | } -------------------------------------------------------------------------------- /static/lang/swedish.json: -------------------------------------------------------------------------------- 1 | { 2 | "buttons": { 3 | "text": "Webb", 4 | "image": "Bilder", 5 | "video": "Videor", 6 | "reddit": "Reddit", 7 | "torrent": "Torrents", 8 | "next": "Nästa", 9 | "previous": "Föregående" 10 | }, 11 | "results": { 12 | "results": "Resultat hämtat på", 13 | "seconds": "sekunder" 14 | }, 15 | "footer": { 16 | "settings": "Inställningar", 17 | "source_code": "Källkod", 18 | "commit": "Bekräftelse", 19 | "donate": "Donera" 20 | }, 21 | "search_buttons": { 22 | "search_text": "Sök med {araa_name}", 23 | "search_images": "Sök bilder med {araa_name}" 24 | }, 25 | "settings": { 26 | "theme": "Tema", 27 | "user_theme": "Användartema", 28 | "all_settings": "Alla inställningar", 29 | "settings_header": "Inställningar", 30 | "discover_themes": "Bläddra bland teman skapade av gemenskapen", 31 | "discover_themes_button": "Upptäck Teman", 32 | "preferred_language": "Föredraget språk för dina sökresultat", 33 | "google_domain": "Google-domän", 34 | "safe_search": "Säker sökning", 35 | "open_links_new_tab": "Öppna länkar i en ny flik", 36 | "disable_javascript": "Inaktivera JavaScript (inte rekommenderat) påverkar webbplatsfunktioner, stänger av automatisk ifyllning och saktar ned vissa funktioner. Att hålla JavaScript aktiverat kommer inte att äventyra din integritet, eftersom förfrågningar behandlas via serverbaserade ändpunkter.", 37 | "save_settings": "Spara inställningar", 38 | "return_to_settings": "Återgå till inställningar", 39 | "preferredux_language": "Föredraget språk för rubriker, knappar och annan text från {araa_name}", 40 | "on": "på", 41 | "off": "av", 42 | "any_language": "Valfritt språk" 43 | }, 44 | "api_links": { 45 | "api_link": "API", 46 | "suggestions_api_link": "Förslags-API" 47 | } 48 | } -------------------------------------------------------------------------------- /static/lang/norwegian.json: -------------------------------------------------------------------------------- 1 | { 2 | "buttons": { 3 | "text": "Nett", 4 | "image": "Bilder", 5 | "video": "Videoer", 6 | "reddit": "Reddit", 7 | "torrent": "Torrenter", 8 | "next": "Neste", 9 | "previous": "Forrige" 10 | }, 11 | "results": { 12 | "results": "Resultater hentet på", 13 | "seconds": "sekunder" 14 | }, 15 | "footer": { 16 | "settings": "Innstillinger", 17 | "source_code": "Kildekode", 18 | "commit": "Bekreftelse", 19 | "donate": "Doner" 20 | }, 21 | "search_buttons": { 22 | "search_text": "Søk med {araa_name}", 23 | "search_images": "Søk bilder med {araa_name}" 24 | }, 25 | "settings": { 26 | "theme": "Tema", 27 | "user_theme": "Brukertema", 28 | "all_settings": "Alle innstillinger", 29 | "settings_header": "Innstillinger", 30 | "discover_themes": "Bla gjennom temaer laget av fellesskapet", 31 | "discover_themes_button": "Oppdag temaer", 32 | "preferred_language": "Foretrukket språk for søkeresultatene dine", 33 | "google_domain": "Google-domene", 34 | "safe_search": "Sikkerhetsfiltrering", 35 | "open_links_new_tab": "Åpne lenker i en ny fane", 36 | "disable_javascript": "Deaktivering av JavaScript (ikke anbefalt) påvirker websidefunksjoner, deaktiverer automatisk utfylling og bremser visse funksjoner. Å holde JavaScript aktivert vil ikke kompromittere personvernet ditt, da forespørsler behandles via serverbaserte endepunkter.", 37 | "save_settings": "Lagre innstillingene dine", 38 | "return_to_settings": "Tilbake til innstillinger", 39 | "preferredux_language": "Foretrukket språk for overskrifter, knapper og annen tekst fra {araa_name}", 40 | "on": "på", 41 | "off": "av", 42 | "any_language": "Alle språk" 43 | }, 44 | "api_links": { 45 | "api_link": "API", 46 | "suggestions_api_link": "Forslag API" 47 | } 48 | } -------------------------------------------------------------------------------- /static/lang/turkish.json: -------------------------------------------------------------------------------- 1 | { 2 | "buttons": { 3 | "text": "Web", 4 | "image": "Resimler", 5 | "video": "Videolar", 6 | "reddit": "Reddit", 7 | "torrent": "Torrentler", 8 | "next": "Sonraki", 9 | "previous": "Önceki" 10 | }, 11 | "results": { 12 | "results": "Sonuçlar 1.33 saniyede alındı", 13 | "seconds": "saniye" 14 | }, 15 | "footer": { 16 | "settings": "Ayarlar", 17 | "source_code": "Kaynak Kodu", 18 | "commit": "Taahhüt", 19 | "donate": "Bağış Yap" 20 | }, 21 | "search_buttons": { 22 | "search_text": "{araa_name} ile ara", 23 | "search_images": "{araa_name} ile resim ara" 24 | }, 25 | "settings": { 26 | "theme": "Tema", 27 | "user_theme": "Kullanıcı Teması", 28 | "all_settings": "Tüm Ayarlar", 29 | "settings_header": "Ayarlar", 30 | "discover_themes": "Topluluk tarafından oluşturulan temaları keşfedin", 31 | "discover_themes_button": "Temaları Keşfet", 32 | "preferred_language": "Arama sonuçları için tercih edilen dil", 33 | "google_domain": "Google Alanı", 34 | "safe_search": "Güvenli Arama", 35 | "open_links_new_tab": "Bağlantıları yeni sekmede aç", 36 | "disable_javascript": "JavaScript'i devre dışı bırakma (tavsiye edilmez) web sitesi işlevselliğini etkiler, otomatik tamamlamayı devre dışı bırakır ve bazı işlevleri yavaşlatır. JavaScript'i etkin tutmak gizliliğinizi tehlikeye atmaz, çünkü istekler sunucu tarafındaki uç noktalardan işlenir.", 37 | "save_settings": "Ayarları Kaydet", 38 | "return_to_settings": "Ayarlar'a Dön", 39 | "preferredux_language": "{araa_name}'dan Başlık, Düğmeler ve Diğer Metinler için Tercih Edilen Dil", 40 | "on": "açık", 41 | "off": "kapalı", 42 | "any_language": "Herhangi bir dil" 43 | }, 44 | "api_links": { 45 | "api_link": "API", 46 | "suggestions_api_link": "Öneriler API'si" 47 | } 48 | } -------------------------------------------------------------------------------- /static/css/dark.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --html-bg: #1c1c1c; 3 | --font-fg: #f1f3f4; 4 | --fg: #BABCBE; 5 | 6 | --search-bg: #161616; 7 | --search-bg-input: #333333; 8 | --search-bg-input-border: #3C4043; 9 | --search-select: #282828; 10 | 11 | --border: #303134; 12 | 13 | --link: #8ab4f8; 14 | --link-visited: #c58af9; 15 | 16 | --snip-border: #303134; 17 | --snip-background: #282828; 18 | --snip-text: #f1f3f4; 19 | 20 | --settings-border: #5f6368; 21 | --button: #333333; 22 | 23 | --footer-bg: #161616; 24 | --footer-font: #999da2; 25 | 26 | --highlight: #bcc0c3; 27 | 28 | --blue: #8ab4f8; 29 | 30 | --green: #31b06e; 31 | 32 | --search-button: #BABCBE; 33 | 34 | --image-view: #161616; 35 | --image-view-titlebar: #161616; 36 | --view-image-color: #000000; 37 | --image-select: #303030; 38 | --fff: #fff; 39 | 40 | --publish-info: #7f869e; 41 | 42 | color-scheme: dark; 43 | } 44 | 45 | .calc-btn:hover { 46 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); 47 | } 48 | 49 | .calc-btn-2:hover { 50 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); 51 | } 52 | 53 | .calc-btn-2 { 54 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 55 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1); 56 | } 57 | 58 | .calc-btn { 59 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 60 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1); 61 | } 62 | 63 | .calc { 64 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); 65 | } 66 | 67 | .view-image-search { 68 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 69 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1); 70 | } 71 | 72 | .view-image-search:hover { 73 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); 74 | } 75 | -------------------------------------------------------------------------------- /static/css/darker.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --html-bg: #0f0f0f; 3 | --font-fg: #f1f3f4; 4 | --fg: #BABCBE; 5 | 6 | --search-bg: #0f0f0f; 7 | --search-bg-input: #121212; 8 | --search-bg-input-border: #303030; 9 | --search-select: #272727; 10 | 11 | --border: #303030; 12 | 13 | --link: #8ab4f8; 14 | --link-visited: #c58af9; 15 | 16 | --snip-border: #303030; 17 | --snip-background: #0f0f0f; 18 | --snip-text: #f1f3f4; 19 | 20 | --settings-border: #303030; 21 | --button: #272727; 22 | 23 | --footer-bg: #0f0f0f; 24 | --footer-font: #BABCBE; 25 | 26 | --highlight: #bcc0c3; 27 | 28 | --blue: #8ab4f8; 29 | 30 | --green: #31b06e; 31 | 32 | --image-view: #0f0f0f; 33 | --image-view-titlebar: #0f0f0f; 34 | --view-image-color: #000000; 35 | --image-select: #303030; 36 | --fff: #fff; 37 | 38 | --search-button: #BABCBE; 39 | 40 | --publish-info: #7f869e; 41 | 42 | color-scheme: dark; 43 | } 44 | 45 | .calc-btn:hover { 46 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); 47 | } 48 | 49 | .calc-btn-2:hover { 50 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); 51 | } 52 | 53 | .calc-btn-2 { 54 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 55 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1); 56 | } 57 | 58 | .calc-btn { 59 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 60 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1); 61 | } 62 | 63 | .calc { 64 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); 65 | } 66 | 67 | .view-image-search { 68 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 69 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1); 70 | } 71 | 72 | .view-image-search:hover { 73 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); 74 | } 75 | -------------------------------------------------------------------------------- /static/css/classic.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --html-bg: #202124; 3 | --font-fg: #f1f3f4; 4 | --fg: #BABCBE; 5 | 6 | --search-bg: #202124; 7 | --search-bg-input: #303134; 8 | --search-bg-input-border: #3C4043; 9 | --search-select: #3c4043; 10 | 11 | --border: #3c4043; 12 | 13 | --link: #8ab4f8; 14 | --link-visited: #c58af9; 15 | 16 | --snip-border: #3c4043; 17 | --snip-background: #202124; 18 | --snip-text: #f1f3f4; 19 | 20 | --settings-border: #5f6368; 21 | --button: #303134; 22 | 23 | --footer-bg: #171717; 24 | --footer-font: #BABCBE; 25 | 26 | --highlight: #bcc0c3; 27 | 28 | --blue: #8ab4f8; 29 | 30 | --green: #31b06e; 31 | 32 | --search-button: #BABCBE; 33 | 34 | --image-view: #171717; 35 | --image-view-titlebar: #171717; 36 | --view-image-color: #000000; 37 | --image-select: #303030; 38 | --fff: #fff; 39 | 40 | --publish-info: #7f869e; 41 | 42 | color-scheme: dark; 43 | } 44 | 45 | .calc-btn:hover { 46 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); 47 | } 48 | 49 | .calc-btn-2:hover { 50 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); 51 | } 52 | 53 | .calc-btn-2 { 54 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 55 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1); 56 | } 57 | 58 | .calc-btn { 59 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 60 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1); 61 | } 62 | 63 | .calc { 64 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); 65 | } 66 | 67 | .view-image-search { 68 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 69 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1); 70 | } 71 | 72 | .view-image-search:hover { 73 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); 74 | } 75 | -------------------------------------------------------------------------------- /static/css/github_night.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --html-bg: #0d1117; 3 | --font-fg: #f0f6fc; 4 | --fg: #8b949e; 5 | 6 | --search-bg: #161b22; 7 | --search-bg-input: #1f242b; 8 | --search-bg-input-border: #303842; 9 | --search-select: #316dca; 10 | 11 | --border: #303842; 12 | 13 | --link: #58a6ff; 14 | --link-visited: #bd93f9; 15 | 16 | --snip-border: #303842; 17 | --snip-background: #161b22; 18 | --snip-text: #f1f3f4; 19 | 20 | --settings-border: #303842; 21 | --button: #2b3036; 22 | 23 | --footer-bg: #161b22; 24 | --footer-font: #8b949e; 25 | 26 | --highlight: #e6edf3; 27 | 28 | --blue: #8ab4f8; 29 | 30 | --green: #31b06e; 31 | 32 | --image-view: #161b22; 33 | --image-view-titlebar: #161b22; 34 | --view-image-color: #000000; 35 | --image-select: #303030; 36 | --fff: #fff; 37 | 38 | --search-button: #BABCBE; 39 | 40 | --publish-info: #7f869e; 41 | 42 | color-scheme: dark; 43 | } 44 | 45 | .calc-btn:hover { 46 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); 47 | } 48 | 49 | .calc-btn-2:hover { 50 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); 51 | } 52 | 53 | .calc-btn-2 { 54 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 55 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1); 56 | } 57 | 58 | .calc-btn { 59 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 60 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1); 61 | } 62 | 63 | .calc { 64 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); 65 | } 66 | 67 | .view-image-search { 68 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 69 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1); 70 | } 71 | 72 | .view-image-search:hover { 73 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); 74 | } 75 | -------------------------------------------------------------------------------- /static/css/night.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --html-bg: #171b25; 3 | --font-fg: #ebecf7; 4 | --fg: #ebecf7; 5 | 6 | --search-bg: #0c0d0f; 7 | --search-bg-input: #2e3443; 8 | --search-bg-input-border: rgb(46, 52, 67); 9 | --search-select: #3a445c; 10 | 11 | --border: rgb(46, 52, 67); 12 | 13 | --link: #a7b1fc; 14 | --link-visited: #ad71bc; 15 | 16 | --snip-border: rgb(46, 52, 67); 17 | --snip-background: #1e222d; 18 | --snip-text: #f1f3f4; 19 | 20 | --settings-border: #5f6368; 21 | --button: #0c0d0f; 22 | 23 | --footer-bg: #0c0d0f; 24 | --footer-font: #ebecf7; 25 | 26 | --highlight: #ebecf7; 27 | 28 | --blue: #8ab4f8; 29 | 30 | --green: #31b06e; 31 | 32 | --image-view: #0c0d0f; 33 | --image-view-titlebar: #0c0d0f; 34 | --view-image-color: #000000; 35 | --image-select: #303030; 36 | --fff: #fff; 37 | 38 | --search-button: #BABCBE; 39 | 40 | --publish-info: #7f869e; 41 | 42 | color-scheme: dark; 43 | } 44 | 45 | .calc-btn:hover { 46 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); 47 | } 48 | 49 | .calc-btn-2:hover { 50 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); 51 | } 52 | 53 | .calc-btn-2 { 54 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 55 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1); 56 | } 57 | 58 | .calc-btn { 59 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 60 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1); 61 | } 62 | 63 | .calc { 64 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); 65 | } 66 | 67 | .view-image-search { 68 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 69 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1); 70 | } 71 | 72 | .view-image-search:hover { 73 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); 74 | } 75 | -------------------------------------------------------------------------------- /static/lang/russian.json: -------------------------------------------------------------------------------- 1 | { 2 | "buttons": { 3 | "text": "Веб", 4 | "image": "Изображения", 5 | "video": "Видео", 6 | "reddit": "Reddit", 7 | "torrent": "Торренты", 8 | "next": "Следующий", 9 | "previous": "Предыдущий" 10 | }, 11 | "results": { 12 | "results": "Результаты получены за", 13 | "seconds": "секунд" 14 | }, 15 | "footer": { 16 | "settings": "Настройки", 17 | "source_code": "Исходный код", 18 | "commit": "Подтверждение", 19 | "donate": "Пожертвовать" 20 | }, 21 | "search_buttons": { 22 | "search_text": "Искать с {araa_name}", 23 | "search_images": "Искать изображения с {araa_name}" 24 | }, 25 | "settings": { 26 | "theme": "Тема", 27 | "user_theme": "Тема пользователя", 28 | "all_settings": "Все настройки", 29 | "settings_header": "Настройки", 30 | "discover_themes": "Просматривайте темы, созданные сообществом", 31 | "discover_themes_button": "Открывайте темы", 32 | "preferred_language": "Предпочтительный язык для ваших результатов поиска", 33 | "google_domain": "Домен Google", 34 | "safe_search": "Безопасный поиск", 35 | "open_links_new_tab": "Открывать ссылки в новой вкладке", 36 | "disable_javascript": "Отключение JavaScript (не рекомендуется) влияет на функциональность веб-сайта, отключает автозаполнение и замедляет некоторые функции. Включение JavaScript не ущемляет вашу конфиденциальность, поскольку запросы обрабатываются через серверные точки.", 37 | "save_settings": "Сохранить настройки", 38 | "return_to_settings": "Вернуться к настройкам", 39 | "preferredux_language": "Предпочтительный язык для заголовков, кнопок и другого текста из {araa_name}", 40 | "on": "Включено", 41 | "off": "Выключено", 42 | "any_language": "Любой язык" 43 | }, 44 | "api_links": { 45 | "api_link": "API", 46 | "suggestions_api_link": "API предложений" 47 | } 48 | } -------------------------------------------------------------------------------- /static/lang/dutch.json: -------------------------------------------------------------------------------- 1 | { 2 | "buttons": { 3 | "text": "Web", 4 | "image": "Afbeeldingen", 5 | "video": "Video's", 6 | "reddit": "Reddit", 7 | "torrent": "Torrents", 8 | "next": "Volgende", 9 | "previous": "Vorige" 10 | }, 11 | "results": { 12 | "results": "Resultaten opgehaald in", 13 | "seconds": "seconden" 14 | }, 15 | "footer": { 16 | "settings": "Instellingen", 17 | "source_code": "Broncode", 18 | "commit": "Commit", 19 | "donate": "Doneren" 20 | }, 21 | "search_buttons": { 22 | "search_text": "Zoeken met {araa_name}", 23 | "search_images": "Zoek afbeeldingen met {araa_name}" 24 | }, 25 | "settings": { 26 | "theme": "Thema", 27 | "user_theme": "Gebruikersthema", 28 | "all_settings": "Alle instellingen", 29 | "settings_header": "Instellingen", 30 | "discover_themes": "Blader door thema's gemaakt door de gemeenschap", 31 | "discover_themes_button": "Thema's ontdekken", 32 | "preferred_language": "Voorkeurstaal voor uw zoekresultaten", 33 | "google_domain": "Google-domein", 34 | "safe_search": "Veilig zoeken", 35 | "open_links_new_tab": "Links in een nieuw tabblad openen", 36 | "disable_javascript": "Het uitschakelen van JavaScript (niet aanbevolen) heeft invloed op websitefunctionaliteiten, schakelt automatisch aanvullen uit en vertraagt bepaalde functies. Het ingeschakeld houden van JavaScript compromitteert uw privacy niet, aangezien verzoeken worden verwerkt via serverzijde endpoints.", 37 | "save_settings": "Instellingen opslaan", 38 | "return_to_settings": "Terug naar instellingen", 39 | "preferredux_language": "Voorkeurstaal voor Koppen, Knoppen en Andere Tekst van {araa_name}", 40 | "on": "aan", 41 | "off": "uit", 42 | "any_language": "Elke taal" 43 | }, 44 | "api_links": { 45 | "api_link": "API", 46 | "suggestions_api_link": "Suggesties API" 47 | } 48 | } -------------------------------------------------------------------------------- /static/lang/ukrainian.json: -------------------------------------------------------------------------------- 1 | { 2 | "buttons": { 3 | "text": "Веб", 4 | "image": "Зображення", 5 | "video": "Відео", 6 | "reddit": "Reddit", 7 | "torrent": "Торренти", 8 | "next": "Наступний", 9 | "previous": "Попередній" 10 | }, 11 | "results": { 12 | "results": "Результати отримано за", 13 | "seconds": "секунд" 14 | }, 15 | "footer": { 16 | "settings": "Налаштування", 17 | "source_code": "Вихідний код", 18 | "commit": "Підтвердження", 19 | "donate": "Пожертвувати" 20 | }, 21 | "search_buttons": { 22 | "search_text": "Пошук за допомогою {araa_name}", 23 | "search_images": "Пошук зображень за допомогою {araa_name}" 24 | }, 25 | "settings": { 26 | "theme": "Тема", 27 | "user_theme": "Тема користувача", 28 | "all_settings": "Усі налаштування", 29 | "settings_header": "Налаштування", 30 | "discover_themes": "Переглядайте теми, створені спільнотою", 31 | "discover_themes_button": "Відкривайте теми", 32 | "preferred_language": "Улюблена мова для ваших результатів пошуку", 33 | "google_domain": "Домен Google", 34 | "safe_search": "Безпечний пошук", 35 | "open_links_new_tab": "Відкривати посилання в новій вкладці", 36 | "disable_javascript": "Вимкнення JavaScript (не рекомендується) впливає на функціональність веб-сайту, вимикає автозаповнення і сповільнює деякі функції. Включення JavaScript не порушує вашу конфіденційність, оскільки запити обробляються через серверні кінцеві точки.", 37 | "save_settings": "Зберегти налаштування", 38 | "return_to_settings": "Повернутися до налаштувань", 39 | "preferredux_language": "Обрана мова для заголовків, кнопок та іншого тексту від {araa_name}", 40 | "on": "ввімкнено", 41 | "off": "вимкнено", 42 | "any_language": "Будь-яка мова" 43 | }, 44 | "api_links": { 45 | "api_link": "API", 46 | "suggestions_api_link": "API пропозицій" 47 | } 48 | } -------------------------------------------------------------------------------- /static/lang/romanian.json: -------------------------------------------------------------------------------- 1 | { 2 | "buttons": { 3 | "text": "Web", 4 | "image": "Imagini", 5 | "video": "Videoclipuri", 6 | "reddit": "Reddit", 7 | "torrent": "Torrente", 8 | "next": "Următor", 9 | "previous": "Înainte" 10 | }, 11 | "results": { 12 | "results": "A obținut rezultatele în", 13 | "seconds": "secunde" 14 | }, 15 | "footer": { 16 | "settings": "Setări", 17 | "source_code": "Cod sursă", 18 | "commit": "Commit", 19 | "donate": "Donați" 20 | }, 21 | "search_buttons": { 22 | "search_text": "Căutare cu {araa_name}", 23 | "search_images": "Caută imagini cu {araa_name}" 24 | }, 25 | "settings": { 26 | "theme": "Temă", 27 | "user_theme": "Tema userului", 28 | "all_settings": "Toate setările", 29 | "settings_header": "Setări", 30 | "discover_themes": "Explorați temele create de comunitate", 31 | "discover_themes_button": "Descoperă temele", 32 | "preferred_language": "Limba preferată pentru rezultatele căutării", 33 | "google_domain": "Domeniul Google", 34 | "safe_search": "Căutare sigură", 35 | "open_links_new_tab": "Deschideți linkurile într-o filă nouă", 36 | "disable_javascript": "Dezactivarea JavaScript (nerecomandată) afectează caracteristicile paginilor web, dezactivează completarea automată și încetinește anumite funcții. Menținerea activării JavaScript nu vă va compromite confidențialitatea, deoarece solicitările sunt procesate prin intermediul unor puncte finale de pe server.", 37 | "save_settings": "Salvați setările", 38 | "return_to_settings": "Reveniți la setări", 39 | "preferredux_language": "Limba preferată pentru titlurile, butoanele și alte texte din {araa_name}", 40 | "on": "activat", 41 | "off": "dezactivat", 42 | "any_language": "Orice limbă" 43 | }, 44 | "api_links": { 45 | "api_link": "API", 46 | "suggestions_api_link": "Sugestii API" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /static/lang/italian.json: -------------------------------------------------------------------------------- 1 | { 2 | "buttons": { 3 | "text": "Web", 4 | "image": "Immagini", 5 | "video": "Video", 6 | "reddit": "Reddit", 7 | "torrent": "Torrent", 8 | "next": "Avanti", 9 | "previous": "Precedente" 10 | }, 11 | "results": { 12 | "results": "Risultati ottenuti in", 13 | "seconds": "secondi" 14 | }, 15 | "footer": { 16 | "settings": "Impostazioni", 17 | "source_code": "Codice sorgente", 18 | "commit": "Conferma", 19 | "donate": "Donare" 20 | }, 21 | "search_buttons": { 22 | "search_text": "Cerca con {araa_name}", 23 | "search_images": "Cerca immagini con {araa_name}" 24 | }, 25 | "settings": { 26 | "theme": "Tema", 27 | "user_theme": "Tema dell'utente", 28 | "all_settings": "Tutte le impostazioni", 29 | "settings_header": "Impostazioni", 30 | "discover_themes": "Sfoglia i temi creati dalla comunità", 31 | "discover_themes_button": "Scopri i temi", 32 | "preferred_language": "Lingua preferita per i tuoi risultati di ricerca", 33 | "google_domain": "Dominio Google", 34 | "safe_search": "Ricerca sicura", 35 | "open_links_new_tab": "Apri i collegamenti in una nuova scheda", 36 | "disable_javascript": "La disabilitazione di JavaScript (non raccomandata) influisce sulle funzionalità del sito, disabilita il completamento automatico e rallenta alcune funzioni. Mantenere JavaScript abilitato non compromette la privacy, poiché le richieste vengono elaborate tramite endpoint lato server.", 37 | "save_settings": "Salva le impostazioni", 38 | "return_to_settings": "Torna alle impostazioni", 39 | "preferredux_language": "Lingua preferita per titoli, pulsanti e altri testi da {araa_name}", 40 | "on": "acceso", 41 | "off": "spento", 42 | "any_language": "Qualsiasi Lingua" 43 | }, 44 | "api_links": { 45 | "api_link": "API", 46 | "suggestions_api_link": "API di suggerimenti" 47 | } 48 | } -------------------------------------------------------------------------------- /static/lang/german.json: -------------------------------------------------------------------------------- 1 | { 2 | "buttons": { 3 | "text": "Web", 4 | "image": "Bilder", 5 | "video": "Videos", 6 | "reddit": "Reddit", 7 | "torrent": "Torrents", 8 | "next": "Weiter", 9 | "previous": "Zurück" 10 | }, 11 | "results": { 12 | "results": "Ergebnisse abgerufen in", 13 | "seconds": "Sekunden" 14 | }, 15 | "footer": { 16 | "settings": "Einstellungen", 17 | "source_code": "Quellcode", 18 | "commit": "Bestätigung", 19 | "donate": "Spenden" 20 | }, 21 | "search_buttons": { 22 | "search_text": "Suchen mit {araa_name}", 23 | "search_images": "Suche Bilder mit {araa_name}" 24 | }, 25 | "settings": { 26 | "theme": "Thema", 27 | "user_theme": "Benutzerthema", 28 | "all_settings": "Alle Einstellungen", 29 | "settings_header": "Einstellungen", 30 | "discover_themes": "Durchsuchen Sie von der Gemeinschaft erstellte Themen", 31 | "discover_themes_button": "Themen entdecken", 32 | "preferred_language": "Bevorzugte Sprache für Ihre Suchergebnisse", 33 | "google_domain": "Google-Domain", 34 | "safe_search": "Sichere Suche", 35 | "open_links_new_tab": "Links in einem neuen Tab öffnen", 36 | "disable_javascript": "Das Deaktivieren von JavaScript (nicht empfohlen) beeinträchtigt Webseitenfunktionen, deaktiviert die Autovervollständigung und verlangsamt bestimmte Funktionen. Das Aktivieren von JavaScript beeinträchtigt nicht Ihre Privatsphäre, da Anfragen über serverseitige Endpunkte verarbeitet werden.", 37 | "save_settings": "Einstellungen speichern", 38 | "return_to_settings": "Zurück zu den Einstellungen", 39 | "preferredux_language": "Bevorzugte Sprache für Überschriften, Schaltflächen und anderen Text von {araa_name}", 40 | "on": "Ein", 41 | "off": "Aus", 42 | "any_language": "Jede Sprache" 43 | }, 44 | "api_links": { 45 | "api_link": "API", 46 | "suggestions_api_link": "Vorschläge API" 47 | } 48 | } -------------------------------------------------------------------------------- /static/css/dark_default.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --html-bg: #171717; 3 | --font-fg: #f1f3f4; 4 | --fg: #BABCBE; 5 | 6 | --search-bg: #101010; 7 | --search-bg-input: #202020; 8 | --search-bg-input-border: #2a2a2a; 9 | --search-select: #303030; 10 | 11 | --border: #2a2a2a; 12 | 13 | --link: #8ab4f8; 14 | --link-visited: #c58af9; 15 | 16 | --snip-border: #282828; 17 | --snip-background: #202020; 18 | --snip-text: #f1f3f4; 19 | 20 | --settings-border: #5f6368; 21 | --button: #202020; 22 | 23 | --footer-bg: #101010; 24 | --footer-font: #999da2; 25 | 26 | --highlight: #bcc0c3; 27 | 28 | --blue: #8ab4f8; 29 | 30 | --green: #31b06e; 31 | 32 | --search-button: #BABCBE; 33 | 34 | --image-view: #101010; 35 | --image-view-titlebar: #101010; 36 | --view-image-color: #000000; 37 | --image-select: #303030; 38 | --fff: #fff; 39 | 40 | --publish-info: #7f869e; 41 | 42 | color-scheme: dark; 43 | } 44 | 45 | .snip { 46 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); 47 | } 48 | 49 | .calc-btn:hover { 50 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); 51 | } 52 | 53 | .calc-btn-2:hover { 54 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); 55 | } 56 | 57 | .calc-btn-2 { 58 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 59 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1); 60 | } 61 | 62 | .calc-btn { 63 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 64 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1); 65 | } 66 | 67 | .calc { 68 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); 69 | } 70 | 71 | .view-image-search { 72 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 73 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1); 74 | } 75 | 76 | .view-image-search:hover { 77 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); 78 | } -------------------------------------------------------------------------------- /static/lang/polish.json: -------------------------------------------------------------------------------- 1 | { 2 | "buttons": { 3 | "text": "Strona internetowa", 4 | "image": "Zdjęcia", 5 | "video": "Filmy", 6 | "reddit": "Reddit", 7 | "torrent": "Torrenty", 8 | "next": "Następny", 9 | "previous": "Poprzedni" 10 | }, 11 | "results": { 12 | "results": "Wyniki uzyskano w", 13 | "seconds": "sekundy" 14 | }, 15 | "footer": { 16 | "settings": "Ustawienia", 17 | "source_code": "Kod źródłowy", 18 | "commit": "Zatwierdzenie", 19 | "donate": "Przekaż darowiznę" 20 | }, 21 | "search_buttons": { 22 | "search_text": "Szukaj z {araa_name}", 23 | "search_images": "Szukaj obrazów z {araa_name}" 24 | }, 25 | "settings": { 26 | "theme": "Motyw", 27 | "user_theme": "Motyw użytkownika", 28 | "all_settings": "Wszystkie ustawienia", 29 | "settings_header": "Ustawienia", 30 | "discover_themes": "Przeglądaj motywy stworzone przez społeczność", 31 | "discover_themes_button": "Odkryj motywy", 32 | "preferred_language": "Preferowany język dla wyników wyszukiwania", 33 | "google_domain": "Domena Google", 34 | "safe_search": "Bezpieczne wyszukiwanie", 35 | "open_links_new_tab": "Otwieraj linki w nowej karcie", 36 | "disable_javascript": "Wyłączenie JavaScript (niezalecane) wpływa na funkcje strony internetowej, dezaktywuje autouzupełnianie i spowalnia niektóre funkcje. Pozostawienie JavaScript włączonego nie naruszy Twojej prywatności, ponieważ zapytania są przetwarzane za pośrednictwem punktów końcowych po stronie serwera.", 37 | "save_settings": "Zapisz ustawienia", 38 | "return_to_settings": "Powrót do ustawień", 39 | "preferredux_language": "Preferowany język dla nagłówków, przycisków i innych tekstów z {araa_name}", 40 | "on": "włączony", 41 | "off": "wyłączony", 42 | "any_language": "Dowolny język" 43 | }, 44 | "api_links": { 45 | "api_link": "API", 46 | "suggestions_api_link": "API sugestii" 47 | } 48 | } -------------------------------------------------------------------------------- /static/css/dark_blur.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --html-bg: #171717; 3 | --font-fg: #f1f3f4; 4 | --fg: #BABCBE; 5 | 6 | --search-bg: #101010; 7 | --search-bg-input: #202020; 8 | --search-bg-input-border: #2a2a2a; 9 | --search-select: #303030; 10 | 11 | --border: #2a2a2a; 12 | 13 | --link: #8ab4f8; 14 | --link-visited: #c58af9; 15 | 16 | --snip-border: #282828; 17 | --snip-background: #202020; 18 | --snip-text: #f1f3f4; 19 | 20 | --settings-border: #5f6368; 21 | --button: #202020; 22 | 23 | --footer-bg: #101010; 24 | --footer-font: #999da2; 25 | 26 | --highlight: #bcc0c3; 27 | 28 | --blue: #8ab4f8; 29 | 30 | --green: #31b06e; 31 | 32 | --search-button: #BABCBE; 33 | 34 | --image-view: #101010; 35 | --image-view-titlebar: #101010; 36 | --view-image-color: #000000; 37 | --image-select: #303030; 38 | --fff: #fff; 39 | 40 | --publish-info: #7f869e; 41 | 42 | color-scheme: dark; 43 | } 44 | 45 | .snip { 46 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); 47 | } 48 | 49 | .calc-btn:hover { 50 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); 51 | } 52 | 53 | .calc-btn-2:hover { 54 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); 55 | } 56 | 57 | .calc-btn-2 { 58 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 59 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1); 60 | } 61 | 62 | .calc-btn { 63 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 64 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1); 65 | } 66 | 67 | .calc { 68 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); 69 | } 70 | 71 | .view-image-search { 72 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 73 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1); 74 | } 75 | 76 | .view-image-search:hover { 77 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); 78 | } 79 | -------------------------------------------------------------------------------- /static/lang/portuguese.json: -------------------------------------------------------------------------------- 1 | { 2 | "buttons": { 3 | "text": "Web", 4 | "image": "Imagens", 5 | "video": "Vídeos", 6 | "reddit": "Reddit", 7 | "torrent": "Torrents", 8 | "next": "Próximo", 9 | "previous": "Anterior" 10 | }, 11 | "results": { 12 | "results": "Resultados obtidos em", 13 | "seconds": "segundos" 14 | }, 15 | "footer": { 16 | "settings": "Configurações", 17 | "source_code": "Código-fonte", 18 | "commit": "Confirmação", 19 | "donate": "Doar" 20 | }, 21 | "search_buttons": { 22 | "search_text": "Pesquisar com {araa_name}", 23 | "search_images": "Pesquisar imagens com {araa_name}" 24 | }, 25 | "settings": { 26 | "theme": "Tema", 27 | "user_theme": "Tema do usuário", 28 | "all_settings": "Todas as configurações", 29 | "settings_header": "Configurações", 30 | "discover_themes": "Navegue por temas criados pela comunidade", 31 | "discover_themes_button": "Descobrir Temas", 32 | "preferred_language": "Idioma preferido para seus resultados de pesquisa", 33 | "google_domain": "Domínio do Google", 34 | "safe_search": "Busca Segura", 35 | "open_links_new_tab": "Abrir links em uma nova aba", 36 | "disable_javascript": "Desativar JavaScript (não recomendado) afeta as funcionalidades do site, desativa o preenchimento automático e diminui a velocidade de certas funções. Manter o JavaScript habilitado não compromete sua privacidade, pois as solicitações são processadas por meio de pontos de extremidade no servidor.", 37 | "save_settings": "Salvar configurações", 38 | "return_to_settings": "Voltar para as configurações", 39 | "preferredux_language": "Idioma preferido para manchetes, botões e outros textos do {araa_name}", 40 | "on": "ligado", 41 | "off": "desligado", 42 | "any_language": "Qualquer Idioma" 43 | }, 44 | "api_links": { 45 | "api_link": "API", 46 | "suggestions_api_link": "API de Sugestões" 47 | } 48 | } -------------------------------------------------------------------------------- /static/lang/spanish.json: -------------------------------------------------------------------------------- 1 | { 2 | "buttons": { 3 | "text": "Web", 4 | "image": "Imágenes", 5 | "video": "Videos", 6 | "reddit": "Reddit", 7 | "torrent": "Torrents", 8 | "next": "Siguiente", 9 | "previous": "Anterior" 10 | }, 11 | "results": { 12 | "results": "Resultados obtenidos en", 13 | "seconds": "segundos" 14 | }, 15 | "footer": { 16 | "settings": "Configuración", 17 | "source_code": "Código fuente", 18 | "commit": "Confirmación", 19 | "donate": "Donar" 20 | }, 21 | "search_buttons": { 22 | "search_text": "Buscar con {araa_name}", 23 | "search_images": "Buscar imágenes con {araa_name}" 24 | }, 25 | "settings": { 26 | "theme": "Tema", 27 | "user_theme": "Tema de usuario", 28 | "all_settings": "Todas las configuraciones", 29 | "settings_header": "Configuración", 30 | "discover_themes": "Explorar temas creados por la comunidad", 31 | "discover_themes_button": "Descubrir temas", 32 | "preferred_language": "Idioma preferido para tus resultados de búsqueda", 33 | "google_domain": "Dominio de Google", 34 | "safe_search": "Búsqueda segura", 35 | "open_links_new_tab": "Abrir enlaces en una nueva pestaña", 36 | "disable_javascript": "La desactivación de JavaScript (no recomendada) afecta las funciones del sitio web, deshabilita el autocompletado y ralentiza ciertas funciones. Mantener JavaScript habilitado no compromete su privacidad, ya que las solicitudes se procesan a través de puntos finales del lado del servidor.", 37 | "save_settings": "Guardar configuraciones", 38 | "return_to_settings": "Volver a la configuración", 39 | "preferredux_language": "Idioma preferido para encabezados, botones y otros textos de {araa_name}", 40 | "on": "os", 41 | "off": "off", 42 | "any_language": "Cualquier idioma" 43 | }, 44 | "api_links": { 45 | "api_link": "API", 46 | "suggestions_api_link": "API de Sugerencias" 47 | } 48 | } -------------------------------------------------------------------------------- /static/lang/french_canadian.json: -------------------------------------------------------------------------------- 1 | { 2 | "buttons": { 3 | "text": "Web", 4 | "image": "Images", 5 | "video": "Vidéos", 6 | "reddit": "Reddit", 7 | "torrent": "Torrents", 8 | "next": "Suivant", 9 | "previous": "Précédent" 10 | }, 11 | "results": { 12 | "results": "Résultats obtenus en", 13 | "seconds": "secondes" 14 | }, 15 | "footer": { 16 | "settings": "Paramètres", 17 | "source_code": "Code source", 18 | "commit": "Validation", 19 | "donate": "Donner" 20 | }, 21 | "search_buttons": { 22 | "search_text": "Rechercher avec {araa_name}", 23 | "search_images": "Rechercher des images avec {araa_name}" 24 | }, 25 | "settings": { 26 | "theme": "Thème", 27 | "user_theme": "Thème de l'utilisateur", 28 | "all_settings": "Tous les paramètres", 29 | "settings_header": "Paramètres", 30 | "discover_themes": "Parcourez les thèmes créés par la communauté", 31 | "discover_themes_button": "Découvrir les thèmes", 32 | "preferred_language": "Langue préférée pour vos résultats de recherche", 33 | "google_domain": "Domaine Google", 34 | "safe_search": "Recherche sécurisée", 35 | "open_links_new_tab": "Ouvrir les liens dans un nouvel onglet", 36 | "disable_javascript": "La désactivation de JavaScript (non recommandée) affecte les fonctionnalités du site, désactive l'autocomplétion et ralentit certaines fonctions. Activer JavaScript ne compromet pas votre vie privée, car les demandes sont traitées via des points de terminaison côté serveur.", 37 | "save_settings": "Enregistrer les paramètres", 38 | "return_to_settings": "Revenir aux paramètres", 39 | "preferredux_language": "Langue préférée pour les titres, les boutons et autres textes provenant d'{araa_name}", 40 | "on": "activé", 41 | "off": "désactivé", 42 | "any_language": "Toute langue" 43 | }, 44 | "api_links": { 45 | "api_link": "API", 46 | "suggestions_api_link": "API de suggestions" 47 | } 48 | } -------------------------------------------------------------------------------- /static/lang/french.json: -------------------------------------------------------------------------------- 1 | { 2 | "buttons": { 3 | "text": "Web", 4 | "image": "Images", 5 | "video": "Vidéos", 6 | "reddit": "Reddit", 7 | "torrent": "Torrents", 8 | "next": "Suivant", 9 | "previous": "Précédent" 10 | }, 11 | "results": { 12 | "results": "Résultats obtenus en", 13 | "seconds": "secondes" 14 | }, 15 | "footer": { 16 | "settings": "Paramètres", 17 | "source_code": "Code source", 18 | "commit": "Validation", 19 | "donate": "Donner" 20 | }, 21 | "search_buttons": { 22 | "search_text": "Rechercher avec {araa_name}", 23 | "search_images": "Rechercher des images avec {araa_name}" 24 | }, 25 | "settings": { 26 | "theme": "Thème", 27 | "user_theme": "Thème de l'utilisateur", 28 | "all_settings": "Tous les paramètres", 29 | "settings_header": "Paramètres", 30 | "discover_themes": "Parcourir les thèmes créés par la communauté", 31 | "discover_themes_button": "Découvrir les thèmes", 32 | "preferred_language": "Langue préférée pour les résultats de recherche", 33 | "google_domain": "Domaine Google", 34 | "safe_search": "Filtrage SafeSearch", 35 | "open_links_new_tab": "Ouvrir les liens dans un nouvel onglet", 36 | "disable_javascript": "La désactivation de JavaScript (non recommandée) affecte les fonctionnalités des pages Web, désactive l'autocomplétion et ralentit certaines fonctions. Le maintien de JavaScript activé ne compromettra pas votre vie privée, car les requêtes sont traitées via des points de terminaison côté serveur.", 37 | "save_settings": "Enregistrer vos paramètres", 38 | "return_to_settings": "Retour aux paramètres", 39 | "preferredux_language": "Langue préférée pour les titres, les boutons et autres textes provenant d'{araa_name}", 40 | "on": "activé", 41 | "off": "désactivé", 42 | "any_language": "Toute langue" 43 | }, 44 | "api_links": { 45 | "api_link": "API", 46 | "suggestions_api_link": "API de suggestions" 47 | } 48 | } -------------------------------------------------------------------------------- /static/css/red.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --html-bg: #7d0011; 3 | --font-fg: #ffffff; 4 | --fg: #ffffff; 5 | 6 | --search-bg: #0f0f0f; 7 | --search-bg-input: #121212; 8 | --search-bg-input-border: #303030; 9 | --search-select: #111111; 10 | 11 | --border: #303030; 12 | 13 | --link: #8ab4f8; 14 | --link-visited: #c58af9; 15 | 16 | --snip-border: #303030; 17 | --snip-background: #0f0f0f; 18 | --snip-text: #ffffff; 19 | 20 | --settings-border: #303030; 21 | --button: #111111; 22 | 23 | --footer-bg: #0f0f0f; 24 | --footer-font: #ffffff; 25 | 26 | --highlight: #ofofof; 27 | 28 | --blue: #007032; 29 | 30 | --green: #31b06e; 31 | 32 | --image-view: #0f0f0f; 33 | --image-view-titlebar: #0f0f0f; 34 | --view-image-color: #000000; 35 | --image-select: #303030; 36 | --fff: #fff; 37 | 38 | --search-button: #ffffff; 39 | 40 | --publish-info: #7f869e; 41 | 42 | color-scheme: dark; 43 | } 44 | 45 | .results { 46 | background: #0f0f0f; 47 | border: 7px solid #0f0f0f; 48 | 49 | } 50 | 51 | .result_sublink { 52 | background: #0f0f0f; 53 | border: 8px solid #0f0f0f; 54 | } 55 | 56 | .calc-btn:hover { 57 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); 58 | } 59 | 60 | .calc-btn-2:hover { 61 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); 62 | } 63 | 64 | .calc-btn-2 { 65 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 66 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1); 67 | } 68 | 69 | .calc-btn { 70 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 71 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1); 72 | } 73 | 74 | .calc { 75 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); 76 | } 77 | 78 | .view-image-search { 79 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 80 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1); 81 | } 82 | 83 | .view-image-search:hover { 84 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); 85 | } 86 | -------------------------------------------------------------------------------- /static/lang/greek.json: -------------------------------------------------------------------------------- 1 | { 2 | "buttons": { 3 | "text": "Ιστοσελίδες", 4 | "image": "Εικόνες", 5 | "video": "Βίντεο", 6 | "reddit": "Reddit", 7 | "torrent": "Torrents", 8 | "next": "Επόμενο", 9 | "previous": "Προηγούμενο" 10 | }, 11 | "results": { 12 | "results": "Τα αποτελέσματα λήφθηκαν σε", 13 | "seconds": "δευτερόλεπτα" 14 | }, 15 | "footer": { 16 | "settings": "Ρυθμίσεις", 17 | "source_code": "Κώδικας Πηγής", 18 | "commit": "Καταχώρηση", 19 | "donate": "Δωρίστε" 20 | }, 21 | "search_buttons": { 22 | "search_text": "Αναζήτηση με {araa_name}", 23 | "search_images": "Αναζήτηση εικόνων με {araa_name}" 24 | }, 25 | "settings": { 26 | "theme": "Θέμα", 27 | "user_theme": "Θέμα χρήστη", 28 | "all_settings": "Όλες οι ρυθμίσεις", 29 | "settings_header": "Ρυθμίσεις", 30 | "discover_themes": "Αναζητήστε θέματα που δημιουργήθηκαν από την κοινότητα", 31 | "discover_themes_button": "Ανακαλύψτε Θέματα", 32 | "preferred_language": "Προτιμώμενη γλώσσα για τα αποτελέσματα αναζήτησης", 33 | "google_domain": "Δικτυακός τόπος Google", 34 | "safe_search": "Ασφαλής Αναζήτηση", 35 | "open_links_new_tab": "Άνοιγμα συνδέσεων σε νέα καρτέλα", 36 | "disable_javascript": "Η απενεργοποίηση του JavaScript (δεν συνιστάται) επηρεάζει τη λειτουργικότητα του ιστότοπου, απενεργοποιεί την αυτόματη συμπλήρωση και επιβραδύνει ορισμένες λειτουργίες. Η διατήρηση του JavaScript ενεργοποιημένου δεν θίγει την ιδιωτικότητά σας, διότι οι αιτήσεις επεξεργάζονται μέσω των τερματικών του διακομιστή.", 37 | "save_settings": "Αποθήκευση των ρυθμίσεων", 38 | "return_to_settings": "Επιστροφή στις ρυθμίσεις", 39 | "preferredux_language": "Προτιμώμενη γλώσσα για τίτλους, κουμπιά και άλλο κείμενο από το {araa_name}", 40 | "on": "ενεργό", 41 | "off": "ανενεργό", 42 | "any_language": "Οποιαδήποτε Γλώσσα" 43 | }, 44 | "api_links": { 45 | "api_link": "Διεπαφή Προγραμματισμού Εφαρμογών", 46 | "suggestions_api_link": "Διεπαφή Προτάσεων Εφαρμογών" 47 | } 48 | } -------------------------------------------------------------------------------- /templates/preresults_layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{ araa_name }} 10 | 11 | 12 | 13 | 14 | 15 | 16 | {% if request.path == '/settings' or request.path == '/discover' %} 17 | 18 | {% endif %} 19 | 20 | 21 | 22 | 23 | 24 | {% block body %}{% endblock %} 25 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /static/cookies-settings-version.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @source: ./cookies-settings-version.js 3 | * 4 | * @licstart The following is the entire license notice for the 5 | * JavaScript code in this page. 6 | * 7 | * Copyright (C) 2023 Extravi 8 | * 9 | * The JavaScript code in this page is free software: you can 10 | * redistribute it and/or modify it under the terms of the GNU Affero 11 | * General Public License as published by the Free Software Foundation, 12 | * either version 3 of the License, or (at your option) any later version. 13 | * 14 | * The code is distributed WITHOUT ANY WARRANTY; without even the 15 | * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 16 | * See the GNU Affero General Public License for more details. 17 | * 18 | * As additional permission under GNU Affero General Public License 19 | * section 7, you may distribute non-source (e.g., minimized or compacted) 20 | * forms of that code without the copy of the GNU Affero General Public 21 | * License normally required by section 4, provided you include this 22 | * license notice and a URL through which recipients can access the 23 | * Corresponding Source. 24 | * 25 | * @licend The above is the entire license notice 26 | * for the JavaScript code in this page. 27 | */ 28 | 29 | function setCookie(name, value) { 30 | document.cookie = `${name}=${value}; HostOnly=true; SameSite=None; Secure; Max-Age=2147483647`; 31 | } 32 | 33 | document.addEventListener("DOMContentLoaded", function () { 34 | const saveButton = document.querySelector(".save-settings-page"); 35 | 36 | if (saveButton === null) { 37 | return; 38 | } 39 | 40 | saveButton.addEventListener("click", function () { 41 | let setting_list = ["lang", "domain", "theme", "safe", "open-new-tab", "ux_lang", "ac"]; 42 | for (let i = 0; i < setting_list.length; i++) { 43 | setting = setting_list[i]; 44 | settingSelect = document.getElementById(setting); 45 | if (settingSelect) { 46 | const selectedOption = settingSelect.options[settingSelect.selectedIndex]; 47 | const selectedValue = selectedOption.value; 48 | setCookie(setting, selectedValue); 49 | } 50 | } 51 | }); 52 | }); 53 | 54 | document.getElementById("discoverButton").addEventListener("click", function (event) { 55 | event.preventDefault(); 56 | window.location.href = "/discover"; 57 | }); 58 | -------------------------------------------------------------------------------- /static/css/ancient.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --html-bg: #306763; 3 | --font-fg: #000000; 4 | --fg: #000000; 5 | 6 | --search-bg: #c0c0c0; 7 | --search-bg-input: #c0c0c0; 8 | --search-bg-input-border: #303030; 9 | --search-select: #c0c0c0; 10 | 11 | --border: #303030; 12 | 13 | --link: #8ab4f8; 14 | --link-visited: #c58af9; 15 | 16 | --snip-border: #303030; 17 | --snip-background: #c0c0c0; 18 | --snip-text: #ffffff; 19 | 20 | --settings-border: #303030; 21 | --button: #c0c0c0; 22 | 23 | --footer-bg: #c0c0c0; 24 | --footer-font: #000000; 25 | 26 | --highlight: #000000; 27 | --link: #1a0dab; 28 | --link-visited: #681da8; 29 | 30 | --blue: #306763; 31 | 32 | --green: #31b06e; 33 | 34 | --image-view: #c0c0c0; 35 | --image-view-titlebar: #c0c0c0; 36 | --view-image-color: #000000; 37 | --image-select: #303030; 38 | --fff: #fff; 39 | 40 | --search-button: #000000; 41 | 42 | --publish-info: #7f869e; 43 | 44 | color-scheme: light; 45 | } 46 | 47 | .results-settings { 48 | background: #c0c0c0; 49 | border: 8px solid #c0c0c0; 50 | box-shadow: 2px 2px #808080; 51 | color: #000000; 52 | } 53 | 54 | button { 55 | background: #c0c0c0; 56 | border: 8px solid #c0c0c0; 57 | box-shadow: 2px 2px #808080; 58 | color: #000000; 59 | } 60 | 61 | .results { 62 | background: #c0c0c0; 63 | border: 8px solid #c0c0c0; 64 | box-shadow: 2px 2px #808080; 65 | color: #000000; 66 | } 67 | 68 | 69 | .result_sublink { 70 | background: #c0c0c0; 71 | border: 8px solid #c0c0c0; 72 | box-shadow: 2px 2px #808080; 73 | color: #000000; 74 | } 75 | 76 | .calc-btn:hover { 77 | background: #c0c0c0; 78 | border: 1px solid #c0c0c0; 79 | box-shadow: 2px 2px #808080; 80 | color: #000000; 81 | } 82 | 83 | .calc-btn-2:hover { 84 | background: #c0c0c0; 85 | } 86 | 87 | .calc-btn { 88 | background: #c0c0c0; 89 | } 90 | 91 | .calc { 92 | background: #c0c0c0; 93 | box-shadow: -2px -2px #c0c0c0; 94 | color: #000000; 95 | } 96 | 97 | .image { 98 | background: #c0c0c0; 99 | border: 8px solid #c0c0c0; 100 | box-shadow: 2px 2px #808080; 101 | color: #000000; 102 | } 103 | 104 | .view-image-search { 105 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 106 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1); 107 | } 108 | 109 | .view-image-search:hover { 110 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); 111 | } -------------------------------------------------------------------------------- /static/uxlang.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @source: ./uxlang.js 3 | * 4 | * @licstart The following is the entire license notice for the 5 | * JavaScript code in this page. 6 | * 7 | * Copyright (C) 2023 Extravi 8 | * 9 | * The JavaScript code in this page is free software: you can 10 | * redistribute it and/or modify it under the terms of the GNU Affero 11 | * General Public License as published by the Free Software Foundation, 12 | * either version 3 of the License, or (at your option) any later version. 13 | * 14 | * The code is distributed WITHOUT ANY WARRANTY; without even the 15 | * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 16 | * See the GNU Affero General Public License for more details. 17 | * 18 | * As additional permission under GNU Affero General Public License 19 | * section 7, you may distribute non-source (e.g., minimized or compacted) 20 | * forms of that code without the copy of the GNU Affero General Public 21 | * License normally required by section 4, provided you include this 22 | * license notice and a URL through which recipients can access the 23 | * Corresponding Source. 24 | * 25 | * @licend The above is the entire license notice 26 | * for the JavaScript code in this page. 27 | */ 28 | 29 | function setCookie(name, value) { 30 | document.cookie = `${name}=${value}; HostOnly=true; SameSite=None; Secure; Max-Age=2147483647`; 31 | } 32 | 33 | function mapToValidLanguage(userLanguage) { 34 | const languageMappings = { 35 | "en": "english", 36 | "da": "danish", 37 | "nl": "dutch", 38 | "fr": "french", 39 | "fr-CA": "french_canadian", 40 | "de": "german", 41 | "el": "greek", 42 | "it": "italian", 43 | "ja": "japanese", 44 | "ko": "korean", 45 | "zh": "mandarin_chinese", 46 | "no": "norwegian", 47 | "pl": "polish", 48 | "pt": "portuguese", 49 | "ru": "russian", 50 | "es": "spanish", 51 | "sv": "swedish", 52 | "tr": "turkish", 53 | "uk": "ukrainian" 54 | }; 55 | 56 | const browserLanguage = userLanguage.split("-")[0]; 57 | return languageMappings[browserLanguage] || "english"; 58 | } 59 | 60 | function setLanguageCookie() { 61 | const userLanguage = navigator.language.toLowerCase(); 62 | const mappedLanguage = mapToValidLanguage(userLanguage); 63 | setCookie("ux_lang", mappedLanguage); 64 | if (mappedLanguage != "english") { 65 | // Default language is english, so only refresh if lang != english 66 | window.location.reload(); 67 | } 68 | } 69 | 70 | if (!document.cookie.includes("ux_lang")) { 71 | setLanguageCookie(); 72 | } 73 | -------------------------------------------------------------------------------- /templates/discover.html: -------------------------------------------------------------------------------- 1 | {% extends "preresults_layout.html" %} 2 | 3 | {% block body %} 4 | 5 |
6 | 7 |
8 |
9 |

{{ lang_data.settings.discover_themes_button }}

10 |
11 |
12 |
13 |
14 |
Dark (Default)
15 |
Dark (no background)
16 |
Light
17 |
Dark
18 |
Darker
19 |
Github Night
20 |
Night
21 |
Classic
22 |
Ancient
23 |
Catppuccin Mocha Blue
24 |
Catppuccin Mocha Green
25 |
Gentoo Lavender
26 |
Red
27 |
28 |
29 |
30 |
31 | {% if settings.javascript == "enabled" %} 32 | 33 | 34 | {% endif %} 35 |
36 | 37 | {% endblock %} 38 | -------------------------------------------------------------------------------- /src/torrent_sites/torrentgalaxy.py: -------------------------------------------------------------------------------- 1 | from _config import * 2 | from src import helpers 3 | from urllib.parse import quote 4 | 5 | def name(): 6 | return "torrentgalaxy" 7 | 8 | def get_catagory_code(cat): 9 | match cat: 10 | case "all": 11 | return "" 12 | case "audiobook": 13 | return "&c13=1" 14 | case "movie": 15 | return "&c3=1&c46=1&c45=1&c42=1&c4=1&c1=1" 16 | case "tv": 17 | return "&c41=1&c5=1&c11=1&c6=1&c7=1" 18 | case "games": 19 | return "&c43=1&c10=1" 20 | case "software": 21 | return "&c20=1&c21=1&c18=1" 22 | case "anime": 23 | return "&c28=1" 24 | case "music": 25 | return "&c28=1&c22=1&c26=1&c23=1&c25=1&c24=1" 26 | case "xxx": 27 | safesearch = (request.cookies.get("safe", "active") == "active") 28 | if safesearch: 29 | return "ignore" 30 | return "&c48=1&c35=1&c47=1&c34=1" 31 | case _: 32 | return "" 33 | 34 | 35 | def search(query, catagory="all"): 36 | catagory = get_catagory_code(catagory) 37 | if catagory == "ignore": 38 | return [] 39 | soup, response_code = helpers.makeHTMLRequest(f"https://{TORRENTGALAXY_DOMAIN}/torrents.php?search={quote(query)}{catagory}#results") 40 | 41 | result_divs = soup.findAll("div", {"class": "tgxtablerow"}) 42 | title = [div.find("div", {"id": "click"}) for div in result_divs] 43 | title = [title.text.strip() for title in title] 44 | magnet_links = [ 45 | div.find("a", href=lambda href: href and href.startswith("magnet")).get("href") 46 | for div in result_divs 47 | ] 48 | byte_sizes = [ 49 | helpers.string_to_bytes(div.find("span", {"class": "badge-secondary"}).text.strip()) 50 | for div in result_divs 51 | ] 52 | view_counts = [int(div.find("font", {"color": "orange"}).text.replace(',', '')) for div in result_divs] 53 | seeders = [int(div.find("font", {"color": "green"}).text.replace(',', '')) for div in result_divs] 54 | leechers = [int(div.find("font", {"color": "#ff0000"}).text.replace(',', '')) for div in result_divs] 55 | 56 | # list 57 | results = [] 58 | for title, magnet_link, byte_size, view_count, seeder, leecher in zip( 59 | title, magnet_links, byte_sizes, view_counts, seeders, leechers): 60 | results.append({ 61 | "href": TORRENTGALAXY_DOMAIN, 62 | "title": title, 63 | "magnet": helpers.apply_trackers(magnet_link), 64 | "bytes": byte_size, 65 | "size": helpers.bytes_to_string(byte_size), 66 | "views": view_count, 67 | "seeders": seeder, 68 | "leechers": leecher 69 | }) 70 | 71 | return results 72 | -------------------------------------------------------------------------------- /templates/torrents.html: -------------------------------------------------------------------------------- 1 | {% extends "results_layout.html" %} 2 | 3 | {% block body %} 4 |

{{ lang_data.results.results }} {{ fetched }} {{ lang_data.results.seconds }}

5 | 6 | {% if results %} 7 |
8 | 9 | 10 | 16 | 29 | 30 |
31 |
32 | {% for result in results %} 33 |
34 | {{ result["href"] }} 35 |

{{ result["title"] }}

36 |

{% if result["views"] %}{{ result["views"] }} views • {% endif %}{{ result["size"] }}

37 |

Seeders: {{ result["seeders"] }} | Leechers: {{ result["leechers"] }}

38 |
39 | {% endfor %} 40 |
41 | 42 | {% else %} 43 |
44 | Your search '{{ q }}' came back with no results.
45 | Try rephrasing your search term and/or recorrect any spelling mistakes. 46 |
47 | {% endif %} 48 | {% endblock %} 49 | -------------------------------------------------------------------------------- /static/menu.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @source: ./menu.js 3 | * 4 | * @licstart The following is the entire license notice for the 5 | * JavaScript code in this page. 6 | * 7 | * Copyright (C) 2023 Extravi 8 | * 9 | * The JavaScript code in this page is free software: you can 10 | * redistribute it and/or modify it under the terms of the GNU Affero 11 | * General Public License as published by the Free Software Foundation, 12 | * either version 3 of the License, or (at your option) any later version. 13 | * 14 | * The code is distributed WITHOUT ANY WARRANTY; without even the 15 | * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 16 | * See the GNU Affero General Public License for more details. 17 | * 18 | * As additional permission under GNU Affero General Public License 19 | * section 7, you may distribute non-source (e.g., minimized or compacted) 20 | * forms of that code without the copy of the GNU Affero General Public 21 | * License normally required by section 4, provided you include this 22 | * license notice and a URL through which recipients can access the 23 | * Corresponding Source. 24 | * 25 | * @licend The above is the entire license notice 26 | * for the JavaScript code in this page. 27 | */ 28 | 29 | const menuVisible = document.querySelector('.search-menu'); 30 | const menuDiv = document.querySelector('.settings-search-div-search'); 31 | 32 | function getCookie(name) { 33 | const cookies = document.cookie.split("; "); 34 | for (const cookie of cookies) { 35 | const [cookieName, cookieValue] = cookie.split("="); 36 | if (cookieName === name) { 37 | return cookieValue; 38 | } 39 | } 40 | return null; 41 | } 42 | 43 | function setThemeBasedOnCookie() { 44 | const themeCookie = getCookie("theme"); 45 | const themeNameSpan = document.getElementById("theme_name"); 46 | const themes = { 47 | "dark_blur": "Dark (Default)", 48 | "dark_default": "Dark (no background)", 49 | "light": "Light", 50 | }; 51 | 52 | if (themes.hasOwnProperty(themeCookie)) { 53 | themeNameSpan.textContent = themes[themeCookie]; 54 | } 55 | } 56 | 57 | setThemeBasedOnCookie(); 58 | 59 | document.getElementById("settingsButton").addEventListener("click", function () { 60 | window.location.href = "/settings"; 61 | }); 62 | 63 | menuDiv.addEventListener('click', function (event) { 64 | event.stopPropagation(); 65 | 66 | if (menuVisible.classList.contains('settings-menu-visible')) { 67 | menuVisible.classList.remove('settings-menu-visible'); 68 | menuVisible.classList.add('settings-menu-hidden'); 69 | } else { 70 | menuVisible.classList.remove('settings-menu-hidden'); 71 | menuVisible.classList.add('settings-menu-visible'); 72 | } 73 | }); 74 | 75 | document.addEventListener('click', function (event) { 76 | if (!menuDiv.contains(event.target) && !menuVisible.contains(event.target)) { 77 | menuVisible.classList.remove('settings-menu-visible'); 78 | menuVisible.classList.add('settings-menu-hidden'); 79 | } 80 | }); 81 | -------------------------------------------------------------------------------- /src/torrents.py: -------------------------------------------------------------------------------- 1 | import time 2 | import json 3 | from src import helpers 4 | from _config import * 5 | from flask import request, render_template, jsonify, Response 6 | from src.torrent_sites import torrentgalaxy, nyaa, thepiratebay, rutor 7 | 8 | def torrentResults(query) -> Response: 9 | settings = helpers.Settings() 10 | 11 | if not TORRENTSEARCH_ENABLED: 12 | return jsonify({"error": "Torrent search disabled by instance operator"}), 503 13 | 14 | # Define where to get request args from. If the request is using GET, 15 | # use request.args. Otherwise (POST), use request.form 16 | if request.method == "GET": 17 | args = request.args 18 | else: 19 | args = request.form 20 | 21 | # get user language settings 22 | json_path = f'static/lang/{settings.ux_lang}.json' 23 | with open(json_path, 'r') as file: 24 | lang_data = helpers.format_araa_name(json.load(file)) 25 | 26 | # remember time we started 27 | start_time = time.time() 28 | 29 | api = args.get("api", "false") 30 | catagory = args.get("cat", "all") 31 | query = args.get("q", " ").strip() 32 | sort = args.get("sort", "seed") 33 | if sort not in ["seed", "leech", "lth", "htl"]: 34 | sort = "seed" 35 | 36 | sites = [ 37 | torrentgalaxy, 38 | nyaa, 39 | thepiratebay, 40 | rutor, 41 | ] 42 | 43 | results = [] 44 | for site in sites: 45 | if site.name() in ENABLED_TORRENT_SITES: 46 | try: 47 | # For some reason, rutor doesn't give reliable catagories. 48 | if catagory != "all" and site.name() == "rutor": 49 | continue 50 | results += site.search(query, catagory=catagory) 51 | except: 52 | continue 53 | 54 | # Allow user to decide how the results are sorted 55 | match sort: 56 | case "leech": 57 | results = sorted(results, key=lambda x: x["leechers"])[::-1] 58 | case "lth": # Low to High file size 59 | results = sorted(results, key=lambda x: x["bytes"]) 60 | case "htl": # High to low file size 61 | results = sorted(results, key=lambda x: x["bytes"])[::-1] 62 | case _: # Defaults to seeders 63 | results = sorted(results, key=lambda x: x["seeders"])[::-1] 64 | 65 | 66 | # calc. time spent 67 | end_time = time.time() 68 | elapsed_time = end_time - start_time 69 | 70 | if api == "true" and API_ENABLED: 71 | # return the results list as a JSON response 72 | return jsonify(results) 73 | 74 | return render_template("torrents.html", 75 | results=results, title=f"{query} - {ARAA_NAME}", 76 | q=f"{query}", fetched=f"{elapsed_time:.2f}", 77 | cat=catagory, type="torrent", repo_url=REPO, donate_url=DONATE, 78 | API_ENABLED=API_ENABLED, TORRENTSEARCH_ENABLED=TORRENTSEARCH_ENABLED, 79 | lang_data=lang_data, commit=helpers.latest_commit(), sort=sort, settings=settings, 80 | araa_name=ARAA_NAME 81 | ) 82 | -------------------------------------------------------------------------------- /templates/images.html: -------------------------------------------------------------------------------- 1 | {% extends "results_layout.html" %} 2 | 3 | {% block body %} 4 |

{{ lang_data.results.results }} {{ fetched }} {{ lang_data.results.seconds }}

5 | {% if results %} 6 |
7 |
8 |
9 | 10 | 11 | 12 |
13 | 14 |
15 | 16 |
17 |
18 |

19 |

View source:

20 |

View image via 21 | source - (full res) 22 | proxy - (low res) 23 |

24 |

Engine: qwant image search

25 |
26 |

Full-resolution image size:

27 |
28 | {% for result in results %} 29 | 44 | {% endfor %} 45 |
46 |
47 |
48 | 49 | 50 | {% set p = request.args.get('p', 1)|int %} 51 | {% if p >= 2 %} 52 | 53 | {% endif %} 54 | 55 |
56 |
57 | {% else %} 58 |
59 | Your search '{{ q }}' came back with no results.
60 | Try rephrasing your search term and/or recorrect any spelling mistakes. 61 |
62 | {% endif %} 63 | {% endblock %} 64 | -------------------------------------------------------------------------------- /resources/_config.py.gen.template: -------------------------------------------------------------------------------- 1 | # Additional variables that cannot be configured with the generator. 2 | user_agents=[ 3 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36", 4 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36", 5 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:107.0) Gecko/20100101 Firefox/107.0", 6 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36", 7 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:106.0) Gecko/20100101 Firefox/106.0", 8 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15", 9 | ] 10 | VALID_IP_PROMPTS=[ 11 | "what is my ip", 12 | "what is my ip address", 13 | "what's my ip", 14 | "whats my ip" 15 | ] 16 | VALID_UA_PROMPTS=[ 17 | "what is my user agent", 18 | "what is my useragent", 19 | "whats my useragent", 20 | "whats my user agent", 21 | "what's my useragent", 22 | "what's my user agent", 23 | ] 24 | WHITELISTED_DOMAINS=[ 25 | "www.google.com", 26 | "wikipedia.org", 27 | PIPED_INSTANCE, 28 | PIPED_INSTANCE_API, 29 | PIPED_INSTANCE_PROXY, 30 | "api.qwant.com", 31 | TORRENTGALAXY_DOMAIN, 32 | NYAA_DOMAIN, 33 | API_BAY_DOMAIN, 34 | RUTOR_DOMAIN, 35 | ] 36 | TORRENT_TRACKERS = [ 37 | 'http://nyaa.tracker.wf:7777/announce', 38 | 'udp://open.stealth.si:80/announce', 39 | 'udp://tracker.opentrackr.org:1337/announce', 40 | 'udp://exodus.desync.com:6969/announce', 41 | 'udp://tracker.torrent.eu.org:451/announce' 42 | ] 43 | COOKIE_AGE=2147483647 44 | UX_LANGUAGES=[ 45 | {'lang_lower': 'english', 'lang_fancy': 'English'}, 46 | {'lang_lower': 'danish', 'lang_fancy': 'Danish (Dansk)'}, 47 | {'lang_lower': 'dutch', 'lang_fancy': 'Dutch (Nederlands)'}, 48 | {'lang_lower': 'french', 'lang_fancy': 'French (Français)'}, 49 | {'lang_lower': 'french_canadian', 'lang_fancy': 'French Canadian (Français canadien)'}, 50 | {'lang_lower': 'german', 'lang_fancy': 'German (Deutsch)'}, 51 | {'lang_lower': 'greek', 'lang_fancy': 'Greek (Ελληνικά)'}, 52 | {'lang_lower': 'italian', 'lang_fancy': 'Italian (Italiano)'}, 53 | {'lang_lower': 'japanese', 'lang_fancy': 'Japanese (日本語)'}, 54 | {'lang_lower': 'korean', 'lang_fancy': 'Korean (한국어)'}, 55 | {'lang_lower': 'mandarin_chinese', 'lang_fancy': 'Mandarin Chinese (普通话 or 中文)'}, 56 | {'lang_lower': 'norwegian', 'lang_fancy': 'Norwegian (Norsk)'}, 57 | {'lang_lower': 'polish', 'lang_fancy': 'Polish (Polski)'}, 58 | {'lang_lower': 'portuguese', 'lang_fancy': 'Portuguese (Português)'}, 59 | {'lang_lower': 'russian', 'lang_fancy': 'Russian (Русский)'}, 60 | {'lang_lower': 'spanish', 'lang_fancy': 'Spanish (Español)'}, 61 | {'lang_lower': 'swedish', 'lang_fancy': 'Swedish (Svenska)'}, 62 | {'lang_lower': 'turkish', 'lang_fancy': 'Turkish (Türkçe)'}, 63 | {'lang_lower': 'ukrainian', 'lang_fancy': 'Ukrainian (Українська)'}, 64 | {'lang_lower': 'romanian', 'lang_fancy': 'Romanian (Română)'}, 65 | ] 66 | DEFAULT_GOOGLE_DOMAIN='/search?gl=us' 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Araa 3 |

4 | 5 |

6 | 7 | 8 |

9 | 10 |

A privacy-respecting, ad-free, self-hosted metasearch engine.

11 | 12 | [![Counter](https://visitor-badge.laobi.icu/badge?page_id=Extravi.tailsx)](https://github.com/Extravi/araa-search) 13 | [![License](https://img.shields.io/github/license/Extravi/araa-search)](https://github.com/Extravi/araa-search/blob/main/LICENSE) 14 | [![Stars](https://img.shields.io/github/stars/Extravi/araa-search?style=social)](https://github.com/Extravi/araa-search/stargazers) 15 | 16 | If you're looking to [install](https://extravi.dev/araa) Araa, here is a how-to guide for it. 17 | 18 | ### Instances 19 | 20 | | Clearnet | Tor | SSL | Country | Status | 21 | |-|-|-|-|-| 22 | | [araa.extravi.dev](https://araa.extravi.dev/) | N/A | [www.ssllabs.com](https://www.ssllabs.com/ssltest/analyze.html?d=araa.extravi.dev)✅| United States 🇺🇸 | Official instance | 23 | | [tailsx.com](https://tailsx.com/) | [Link](http://inbbfryz7elofjk23pi7txnibttnuyz3rg2vwqmfengteeyhrmvex4id.onion/) | [www.ssllabs.com](https://www.ssllabs.com/ssltest/analyze.html?d=tailsx.com)✅| Canada 🇨🇦 | Unofficial instance | 24 | 25 | ### Contributors 26 | ![Contributors](https://contrib.rocks/image?repo=Extravi/araa-search) 27 | 28 | ## Features 29 | Here are some of the features that Araa a privacy-respecting, ad-free, self-hosted Google metasearch engine with strong security provides: 30 | 31 | * Full API support for easy integration into third-party apps and services 32 | * Utilizes Qwant for image search, which is known for its strong privacy protections 33 | * DuckDuckGo is used for auto-complete, offering privacy-enhanced search suggestions 34 | * Hosted on your own server, providing complete control over your data and ensuring privacy 35 | * Strong security measures implemented, including SSL encryption and firewalls 36 | * Ad-free search results, with no tracking or data collection for advertising purposes 37 | 38 | ## Screenshots 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | ## Contact 50 | Email: dante@extravi.dev 51 | -------------------------------------------------------------------------------- /src/text_engines/mullvad.py: -------------------------------------------------------------------------------- 1 | from src import helpers 2 | from urllib.parse import unquote, urlparse, parse_qs, urlencode 3 | from _config import * 4 | from src.text_engines.objects.fullEngineResults import FullEngineResults 5 | from src.text_engines.objects.wikiSnippet import WikiSnippet 6 | from src.text_engines.objects.textResult import TextResult 7 | from flask import request 8 | 9 | NAME = "mullvad" 10 | 11 | def search(query: str, page: int, search_type: str, user_settings: helpers.Settings) -> FullEngineResults: 12 | # Mullvad expects an `engine` parameter indicating the search backend 13 | # We always send `engine=google` to match the other engine-based implementations 14 | link_args = { 15 | "q": query, 16 | "engine": "google" 17 | } 18 | link = f"https://leta.mullvad.net/search/__data.json?" + urlencode(link_args) 19 | 20 | # print the url for debugging 21 | print(f"[mullvad.py] Requesting URL: {link}") 22 | 23 | # Mullvad returns JSON at this endpoint. Use the JSON helper which also 24 | # enforces domain whitelisting. 25 | soup, response_code = helpers.makeJSONRequest(link) 26 | 27 | if response_code != 200 or soup is None: 28 | return FullEngineResults(engine="mullvad", search_type=search_type, ok=False, code=response_code) 29 | 30 | # Convert the Mullvad "nodes" format into a flat list of result dicts. 31 | output = [] 32 | nodes = soup.get("nodes", []) if isinstance(soup, dict) else [] 33 | for node in nodes: 34 | if node is None or node.get("type") != "data": 35 | continue 36 | node_data = node.get("data", []) 37 | if not isinstance(node_data, list) or len(node_data) < 1: 38 | continue 39 | # node_data[0] contains metadata, including the index of the `items` list 40 | meta = node_data[0] if isinstance(node_data[0], dict) else {} 41 | items_index = meta.get("items") 42 | if items_index is None or items_index >= len(node_data): 43 | continue 44 | result_pointers = node_data[items_index] 45 | if not isinstance(result_pointers, list): 46 | continue 47 | 48 | for pointer in result_pointers: 49 | if pointer is None or pointer >= len(node_data): 50 | continue 51 | start = node_data[pointer] 52 | if not isinstance(start, dict): 53 | continue 54 | result = {} 55 | for name, p2 in start.items(): 56 | # guard index access 57 | try: 58 | value = node_data[p2] 59 | except Exception: 60 | value = None 61 | result[name] = value 62 | output.append(result) 63 | 64 | # Build TextResult objects from the parsed output 65 | results = [] 66 | for item in output: 67 | # link, title, snippet, favicon 68 | url = item.get("link") or "" 69 | title = item.get("title") or "" 70 | snippet = item.get("snippet") or "" 71 | # Ensure values are strings 72 | try: 73 | url = unquote(url) 74 | except Exception: 75 | url = str(url) 76 | title = str(title) 77 | snippet = str(snippet) 78 | 79 | results.append(TextResult( 80 | url=url, 81 | title=title, 82 | desc=snippet, 83 | sublinks=[] 84 | )) 85 | 86 | return FullEngineResults( 87 | engine="mullvad", 88 | search_type=search_type, 89 | results=results, 90 | ok=True, 91 | code=200 92 | ) -------------------------------------------------------------------------------- /src/video.py: -------------------------------------------------------------------------------- 1 | from src.helpers import makeHTMLRequest 2 | from src import helpers 3 | from _config import * 4 | from flask import request, render_template, jsonify, Response 5 | import time 6 | import json 7 | from src.helpers import latest_commit 8 | from urllib.parse import quote 9 | 10 | 11 | def parse_time(time): 12 | hours = time // 3600 13 | minutes = (time % 3600) // 60 14 | seconds = time % 60 15 | time_string = "" 16 | if hours != 0: 17 | time_string += f"{hours:02d}:" 18 | return f"{time_string}{minutes:02d}:{seconds:02d}" 19 | 20 | 21 | def videoResults(query) -> Response: 22 | settings = helpers.Settings() 23 | 24 | # Define where to get request args from. If the request is using GET, 25 | # use request.args. Otherwise (POST), use request.form 26 | if request.method == "GET": 27 | args = request.args 28 | else: 29 | args = request.form 30 | 31 | json_path = f'static/lang/{settings.ux_lang}.json' 32 | with open(json_path, 'r') as file: 33 | lang_data = helpers.format_araa_name(json.load(file)) 34 | 35 | # remember time we started 36 | start_time = time.time() 37 | 38 | api = args.get("api", "false") 39 | 40 | # grab & format webpage 41 | soup, response_code = makeHTMLRequest(f"https://{PIPED_INSTANCE_API}/search?q={quote(query)}&filter=all", is_piped=True) 42 | data = json.loads(soup.text) 43 | 44 | # retrieve links 45 | ytIds = [item["url"] for item in data["items"] if item.get("type") not in ["channel", "playlist"]] 46 | hrefs = [f"https://{PIPED_INSTANCE}{ytId}" for ytId in ytIds] 47 | 48 | # retrieve title 49 | title = [item["title"] for item in data["items"] if item.get("type") not in ["channel", "playlist"]] 50 | 51 | # retrieve date 52 | date_span = [item["uploadedDate"] for item in data["items"] if item.get("type") not in ["channel", "playlist"]] 53 | 54 | # retrieve views 55 | views = [f"{views//1000000000}B views" if views >= 1000000000 else f"{views//1000000}M views" if views >= 1000000 else f"{views/1000:.1f}K views" if 1000 < views < 10000 else f"{views//1000}K views" if views >= 10000 else f"{views} views" for views in [item["views"] for item in data["items"] if item.get("type") not in ["channel", "playlist"]]] 56 | 57 | # retrieve creator 58 | creator_text = [item["uploaderName"] for item in data["items"] if item.get("type") not in ["channel", "playlist"]] 59 | 60 | # retrieve publisher 61 | publisher_text = ["Piped" for item in data["items"] if item.get("type") not in ["channel", "playlist"]] 62 | 63 | # retrieve images 64 | filtered_urls = [item["thumbnail"] for item in data["items"] if item.get("type") not in ["channel", "playlist"]] 65 | filtered_urls = [f'/img_proxy?url={filtered_url}' for filtered_url in filtered_urls] 66 | 67 | # retrieve time 68 | duration = [item["duration"] for item in data["items"] if item.get("type") not in ["channel", "playlist"]] 69 | formatted_durations = [parse_time(duration) for duration in duration] 70 | 71 | # list 72 | results = [] 73 | for href, title, date, view, creator, publisher, image, duration in zip(hrefs, title, date_span, views, creator_text, publisher_text, filtered_urls, formatted_durations): 74 | results.append([href, title, date, view, creator, publisher, image, duration]) 75 | 76 | # calc. time spent 77 | end_time = time.time() 78 | elapsed_time = end_time - start_time 79 | 80 | if api == "true" and API_ENABLED == True: 81 | # return the results list as a JSON response 82 | return jsonify(results) 83 | else: 84 | return render_template("videos.html", 85 | results=results, title=f"{query} - {ARAA_NAME}", 86 | q=f"{query}", fetched=f"{elapsed_time:.2f}", 87 | type="video", repo_url=REPO, donate_url=DONATE, API_ENABLED=API_ENABLED, TORRENTSEARCH_ENABLED=TORRENTSEARCH_ENABLED, 88 | lang_data=lang_data, commit=latest_commit(), settings=settings, araa_name=ARAA_NAME 89 | ) 90 | -------------------------------------------------------------------------------- /src/text_engines/qwant.py: -------------------------------------------------------------------------------- 1 | import re 2 | from src.text_engines.objects.fullEngineResults import FullEngineResults 3 | from src.text_engines.objects.textResult import TextResult 4 | from src.text_engines.objects.wikiSnippet import WikiSnippet 5 | from urllib.parse import urlparse, urlencode, unquote 6 | from src import helpers 7 | 8 | NAME = "qwant" 9 | 10 | 11 | def sanitize_wiki(desc): 12 | desc = re.sub(r"\[\d{1,}\]", "", desc) 13 | return desc 14 | 15 | 16 | # NOTE: Qwant engine made by amongusussy. Taken from https://github.com/Extravi/araa-search/pull/106 17 | # Slightly modified to adapt different text results engine. 18 | def search(query: str, page: int, search_type: str, user_settings: helpers.Settings) -> FullEngineResults: 19 | if search_type == "reddit": 20 | query += " site:reddit.com" 21 | 22 | url_args = { 23 | "t": "web", 24 | "q": query, 25 | "count": 10, 26 | "locale": "en_us", 27 | "offset": page, 28 | "device": "desktop", 29 | "safesearch": 2 if user_settings.safe == "active" else 0, 30 | "tgp": 1, 31 | } 32 | 33 | json_data, code = helpers.makeJSONRequest( 34 | "https://api.qwant.com/v3/search/web?{}".format(urlencode(url_args)), 35 | is_qwant=True 36 | ) 37 | print( 38 | "https://api.qwant.com/v3/search/web?{}".format(urlencode(url_args)), 39 | ) 40 | 41 | if code == 403 and user_settings.safe == "active": 42 | # Qwant returns 403 when safesearch restricted all content. 43 | # This is just to prevent an 'engine failure' error. 44 | return FullEngineResults( 45 | engine="qwant", 46 | search_type=search_type, 47 | ok=True, 48 | code=code, 49 | ) 50 | 51 | if json_data['status'] != "success": 52 | # Add error handling later 53 | return FullEngineResults( 54 | engine="qwant", 55 | search_type=search_type, 56 | ok=False, 57 | code=code, 58 | ) 59 | 60 | try: 61 | resp_results = json_data["data"]["result"]["items"]["mainline"] 62 | except KeyError: 63 | return FullEngineResults( 64 | engine="qwant", 65 | search_type=search_type, 66 | ok=False, 67 | code=code, 68 | ) 69 | 70 | web_results = [] 71 | for group in resp_results: 72 | if group.get("type") == "web": 73 | # Only get web results. No images/ads. 74 | web_results += group.get("items", []) 75 | 76 | results = [] 77 | wiki = None 78 | for result in web_results: 79 | if len(result['desc']) > 166: 80 | short_desc = result['desc'][:166] + "..." 81 | else: 82 | short_desc = result['desc'] 83 | 84 | if result.get("links") is not None: 85 | sublinks = result.get("links") 86 | else: 87 | sublinks = [] 88 | 89 | results.append(TextResult( 90 | title=result['title'], 91 | desc=short_desc, 92 | url=unquote(result['url']), 93 | sublinks=sublinks 94 | )) 95 | 96 | # wikipedia snippet scraper 97 | if wiki is None and 'wikipedia.org' in urlparse(result['source']).netloc: 98 | wiki_proxy_link, wiki_image = helpers.grab_wiki_image_from_url(result['source'], user_settings) 99 | 100 | wiki = WikiSnippet( 101 | title=result['title'], 102 | desc=sanitize_wiki(result['desc']), 103 | link=result['source'], 104 | image=wiki_image, 105 | wiki_thumb_proxy_link=wiki_proxy_link, 106 | ) 107 | 108 | spell = json_data['data']['query']['queryContext'].get('alteredQuery', '') 109 | 110 | return FullEngineResults( 111 | engine="qwant", 112 | search_type=search_type, 113 | ok=True, 114 | code=200, 115 | results=results, 116 | wiki=wiki, 117 | correction=spell, 118 | ) 119 | -------------------------------------------------------------------------------- /static/cookies.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @source: ./cookies.js 3 | * 4 | * @licstart The following is the entire license notice for the 5 | * JavaScript code in this page. 6 | * 7 | * Copyright (C) 2023 Extravi 8 | * 9 | * The JavaScript code in this page is free software: you can 10 | * redistribute it and/or modify it under the terms of the GNU Affero 11 | * General Public License as published by the Free Software Foundation, 12 | * either version 3 of the License, or (at your option) any later version. 13 | * 14 | * The code is distributed WITHOUT ANY WARRANTY; without even the 15 | * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 16 | * See the GNU Affero General Public License for more details. 17 | * 18 | * As additional permission under GNU Affero General Public License 19 | * section 7, you may distribute non-source (e.g., minimized or compacted) 20 | * forms of that code without the copy of the GNU Affero General Public 21 | * License normally required by section 4, provided you include this 22 | * license notice and a URL through which recipients can access the 23 | * Corresponding Source. 24 | * 25 | * @licend The above is the entire license notice 26 | * for the JavaScript code in this page. 27 | */ 28 | 29 | function setCookie(name, value) { 30 | document.cookie = `${name}=${value}; HostOnly=true; SameSite=None; Secure; Max-Age=2147483647`; 31 | } 32 | 33 | function reloadPageForTheme() { 34 | const themeCookie = document.cookie.split(";").find((cookie) => cookie.trim().startsWith("theme=")); 35 | 36 | if (themeCookie) { 37 | window.location.reload(); 38 | } 39 | } 40 | 41 | document.addEventListener("DOMContentLoaded", function () { 42 | const langSelect = document.querySelector(".lang"); 43 | 44 | if (langSelect) { 45 | langSelect.addEventListener("change", function () { 46 | const selectedOption = langSelect.options[langSelect.selectedIndex]; 47 | const selectedValue = selectedOption.value; 48 | setCookie("lang", selectedValue); 49 | window.location.reload(); 50 | }); 51 | } 52 | 53 | const domainSelect = document.querySelector(".domain"); 54 | 55 | if (domainSelect) { 56 | domainSelect.addEventListener("change", function () { 57 | const selectedOption = domainSelect.options[domainSelect.selectedIndex]; 58 | const selectedValue = selectedOption.value; 59 | setCookie("domain", selectedValue); 60 | window.location.reload(); 61 | }); 62 | } 63 | 64 | const engineSelect = document.querySelector("#engine_select") 65 | if (engineSelect) { 66 | engineSelect.addEventListener("change", function () { 67 | const selectedOption = engineSelect.options[engineSelect.selectedIndex]; 68 | const selectedValue = selectedOption.value; 69 | setCookie("engine", selectedValue); 70 | window.location.reload(); 71 | }); 72 | } 73 | 74 | const themeDivs = document.querySelectorAll(".themes-settings-menu div"); 75 | 76 | themeDivs.forEach(function (div) { 77 | div.addEventListener("click", function () { 78 | const clickedDivId = div.firstElementChild.id; 79 | setCookie("theme", clickedDivId); 80 | reloadPageForTheme(); 81 | }); 82 | }); 83 | 84 | const safeSearchSelect = document.getElementById("safeSearchSelect"); 85 | 86 | if (safeSearchSelect) { 87 | safeSearchSelect.addEventListener("change", function () { 88 | const selectedValue = safeSearchSelect.value; 89 | setCookie("safe", selectedValue); 90 | window.location.reload(); 91 | }); 92 | } 93 | 94 | const languageSelect = document.getElementById("languageSelect"); 95 | 96 | if (languageSelect) { 97 | languageSelect.addEventListener("change", function () { 98 | const selectedValue = languageSelect.value; 99 | setCookie("lang", selectedValue); 100 | window.location.reload(); 101 | }); 102 | } 103 | }); 104 | -------------------------------------------------------------------------------- /src/images.py: -------------------------------------------------------------------------------- 1 | from src import helpers 2 | from urllib.parse import unquote, quote, urlparse 3 | from _config import * 4 | from flask import request, render_template, jsonify, Response, redirect 5 | import time 6 | import json 7 | import requests 8 | import httpx 9 | import trio 10 | import random 11 | 12 | # Debug code uncomment when needed 13 | #import logging, timeit 14 | #logging.basicConfig(level=logging.DEBUG, format="%(message)s") 15 | 16 | # Force all requests to only use IPv4 17 | requests.packages.urllib3.util.connection.HAS_IPV6 = False 18 | 19 | # Force all HTTPX requests to only use IPv4 20 | transport = httpx.HTTPTransport(local_address="0.0.0.0") 21 | 22 | # Pool limit configuration 23 | limits = httpx.Limits(max_keepalive_connections=None, max_connections=None, keepalive_expiry=None) 24 | 25 | # Make a persistent session 26 | qwant = httpx.Client(http2=True, follow_redirects=True, transport=transport, limits=limits) 27 | 28 | def imageResults(query) -> Response: 29 | # get user language settings 30 | settings = helpers.Settings() 31 | 32 | if request.method == "GET": 33 | args = request.args 34 | else: 35 | args = request.form 36 | 37 | json_path = f'static/lang/{settings.ux_lang}.json' 38 | with open(json_path, 'r') as file: 39 | lang_data = helpers.format_araa_name(json.load(file)) 40 | 41 | # remember time we started 42 | start_time = time.time() 43 | 44 | api = args.get("api", "false") 45 | 46 | p = args.get('p', '1') 47 | if not p.isdigit(): 48 | return redirect('/search') 49 | 50 | # returns 1 if active, else 0 51 | safe_search = int(settings.safe == "active") 52 | 53 | # grab & format webpage 54 | user_agent = random.choice(user_agents) 55 | headers = {"User-Agent": user_agent} 56 | response = qwant.get(f"https://api.qwant.com/v3/search/images?t=images&q={quote(query)}&count=50&locale=en_CA&offset={p}&device=desktop&tgp=2&safesearch={safe_search}", headers=headers) 57 | 58 | # If the image engine returned a non-200 response or invalid JSON, 59 | # handle it gracefully by rendering the images template with no 60 | # results (the template will show the "no results" message). 61 | if response.status_code != 200: 62 | print(f"WARN: Image engine returned status {response.status_code} for query={query}") 63 | images = None 64 | else: 65 | try: 66 | json_data = response.json() 67 | except Exception as e: 68 | # Could not parse JSON (empty body or invalid); treat as no results 69 | print(f"WARN: Failed to parse image engine response JSON: {e}") 70 | images = None 71 | else: 72 | # Get all the images from the response, while avoiding any errors. 73 | images = json_data.get("data", {}).get("result", {}).get("items", None) 74 | 75 | if images is None: 76 | # Render the images page with no results instead of redirecting 77 | # to /search. This provides a friendlier error path for users 78 | # when no image engine is available. 79 | elapsed_time = time.time() - start_time 80 | if api == "true" and API_ENABLED: 81 | return jsonify([]) 82 | return render_template("images.html", results=None, title=f"{query} - {ARAA_NAME}", 83 | q=f"{query}", fetched=f"{elapsed_time:.2f}", type="image", 84 | repo_url=REPO, donate_url=DONATE, API_ENABLED=API_ENABLED, 85 | TORRENTSEARCH_ENABLED=TORRENTSEARCH_ENABLED, lang_data=lang_data, 86 | commit=helpers.latest_commit(), settings=settings, araa_name=ARAA_NAME) 87 | 88 | results = [] 89 | for image in images: 90 | # Get original bing image URL 91 | bing_url = unquote(urlparse(image['thumbnail']).query).split("u=")[1].split("&")[0] 92 | 93 | image['thumb_proxy'] = f"/img_proxy?url={quote(bing_url)}" 94 | 95 | # Get domain name 96 | image['source'] = urlparse(image['url']).netloc 97 | 98 | results.append(image) 99 | 100 | # calc. time spent 101 | end_time = time.time() 102 | elapsed_time = end_time - start_time 103 | 104 | # render 105 | if api == "true" and API_ENABLED: 106 | # return the results list as a JSON response 107 | return jsonify(results) 108 | else: 109 | return render_template("images.html", results=results, title=f"{query} - {ARAA_NAME}", 110 | q=f"{query}", fetched=f"{elapsed_time:.2f}", 111 | type="image", 112 | repo_url=REPO, donate_url=DONATE, API_ENABLED=API_ENABLED, 113 | TORRENTSEARCH_ENABLED=TORRENTSEARCH_ENABLED, lang_data=lang_data, 114 | commit=helpers.latest_commit(), settings=settings, araa_name=ARAA_NAME) 115 | -------------------------------------------------------------------------------- /scripts/generate-pyconfig.py: -------------------------------------------------------------------------------- 1 | #!/bin/python 2 | # This python script was made to generate a suitable `/_config.py` 3 | # using the current environment variables OR the defaults specified in 4 | # the dictionary below. 5 | # 6 | # This script uses `/resource/_config.py.gen.template` as a template 7 | # for the generated `/_config.py` 8 | # 9 | # Also, it's meant to be ran with the cwd at the root of this repository. 10 | 11 | # A reference for acceptable environment variables & how to use them 12 | # for `/_config.py` 13 | ENV_VARS = { 14 | 'PORT': { # Name of the env. var. 15 | 'default_val': 8000, # Default value (obvious) 16 | 'pyname': 'PORT', # The name of the variable in Python (_config.py) 17 | 'type': int, # Type of the variable 18 | }, 19 | 'SHEBANG': { 20 | 'default_val': '!', 21 | 'pyname': 'BANG', 22 | 'type': str, 23 | }, 24 | 'ORIGIN_REPO': { 25 | 'default_val': 'https://github.com/Extravi/araa-search', 26 | 'pyname': 'REPO', 27 | 'type': str, 28 | }, 29 | 'DONATE_URL': { 30 | 'default_val': 'https://github.com/sponsors/Extravi', 31 | 'pyname': 'DONATE', 32 | 'type': str, 33 | }, 34 | 'DEFAULT_THEME': { 35 | 'default_val': 'dark_blur', 36 | 'pyname': 'DEFAULT_THEME', 37 | 'type': str, 38 | }, 39 | 'ENABLE_API': { 40 | 'default_val': False, 41 | 'pyname': 'API_ENABLED', 42 | 'type': bool, 43 | }, 44 | 'ENABLE_TORRENTS': { 45 | 'default_val': True, 46 | 'pyname': 'TORRENTSEARCH_ENABLED', 47 | 'type': bool, 48 | }, 49 | 'PIPED_INSTANCE': { 50 | 'default_val': 'yt.extravi.dev', 51 | 'pyname': 'PIPED_INSTANCE', 52 | 'type': str, 53 | }, 54 | 'PIPED_API': { 55 | 'default_val': 'ytapi.extravi.dev', 56 | 'pyname': 'PIPED_INSTANCE_API', 57 | 'type': str, 58 | }, 59 | 'PIPED_PROXY': { 60 | 'default_val': 'ytproxy.extravi.dev', 61 | 'pyname': 'PIPED_INSTANCE_PROXY', 62 | 'type': str, 63 | }, 64 | 'TORRENTGALAXY_DOMAIN': { 65 | 'default_val': 'torrentgalaxy.to', 66 | 'pyname': 'TORRENTGALAXY_DOMAIN', 67 | 'type': str, 68 | }, 69 | 'NYAA_DOMAIN': { 70 | 'default_val': 'nyaa.si', 71 | 'pyname': 'NYAA_DOMAIN', 72 | 'type': str, 73 | }, 74 | 'APIBAY_DOMAIN': { 75 | 'default_val': 'apibay.org', 76 | 'pyname': 'API_BAY_DOMAIN', 77 | 'type': str, 78 | }, 79 | 'RUTOR_DOMAIN': { 80 | 'default_val': 'rutor.info', 81 | 'pyname': 'RUTOR_DOMAIN', 82 | 'type': str, 83 | }, 84 | 'TORRENT_SITES': { 85 | 'default_val': [ 86 | 'nyaa', 87 | 'torrentgalaxy', 88 | 'tpb', 89 | 'rutor', 90 | ], 91 | 'pyname': 'ENABLED_TORRENT_SITES', 92 | 'type': list, 93 | }, 94 | 'DEFAULT_METHOD': { 95 | 'default_val': 'GET', 96 | 'pyname': 'DEFAULT_METHOD', 97 | 'type': str, 98 | }, 99 | 'DEFAULT_AC_ENGINE': { 100 | 'default_val': 'google', 101 | 'pyname': 'DEFAULT_AUTOCOMPLETE', 102 | 'type': str, 103 | }, 104 | 'DEFAULT_LANG': { 105 | 'default_val': 'english', 106 | 'pyname': 'DEFAULT_UX_LANG', 107 | 'type': str, 108 | }, 109 | 'ENGINE_RATELIMIT_COOLDOWN': { 110 | 'default_val': 10, 111 | 'pyname': 'ENGINE_RATELIMIT_COOLDOWN_MINUTES', 112 | 'type': int, 113 | }, 114 | } 115 | 116 | import os 117 | 118 | config_py = open('_config.py', 'w') 119 | 120 | # Write a disclaimer saying that this file was automatically generated. 121 | config_py.write( 122 | '# This _config.py was automatically generated using scripts/generate-pyconfig.py.\n' 123 | ) 124 | 125 | for env_var in ENV_VARS.keys(): 126 | val = os.environ.get(env_var) 127 | 128 | # If environ.get() returns None (the env. var. wasn't supplied), or 129 | # the value is blank, then fall back on the default. 130 | if val == None or val == "": 131 | val = ENV_VARS[env_var]['default_val'] 132 | # Wrap strings with quotes 133 | pretty_val = f"'{val}'" if ENV_VARS[env_var]['type'] == str else val 134 | print(f"Config var. '{env_var}' not specified. Defaulting to {pretty_val}.") 135 | 136 | # Put quotes around each variable if it's a string. 137 | if ENV_VARS[env_var]['type'] == str: 138 | val = f"'{val}'" 139 | 140 | config_py.write(f"{ENV_VARS[env_var]['pyname']}={val}\n") 141 | 142 | # Write the rest of the template's variables. 143 | # These variables are not yet configurable with this generator. 144 | conf_template = open('resources/_config.py.gen.template', 'r') 145 | config_py.write(conf_template.read()) 146 | conf_template.close() 147 | 148 | config_py.close() 149 | -------------------------------------------------------------------------------- /_config.py: -------------------------------------------------------------------------------- 1 | ARAA_NAME = "Araa" 2 | 3 | # The char used to denote bangs (see below). 4 | # EG BANG='!': "!ddg cats" will search "cats" on DuckDuckGo. 5 | BANG = '!' 6 | 7 | # Search engine bangs for ppl who want to use another engine through 8 | # Araa's search bar. 9 | # Bangs with their assosiated URLs can be found in /bangs.json. 10 | 11 | # The repository this instance is based off on. 12 | REPO = 'https://github.com/Extravi/araa-search' 13 | DONATE = 'https://github.com/sponsors/Extravi' 14 | 15 | DEFAULT_ENGINE = "mullvad" 16 | 17 | # Engines that are currently in maintenance mode and will not be used 18 | # For example, if google makes a change to their engine that breaks searching, 19 | # add "google" to this list to temporarily disable it until a fix is made. 20 | MAINTENANCE_MODE = [ 21 | "google", 22 | "qwant", 23 | ] 24 | 25 | # Default theme 26 | DEFAULT_THEME = 'dark_default' 27 | 28 | # Default method 29 | DEFAULT_METHOD = "GET" 30 | 31 | # Default autocomplete "google" will use Google, and "ddg" will use DuckDuckGo 32 | DEFAULT_AUTOCOMPLETE = "google" 33 | 34 | # The port for this server to listen on 35 | PORT = 8000 36 | 37 | # Torrent domains 38 | TORRENTGALAXY_DOMAIN = "torrentgalaxy.to" 39 | NYAA_DOMAIN = "nyaa.si" 40 | # apibay is the api for thepiratebay.org 41 | API_BAY_DOMAIN = "apibay.org" 42 | RUTOR_DOMAIN = "rutor.info" 43 | 44 | # Domain of the Piped instance to use 45 | PIPED_INSTANCE_API = "ytapi.ttj.dev" 46 | PIPED_INSTANCE = "yt.ttj.dev" 47 | PIPED_INSTANCE_PROXY = "ytproxy.ttj.dev" 48 | 49 | # Useragents to use in the request. 50 | user_agents = [ 51 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.3", 52 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", 53 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 14.1; rv:109.0) Gecko/20100101 Firefox/121.0", 54 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.2210.89", 55 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0", 56 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15", 57 | ] 58 | 59 | # prompts for user agent & ip queries 60 | VALID_IP_PROMPTS = [ 61 | "what is my ip", 62 | "what is my ip address", 63 | "what's my ip", 64 | "whats my ip" 65 | ] 66 | VALID_UA_PROMPTS = [ 67 | "what is my user agent", 68 | "what is my useragent", 69 | "whats my useragent", 70 | "whats my user agent", 71 | "what's my useragent", 72 | "what's my user agent", 73 | ] 74 | 75 | 76 | WHITELISTED_DOMAINS = [ 77 | "www.google.com", 78 | "wikipedia.org", 79 | PIPED_INSTANCE, 80 | PIPED_INSTANCE_API, 81 | PIPED_INSTANCE_PROXY, 82 | "api.qwant.com", 83 | TORRENTGALAXY_DOMAIN, 84 | NYAA_DOMAIN, 85 | API_BAY_DOMAIN, 86 | RUTOR_DOMAIN, 87 | "leta.mullvad.net", 88 | ] 89 | 90 | ENABLED_TORRENT_SITES = [ 91 | "nyaa", 92 | "torrentgalaxy", 93 | "tpb", 94 | "rutor", 95 | ] 96 | 97 | TORRENT_TRACKERS = [ 98 | 'http://nyaa.tracker.wf:7777/announce', 99 | 'udp://open.stealth.si:80/announce', 100 | 'udp://tracker.opentrackr.org:1337/announce', 101 | 'udp://exodus.desync.com:6969/announce', 102 | 'udp://tracker.torrent.eu.org:451/announce' 103 | ] 104 | 105 | COOKIE_AGE = 2147483647 106 | 107 | # set to true to enable api support 108 | API_ENABLED = False 109 | 110 | # set to false to disable torrent search 111 | TORRENTSEARCH_ENABLED = True 112 | 113 | UX_LANGUAGES = [ 114 | {'lang_lower': 'english', 'lang_fancy': 'English'}, 115 | {'lang_lower': 'danish', 'lang_fancy': 'Danish (Dansk)'}, 116 | {'lang_lower': 'dutch', 'lang_fancy': 'Dutch (Nederlands)'}, 117 | {'lang_lower': 'french', 'lang_fancy': 'French (Français)'}, 118 | {'lang_lower': 'french_canadian', 'lang_fancy': 'French Canadian (Français canadien)'}, 119 | {'lang_lower': 'german', 'lang_fancy': 'German (Deutsch)'}, 120 | {'lang_lower': 'greek', 'lang_fancy': 'Greek (Ελληνικά)'}, 121 | {'lang_lower': 'italian', 'lang_fancy': 'Italian (Italiano)'}, 122 | {'lang_lower': 'japanese', 'lang_fancy': 'Japanese (日本語)'}, 123 | {'lang_lower': 'korean', 'lang_fancy': 'Korean (한국어)'}, 124 | {'lang_lower': 'mandarin_chinese', 'lang_fancy': 'Mandarin Chinese (普通话 or 中文)'}, 125 | {'lang_lower': 'norwegian', 'lang_fancy': 'Norwegian (Norsk)'}, 126 | {'lang_lower': 'polish', 'lang_fancy': 'Polish (Polski)'}, 127 | {'lang_lower': 'portuguese', 'lang_fancy': 'Portuguese (Português)'}, 128 | {'lang_lower': 'russian', 'lang_fancy': 'Russian (Русский)'}, 129 | {'lang_lower': 'spanish', 'lang_fancy': 'Spanish (Español)'}, 130 | {'lang_lower': 'swedish', 'lang_fancy': 'Swedish (Svenska)'}, 131 | {'lang_lower': 'turkish', 'lang_fancy': 'Turkish (Türkçe)'}, 132 | {'lang_lower': 'ukrainian', 'lang_fancy': 'Ukrainian (Українська)'}, 133 | {'lang_lower': 'romanian', 'lang_fancy': 'Romanian (Română)'}, 134 | ] 135 | 136 | # See all the 'lang_lower' values in UX_LANGUAGES 137 | DEFAULT_UX_LANG = "english" 138 | 139 | DEFAULT_GOOGLE_DOMAIN = "/search?gl=us" 140 | 141 | ENGINE_RATELIMIT_COOLDOWN_MINUTES = 28 142 | -------------------------------------------------------------------------------- /src/text_engines/google.py: -------------------------------------------------------------------------------- 1 | from src import helpers 2 | from urllib.parse import unquote, urlparse, parse_qs, urlencode 3 | from _config import * 4 | from src.text_engines.objects.fullEngineResults import FullEngineResults 5 | from src.text_engines.objects.wikiSnippet import WikiSnippet 6 | from src.text_engines.objects.textResult import TextResult 7 | from flask import request 8 | 9 | 10 | NAME = "google" 11 | 12 | 13 | def __local_href__(url): 14 | url_parsed = parse_qs(urlparse(url).query) 15 | if "q" not in url_parsed: 16 | return "" 17 | return f"/search?q={url_parsed['q'][0]}&p=0&t=text" 18 | 19 | 20 | def search(query: str, page: int, search_type: str, user_settings: helpers.Settings) -> FullEngineResults: 21 | if search_type == "reddit": 22 | query += " site:reddit.com" 23 | 24 | after_date = request.args.get("after", "") 25 | before_date = request.args.get("before", "") 26 | if after_date != "": 27 | query += f" after:{after_date}" 28 | if before_date != "": 29 | query += f" before:{before_date}" 30 | 31 | # Random characters are to trick google into thinking it's a mobile phone 32 | # loading more results 33 | # -> https://github.com/searxng/searxng/issues/159 34 | link_args = { 35 | "q": query, 36 | "start": page, 37 | "lr": user_settings.lang, 38 | "num": 20, 39 | "safe": user_settings.safe, 40 | "vet": "12ahUKEwjE4O6xoajxAhWL_KQKHVCLBKoQxK8CegQIAhAG..i", 41 | "ved": "2ahUKEwjE4O6xoajxAhWL_KQKHVCLBKoQqq4CegQIAhAI", 42 | "yv": 3, 43 | "prmd": "vmin", 44 | "ei": "c0fQYITbBIv5kwXQlpLQCg", 45 | "sa": "N", 46 | "asearch": "arc", 47 | "async": "arc_id:srp_510,ffilt:all,ve_name:MoreResultsContainer,next_id:srp_5,use_ac:true,_id:arc-srp_510,_pms:qs,_fmt:pc" 48 | } 49 | link = f"https://www.google.com{user_settings.domain}&" + urlencode(link_args) 50 | 51 | soup, response_code = helpers.makeHTMLRequest(link, is_google=True) 52 | 53 | if response_code != 200: 54 | return FullEngineResults(engine="google", search_type=search_type, ok=False, code=response_code) 55 | 56 | # retrieve links 57 | result_divs = soup.findAll("div", {"class": "yuRUbf"}) 58 | links = [div.find("a") for div in result_divs] 59 | hrefs = [link.get("href") for link in links] 60 | 61 | # retrieve title 62 | h3 = [div.find("h3") for div in result_divs] 63 | titles = [titles.text.strip() for titles in h3] 64 | 65 | # retrieve description 66 | result_desc = soup.findAll("div", {"class": "VwiC3b"}) 67 | descriptions = [descs.text.strip() for descs in result_desc] 68 | 69 | # retrieve sublinks 70 | try: 71 | result_sublinks = soup.findAll("tr", {"class": lambda x: x and x.startswith("mslg")}) 72 | sublinks_divs = [sublink.find("div", {"class": "zz3gNc"}) for sublink in result_sublinks] 73 | sublinks = [sublink.text.strip() for sublink in sublinks_divs] 74 | sublinks_links = [sublink.find("a") for sublink in result_sublinks] 75 | sublinks_hrefs = [link.get("href") for link in sublinks_links] 76 | sublinks_titles = [title.text.strip() for title in sublinks_links] 77 | except: 78 | sublinks = "" 79 | sublinks_hrefs = "" 80 | sublinks_titles = "" 81 | 82 | # retrieve kno-rdesc 83 | try: 84 | rdesc = soup.find("div", {"class": "CYJS5e"}) 85 | span_element = rdesc.find("span", {"class": "QoPDcf"}) 86 | desc_link = rdesc.find("a", {"class": "y171A"}) 87 | kno_link = desc_link.get("href") 88 | kno = span_element.find("span").get_text() 89 | except: 90 | kno = "" 91 | kno_link = "" 92 | 93 | # retrieve kno-title 94 | try: # look for the title inside of a span in div.SPZz6b 95 | rtitle = soup.find("div", {"class": "SPZz6b"}) 96 | rt_span = rtitle.find("span") 97 | rkno_title = rt_span.text.strip() 98 | # if we didn't find anyhing useful, move to next tests 99 | if rkno_title in ["", "See results about"]: 100 | raise 101 | except: 102 | for ellement, class_name in zip(["div", "span", "div"], ["DoxwDb", "yKMVIe", "DoxwDb"]): 103 | try: 104 | rtitle = soup.find(ellement, {"class": class_name}) 105 | rkno_title = rtitle.text.strip() 106 | except: 107 | continue # couldn't scrape anything. continue if we can. 108 | else: 109 | if rkno_title not in ["", "See results about"]: 110 | break # we got one 111 | else: 112 | rkno_title = "" 113 | 114 | for div in soup.find_all("div", {'class': 'nnFGuf'}): 115 | div.decompose() 116 | 117 | # retrieve featured snippet 118 | try: 119 | featured_snip = soup.find("span", {"class": "hgKElc"}) 120 | snip = featured_snip.text.strip() 121 | except: 122 | snip = "" 123 | 124 | # retrieve spell check 125 | try: 126 | spell = soup.find("a", {"class": "gL9Hy"}) 127 | check = spell.text.strip() 128 | except: 129 | check = "" 130 | if search_type == "reddit": 131 | check = check.replace("site:reddit.com", "").strip() 132 | 133 | kno_image = None 134 | kno_title = None 135 | 136 | # get wiki image 137 | if kno_link != "": 138 | kno_title, kno_image = helpers.grab_wiki_image_from_url(kno_link, user_settings) 139 | 140 | wiki_known_for = soup.find("div", {'class': 'loJjTe'}) 141 | if wiki_known_for is not None: 142 | wiki_known_for = wiki_known_for.get_text().strip() 143 | 144 | wiki_info = {} 145 | wiki_info_divs = soup.find_all("div", {"class": "rVusze"}) 146 | for info in wiki_info_divs: 147 | spans = info.findChildren("span" , recursive=False) 148 | for a in spans[1].find_all("a"): 149 | # Delete all non-href attributes 150 | a.attrs = {"href": a.get("href", "")} 151 | if "sca_esv=" in a['href']: 152 | # Remove any trackers for google domains 153 | a['href'] = __local_href__(a.get("href", "")) 154 | 155 | wiki_info[spans[0].get_text()] = spans[1] 156 | 157 | results = [] 158 | for href, title, desc in zip(hrefs, titles, descriptions): 159 | results.append(TextResult( 160 | url = unquote(href), 161 | title = title, 162 | desc = desc, 163 | sublinks=[] 164 | )) 165 | sublink = [] 166 | for sublink_href, sublink_title, sublink_desc in zip(sublinks_hrefs, sublinks_titles, sublinks): 167 | sublink.append(TextResult( 168 | url = unquote(sublink_href), 169 | title = sublink_title, 170 | desc = sublink_desc, 171 | sublinks=[] 172 | )) 173 | 174 | wiki = None if kno == "" else WikiSnippet( 175 | title = rkno_title, 176 | image = kno_image, 177 | desc = kno, 178 | link = unquote(kno_link), 179 | wiki_thumb_proxy_link = kno_title, 180 | known_for = wiki_known_for, 181 | info = wiki_info, 182 | ) 183 | 184 | return FullEngineResults( 185 | engine = "google", 186 | search_type = search_type, 187 | ok = True, 188 | code = 200, 189 | results = results, 190 | wiki = wiki, 191 | featured = None if snip == "" else snip, 192 | correction = None if check == "" else check, 193 | top_result_sublinks = sublink, 194 | ) 195 | -------------------------------------------------------------------------------- /static/calculator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @source: ./calculator.js 3 | * 4 | * @licstart The following is the entire license notice for the 5 | * JavaScript code in this page. 6 | * 7 | * Copyright (C) 2023 Extravi 8 | * 9 | * The JavaScript code in this page is free software: you can 10 | * redistribute it and/or modify it under the terms of the GNU Affero 11 | * General Public License as published by the Free Software Foundation, 12 | * either version 3 of the License, or (at your option) any later version. 13 | * 14 | * The code is distributed WITHOUT ANY WARRANTY; without even the 15 | * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 16 | * See the GNU Affero General Public License for more details. 17 | * 18 | * As additional permission under GNU Affero General Public License 19 | * section 7, you may distribute non-source (e.g., minimized or compacted) 20 | * forms of that code without the copy of the GNU Affero General Public 21 | * License normally required by section 4, provided you include this 22 | * license notice and a URL through which recipients can access the 23 | * Corresponding Source. 24 | * 25 | * @licend The above is the entire license notice 26 | * for the JavaScript code in this page. 27 | */ 28 | 29 | const calcInput = document.getElementById('current_calc'); 30 | const numberButtons = document.querySelectorAll('.calc-btn-style:not(#ce):not(#backspace)'); 31 | const addBtn = document.getElementById('add'); 32 | const subtractBtn = document.getElementById('subtract'); 33 | const multiplyBtn = document.getElementById('multiply'); 34 | const divideBtn = document.getElementById('divide'); 35 | const equalsBtn = document.getElementById('equals'); 36 | const clearBtn = document.getElementById('ce'); 37 | const backspaceBtn = document.getElementById('backspace'); 38 | 39 | document.body.addEventListener('keydown', (key) => { 40 | if (key.target.tagName.toLowerCase() == "input") { 41 | return; 42 | } 43 | 44 | if ('0123456789().'.includes(key.key)) { 45 | numberButtonHandle(key.key); 46 | } 47 | else if ('+-*/'.includes(key.key)) { 48 | operatorButtonHandle(key.key); 49 | } 50 | else switch (key.key) { 51 | case 'Backspace': 52 | doBackspace(); 53 | break; 54 | case 'Enter': 55 | evaluateExpression(); 56 | break; 57 | } 58 | }) 59 | 60 | numberButtons.forEach(button => { 61 | button.addEventListener('click', () => numberButtonHandle(button.textContent)); 62 | }); 63 | 64 | addBtn.addEventListener('click', () => operatorButtonHandle('+')); 65 | subtractBtn.addEventListener('click', () => operatorButtonHandle('-')); 66 | multiplyBtn.addEventListener('click', () => operatorButtonHandle('*')); 67 | divideBtn.addEventListener('click', () => operatorButtonHandle('/')); 68 | 69 | clearBtn.addEventListener('click', () => { 70 | calcInput.textContent = '0'; 71 | }); 72 | 73 | backspaceBtn.addEventListener('click', doBackspace); 74 | 75 | equalsBtn.addEventListener('click', evaluateExpression); 76 | 77 | // Executes a 'backspace' on the calculator's expression. 78 | // Made to reduce repetitive code. 79 | function doBackspace() { 80 | do { 81 | calcInput.textContent = calcInput.textContent.slice(0, -1); 82 | } while (calcInput.textContent.endsWith(' ')); 83 | // ^^^ Sometimes there are spaces in the expression (see above listeners). 84 | // Remove them aswell. 85 | 86 | // If the contents of calcInput have been completely cleared, output 0 87 | // to the textbox to match the clear button's behaviour. 88 | if (calcInput.textContent.length === 0) { 89 | calcInput.textContent = '0'; 90 | } 91 | } 92 | 93 | // Handles the presses to all operator buttons. 94 | function operatorButtonHandle(operator) { 95 | // Avoid multiple operators being added. 96 | // This will override the current operator with the new operator being added. 97 | if (/\+|\-|\*|\//.test(calcInput.textContent.split(' ').pop())) { 98 | calcInput.textContent = calcInput.textContent.substring(0, calcInput.textContent.length - 1); 99 | } 100 | 101 | // Make a decimal that's '1234.' into '1234.0' before adding an operator. 102 | if (calcInput.textContent[calcInput.textContent.length - 1] === '.') { 103 | calcInput.textContent += '0'; 104 | } 105 | 106 | calcInput.textContent += ` ${operator}`; 107 | } 108 | 109 | // Handles the presses to all numberButtons. 110 | function numberButtonHandle(button) { 111 | // The absolute last char of the expression; i.e. '3' in '4 + 5 * 6 + 2.3' 112 | const lastChar = calcInput.textContent[calcInput.textContent.length - 1]; 113 | 114 | // If the end of calcInput has an operator, append an extra space for 115 | // the number to make the expression look better. 116 | if (/\+|\-|\*|\//.test(lastChar)) { 117 | calcInput.textContent += ' '; 118 | } 119 | // Add a multiplication operator around brackets. 120 | else if ((lastChar === ")" || button === "(") && calcInput.textContent !== '0') { 121 | operatorButtonHandle('* '); 122 | } 123 | 124 | // The 'trailing' substring in an expression; i.e. 2.3 in '4 + 5 * 6 + 2.3' 125 | // Collected here as the expression may have been modified by the above code 126 | const trailing = calcInput.textContent.split(' ').pop(); 127 | 128 | // Do some specific things for decimals. 129 | if (button === '.') { 130 | // Prevent multiple dots in a number. 131 | if (trailing.includes('.')) { 132 | return; 133 | } 134 | 135 | // Add an extra 0 if the trailing substring is blank or opening parenthesis 136 | // Makes thinks look nicer (0.3 instead of .3). 137 | if (trailing.length === 0 || trailing === '(') { 138 | calcInput.textContent += '0'; 139 | } 140 | } 141 | // Remove any 0 output if a dot is not being added. 142 | // i.e if 9 is input and the expression is '9 + 0', it'll change 143 | // to '9 + 9' because of this if statement. 144 | else if (trailing === '0') { 145 | calcInput.textContent = calcInput.textContent.substring(0, calcInput.textContent.length - 1); 146 | } 147 | 148 | calcInput.textContent += button; 149 | } 150 | 151 | // Implementation from https://github.com/TommyPang/SimpleCalculator. 152 | // Slightly modified. 153 | 154 | function evaluateExpression() { 155 | let expression = calcInput.textContent; 156 | document.querySelector(".prev_calculation").textContent = expression; 157 | expression = expression.replace(/\s/g, ''); 158 | calcInput.textContent = helper(Array.from(expression)); 159 | } 160 | 161 | function helper(s, idx = 0) { 162 | var stk = []; 163 | let sign = '+'; 164 | let num = 0; 165 | let decimalFlag = false; 166 | let decimalDivider = 1; 167 | 168 | for (let i = idx; i < s.length; i++) { 169 | let c = s[i]; 170 | 171 | if (c >= '0' && c <= '9') { 172 | if (decimalFlag) { 173 | // Handle numbers after decimal point 174 | decimalDivider *= 10; 175 | num = num + (parseInt(c) / decimalDivider); 176 | } else { 177 | // Handle whole numbers 178 | num = num * 10 + (c - '0'); 179 | } 180 | } else if (c === '.') { 181 | decimalFlag = true; 182 | } 183 | 184 | if ((!(c >= '0' && c <= '9') && c !== '.') || i === s.length - 1) { 185 | if (c === '(') { 186 | num = helper(s, i + 1); 187 | let l = 1, 188 | r = 0; 189 | for (let j = i + 1; j < s.length; j++) { 190 | if (s[j] === ')') { 191 | r++; 192 | if (r === l) { 193 | i = j; 194 | break; 195 | } 196 | } else if (s[j] === '(') l++; 197 | } 198 | } 199 | 200 | let pre = -1; 201 | switch (sign) { 202 | case '+': 203 | stk.push(num); 204 | break; 205 | case '-': 206 | stk.push(num * -1); 207 | break; 208 | case '*': 209 | pre = stk.pop(); 210 | stk.push(pre * num); 211 | break; 212 | case '/': 213 | pre = stk.pop(); 214 | stk.push(pre / num); 215 | break; 216 | } 217 | sign = c; 218 | num = 0; 219 | decimalFlag = false; 220 | decimalDivider = 1; 221 | 222 | if (c === ')') { 223 | break; 224 | } 225 | } 226 | } 227 | 228 | let ans = 0; 229 | while (stk.length > 0) { 230 | ans += stk.pop(); 231 | } 232 | return ans; 233 | } 234 | -------------------------------------------------------------------------------- /src/textResults.py: -------------------------------------------------------------------------------- 1 | from src import helpers 2 | from _config import * 3 | from flask import request, render_template, jsonify, Response 4 | import time 5 | import json 6 | import re 7 | from math import isclose # For float comparisons 8 | from src.text_engines import google, qwant, mullvad 9 | 10 | # All known engine modules. This list may be filtered by the 11 | # `MAINTENANCE_MODE` config below so engines under maintenance 12 | # are not used by the instance. 13 | ENGINES = [ 14 | google, 15 | qwant, 16 | mullvad, 17 | ] 18 | 19 | # filter out any engines flagged in the config's MAINTENANCE_MODE so they 20 | # won't be used by the search logic. Comparison is case-insensitive. 21 | _maintenance_lower = [m.lower() for m in MAINTENANCE_MODE] 22 | ACTIVE_ENGINES = [e for e in ENGINES if getattr(e, 'NAME', '').lower() not in _maintenance_lower] 23 | ratelimited_timestamps = {} 24 | 25 | 26 | def handleUserInfoQueries(query: str) -> str | None: 27 | if any(query.lower().find(valid_ip_prompt) != -1 for valid_ip_prompt in VALID_IP_PROMPTS): 28 | xff = request.headers.get("X-Forwarded-For") 29 | if xff: 30 | return xff.split(",")[-1].strip() 31 | return request.remote_addr or "unknown" 32 | elif any(query.lower().find(valid_ua_prompt) != -1 for valid_ua_prompt in VALID_UA_PROMPTS): 33 | return request.headers.get("User-Agent") or "unknown" 34 | return None 35 | 36 | 37 | def textResults(query: str) -> Response: 38 | global ratelimited_engines 39 | # get user language settings 40 | settings = helpers.Settings() 41 | 42 | # If the user's chosen engine is currently in maintenance mode, fall 43 | # back to a safe default. This prevents the UI from selecting an engine 44 | # that won't be used by the backend. 45 | maintenance_lower = [m.lower() for m in MAINTENANCE_MODE] 46 | if settings.engine and settings.engine.lower() in maintenance_lower: 47 | # Prefer DEFAULT_ENGINE if it's not in maintenance, otherwise pick 48 | # the first available active engine, else keep 'mullvad'. 49 | if DEFAULT_ENGINE and DEFAULT_ENGINE.lower() not in maintenance_lower: 50 | settings.engine = DEFAULT_ENGINE 51 | else: 52 | settings.engine = next((e.NAME for e in ACTIVE_ENGINES), 'mullvad') 53 | 54 | # Define where to get request args from. If the request is using GET, 55 | # use request.args. Otherwise (POST), use request.form 56 | if request.method == "GET": 57 | args = request.args 58 | else: 59 | args = request.form 60 | 61 | with open(f'static/lang/{settings.ux_lang}.json', 'r') as file: 62 | lang_data = helpers.format_araa_name(json.load(file)) 63 | 64 | # used to measure time spent 65 | start_time = time.time() 66 | 67 | api = args.get("api", "false") 68 | search_type = args.get("t", "text") 69 | p = args.get("p", 0) 70 | 71 | results = None 72 | ratelimited = True # Used to determine if complete engine failure is due to a bug or due to 73 | # the server getting completely ratelimited from every supported engine. 74 | 75 | # Build the runtime engine list using only active (non-maintenance) 76 | # engines. The user's preferred engine (from cookies) is placed first 77 | # when available. 78 | engine_list = [] 79 | for engine in ACTIVE_ENGINES: 80 | if engine.NAME == settings.engine: 81 | # Put prefered engine at the top of the list 82 | engine_list = [engine] + engine_list 83 | else: 84 | engine_list.append(engine) 85 | 86 | 87 | try: 88 | for ENGINE in engine_list: 89 | # Don't apply the global cooldown/ratelimit timer to Mullvad. 90 | # Mullvad will never be skipped because of the shared `ratelimited_timestamps`. 91 | if getattr(ENGINE, 'NAME', '').lower() != 'mullvad': 92 | if (t := ratelimited_timestamps.get(ENGINE.__name__)) is not None and t + ENGINE_RATELIMIT_COOLDOWN_MINUTES * 60 >= time.time(): 93 | # Current engine is ratelimited. Skip it. 94 | continue 95 | 96 | results = ENGINE.search(query, p, search_type, settings) 97 | if results.code == 429: 98 | t = time.time() 99 | print(f"Text engine {results.engine} was just ratelimited (time={t})") 100 | # Do not record a ratelimit timestamp for Mullvad so it won't be skipped later. 101 | if getattr(ENGINE, 'NAME', '').lower() != 'mullvad': 102 | ratelimited_timestamps[ENGINE.__name__] = t 103 | else: # Server *likely* isn't ratelimited. 104 | ratelimited = False 105 | if results.ok: 106 | break 107 | print(f"WARN: Text engine {results.engine} failed with code {results.code}.") 108 | if results.code == 429: 109 | print("NOTE: this engine just got ratelimited.") 110 | else: 111 | print(f"Response: {results}") 112 | results = None 113 | except Exception as e: 114 | return jsonify({"error": str(e)}), 500 115 | 116 | if results is None: 117 | if ratelimited: # Server is completely ratelimited :(. 118 | return jsonify({"instance_rate_limited": "The instance you are using is rate limited for every supported engine. Try again later."}), 429 119 | else: # *Likely* not ratelimited. Something probably went wrong. 120 | return jsonify({"error": "Complete engine failure. If this occurs multiple times, then " \ 121 | "this is *likely* an extremely unfortanute bug. Some additional " \ 122 | "information is provided with this error.", "query": query, "type": search_type}), 500 123 | 124 | elapsed_time = time.time() - start_time 125 | 126 | # gets users ip or user agent 127 | info = handleUserInfoQueries(query) 128 | calc = "" 129 | exported_math_expression = "" 130 | # calculator (TODO: Maybe remove expression parsing. It behaves in odd ways, and in general people who need a calculator can just search calculator) 131 | if info == None: 132 | info = "" 133 | math_expression = re.search(r'(\d+(\.\d+)?)\s*([\+\-\*/x])\s*(\d+(\.\d+)?)', query) 134 | if math_expression: 135 | exported_math_expression = math_expression.group(0) 136 | num1 = float(math_expression.group(1)) 137 | operator = math_expression.group(3) 138 | num2 = float(math_expression.group(4)) 139 | 140 | if operator == '+': 141 | result = num1 + num2 142 | elif operator == '-': 143 | result = num1 - num2 144 | elif operator == '*': 145 | result = num1 * num2 146 | elif operator == 'x': 147 | result = num1 * num2 148 | elif operator == '/': 149 | result = num1 / num2 if not isclose(num2, 0) else "Err; cannot divide by 0." 150 | 151 | try: 152 | result = float(result) 153 | if result.is_integer(): 154 | result = int(result) 155 | except: 156 | pass 157 | 158 | calc = result 159 | elif "calculator" in query.lower(): 160 | calc = "0" 161 | else: 162 | calc = "" 163 | 164 | if api == "true" and API_ENABLED == True: 165 | # return the results as a JSON response 166 | return jsonify(results.asDICT()) 167 | else: 168 | check = "" if results.correction is None else results.correction 169 | snip = "" if results.featured is None else results.featured 170 | 171 | # prepare engine options for the template (name/display) 172 | available_engines = [{'name': e.NAME, 'display': e.NAME.capitalize()} for e in ACTIVE_ENGINES] 173 | 174 | return render_template("results.html", 175 | engine=results.engine, 176 | results=results.results, sublink=results.top_result_sublinks, p=p, title=f"{query} - {ARAA_NAME}", 177 | q=f"{query}", fetched=f"{elapsed_time:.2f}", 178 | snip=f"{snip}", 179 | user_info=f"{info}", calc=f"{calc}", check=check, current_url=request.url, 180 | type=search_type, repo_url=REPO, donate_url=DONATE, commit=helpers.latest_commit(), 181 | exported_math_expression=exported_math_expression, API_ENABLED=API_ENABLED, 182 | TORRENTSEARCH_ENABLED=TORRENTSEARCH_ENABLED, lang_data=lang_data, 183 | settings=settings, wiki=results.wiki, araa_name=ARAA_NAME, 184 | before=args.get("before", ""), after=args.get("after", "") 185 | , available_engines=available_engines) 186 | -------------------------------------------------------------------------------- /src/helpers.py: -------------------------------------------------------------------------------- 1 | import random 2 | import requests 3 | import httpx 4 | import trio 5 | import re 6 | import json 7 | from bs4 import BeautifulSoup 8 | from urllib.parse import unquote, urlparse 9 | from _config import * 10 | from markupsafe import escape, Markup 11 | from os.path import exists 12 | from thefuzz import fuzz 13 | from flask import request 14 | 15 | # Debug code uncomment when needed 16 | #import logging, timeit 17 | #logging.basicConfig(level=logging.DEBUG, format="%(message)s") 18 | 19 | # Force all requests to only use IPv4 20 | requests.packages.urllib3.util.connection.HAS_IPV6 = False 21 | 22 | # Force all HTTPX requests to only use IPv4 23 | transport = httpx.HTTPTransport(local_address="0.0.0.0") 24 | 25 | # Pool limit configuration 26 | limits = httpx.Limits(max_keepalive_connections=None, max_connections=None, keepalive_expiry=None) 27 | 28 | # Make persistent request sessions 29 | s = requests.Session() # generic 30 | google = httpx.Client(http2=True, follow_redirects=True, transport=transport, limits=limits) # google 31 | wiki = httpx.Client(http2=True, follow_redirects=True, transport=transport, limits=limits) # wikipedia 32 | piped = httpx.Client(http2=True, follow_redirects=True, transport=transport, limits=limits) # piped 33 | qwant = httpx.Client(http2=True, follow_redirects=True, transport=transport, limits=limits) # qwant 34 | 35 | def makeHTMLRequest(url: str, is_google=False, is_wiki=False, is_piped=False): 36 | # block unwanted request from an edited cookie 37 | domain = unquote(url).split('/')[2] 38 | if domain not in WHITELISTED_DOMAINS: 39 | raise Exception(f"The domain '{domain}' is not whitelisted.") 40 | 41 | headers = { 42 | "User-Agent": random.choice(user_agents), # Choose a user-agent at random 43 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", 44 | "Accept-Encoding": "gzip, deflate", 45 | "Accept-Language": "en-US,en;q=0.5", 46 | "Dnt": "1", 47 | "Sec-Fetch-Dest": "document", 48 | "Sec-Fetch-Mode": "navigate", 49 | "Sec-Fetch-Site": "none", 50 | "Sec-Fetch-User": "?1", 51 | "Upgrade-Insecure-Requests": "1" 52 | } 53 | 54 | # Grab HTML content with the specific cookie 55 | if is_google: 56 | html = google.get(url, headers=headers) # persistent session for google 57 | elif is_wiki: 58 | html = wiki.get(url, headers=headers) # persistent session for wikipedia 59 | elif is_piped: 60 | html = piped.get(url, headers=headers) # persistent session for piped 61 | else: 62 | html = s.get(url, headers=headers) # generic persistent session 63 | 64 | # Allow for callers to handle errors better 65 | content = None if html.status_code != 200 else BeautifulSoup(html.text, "lxml") 66 | 67 | # Return the BeautifulSoup object 68 | return (content, html.status_code) 69 | 70 | # search highlights 71 | def highlight_query_words(string, query): 72 | string = escape(string) 73 | query_words = query.lower().split() 74 | highlighted_words = [] 75 | for word in string.split(): 76 | for query_word in query_words: 77 | if fuzz.ratio(word.lower(), query_word) >= 80: 78 | highlighted_word = Markup(f'{word}') 79 | highlighted_words.append(highlighted_word) 80 | break 81 | else: 82 | highlighted_words.append(word) 83 | highlighted = ' '.join(highlighted_words) 84 | return Markup(highlighted) 85 | 86 | 87 | def latest_commit(): 88 | if exists(".git/refs/heads/main"): 89 | with open('./.git/refs/heads/main') as f: 90 | return f.readline() 91 | return "Not in main branch" 92 | 93 | def makeJSONRequest(url: str, is_qwant=False): 94 | # block unwanted request from an edited cookie 95 | domain = unquote(url).split('/')[2] 96 | if domain not in WHITELISTED_DOMAINS: 97 | raise Exception(f"The domain '{domain}' is not whitelisted.") 98 | 99 | # Choose a user-agent at random 100 | user_agent = random.choice(user_agents) 101 | headers = {"User-Agent": user_agent} 102 | # Grab json content 103 | if is_qwant: 104 | response = qwant.get(url, headers=headers) # persistent session for qwant 105 | else: 106 | response = s.get(url, headers=headers) # generic persistent session 107 | 108 | # Try to parse JSON; if the response isn't valid JSON, return None and 109 | # log the response text for debugging instead of raising an exception. 110 | try: 111 | parsed = json.loads(response.text) 112 | return (parsed, response.status_code) 113 | except Exception as e: 114 | # Log useful debug information to the console so debugging is easier. 115 | try: 116 | print(f"[helpers.makeJSONRequest] Failed to parse JSON from {url} (status={response.status_code}):") 117 | # Truncate long responses to avoid huge logs 118 | preview = response.text[:2000] 119 | print(preview) 120 | except Exception: 121 | pass 122 | return (None, response.status_code) 123 | 124 | def get_magnet_hash(magnet): 125 | return magnet.split("btih:")[1].split("&")[0] 126 | 127 | def get_magnet_name(magnet): 128 | return magnet.split("&dn=")[1].split("&tr")[0] 129 | 130 | 131 | def apply_trackers(hash, name="", magnet=True): 132 | if magnet: 133 | name = get_magnet_name(hash) 134 | hash = get_magnet_hash(hash) 135 | 136 | return f"magnet:?xt=urn:btih:{hash}&dn={name}&tr={'&tr='.join(TORRENT_TRACKERS)}" 137 | 138 | def string_to_bytes(file_size): 139 | units = { 140 | 'bytes': 1, 141 | 'kb': 1024, 142 | 'mb': 1024 ** 2, 143 | 'gb': 1024 ** 3, 144 | 'tb': 1024 ** 4, 145 | 'kib': 1024, 146 | 'mib': 1024 ** 2, 147 | 'gib': 1024 ** 3, 148 | 'tib': 1024 ** 4 149 | } 150 | 151 | size, unit = file_size.lower().split() 152 | return float(size) * units[unit] 153 | 154 | def bytes_to_string(size): 155 | units = ['bytes', 'KB', 'MB', 'GB', 'TB'] 156 | index = 0 157 | while size >= 1024 and index < len(units) - 1: 158 | size /= 1024 159 | index += 1 160 | return f"{size:.2f} {units[index]}" 161 | 162 | 163 | class Settings(): 164 | def __init__(self): 165 | self.domain = request.cookies.get("domain", DEFAULT_GOOGLE_DOMAIN) 166 | self.javascript = request.cookies.get("javascript", "enabled") 167 | self.lang = request.cookies.get("lang", "") 168 | self.new_tab = request.cookies.get("new_tab", "") 169 | self.safe = request.cookies.get("safe", "active") 170 | self.ux_lang = request.cookies.get("ux_lang", DEFAULT_UX_LANG) 171 | self.theme = request.cookies.get("theme", DEFAULT_THEME) 172 | self.method = request.cookies.get("method", DEFAULT_METHOD) 173 | self.ac = request.cookies.get("ac", DEFAULT_AUTOCOMPLETE) 174 | self.engine = request.cookies.get("engine", DEFAULT_ENGINE) 175 | self.torrent = request.cookies.get("torrent", "enabled" if TORRENTSEARCH_ENABLED else "disabled") 176 | 177 | 178 | # Returns a tuple of two ellements. 179 | # The first is the wikipedia proxy's URL (used to load an wiki page's image after page load), 180 | # and the second is an image proxy link for the very image of the page itself. 181 | # 182 | # Either the first or second ellement will be a string, but not both (at least one ellement 183 | # will be None). 184 | # 185 | # NOTE: This function may return (None, None) in cases of failure. 186 | def grab_wiki_image_from_url(wikipedia_url: str, user_settings: Settings) -> tuple[str | None]: 187 | kno_title = None 188 | kno_image = None 189 | 190 | if user_settings.javascript == "enabled": 191 | kno_title = wikipedia_url.split("/")[-1] 192 | kno_title = f"/wikipedia?q={kno_title}" 193 | else: 194 | try: 195 | _kno_title = wikipedia_url.split("/")[-1] 196 | soup = makeHTMLRequest(f"https://wikipedia.org/w/api.php?action=query&format=json&prop=pageimages&titles={_kno_title}&pithumbsize=500", is_wiki=True) 197 | data = json.loads(soup.text) 198 | img_src = data['query']['pages'][list(data['query']['pages'].keys())[0]]['thumbnail']['source'] 199 | _kno_image = [f"/img_proxy?url={img_src}"] 200 | _kno_image = ''.join(_kno_image) 201 | finally: 202 | kno_image = _kno_image 203 | 204 | return kno_title, kno_image 205 | 206 | 207 | def format_araa_name(json_obj): 208 | # Recursively format araa_name=ARAA_NAME 209 | if isinstance(json_obj, dict): 210 | return {key: format_araa_name(value) for key, value in json_obj.items()} 211 | elif isinstance(json_obj, list): 212 | return [format_araa_name(item) for item in json_obj] 213 | elif isinstance(json_obj, str): 214 | return json_obj.format(araa_name=ARAA_NAME) 215 | else: 216 | return json_obj 217 | -------------------------------------------------------------------------------- /templates/search.html: -------------------------------------------------------------------------------- 1 | {% extends "preresults_layout.html" %} 2 | 3 | {% block body %} 4 | 5 | {% if settings.theme == "dark_blur" %} 6 | 7 | {% endif %} 8 | 11 |
12 |

{{ lang_data.settings.settings_header }}

13 |
14 | 15 |
16 |

{{ lang_data.settings.theme }}: {{ lang_data.settings.user_theme }}

17 |
18 |
Dark (Default)
19 |
Dark (no background)
20 |
Light
21 |
22 |
23 | 72 | 84 |
85 |
86 |
87 |

{{ araa_name }}

88 |
89 | 90 | 91 | close 92 | 93 |
94 |
    95 |
96 |
97 |
98 |
99 | 100 | 101 |
102 | {% if settings.javascript == "enabled" %} 103 | 104 | 105 | 106 | 107 | {% endif %} 108 |
109 | {% endblock %} 110 | -------------------------------------------------------------------------------- /templates/settings.html: -------------------------------------------------------------------------------- 1 | {% extends "preresults_layout.html" %} 2 | 3 | {% block body %} 4 | 5 |
6 | 7 |
8 |
9 |

{{ lang_data.settings.all_settings }}

10 |
11 |
12 |
13 |
14 |
Dark (Default)
15 |
Dark (no background)
16 |
Light
17 |
18 |
19 |
20 |
21 |

{{ lang_data.settings.discover_themes }}

22 | 23 |
24 |
25 |

{{ lang_data.settings.preferred_language }}

26 | 75 |
76 |
77 |

{{ lang_data.settings.preferredux_language }}

78 | 83 |
84 |
85 |

{{ lang_data.settings.google_domain }}

86 | 98 |
99 |
100 |

{{ lang_data.settings.safe_search }}

101 | 105 |
106 |
107 |

{{ lang_data.settings.open_links_new_tab }}

108 | 112 |
113 |
114 |

Prefered search engine

115 | 120 |
121 |
122 |

Method

123 | 127 |
128 |
129 |

Autocomplete

130 | 134 |
135 |
136 |

{{ lang_data.settings.disable_javascript }}

137 | 141 |
142 | {% if torrent_enabled %} 143 |
144 |

Enable Torrents

145 | 149 |
150 | {% endif %} 151 |
152 |

|

153 | 154 |
155 | {% if settings.javascript == "enabled" %} 156 | 157 | 158 | 159 | 160 | {% endif %} 161 | 162 |
163 | {% endblock %} 164 | -------------------------------------------------------------------------------- /static/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @source: ./script.js 3 | * 4 | * @licstart The following is the entire license notice for the 5 | * JavaScript code in this page. 6 | * 7 | * Copyright (C) 2023 Extravi 8 | * 9 | * The JavaScript code in this page is free software: you can 10 | * redistribute it and/or modify it under the terms of the GNU Affero 11 | * General Public License as published by the Free Software Foundation, 12 | * either version 3 of the License, or (at your option) any later version. 13 | * 14 | * The code is distributed WITHOUT ANY WARRANTY; without even the 15 | * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 16 | * See the GNU Affero General Public License for more details. 17 | * 18 | * As additional permission under GNU Affero General Public License 19 | * section 7, you may distribute non-source (e.g., minimized or compacted) 20 | * forms of that code without the copy of the GNU Affero General Public 21 | * License normally required by section 4, provided you include this 22 | * license notice and a URL through which recipients can access the 23 | * Corresponding Source. 24 | * 25 | * @licend The above is the entire license notice 26 | * for the JavaScript code in this page. 27 | */ 28 | 29 | // Removes the 'Apply Settings' button for Javascript users, 30 | // since changing any of the elements causes the settings to apply 31 | // automatically. 32 | const resultsSave = document.querySelector(".results-save"); 33 | if (resultsSave != null) { 34 | resultsSave.style.display = "none"; 35 | } 36 | 37 | const searchInput = document.getElementById('search-input'); 38 | const searchWrapper = document.querySelectorAll('.wrapper, .wrapper-results')[0]; 39 | const resultsWrapper = document.querySelector('.autocomplete'); 40 | const clearSearch = document.querySelector("#clearSearch"); 41 | 42 | async function getSuggestions(query) { 43 | try { 44 | params = new URLSearchParams({ "q": query }).toString(); 45 | const response = await fetch(`/suggestions?${params}`); 46 | const data = await response.json(); 47 | return data[1]; // Return only the array of suggestion strings 48 | } catch (error) { 49 | console.error(error); 50 | } 51 | } 52 | 53 | let currentIndex = -1; // Keep track of the currently selected suggestion 54 | 55 | let results = []; 56 | searchInput.addEventListener('input', async () => { 57 | let input = searchInput.value; 58 | if (input.length) { 59 | results = await getSuggestions(input); 60 | } 61 | renderResults(results); 62 | currentIndex = -1; // Reset index when we return new results 63 | }); 64 | 65 | searchInput.addEventListener("focus", async () => { 66 | let input = searchInput.value; 67 | if (results.length === 0 && input.length != 0) { 68 | results = await getSuggestions(input); 69 | } 70 | renderResults(results); 71 | }) 72 | 73 | clearSearch.style.visibility = "visible"; // Only show the clear search button for JS users. 74 | clearSearch.addEventListener("click", () => { 75 | searchInput.value = ""; 76 | searchInput.focus(); 77 | }) 78 | 79 | searchInput.addEventListener('keydown', (event) => { 80 | if (event.key === 'ArrowUp' || event.key === 'ArrowDown') { 81 | event.preventDefault(); // Prevent the cursor from moving in the search input 82 | 83 | // Find the currently selected suggestion element 84 | const selectedSuggestion = resultsWrapper.querySelector('.selected'); 85 | if (selectedSuggestion) { 86 | selectedSuggestion.classList.remove('selected'); // Deselect the currently selected suggestion 87 | } 88 | 89 | // Increment or decrement the current index based on the arrow key pressed 90 | if (event.key === 'ArrowUp') { 91 | currentIndex--; 92 | } else { 93 | currentIndex++; 94 | } 95 | 96 | // Wrap around the index if it goes out of bounds 97 | if (currentIndex < 0) { 98 | currentIndex = resultsWrapper.querySelectorAll('li').length - 1; 99 | } else if (currentIndex >= resultsWrapper.querySelectorAll('li').length) { 100 | currentIndex = 0; 101 | } 102 | 103 | // Select the new suggestion 104 | resultsWrapper.querySelectorAll('li')[currentIndex].classList.add('selected'); 105 | // Update the value of the search input 106 | searchInput.value = resultsWrapper.querySelectorAll('li')[currentIndex].textContent; 107 | } 108 | }); 109 | 110 | function renderResults(results) { 111 | if (!results || !results.length || !searchInput.value) { 112 | return searchWrapper.classList.remove('show'); 113 | } 114 | 115 | let content = ''; 116 | results.forEach((item) => { 117 | content += `
  • ${item}
  • `; 118 | }); 119 | 120 | // Only show the autocomplete suggestions if the search input has a non-empty value 121 | if (searchInput.value) { 122 | searchWrapper.classList.add('show'); 123 | } 124 | resultsWrapper.innerHTML = ``; 125 | } 126 | 127 | resultsWrapper.addEventListener('click', (event) => { 128 | if (event.target.tagName === 'LI') { 129 | // Set the value of the search input to the clicked suggestion 130 | searchInput.value = event.target.textContent; 131 | // Reset the current index 132 | currentIndex = -1; 133 | // Submit the form 134 | searchWrapper.querySelector('input[type="submit"]').click(); 135 | // Remove the show class from the search wrapper 136 | searchWrapper.classList.remove('show'); 137 | } 138 | }); 139 | 140 | 141 | document.addEventListener("keypress", (event) => { 142 | if (document.activeElement == searchInput) { 143 | // Allow the '/' character to be pressed when searchInput is active 144 | } else if (document.querySelector(".calc") != null) { 145 | // Do nothing if the calculator is available, so the division keybinding 146 | // will still work 147 | } 148 | else if (event.key == "/") { 149 | event.preventDefault(); 150 | searchInput.focus(); 151 | searchInput.selectionStart = searchInput.selectionEnd = searchInput.value.length; 152 | } 153 | }) 154 | 155 | // Add event listener to hide autocomplete suggestions when clicking outside of search-input or wrapper 156 | document.addEventListener('click', (event) => { 157 | // Check if the target of the event is the search-input or any of its ancestors 158 | if (!searchInput.contains(event.target) && !searchWrapper.contains(event.target)) { 159 | // Remove the show class from the search wrapper 160 | searchWrapper.classList.remove('show'); 161 | } 162 | }); 163 | 164 | // Load material icons. If the file cannot be loaded, 165 | // skip them and put a warning in the console. 166 | const font = new FontFace('Material Icons Round', 'url("/fonts/material-icons-round-v108-latin-regular.woff2") format("woff2")'); 167 | font.load().then(() => { 168 | const icons = document.getElementsByClassName('material-icons-round'); 169 | 170 | // Display all icons. 171 | for (let icon of icons) { 172 | icon.style.visibility = 'visible'; 173 | } 174 | 175 | // Ensure icons for the different types of searches are sized correctly. 176 | document.querySelectorAll('#sub-search-wrapper-ico').forEach((el) => { 177 | el.style.fontSize = '17px'; 178 | }); 179 | }).catch(() => { 180 | console.warn('Failed to load Material Icons Round. Hiding any icons using said pack.'); 181 | }); 182 | 183 | // load image after server side processing 184 | window.addEventListener('DOMContentLoaded', function () { 185 | var knoTitleElement = document.getElementById('kno_title'); 186 | var kno_title = knoTitleElement.dataset.knoTitle; 187 | fetch(kno_title) 188 | .then(response => response.json()) 189 | .then(data => { 190 | const pageId = Object.keys(data.query.pages)[0]; 191 | const thumbnailSource = data.query.pages[pageId].thumbnail.source; 192 | const url = "/img_proxy?url=" + thumbnailSource; 193 | 194 | // update the img tag with url and add kno_wiki_show 195 | var imgElement = document.querySelector('.kno_wiki'); 196 | imgElement.src = url; 197 | imgElement.classList.add('kno_wiki_show'); 198 | 199 | console.log(url); 200 | }) 201 | .catch(error => { 202 | console.log('Error fetching data:', error); 203 | }); 204 | }); 205 | 206 | const urlParams = new URLSearchParams(window.location.search); 207 | 208 | if (document.querySelectorAll(".search-active")[1].getAttribute("value") === "image") { 209 | 210 | // image viewer for image search 211 | const closeButton = document.querySelector('.image-close'); 212 | const imageView = document.querySelector('.image_view'); 213 | const images = document.querySelector('.images'); 214 | const viewImageImg = document.querySelector('.view-image-img'); 215 | const imageSource = document.querySelector('.image-source'); 216 | const imageFull = document.querySelector(".full-size"); 217 | const imageProxy = document.querySelector('.proxy-size'); 218 | const imageViewerLink = document.querySelector('.image-viewer-link'); 219 | const imageSize = document.querySelector('.image-size'); 220 | const fullImageSize = document.querySelector(".full-image-size"); 221 | const imageAlt = document.querySelector('.image-alt'); 222 | const openImageViewer = document.querySelectorAll('.open-image-viewer'); 223 | const imageBefore = document.querySelector('.image-before'); 224 | const imageNext = document.querySelector('.image-next'); 225 | let currentImageIndex = 0; 226 | 227 | closeButton.addEventListener('click', function () { 228 | imageView.classList.remove('image_show'); 229 | imageView.classList.add('image_hide'); 230 | for (const image of document.querySelectorAll(".image_selected")) { 231 | image.classList = ['image']; 232 | } 233 | images.classList.add('images_viewer_hidden'); 234 | }); 235 | 236 | openImageViewer.forEach((image, index) => { 237 | image.addEventListener('click', function (event) { 238 | event.preventDefault(); 239 | currentImageIndex = index; 240 | showImage(); 241 | }); 242 | }); 243 | 244 | document.addEventListener('keydown', function (event) { 245 | if (searchInput == document.activeElement) 246 | return; 247 | if (event.key === 'ArrowLeft') { 248 | currentImageIndex = (currentImageIndex - 1 + openImageViewer.length) % openImageViewer.length; 249 | showImage(); 250 | } 251 | else if (event.key === 'ArrowRight') { 252 | currentImageIndex = (currentImageIndex + 1) % openImageViewer.length; 253 | showImage(); 254 | } 255 | }); 256 | 257 | imageBefore.addEventListener('click', function () { 258 | currentImageIndex = (currentImageIndex - 1 + openImageViewer.length) % openImageViewer.length; 259 | showImage(); 260 | }); 261 | 262 | imageNext.addEventListener('click', function () { 263 | currentImageIndex = (currentImageIndex + 1) % openImageViewer.length; 264 | showImage(); 265 | }); 266 | 267 | function showImage() { 268 | for (const image of document.querySelectorAll(".image_selected")) { 269 | image.classList = ['image']; 270 | } 271 | const current_image = document.querySelectorAll(".image")[currentImageIndex]; 272 | current_image.classList.add("image_selected"); 273 | var rect = current_image.getBoundingClientRect(); 274 | if (!(rect.top >= 0 && rect.left >= 0 && 275 | rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && 276 | rect.right <= (window.innerWidth || document.documentElement.clientWidth))) { 277 | current_image.scrollIntoView(false); 278 | } 279 | 280 | const src = openImageViewer[currentImageIndex].getAttribute('src'); 281 | const alt = openImageViewer[currentImageIndex].getAttribute('alt'); 282 | const data = openImageViewer[currentImageIndex].getAttribute('data'); 283 | const clickableLink = openImageViewer[currentImageIndex].closest('.clickable'); 284 | const href = clickableLink.getAttribute('href'); 285 | viewImageImg.src = src; 286 | imageProxy.href = src; 287 | imageFull.href = data; 288 | imageSource.href = href; 289 | imageSource.textContent = href; 290 | imageViewerLink.href = href; 291 | images.classList.remove('images_viewer_hidden'); 292 | imageView.classList.remove('image_hide'); 293 | imageView.classList.add('image_show'); 294 | imageAlt.textContent = alt; 295 | fullImageSize.textContent = document.querySelector(".image_selected .resolution").textContent; 296 | 297 | getImageSize(src).then(size => { 298 | imageSize.textContent = size; 299 | }); 300 | } 301 | 302 | function getImageSize(url) { 303 | return new Promise((resolve, reject) => { 304 | const img = new Image(); 305 | img.onload = function () { 306 | const size = `${this.width} x ${this.height}`; 307 | resolve(size); 308 | }; 309 | img.onerror = function () { 310 | reject('Error loading image'); 311 | }; 312 | img.src = url; 313 | }); 314 | } 315 | } 316 | --------------------------------------------------------------------------------