├── .dockerignore ├── .env ├── .gitignore ├── LICENSE ├── README.md ├── SECURITY.md ├── __init__.py ├── _config.py ├── alpine-based.dockerfile ├── bangs.json ├── compose.yaml ├── requirements.txt ├── resources └── _config.py.gen.template ├── scripts ├── docker-cmd.sh ├── generate-opensearch.sh └── generate-pyconfig.py ├── src ├── helpers.py ├── images.py ├── textResults.py ├── text_engines │ ├── google.py │ ├── objects │ │ ├── fullEngineResults.py │ │ ├── textResult.py │ │ └── wikiSnippet.py │ └── qwant.py ├── torrent_sites │ ├── nyaa.py │ ├── rutor.py │ ├── thepiratebay.py │ └── torrentgalaxy.py ├── torrents.py └── video.py ├── static ├── calculator.js ├── cookies-settings-version.js ├── cookies.js ├── css │ ├── ancient.css │ ├── catppuccin_mocha_blue.css │ ├── catppuccin_mocha_green.css │ ├── classic.css │ ├── dark.css │ ├── dark_blur.css │ ├── dark_blur_beta.css │ ├── dark_default.css │ ├── darker.css │ ├── gentoo_lavender.css │ ├── github_night.css │ ├── light.css │ ├── menu.css │ ├── night.css │ ├── red.css │ ├── search.css │ ├── settings-style.css │ └── style.css ├── favicon.ico ├── favicon.png ├── fonts │ ├── inter-v12-latin-300.woff │ ├── inter-v12-latin-300.woff2 │ ├── inter-v12-latin-700.woff │ ├── inter-v12-latin-700.woff2 │ ├── inter-v12-latin-regular.woff │ ├── inter-v12-latin-regular.woff2 │ └── material-icons-round-v108-latin-regular.woff2 ├── imagesearch.png ├── lang │ ├── README.md │ ├── danish.json │ ├── dutch.json │ ├── english.json │ ├── french.json │ ├── french_canadian.json │ ├── german.json │ ├── greek.json │ ├── italian.json │ ├── japanese.json │ ├── korean.json │ ├── mandarin_chinese.json │ ├── norwegian.json │ ├── polish.json │ ├── portuguese.json │ ├── romanian.json │ ├── russian.json │ ├── spanish.json │ ├── swedish.json │ ├── turkish.json │ └── ukrainian.json ├── menu.js ├── opensearch.xml.example ├── preview1.webp ├── preview2.webp ├── preview3.webp ├── script.js ├── searchicon.png ├── sheng-l-q2dUSl9S4Xg-unsplash.webp ├── themepreview │ ├── preview1.webp │ ├── preview10.webp │ ├── preview11.webp │ ├── preview12.webp │ ├── preview13.webp │ ├── preview2.webp │ ├── preview3.webp │ ├── preview4.webp │ ├── preview5.webp │ ├── preview6.webp │ ├── preview7.webp │ ├── preview8.webp │ └── preview9.webp ├── torrentSort.js └── uxlang.js ├── templates ├── discover.html ├── images.html ├── preresults_layout.html ├── results.html ├── results_layout.html ├── search.html ├── settings.html ├── torrents.html └── videos.html └── ubuntu-based.dockerfile /.dockerignore: -------------------------------------------------------------------------------- 1 | *.dockerfile 2 | .* 3 | !.git 4 | compose.yaml 5 | *.md 6 | LICENSE 7 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # Essential server variables 2 | #DOMAIN= 3 | PORT=8000 4 | WORKERS=8 5 | THREADS=2 6 | 7 | # Some basic customization. 8 | SHEBANG=! 9 | DEFAULT_THEME=dark_blur 10 | DEFAULT_METHOD=GET 11 | DONATE_URL=https://github.com/sponsors/Extravi 12 | ENABLE_API=False 13 | DEFAULT_LANG=english 14 | 15 | # Piped (an alternative yt frontend & proxy) config 16 | PIPED_INSTANCE=yt.extravi.dev 17 | PIPED_API=ytapi.extravi.dev 18 | PIPED_PROXY=ytproxy.extravi.dev 19 | 20 | # Torrenting configuration. 21 | ENABLE_TORRENTS=True 22 | TORRENT_SITES=[ 'nyaa', 'torrentgalaxy', 'tpb', 'rutor' ] 23 | TORRENTGALAXY_DOMAIN=torrentgalaxy.to 24 | NYAA_DOMAIN=nyaa.si 25 | APIBAY_DOMAIN=apibay.org 26 | RUTOR_DOMAIN=rutor.info 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | **/__pycache__ 3 | venv 4 | **/opensearch.xml 5 | .vscode 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Araa 3 |

4 | 5 |

6 | 7 | 8 |

9 | 10 |

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

