├── .dockerignore ├── .gitignore ├── requirements.txt ├── templates ├── stock.html ├── flash.html ├── results.html ├── news.html ├── podcasts.html ├── video.html ├── index.html ├── ai.html └── images.html ├── docker └── Dockerfile ├── compose.yml ├── README.md └── main.py /.dockerignore: -------------------------------------------------------------------------------- 1 | *.md -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | env 2 | env/ -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | duckduckgo-search 2 | podsearch 3 | yfinance==0.2.54 4 | Flask 5 | -------------------------------------------------------------------------------- /templates/stock.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | {{ pricecheck }} 9 | 10 | 11 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim 2 | 3 | WORKDIR /app 4 | COPY requirements.txt . 5 | RUN pip install --no-cache-dir -r requirements.txt gunicorn 6 | COPY . . 7 | EXPOSE 5000 8 | ENV FLASK_ENV=production 9 | CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "main:app"] 10 | -------------------------------------------------------------------------------- /compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | libresail: 3 | build: 4 | context: . 5 | dockerfile: docker/Dockerfile 6 | container_name: libresail 7 | image: eodowd/libresail:latest 8 | ports: 9 | - "5000:5000" 10 | environment: 11 | - FLASK_ENV=development 12 | restart: unless-stopped 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libresail 2 | building the next gen ai powered meta search engine 3 | 4 | ## License 5 | MIT License 6 | 7 | ## Hosted Demo 8 | https://www.libresail.com 9 | 10 | ## how to use 11 | ### Bare-metal 12 | - clone this repo 13 | - install requirements with: `pip install -r requirements.txt` 14 | - run dev server with: `flask --app main run` 15 | - view app by visiting in browser: `http://127.0.0.1:5000` 16 | 17 | ### docker 18 | - run: `docker compose up -d` 19 | - view app by visiting in browser: `http://127.0.0.1:5000` 20 | 21 | ## Screenshot 22 | 23 | ![Screenshot from 2025-03-13 17-23-16](https://github.com/user-attachments/assets/fc6ed4c4-7d3b-4a75-acab-968341621771) 24 | 25 | ![Screenshot from 2025-03-13 17-27-11](https://github.com/user-attachments/assets/eae60eb4-ccd6-4003-be55-1f118fc3eef7) 26 | -------------------------------------------------------------------------------- /templates/flash.html: -------------------------------------------------------------------------------- 1 |
2 | {% for category, message in messages %} 3 | {% if category == 'danger' %} 4 |
5 | 15 |
16 | {% endif %} 17 | {% endfor %} 18 |
19 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, request, redirect, flash, jsonify 2 | import requests, json 3 | from duckduckgo_search import DDGS 4 | import podsearch 5 | import yfinance as yf 6 | app = Flask(__name__) 7 | app.secret_key = "WhoOwnSearchResults" #change in production 8 | 9 | @app.route('/', methods =["GET", "POST"]) 10 | def home(): 11 | if request.method == "POST": 12 | search_term = request.form.get("search_term") 13 | if search_term[:3] == "ai|": 14 | resultsCHAT = DDGS().chat(str(search_term[3:]), model='claude-3-haiku') 15 | return render_template("ai.html",resultsCHAT=resultsCHAT,search_term=search_term) 16 | if search_term[:5] == "news|": 17 | resultsnews = DDGS().news(str(search_term[5:]), max_results=15) 18 | return render_template("news.html",resultsnews=resultsnews,search_term=search_term) 19 | if search_term[:6] == "image|": 20 | resultsimages = DDGS().images(str(search_term[6:]), max_results=200) 21 | return render_template("images.html",resultsimages=resultsimages,search_term=search_term) 22 | if search_term[:6] == "video|": 23 | resultsvid = DDGS().videos(str(search_term[6:]), max_results=10) 24 | return render_template("video.html",resultsvid=resultsvid,search_term=search_term) 25 | if search_term[:8] == "podcast|": 26 | resultspod = podsearch.search(str(search_term[8:]), country="IE", limit=20) 27 | return render_template("podcasts.html",resultspod=resultspod,search_term=search_term) 28 | return redirect("/"+search_term) 29 | return render_template("index.html") 30 | 31 | @app.route("/search/") 32 | def process(input): 33 | query = request.args.get('query') 34 | if input == 'term': 35 | URL="http://google.com/complete/search?client=chrome&q="+query 36 | headers = {'User-agent':'Mozilla/5.0'} 37 | response = requests.get(URL, headers=headers) 38 | if response.status_code == 200: 39 | suggestions = json.loads(response.content.decode('utf-8'))[1] 40 | return jsonify({"suggestions":suggestions}) 41 | 42 | @app.route('/') 43 | def search(search_term): 44 | results = DDGS().text(str(search_term), region='wt-wt', max_results=25) 45 | sr = search_term 46 | return render_template("results.html", results=results, sr=sr) 47 | 48 | @app.route('/stock/') 49 | def stock(stock): 50 | df = yf.Ticker(str(stock)) 51 | pricecheck = df.info['regularMarketPrice'] 52 | return render_template("stock.html", pricecheck=pricecheck) 53 | 54 | @app.errorhandler(404) 55 | def page_not_found(e): 56 | return "fudge", 404 57 | 58 | @app.errorhandler(500) 59 | def server_error(e): 60 | flash(e, category='danger') 61 | return render_template("index.html"), 500 62 | 63 | -------------------------------------------------------------------------------- /templates/results.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | LibreSail - Search Results 7 | 8 | 9 | 10 | 11 | 12 | 228 | 229 | 230 | 261 |
262 |
263 |
264 | 265 |
266 |
267 |
268 |
269 |

