" \
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 |
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 |
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 |
28 | {% for result in results %}
29 |
44 | {% endfor %}
45 |
46 |
47 |
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 | [](https://github.com/Extravi/araa-search)
13 | [](https://github.com/Extravi/araa-search/blob/main/LICENSE)
14 | [](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 | 
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 |
9 |
10 |
11 |
86 |
109 | {% endblock %}
110 |
--------------------------------------------------------------------------------
/templates/settings.html:
--------------------------------------------------------------------------------
1 | {% extends "preresults_layout.html" %}
2 |
3 | {% block body %}
4 |
5 |