11 | 12 | [![Counter](https://visitor-badge.laobi.icu/badge?page_id=Extravi.tailsx)](https://github.com/Extravi/araa-search) 13 | [![License](https://img.shields.io/github/license/Extravi/araa-search)](https://github.com/Extravi/araa-search/blob/main/LICENSE) 14 | [![Stars](https://img.shields.io/github/stars/Extravi/araa-search?style=social)](https://github.com/Extravi/araa-search/stargazers) 15 | 16 | If you're looking to [install](https://extravi.dev/araa) Araa, here is a how-to guide for it. 17 | 18 | ### Instances 19 | 20 | | Clearnet | Tor | SSL | Country | Status | 21 | |-|-|-|-|-| 22 | | [araa.extravi.dev](https://araa.extravi.dev/) | N/A | [www.ssllabs.com](https://www.ssllabs.com/ssltest/analyze.html?d=araa.extravi.dev)✅| United States 🇺🇸 | Official instance | 23 | | [tailsx.com](https://tailsx.com/) | [Link](http://inbbfryz7elofjk23pi7txnibttnuyz3rg2vwqmfengteeyhrmvex4id.onion/) | [www.ssllabs.com](https://www.ssllabs.com/ssltest/analyze.html?d=tailsx.com)✅| Canada 🇨🇦 | Unofficial instance | 24 | 25 | ### Contributors 26 | ![Contributors](https://contrib.rocks/image?repo=Extravi/araa-search) 27 | 28 | ## Features 29 | Here are some of the features that Araa a privacy-respecting, ad-free, self-hosted Google metasearch engine with strong security provides: 30 | 31 | * Full API support for easy integration into third-party apps and services 32 | * Utilizes Qwant for image search, which is known for its strong privacy protections 33 | * DuckDuckGo is used for auto-complete, offering privacy-enhanced search suggestions 34 | * Hosted on your own server, providing complete control over your data and ensuring privacy 35 | * Strong security measures implemented, including SSL encryption and firewalls 36 | * Ad-free search results, with no tracking or data collection for advertising purposes 37 | 38 | ## Screenshots 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | ## Contact 50 | Email: dante@extravi.dev 51 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | Please report any potential security issues to security@extravi.dev 3 | -------------------------------------------------------------------------------- /_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 = "google" 16 | 17 | 18 | # Default theme 19 | DEFAULT_THEME = 'dark_default' 20 | 21 | # Default method 22 | DEFAULT_METHOD = "GET" 23 | 24 | # Default autocomplete "google" will use Google, and "ddg" will use DuckDuckGo 25 | DEFAULT_AUTOCOMPLETE = "google" 26 | 27 | # The port for this server to listen on 28 | PORT = 8000 29 | 30 | # Torrent domains 31 | TORRENTGALAXY_DOMAIN = "torrentgalaxy.to" 32 | NYAA_DOMAIN = "nyaa.si" 33 | # apibay is the api for thepiratebay.org 34 | API_BAY_DOMAIN = "apibay.org" 35 | RUTOR_DOMAIN = "rutor.info" 36 | 37 | # Domain of the Piped instance to use 38 | PIPED_INSTANCE_API = "ytapi.ttj.dev" 39 | PIPED_INSTANCE = "yt.ttj.dev" 40 | PIPED_INSTANCE_PROXY = "ytproxy.ttj.dev" 41 | 42 | # Useragents to use in the request. 43 | user_agents = [ 44 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.3", 45 | "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", 46 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 14.1; rv:109.0) Gecko/20100101 Firefox/121.0", 47 | "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", 48 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0", 49 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15", 50 | ] 51 | 52 | # prompts for user agent & ip queries 53 | VALID_IP_PROMPTS = [ 54 | "what is my ip", 55 | "what is my ip address", 56 | "what's my ip", 57 | "whats my ip" 58 | ] 59 | VALID_UA_PROMPTS = [ 60 | "what is my user agent", 61 | "what is my useragent", 62 | "whats my useragent", 63 | "whats my user agent", 64 | "what's my useragent", 65 | "what's my user agent", 66 | ] 67 | 68 | 69 | WHITELISTED_DOMAINS = [ 70 | "www.google.com", 71 | "wikipedia.org", 72 | PIPED_INSTANCE, 73 | PIPED_INSTANCE_API, 74 | PIPED_INSTANCE_PROXY, 75 | "api.qwant.com", 76 | TORRENTGALAXY_DOMAIN, 77 | NYAA_DOMAIN, 78 | API_BAY_DOMAIN, 79 | RUTOR_DOMAIN, 80 | ] 81 | 82 | ENABLED_TORRENT_SITES = [ 83 | "nyaa", 84 | "torrentgalaxy", 85 | "tpb", 86 | "rutor", 87 | ] 88 | 89 | TORRENT_TRACKERS = [ 90 | 'http://nyaa.tracker.wf:7777/announce', 91 | 'udp://open.stealth.si:80/announce', 92 | 'udp://tracker.opentrackr.org:1337/announce', 93 | 'udp://exodus.desync.com:6969/announce', 94 | 'udp://tracker.torrent.eu.org:451/announce' 95 | ] 96 | 97 | COOKIE_AGE = 2147483647 98 | 99 | # set to true to enable api support 100 | API_ENABLED = False 101 | 102 | # set to false to disable torrent search 103 | TORRENTSEARCH_ENABLED = True 104 | 105 | UX_LANGUAGES = [ 106 | {'lang_lower': 'english', 'lang_fancy': 'English'}, 107 | {'lang_lower': 'danish', 'lang_fancy': 'Danish (Dansk)'}, 108 | {'lang_lower': 'dutch', 'lang_fancy': 'Dutch (Nederlands)'}, 109 | {'lang_lower': 'french', 'lang_fancy': 'French (Français)'}, 110 | {'lang_lower': 'french_canadian', 'lang_fancy': 'French Canadian (Français canadien)'}, 111 | {'lang_lower': 'german', 'lang_fancy': 'German (Deutsch)'}, 112 | {'lang_lower': 'greek', 'lang_fancy': 'Greek (Ελληνικά)'}, 113 | {'lang_lower': 'italian', 'lang_fancy': 'Italian (Italiano)'}, 114 | {'lang_lower': 'japanese', 'lang_fancy': 'Japanese (日本語)'}, 115 | {'lang_lower': 'korean', 'lang_fancy': 'Korean (한국어)'}, 116 | {'lang_lower': 'mandarin_chinese', 'lang_fancy': 'Mandarin Chinese (普通话 or 中文)'}, 117 | {'lang_lower': 'norwegian', 'lang_fancy': 'Norwegian (Norsk)'}, 118 | {'lang_lower': 'polish', 'lang_fancy': 'Polish (Polski)'}, 119 | {'lang_lower': 'portuguese', 'lang_fancy': 'Portuguese (Português)'}, 120 | {'lang_lower': 'russian', 'lang_fancy': 'Russian (Русский)'}, 121 | {'lang_lower': 'spanish', 'lang_fancy': 'Spanish (Español)'}, 122 | {'lang_lower': 'swedish', 'lang_fancy': 'Swedish (Svenska)'}, 123 | {'lang_lower': 'turkish', 'lang_fancy': 'Turkish (Türkçe)'}, 124 | {'lang_lower': 'ukrainian', 'lang_fancy': 'Ukrainian (Українська)'}, 125 | {'lang_lower': 'romanian', 'lang_fancy': 'Romanian (Română)'}, 126 | ] 127 | 128 | # See all the 'lang_lower' values in UX_LANGUAGES 129 | DEFAULT_UX_LANG = "english" 130 | 131 | DEFAULT_GOOGLE_DOMAIN = "/search?gl=us" 132 | 133 | ENGINE_RATELIMIT_COOLDOWN_MINUTES = 28 134 | -------------------------------------------------------------------------------- /alpine-based.dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:alpine 2 | 3 | # LABEL can be used to attach metadata to the container. 4 | LABEL title="Araa Search" \ 5 | description="A privacy-respecting, ad-free, self-hosted Google metasearch engine with strong security and full API support." \ 6 | git_repo="https://github.com/TEMtheLEM/araa-search" \ 7 | authors="https://github.com/Extravi/araa-search/contributors" \ 8 | maintainer="TEMtheLEM " \ 9 | image="https://hub.docker.com/r/temthelem/araa-search" 10 | 11 | WORKDIR /app 12 | 13 | COPY requirements.txt /app/ 14 | 15 | # We will only be running our own python app in a container, 16 | # so this shouldn't be terrible. 17 | RUN pip install --break-system-packages -r requirements.txt 18 | 19 | COPY . . 20 | 21 | ENV ORIGIN_REPO=https://github.com/TEMtheLEM/araa-search 22 | 23 | CMD [ "sh", "scripts/docker-cmd.sh" ] 24 | -------------------------------------------------------------------------------- /compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | araa-search: 3 | container_name: Araa 4 | image: docker.io/temthelem/araa-search:latest 5 | env_file: 6 | - .env # May be redundant. Who cares ¯\_(ツ)_/¯ 7 | ports: 8 | - "${PORT}:${PORT}" 9 | watchtower: # Not required. Keeps containers up-to-date. 10 | container_name: watchtower 11 | image: docker.io/containrrr/watchtower 12 | volumes: 13 | - /var/run/docker.sock:/var/run/docker.sock 14 | command: --interval 60 15 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | lxml 3 | bs4 4 | gunicorn 5 | requests 6 | thefuzz 7 | httpx[http2] 8 | trio 9 | werkzeug>=3.0.3 # not directly required, pinned by Snyk to avoid a vulnerability 10 | urllib3>=2.2.2 # not directly required, pinned by Snyk to avoid a vulnerability 11 | anyio>=4.4.0 # not directly required, pinned by Snyk to avoid a vulnerability 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /scripts/docker-cmd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # This script is meant to be ran solely in a Docker container. 3 | # Do not run it manually, it's not meant for that. 4 | 5 | sh scripts/generate-opensearch.sh || exit $? 6 | python3 scripts/generate-pyconfig.py || exit $? 7 | 8 | [ "$WORKERS" ] || WORKERS=2 9 | [ "$THREADS" ] || THREADS=8 10 | [ "$PORT" ] || PORT=8000 11 | 12 | exec gunicorn --workers $WORKERS --threads $THREADS --bind="0.0.0.0:$PORT" __init__:app 13 | -------------------------------------------------------------------------------- /scripts/generate-opensearch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Generates the opensearch.xml file based off of a $DOMAIN env. var. 3 | # Fails if $DOMAIN is blank or not set. 4 | 5 | if [ $DOMAIN ]; then 6 | echo " 7 | 8 | Araa 9 | 10 | 11 | UTF-8 12 | UTF-8 13 | en-us 14 | " > ./static/opensearch.xml; 15 | else 16 | echo "Make a DOMAIN env. variable & set it to your domain! 17 | (Ex; DOMAIN=www.yourdomain.com)"; 18 | exit 1; 19 | fi 20 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | # Return the JSON object 109 | return (json.loads(response.text), response.status_code) 110 | 111 | def get_magnet_hash(magnet): 112 | return magnet.split("btih:")[1].split("&")[0] 113 | 114 | def get_magnet_name(magnet): 115 | return magnet.split("&dn=")[1].split("&tr")[0] 116 | 117 | 118 | def apply_trackers(hash, name="", magnet=True): 119 | if magnet: 120 | name = get_magnet_name(hash) 121 | hash = get_magnet_hash(hash) 122 | 123 | return f"magnet:?xt=urn:btih:{hash}&dn={name}&tr={'&tr='.join(TORRENT_TRACKERS)}" 124 | 125 | def string_to_bytes(file_size): 126 | units = { 127 | 'bytes': 1, 128 | 'kb': 1024, 129 | 'mb': 1024 ** 2, 130 | 'gb': 1024 ** 3, 131 | 'tb': 1024 ** 4, 132 | 'kib': 1024, 133 | 'mib': 1024 ** 2, 134 | 'gib': 1024 ** 3, 135 | 'tib': 1024 ** 4 136 | } 137 | 138 | size, unit = file_size.lower().split() 139 | return float(size) * units[unit] 140 | 141 | def bytes_to_string(size): 142 | units = ['bytes', 'KB', 'MB', 'GB', 'TB'] 143 | index = 0 144 | while size >= 1024 and index < len(units) - 1: 145 | size /= 1024 146 | index += 1 147 | return f"{size:.2f} {units[index]}" 148 | 149 | 150 | class Settings(): 151 | def __init__(self): 152 | self.domain = request.cookies.get("domain", DEFAULT_GOOGLE_DOMAIN) 153 | self.javascript = request.cookies.get("javascript", "enabled") 154 | self.lang = request.cookies.get("lang", "") 155 | self.new_tab = request.cookies.get("new_tab", "") 156 | self.safe = request.cookies.get("safe", "active") 157 | self.ux_lang = request.cookies.get("ux_lang", DEFAULT_UX_LANG) 158 | self.theme = request.cookies.get("theme", DEFAULT_THEME) 159 | self.method = request.cookies.get("method", DEFAULT_METHOD) 160 | self.ac = request.cookies.get("ac", DEFAULT_AUTOCOMPLETE) 161 | self.engine = request.cookies.get("engine", DEFAULT_ENGINE) 162 | self.torrent = request.cookies.get("torrent", "enabled" if TORRENTSEARCH_ENABLED else "disabled") 163 | 164 | 165 | # Returns a tuple of two ellements. 166 | # The first is the wikipedia proxy's URL (used to load an wiki page's image after page load), 167 | # and the second is an image proxy link for the very image of the page itself. 168 | # 169 | # Either the first or second ellement will be a string, but not both (at least one ellement 170 | # will be None). 171 | # 172 | # NOTE: This function may return (None, None) in cases of failure. 173 | def grab_wiki_image_from_url(wikipedia_url: str, user_settings: Settings) -> tuple[str | None]: 174 | kno_title = None 175 | kno_image = None 176 | 177 | if user_settings.javascript == "enabled": 178 | kno_title = wikipedia_url.split("/")[-1] 179 | kno_title = f"/wikipedia?q={kno_title}" 180 | else: 181 | try: 182 | _kno_title = wikipedia_url.split("/")[-1] 183 | soup = makeHTMLRequest(f"https://wikipedia.org/w/api.php?action=query&format=json&prop=pageimages&titles={_kno_title}&pithumbsize=500", is_wiki=True) 184 | data = json.loads(soup.text) 185 | img_src = data['query']['pages'][list(data['query']['pages'].keys())[0]]['thumbnail']['source'] 186 | _kno_image = [f"/img_proxy?url={img_src}"] 187 | _kno_image = ''.join(_kno_image) 188 | finally: 189 | kno_image = _kno_image 190 | 191 | return kno_title, kno_image 192 | 193 | 194 | def format_araa_name(json_obj): 195 | # Recursively format araa_name=ARAA_NAME 196 | if isinstance(json_obj, dict): 197 | return {key: format_araa_name(value) for key, value in json_obj.items()} 198 | elif isinstance(json_obj, list): 199 | return [format_araa_name(item) for item in json_obj] 200 | elif isinstance(json_obj, str): 201 | return json_obj.format(araa_name=ARAA_NAME) 202 | else: 203 | return json_obj 204 | -------------------------------------------------------------------------------- /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 | json_data = response.json() 58 | 59 | # Get all the images from the response, while avoiding any errors. 60 | images = json_data.get("data", {}).get("result", {}).get("items", None) 61 | if images is None: 62 | return redirect('/search') 63 | 64 | results = [] 65 | for image in images: 66 | # Get original bing image URL 67 | bing_url = unquote(urlparse(image['thumbnail']).query).split("u=")[1].split("&")[0] 68 | 69 | image['thumb_proxy'] = f"/img_proxy?url={quote(bing_url)}" 70 | 71 | # Get domain name 72 | image['source'] = urlparse(image['url']).netloc 73 | 74 | results.append(image) 75 | 76 | # calc. time spent 77 | end_time = time.time() 78 | elapsed_time = end_time - start_time 79 | 80 | # render 81 | if api == "true" and API_ENABLED: 82 | # return the results list as a JSON response 83 | return jsonify(results) 84 | else: 85 | return render_template("images.html", results=results, title=f"{query} - {ARAA_NAME}", 86 | q=f"{query}", fetched=f"{elapsed_time:.2f}", 87 | type="image", 88 | repo_url=REPO, donate_url=DONATE, API_ENABLED=API_ENABLED, 89 | TORRENTSEARCH_ENABLED=TORRENTSEARCH_ENABLED, lang_data=lang_data, 90 | commit=helpers.latest_commit(), settings=settings, araa_name=ARAA_NAME) 91 | -------------------------------------------------------------------------------- /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 9 | 10 | ENGINES = [ 11 | google, 12 | qwant, 13 | ] 14 | ratelimited_timestamps = {} 15 | 16 | 17 | def handleUserInfoQueries(query: str) -> str | None: 18 | if any(query.lower().find(valid_ip_prompt) != -1 for valid_ip_prompt in VALID_IP_PROMPTS): 19 | xff = request.headers.get("X-Forwarded-For") 20 | if xff: 21 | return xff.split(",")[-1].strip() 22 | return request.remote_addr or "unknown" 23 | elif any(query.lower().find(valid_ua_prompt) != -1 for valid_ua_prompt in VALID_UA_PROMPTS): 24 | return request.headers.get("User-Agent") or "unknown" 25 | return None 26 | 27 | 28 | def textResults(query: str) -> Response: 29 | global ratelimited_engines 30 | # get user language settings 31 | settings = helpers.Settings() 32 | 33 | # Define where to get request args from. If the request is using GET, 34 | # use request.args. Otherwise (POST), use request.form 35 | if request.method == "GET": 36 | args = request.args 37 | else: 38 | args = request.form 39 | 40 | with open(f'static/lang/{settings.ux_lang}.json', 'r') as file: 41 | lang_data = helpers.format_araa_name(json.load(file)) 42 | 43 | # used to measure time spent 44 | start_time = time.time() 45 | 46 | api = args.get("api", "false") 47 | search_type = args.get("t", "text") 48 | p = args.get("p", 0) 49 | 50 | results = None 51 | ratelimited = True # Used to determine if complete engine failure is due to a bug or due to 52 | # the server getting completely ratelimited from every supported engine. 53 | 54 | engine_list = [] 55 | for engine in ENGINES: 56 | if engine.NAME == settings.engine: 57 | # Put prefered engine at the top of the list 58 | engine_list = [engine] + engine_list 59 | else: 60 | engine_list.append(engine) 61 | 62 | 63 | try: 64 | for ENGINE in engine_list: 65 | if (t := ratelimited_timestamps.get(ENGINE.__name__)) is not None and t + ENGINE_RATELIMIT_COOLDOWN_MINUTES * 60 >= time.time(): 66 | # Current engine is ratelimited. Skip it. 67 | continue 68 | results = ENGINE.search(query, p, search_type, settings) 69 | if results.code == 429: 70 | t = time.time() 71 | print(f"Text engine {results.engine} was just ratelimited (time={t})") 72 | ratelimited_timestamps[ENGINE.__name__] = t 73 | else: # Server *likely* isn't ratelimited. 74 | ratelimited = False 75 | if results.ok: 76 | break 77 | print(f"WARN: Text engine {results.engine} failed with code {results.code}.") 78 | if results.code == 429: 79 | print("NOTE: this engine just got ratelimited.") 80 | else: 81 | print(f"Response: {results}") 82 | results = None 83 | except Exception as e: 84 | return jsonify({"error": str(e)}), 500 85 | 86 | if results is None: 87 | if ratelimited: # Server is completely ratelimited :(. 88 | return jsonify({"instance_rate_limited": "The instance you are using is rate limited for every supported engine. Try again later."}), 429 89 | else: # *Likely* not ratelimited. Something probably went wrong. 90 | return jsonify({"error": "Complete engine failure. If this occurs multiple times, then " \ 91 | "this is *likely* an extremely unfortanute bug. Some additional " \ 92 | "information is provided with this error.", "query": query, "type": search_type}), 500 93 | 94 | elapsed_time = time.time() - start_time 95 | 96 | # gets users ip or user agent 97 | info = handleUserInfoQueries(query) 98 | calc = "" 99 | exported_math_expression = "" 100 | # calculator (TODO: Maybe remove expression parsing. It behaves in odd ways, and in general people who need a calculator can just search calculator) 101 | if info == None: 102 | info = "" 103 | math_expression = re.search(r'(\d+(\.\d+)?)\s*([\+\-\*/x])\s*(\d+(\.\d+)?)', query) 104 | if math_expression: 105 | exported_math_expression = math_expression.group(0) 106 | num1 = float(math_expression.group(1)) 107 | operator = math_expression.group(3) 108 | num2 = float(math_expression.group(4)) 109 | 110 | if operator == '+': 111 | result = num1 + num2 112 | elif operator == '-': 113 | result = num1 - num2 114 | elif operator == '*': 115 | result = num1 * num2 116 | elif operator == 'x': 117 | result = num1 * num2 118 | elif operator == '/': 119 | result = num1 / num2 if not isclose(num2, 0) else "Err; cannot divide by 0." 120 | 121 | try: 122 | result = float(result) 123 | if result.is_integer(): 124 | result = int(result) 125 | except: 126 | pass 127 | 128 | calc = result 129 | elif "calculator" in query.lower(): 130 | calc = "0" 131 | else: 132 | calc = "" 133 | 134 | if api == "true" and API_ENABLED == True: 135 | # return the results as a JSON response 136 | return jsonify(results.asDICT()) 137 | else: 138 | check = "" if results.correction is None else results.correction 139 | snip = "" if results.featured is None else results.featured 140 | 141 | return render_template("results.html", 142 | engine=results.engine, 143 | results=results.results, sublink=results.top_result_sublinks, p=p, title=f"{query} - {ARAA_NAME}", 144 | q=f"{query}", fetched=f"{elapsed_time:.2f}", 145 | snip=f"{snip}", 146 | user_info=f"{info}", calc=f"{calc}", check=check, current_url=request.url, 147 | type=search_type, repo_url=REPO, donate_url=DONATE, commit=helpers.latest_commit(), 148 | exported_math_expression=exported_math_expression, API_ENABLED=API_ENABLED, 149 | TORRENTSEARCH_ENABLED=TORRENTSEARCH_ENABLED, lang_data=lang_data, 150 | settings=settings, wiki=results.wiki, araa_name=ARAA_NAME, 151 | before=args.get("before", ""), after=args.get("after", "") 152 | ) 153 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/text_engines/objects/textResult.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | # Provided as a 'blueprint' for a singular result from the text engine. 4 | @dataclass 5 | class TextResult: 6 | title: str 7 | desc: str 8 | url: str 9 | sublinks: list 10 | 11 | def asDICT(self): 12 | return { 13 | "title": self.title, 14 | "desc": self.desc, 15 | "url": self.url, 16 | "sublinks": self.sublinks, 17 | } 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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 | } -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /static/css/menu.css: -------------------------------------------------------------------------------- 1 | .search-menu { 2 | margin-top: -50px !important; 3 | } -------------------------------------------------------------------------------- /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/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/css/search.css: -------------------------------------------------------------------------------- 1 | html { 2 | overflow: hidden; 3 | height: 100%; 4 | } 5 | 6 | body { 7 | min-height: 100%; 8 | overflow-x: hidden; 9 | } -------------------------------------------------------------------------------- /static/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 | } -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/favicon.ico -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/favicon.png -------------------------------------------------------------------------------- /static/fonts/inter-v12-latin-300.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/fonts/inter-v12-latin-300.woff -------------------------------------------------------------------------------- /static/fonts/inter-v12-latin-300.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/fonts/inter-v12-latin-300.woff2 -------------------------------------------------------------------------------- /static/fonts/inter-v12-latin-700.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/fonts/inter-v12-latin-700.woff -------------------------------------------------------------------------------- /static/fonts/inter-v12-latin-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/fonts/inter-v12-latin-700.woff2 -------------------------------------------------------------------------------- /static/fonts/inter-v12-latin-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/fonts/inter-v12-latin-regular.woff -------------------------------------------------------------------------------- /static/fonts/inter-v12-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/fonts/inter-v12-latin-regular.woff2 -------------------------------------------------------------------------------- /static/fonts/material-icons-round-v108-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/fonts/material-icons-round-v108-latin-regular.woff2 -------------------------------------------------------------------------------- /static/imagesearch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/imagesearch.png -------------------------------------------------------------------------------- /static/lang/README.md: -------------------------------------------------------------------------------- 1 | The file should be saved to a file in `static/lang/` and should be titled with the english name of the language in all lower case, followed by '.json'. 2 | 3 | Make sure to add an entry to `UX_LANGUAGES` in _config.py 4 | 5 | It should be formatted like this: 6 | 7 | {'lang_lower': 'french', 'lang_fancy': 'French (Français)'}, 8 | 9 | `lang_lower` should match the first part of the json file (e.g. 'english' for 'english.json'). 10 | `lang_fancy` should be the name of the language in english with the first letter capitalized, followed by the name of the language in its own language in brackets. 11 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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 | } -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /static/opensearch.xml.example: -------------------------------------------------------------------------------- 1 | 2 | 3 | Araa 4 | 5 | 6 | UTF-8 7 | UTF-8 8 | en-us 9 | 10 | -------------------------------------------------------------------------------- /static/preview1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/preview1.webp -------------------------------------------------------------------------------- /static/preview2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/preview2.webp -------------------------------------------------------------------------------- /static/preview3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/preview3.webp -------------------------------------------------------------------------------- /static/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @source: ./script.js 3 | * 4 | * @licstart The following is the entire license notice for the 5 | * JavaScript code in this page. 6 | * 7 | * Copyright (C) 2023 Extravi 8 | * 9 | * The JavaScript code in this page is free software: you can 10 | * redistribute it and/or modify it under the terms of the GNU Affero 11 | * General Public License as published by the Free Software Foundation, 12 | * either version 3 of the License, or (at your option) any later version. 13 | * 14 | * The code is distributed WITHOUT ANY WARRANTY; without even the 15 | * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 16 | * See the GNU Affero General Public License for more details. 17 | * 18 | * As additional permission under GNU Affero General Public License 19 | * section 7, you may distribute non-source (e.g., minimized or compacted) 20 | * forms of that code without the copy of the GNU Affero General Public 21 | * License normally required by section 4, provided you include this 22 | * license notice and a URL through which recipients can access the 23 | * Corresponding Source. 24 | * 25 | * @licend The above is the entire license notice 26 | * for the JavaScript code in this page. 27 | */ 28 | 29 | // Removes the 'Apply Settings' button for Javascript users, 30 | // since changing any of the elements causes the settings to apply 31 | // automatically. 32 | const resultsSave = document.querySelector(".results-save"); 33 | if (resultsSave != null) { 34 | resultsSave.style.display = "none"; 35 | } 36 | 37 | const searchInput = document.getElementById('search-input'); 38 | const searchWrapper = document.querySelectorAll('.wrapper, .wrapper-results')[0]; 39 | const resultsWrapper = document.querySelector('.autocomplete'); 40 | const clearSearch = document.querySelector("#clearSearch"); 41 | 42 | async function getSuggestions(query) { 43 | try { 44 | params = new URLSearchParams({ "q": query }).toString(); 45 | const response = await fetch(`/suggestions?${params}`); 46 | const data = await response.json(); 47 | return data[1]; // Return only the array of suggestion strings 48 | } catch (error) { 49 | console.error(error); 50 | } 51 | } 52 | 53 | let currentIndex = -1; // Keep track of the currently selected suggestion 54 | 55 | let results = []; 56 | searchInput.addEventListener('input', async () => { 57 | let input = searchInput.value; 58 | if (input.length) { 59 | results = await getSuggestions(input); 60 | } 61 | renderResults(results); 62 | currentIndex = -1; // Reset index when we return new results 63 | }); 64 | 65 | searchInput.addEventListener("focus", async () => { 66 | let input = searchInput.value; 67 | if (results.length === 0 && input.length != 0) { 68 | results = await getSuggestions(input); 69 | } 70 | renderResults(results); 71 | }) 72 | 73 | clearSearch.style.visibility = "visible"; // Only show the clear search button for JS users. 74 | clearSearch.addEventListener("click", () => { 75 | searchInput.value = ""; 76 | searchInput.focus(); 77 | }) 78 | 79 | searchInput.addEventListener('keydown', (event) => { 80 | if (event.key === 'ArrowUp' || event.key === 'ArrowDown') { 81 | event.preventDefault(); // Prevent the cursor from moving in the search input 82 | 83 | // Find the currently selected suggestion element 84 | const selectedSuggestion = resultsWrapper.querySelector('.selected'); 85 | if (selectedSuggestion) { 86 | selectedSuggestion.classList.remove('selected'); // Deselect the currently selected suggestion 87 | } 88 | 89 | // Increment or decrement the current index based on the arrow key pressed 90 | if (event.key === 'ArrowUp') { 91 | currentIndex--; 92 | } else { 93 | currentIndex++; 94 | } 95 | 96 | // Wrap around the index if it goes out of bounds 97 | if (currentIndex < 0) { 98 | currentIndex = resultsWrapper.querySelectorAll('li').length - 1; 99 | } else if (currentIndex >= resultsWrapper.querySelectorAll('li').length) { 100 | currentIndex = 0; 101 | } 102 | 103 | // Select the new suggestion 104 | resultsWrapper.querySelectorAll('li')[currentIndex].classList.add('selected'); 105 | // Update the value of the search input 106 | searchInput.value = resultsWrapper.querySelectorAll('li')[currentIndex].textContent; 107 | } 108 | }); 109 | 110 | function renderResults(results) { 111 | if (!results || !results.length || !searchInput.value) { 112 | return searchWrapper.classList.remove('show'); 113 | } 114 | 115 | let content = ''; 116 | results.forEach((item) => { 117 | content += `
  • ${item}
  • `; 118 | }); 119 | 120 | // Only show the autocomplete suggestions if the search input has a non-empty value 121 | if (searchInput.value) { 122 | searchWrapper.classList.add('show'); 123 | } 124 | resultsWrapper.innerHTML = ``; 125 | } 126 | 127 | resultsWrapper.addEventListener('click', (event) => { 128 | if (event.target.tagName === 'LI') { 129 | // Set the value of the search input to the clicked suggestion 130 | searchInput.value = event.target.textContent; 131 | // Reset the current index 132 | currentIndex = -1; 133 | // Submit the form 134 | searchWrapper.querySelector('input[type="submit"]').click(); 135 | // Remove the show class from the search wrapper 136 | searchWrapper.classList.remove('show'); 137 | } 138 | }); 139 | 140 | 141 | document.addEventListener("keypress", (event) => { 142 | if (document.activeElement == searchInput) { 143 | // Allow the '/' character to be pressed when searchInput is active 144 | } else if (document.querySelector(".calc") != null) { 145 | // Do nothing if the calculator is available, so the division keybinding 146 | // will still work 147 | } 148 | else if (event.key == "/") { 149 | event.preventDefault(); 150 | searchInput.focus(); 151 | searchInput.selectionStart = searchInput.selectionEnd = searchInput.value.length; 152 | } 153 | }) 154 | 155 | // Add event listener to hide autocomplete suggestions when clicking outside of search-input or wrapper 156 | document.addEventListener('click', (event) => { 157 | // Check if the target of the event is the search-input or any of its ancestors 158 | if (!searchInput.contains(event.target) && !searchWrapper.contains(event.target)) { 159 | // Remove the show class from the search wrapper 160 | searchWrapper.classList.remove('show'); 161 | } 162 | }); 163 | 164 | // Load material icons. If the file cannot be loaded, 165 | // skip them and put a warning in the console. 166 | const font = new FontFace('Material Icons Round', 'url("/fonts/material-icons-round-v108-latin-regular.woff2") format("woff2")'); 167 | font.load().then(() => { 168 | const icons = document.getElementsByClassName('material-icons-round'); 169 | 170 | // Display all icons. 171 | for (let icon of icons) { 172 | icon.style.visibility = 'visible'; 173 | } 174 | 175 | // Ensure icons for the different types of searches are sized correctly. 176 | document.querySelectorAll('#sub-search-wrapper-ico').forEach((el) => { 177 | el.style.fontSize = '17px'; 178 | }); 179 | }).catch(() => { 180 | console.warn('Failed to load Material Icons Round. Hiding any icons using said pack.'); 181 | }); 182 | 183 | // load image after server side processing 184 | window.addEventListener('DOMContentLoaded', function () { 185 | var knoTitleElement = document.getElementById('kno_title'); 186 | var kno_title = knoTitleElement.dataset.knoTitle; 187 | fetch(kno_title) 188 | .then(response => response.json()) 189 | .then(data => { 190 | const pageId = Object.keys(data.query.pages)[0]; 191 | const thumbnailSource = data.query.pages[pageId].thumbnail.source; 192 | const url = "/img_proxy?url=" + thumbnailSource; 193 | 194 | // update the img tag with url and add kno_wiki_show 195 | var imgElement = document.querySelector('.kno_wiki'); 196 | imgElement.src = url; 197 | imgElement.classList.add('kno_wiki_show'); 198 | 199 | console.log(url); 200 | }) 201 | .catch(error => { 202 | console.log('Error fetching data:', error); 203 | }); 204 | }); 205 | 206 | const urlParams = new URLSearchParams(window.location.search); 207 | 208 | if (document.querySelectorAll(".search-active")[1].getAttribute("value") === "image") { 209 | 210 | // image viewer for image search 211 | const closeButton = document.querySelector('.image-close'); 212 | const imageView = document.querySelector('.image_view'); 213 | const images = document.querySelector('.images'); 214 | const viewImageImg = document.querySelector('.view-image-img'); 215 | const imageSource = document.querySelector('.image-source'); 216 | const imageFull = document.querySelector(".full-size"); 217 | const imageProxy = document.querySelector('.proxy-size'); 218 | const imageViewerLink = document.querySelector('.image-viewer-link'); 219 | const imageSize = document.querySelector('.image-size'); 220 | const fullImageSize = document.querySelector(".full-image-size"); 221 | const imageAlt = document.querySelector('.image-alt'); 222 | const openImageViewer = document.querySelectorAll('.open-image-viewer'); 223 | const imageBefore = document.querySelector('.image-before'); 224 | const imageNext = document.querySelector('.image-next'); 225 | let currentImageIndex = 0; 226 | 227 | closeButton.addEventListener('click', function () { 228 | imageView.classList.remove('image_show'); 229 | imageView.classList.add('image_hide'); 230 | for (const image of document.querySelectorAll(".image_selected")) { 231 | image.classList = ['image']; 232 | } 233 | images.classList.add('images_viewer_hidden'); 234 | }); 235 | 236 | openImageViewer.forEach((image, index) => { 237 | image.addEventListener('click', function (event) { 238 | event.preventDefault(); 239 | currentImageIndex = index; 240 | showImage(); 241 | }); 242 | }); 243 | 244 | document.addEventListener('keydown', function (event) { 245 | if (searchInput == document.activeElement) 246 | return; 247 | if (event.key === 'ArrowLeft') { 248 | currentImageIndex = (currentImageIndex - 1 + openImageViewer.length) % openImageViewer.length; 249 | showImage(); 250 | } 251 | else if (event.key === 'ArrowRight') { 252 | currentImageIndex = (currentImageIndex + 1) % openImageViewer.length; 253 | showImage(); 254 | } 255 | }); 256 | 257 | imageBefore.addEventListener('click', function () { 258 | currentImageIndex = (currentImageIndex - 1 + openImageViewer.length) % openImageViewer.length; 259 | showImage(); 260 | }); 261 | 262 | imageNext.addEventListener('click', function () { 263 | currentImageIndex = (currentImageIndex + 1) % openImageViewer.length; 264 | showImage(); 265 | }); 266 | 267 | function showImage() { 268 | for (const image of document.querySelectorAll(".image_selected")) { 269 | image.classList = ['image']; 270 | } 271 | const current_image = document.querySelectorAll(".image")[currentImageIndex]; 272 | current_image.classList.add("image_selected"); 273 | var rect = current_image.getBoundingClientRect(); 274 | if (!(rect.top >= 0 && rect.left >= 0 && 275 | rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && 276 | rect.right <= (window.innerWidth || document.documentElement.clientWidth))) { 277 | current_image.scrollIntoView(false); 278 | } 279 | 280 | const src = openImageViewer[currentImageIndex].getAttribute('src'); 281 | const alt = openImageViewer[currentImageIndex].getAttribute('alt'); 282 | const data = openImageViewer[currentImageIndex].getAttribute('data'); 283 | const clickableLink = openImageViewer[currentImageIndex].closest('.clickable'); 284 | const href = clickableLink.getAttribute('href'); 285 | viewImageImg.src = src; 286 | imageProxy.href = src; 287 | imageFull.href = data; 288 | imageSource.href = href; 289 | imageSource.textContent = href; 290 | imageViewerLink.href = href; 291 | images.classList.remove('images_viewer_hidden'); 292 | imageView.classList.remove('image_hide'); 293 | imageView.classList.add('image_show'); 294 | imageAlt.textContent = alt; 295 | fullImageSize.textContent = document.querySelector(".image_selected .resolution").textContent; 296 | 297 | getImageSize(src).then(size => { 298 | imageSize.textContent = size; 299 | }); 300 | } 301 | 302 | function getImageSize(url) { 303 | return new Promise((resolve, reject) => { 304 | const img = new Image(); 305 | img.onload = function () { 306 | const size = `${this.width} x ${this.height}`; 307 | resolve(size); 308 | }; 309 | img.onerror = function () { 310 | reject('Error loading image'); 311 | }; 312 | img.src = url; 313 | }); 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /static/searchicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/searchicon.png -------------------------------------------------------------------------------- /static/sheng-l-q2dUSl9S4Xg-unsplash.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/sheng-l-q2dUSl9S4Xg-unsplash.webp -------------------------------------------------------------------------------- /static/themepreview/preview1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/themepreview/preview1.webp -------------------------------------------------------------------------------- /static/themepreview/preview10.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/themepreview/preview10.webp -------------------------------------------------------------------------------- /static/themepreview/preview11.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/themepreview/preview11.webp -------------------------------------------------------------------------------- /static/themepreview/preview12.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/themepreview/preview12.webp -------------------------------------------------------------------------------- /static/themepreview/preview13.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/themepreview/preview13.webp -------------------------------------------------------------------------------- /static/themepreview/preview2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/themepreview/preview2.webp -------------------------------------------------------------------------------- /static/themepreview/preview3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/themepreview/preview3.webp -------------------------------------------------------------------------------- /static/themepreview/preview4.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/themepreview/preview4.webp -------------------------------------------------------------------------------- /static/themepreview/preview5.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/themepreview/preview5.webp -------------------------------------------------------------------------------- /static/themepreview/preview6.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/themepreview/preview6.webp -------------------------------------------------------------------------------- /static/themepreview/preview7.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/themepreview/preview7.webp -------------------------------------------------------------------------------- /static/themepreview/preview8.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/themepreview/preview8.webp -------------------------------------------------------------------------------- /static/themepreview/preview9.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/themepreview/preview9.webp -------------------------------------------------------------------------------- /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/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 |

    {{ lang_data.settings.return_to_settings }}

    5 |
    6 | 7 |
    8 |
    9 |

    {{ lang_data.settings.discover_themes_button }}

    10 |
    11 |
    12 |
    13 |
    14 |
    Dark (Default)
    15 |
    Dark (no background)
    16 |
    Light
    17 |
    Dark
    18 |
    Darker
    19 |
    Github Night
    20 |
    Night
    21 |
    Classic
    22 |
    Ancient
    23 |
    Catppuccin Mocha Blue
    24 |
    Catppuccin Mocha Green
    25 |
    Gentoo Lavender
    26 |
    Red
    27 |
    28 |
    29 |
    30 |
    31 | {% if settings.javascript == "enabled" %} 32 | 33 | 34 | {% endif %} 35 |
    36 | 37 | {% endblock %} 38 | -------------------------------------------------------------------------------- /templates/images.html: -------------------------------------------------------------------------------- 1 | {% extends "results_layout.html" %} 2 | 3 | {% block body %} 4 |

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

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

    19 |

    View source:

    20 |

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

    24 |

    Engine: qwant image search

    25 |
    26 |

    Full-resolution image size:

    27 |
    28 | {% for result in results %} 29 | 44 | {% endfor %} 45 |
    46 |
    47 |
    48 | 49 | 50 | {% set p = request.args.get('p', 1)|int %} 51 | {% if p >= 2 %} 52 | 53 | {% endif %} 54 | 55 |
    56 |
    57 | {% else %} 58 |
    59 | Your search '{{ q }}' came back with no results.
    60 | Try rephrasing your search term and/or recorrect any spelling mistakes. 61 |
    62 | {% endif %} 63 | {% endblock %} 64 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /templates/search.html: -------------------------------------------------------------------------------- 1 | {% extends "preresults_layout.html" %} 2 | 3 | {% block body %} 4 | 5 | {% if settings.theme == "dark_blur" %} 6 | 7 | {% endif %} 8 | 11 |
    12 |

    {{ lang_data.settings.settings_header }}

    13 |
    14 | 15 |
    16 |

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

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

    {{ araa_name }}

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

    {{ lang_data.settings.all_settings }}

    5 |
    6 | 7 |
    8 |
    9 |

    {{ lang_data.settings.all_settings }}

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

    {{ lang_data.settings.discover_themes }}

    22 | 23 |
    24 |
    25 |

    {{ lang_data.settings.preferred_language }}

    26 | 75 |
    76 |
    77 |

    {{ lang_data.settings.preferredux_language }}

    78 | 83 |
    84 |
    85 |

    {{ lang_data.settings.google_domain }}

    86 | 98 |
    99 |
    100 |

    {{ lang_data.settings.safe_search }}

    101 | 105 |
    106 |
    107 |

    {{ lang_data.settings.open_links_new_tab }}

    108 | 112 |
    113 |
    114 |

    Prefered search engine

    115 | 119 |
    120 |
    121 |

    Method

    122 | 126 |
    127 |
    128 |

    Autocomplete

    129 | 133 |
    134 |
    135 |

    {{ lang_data.settings.disable_javascript }}

    136 | 140 |
    141 | {% if torrent_enabled %} 142 |
    143 |

    Enable Torrents

    144 | 148 |
    149 | {% endif %} 150 |
    151 |

    |

    152 | 153 |
    154 | {% if settings.javascript == "enabled" %} 155 | 156 | 157 | 158 | 159 | {% endif %} 160 | 161 |
    162 | {% endblock %} 163 | -------------------------------------------------------------------------------- /templates/torrents.html: -------------------------------------------------------------------------------- 1 | {% extends "results_layout.html" %} 2 | 3 | {% block body %} 4 |

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

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

    {{ result["title"] }}

    36 |

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

    37 |

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

    38 |
    39 | {% endfor %} 40 |
    41 | 42 | {% else %} 43 |
    44 | Your search '{{ q }}' came back with no results.
    45 | Try rephrasing your search term and/or recorrect any spelling mistakes. 46 |
    47 | {% endif %} 48 | {% endblock %} 49 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ubuntu-based.dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:latest 2 | 3 | # LABEL can be used to attach metadata to the container. 4 | LABEL title="Araa Search" \ 5 | description="A privacy-respecting, ad-free, self-hosted Google metasearch engine with strong security and full API support." \ 6 | git_repo="https://github.com/TEMtheLEM/araa-search" \ 7 | authors="https://github.com/Extravi/araa-search/contributors" \ 8 | maintainer="TEMtheLEM " \ 9 | image="https://hub.docker.com/r/temthelem/araa-search" 10 | 11 | WORKDIR /app 12 | 13 | COPY requirements.txt /app/ 14 | 15 | RUN apt update && apt upgrade -y 16 | RUN apt install python3 python3-venv python3-pip --no-install-recommends -y 17 | 18 | # We will only be running our own python app in a container, 19 | # so this shouldn't be terrible. 20 | RUN pip install --break-system-packages -r requirements.txt 21 | 22 | COPY . . 23 | 24 | ENV ORIGIN_REPO=https://github.com/TEMtheLEM/araa-search 25 | 26 | CMD [ "sh", "scripts/docker-cmd.sh" ] 27 | --------------------------------------------------------------------------------