Results for: {{sr}}

270 |
271 | 272 |
273 | {% for i in results %} 274 |
275 |
276 | favicon 277 | {{ i['title'] }} 278 |
279 |
280 | 281 | {{ i['href'] }} 282 | 283 |

{{ i['body'] }}

284 | 285 | {% set words = i['body'].split(' ') %} 286 | {% if words|length > 5 %} 287 |
288 | {% for tag in range(3) %} 289 | {{ words[loop.index * 5]|replace(',', '')|replace('.', '') }} 290 | {% endfor %} 291 |
292 | {% endif %} 293 |
294 |
295 | {% endfor %} 296 |
297 | 298 | 304 |
305 |
306 |
307 |
308 | 309 | 310 | 353 | 354 | 355 | -------------------------------------------------------------------------------- /templates/news.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | LibreSail - News Results 7 | 8 | 9 | 10 | 11 | 12 | 223 | 224 | 225 | 257 | 258 |
259 |
260 |
261 | 262 |
263 |
264 |
265 |
266 | 267 |

News Results for: {{search_term[5:]}}

268 |
269 | 270 |
271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 |
280 | 281 | {% for i in resultsnews %} 282 |
283 |
284 | {{i['date']}} 285 | News 286 |
287 |
288 | 289 | {{i['url']}} 290 | 291 |

{{i['body']}}

292 | 293 |
294 | 297 | 300 |
301 |
302 |
303 | {% endfor %} 304 | 305 |
306 | 309 |
310 |
311 |
312 |
313 |
314 | 315 | 316 | 338 | 339 | 340 | -------------------------------------------------------------------------------- /templates/podcasts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | LibreSail - Podcast Results 7 | 8 | 9 | 10 | 11 | 12 | 272 | 273 | 274 | 289 | 290 |
291 |
292 |
293 | 294 |
295 |
296 |
297 |
298 | 299 |

Podcast Results for: {{search_term[8:]}}

300 |
301 | 302 |
303 | 304 | 305 | 306 | 307 |
308 | 309 | 310 |
311 |
312 | 313 |
314 | {% set ns = namespace(counter=-1) %} 315 | {% for i in range(resultspod|length) %} 316 | {% set ns.counter = ns.counter + 1 %} 317 |
318 |
319 |
320 | {{resultspod[ns.counter].name}} 321 |
{{resultspod[ns.counter].category}}
322 | 323 |
324 | 325 |
326 |
327 |
328 |
329 |
{{resultspod[ns.counter].name}}
330 |
331 | 332 | 333 |
334 |

335 |
336 | 337 | listen 338 | 339 | 342 |
343 |
344 |
345 |
346 | {% endfor %} 347 |
348 | 349 |
350 | 353 |
354 |
355 |
356 |
357 |
358 | 359 | 360 | 410 | 411 | 412 | -------------------------------------------------------------------------------- /templates/video.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | LibreSail - Video Results 7 | 8 | 9 | 10 | 11 | 12 | 239 | 240 | 241 | 273 | 274 |
275 |
276 |
277 | 278 |
279 |
280 |
281 |
282 | 283 |

Video Results for: {{search_term[6:]}}

284 |
285 | 286 |
287 | 288 | 289 | 290 | 291 |
292 | 293 | 294 |
295 |
296 | 297 |
298 | {% for i in resultsvid %} 299 |
300 |
301 |
302 | {{i['title']}} 303 |
{{i['duration']}}
304 |
305 | 306 |
307 |
308 |
309 |
{{i['title']}}
310 |
311 | {{ i['content'].split('/')[2] }} 312 | {{ i.get('date', 'Recent') }} 313 |
314 |

{{i['body']}}

315 |
316 | 317 | Watch 318 | 319 | 322 |
323 |
324 |
325 |
326 | {% endfor %} 327 |
328 | 329 |
330 | 333 |
334 |
335 |
336 |
337 |
338 | 339 | 340 | 376 | 377 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | LibreSail - Free Web Search 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 22 | 199 | 200 | 201 |
202 |
203 |
204 |
205 |
206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 |

225 | libresail 226 |

227 | 228 |
229 | 230 | 231 |
232 | {% with messages = get_flashed_messages(with_categories=true) %} 233 | {% if messages %} 234 | {% include 'flash.html' %} 235 | {% endif %} 236 | {% endwith %} 237 |
238 |

Try using prefixes for special searches:

239 |
240 | ai|your question 241 | news|topic 242 | image|search 243 | video|search 244 |
245 |
246 | 247 | 248 | 249 | 250 |
251 |
252 |
253 |
254 |
255 |
256 | 349 | 359 | 378 | 379 | 380 | -------------------------------------------------------------------------------- /templates/ai.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | LibreSail - AI Chat 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 290 | 291 | 292 |
293 | 294 | 309 | 310 |
311 |
312 |
313 | 314 |
315 |
316 |
317 |
318 | 319 |

AI Response to: {{search_term[3:]}}

320 |
321 | 322 |
323 |
324 | AI Response 325 |
326 |
327 | 328 |
329 |
330 | Loading... 331 |
332 |

Formatting response...

333 |
334 |
335 |
336 | 337 |
338 |

Powered by llama3.2:1B via ollama

339 |
340 | Home 341 | 342 |
343 |
344 |
345 |
346 |
347 |
348 | 349 | 350 | 456 | 457 | 458 | -------------------------------------------------------------------------------- /templates/images.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | LibreSail - Image Results 7 | 8 | 9 | 10 | 11 | 12 | 314 | 315 | 316 | 348 | 349 |
350 |
351 |
352 | 353 |
354 |
355 |
356 |
357 | 358 |

Image Results for: {{search_term[6:]}}

359 |
360 | 361 |
362 | 363 | 364 | 365 | 366 | 367 |
368 | 369 | 370 |
371 |
372 | 373 |
374 | {% for i in resultsimages %} 375 |
376 |
377 |
378 | 381 | 384 | 387 |
388 |
389 | {{i['title']}} 390 |
391 | {{i['title']}} 392 |
393 |
394 | {% endfor %} 395 |
396 | 397 |
398 |
399 |
400 |
401 |
402 | 403 |
404 | 407 |
408 |
409 |
410 |
411 |
412 | 413 | 414 | 433 | 434 | 435 | 517 | 518 | --------------------------------------------------------------------------------