Skip to content

Commit

Permalink
Add first working version of column reordering and proper results dis…
Browse files Browse the repository at this point in the history
…play
  • Loading branch information
BurnySc2 committed Oct 24, 2024
1 parent 62f702a commit 0de2506
Show file tree
Hide file tree
Showing 7 changed files with 344 additions and 113 deletions.
16 changes: 15 additions & 1 deletion fastapi_server/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions fastapi_server/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ beautifulsoup4 = "^4" # xml parsing
edge-tts = "^6" # Audio generation
stream-zip = "^0" # Zip files with low memory footprint
minio = "^7" # AWS S3
humanize = "^4.11.0"

[tool.poetry.group.dev.dependencies]
# Test library
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

load_dotenv()


# pyre-fixme[16]
ALLOWED_LIST_OF_TWITCH_USERS = set(os.getenv("ALLOWED_TWITCH_USERS_FOR_TELEGRAM_BROWSER").lower().split(";"))


Expand Down
119 changes: 102 additions & 17 deletions fastapi_server/src/routes/telegram_browser/telegram_browser.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
from __future__ import annotations

import json
from datetime import timedelta
from typing import Annotated, Literal
from typing import Annotated, Any, Literal

import arrow
from litestar import Controller, get, post
import humanize
from litestar import Controller, Response, get, post
from litestar.contrib.htmx.request import HTMXRequest
from litestar.contrib.htmx.response import HTMXTemplate
from litestar.datastructures import Cookie
from litestar.enums import RequestEncodingType
from litestar.params import Body
from litestar.params import Body, Parameter
from litestar.response import Template
from litestar.stores.memory import MemoryStore
from loguru import logger
Expand All @@ -21,28 +24,45 @@

telegram_store = MemoryStore()

COOKIES = {"active_columns": "active-columns"}

RESULT_COLUMNS = {
# key: column head row name
"message_link": "Link",
"message_date": "Date",
"message_text": "Text",
"amount_of_reactions": "#Reactions",
"amount_of_comments": "#Comments",
"file_extension": "File type",
"file_size_bytes": "Size",
"file_duration_seconds": "Duration",
}


async def all_channels_cache() -> list[models.TelegramChannel]:
# TODO Use helper-function from caches.py
all_channels = await telegram_store.get("all_channels")
if all_channels is None:
async with get_db() as db:
all_channels = await db.telegramchannel.find_many(
# pyre-fixme[55]
where={"channel_username": {"not": None}},
order={
"channel_username": "asc",
},
)
# pyre-fixme[6]
await telegram_store.set("all_channels", all_channels, expires_in=300) # 5 Minutes
# pyre-fixme[7]
return await telegram_store.get("all_channels")


async def all_file_formats_cache() -> list[models.TelegramChannel]:
# TODO Use helper-function from caches.py
all_file_formats = await telegram_store.get("all_file_formats")
all_file_formats: list[dict] = await telegram_store.get("all_file_formats")
if all_file_formats is None:
async with get_db() as db:
all_file_formats: list[dict] = await db.query_raw(
all_file_formats = await db.query_raw(
"""
SELECT DISTINCT LOWER(file_extension) AS file_extension_lower
FROM public.litestar_telegram_message
Expand Down Expand Up @@ -85,6 +105,23 @@ def string_to_seconds(duration: str) -> int:
return int(delta.total_seconds())


def seconds_to_string(duration: float) -> str:
minutes, seconds = divmod(int(duration), 60)
hours, minutes = divmod(minutes, 60)
if hours > 0:
return f"{hours}:{minutes:02d}:{seconds:02d}"
return f"{minutes}:{seconds:02d}"


def get_actived_and_disabled_columns(active_columns_str: str | None) -> tuple[dict[str, Any], list[dict[str, Any]]]:
if active_columns_str is None:
return RESULT_COLUMNS, {}
active_columns_list: list[str] = json.loads(active_columns_str)
active_columns_dict = {key: RESULT_COLUMNS[key] for key in active_columns_list if key in RESULT_COLUMNS}
disabled_columns_dict = {k: v for k, v in RESULT_COLUMNS.items() if k not in active_columns_dict}
return active_columns_dict, disabled_columns_dict


class MyTelegramBrowserRoute(Controller):
path = "/telegram-browser"
guards = [is_logged_in_allowed_accounts_guard]
Expand All @@ -93,11 +130,17 @@ class MyTelegramBrowserRoute(Controller):
@get("/")
async def index(
self,
active_columns_str: Annotated[str | None, Parameter(cookie=COOKIES["active_columns"])] = None,
) -> Template:
# all_channels = await all_channels_cache()
# all_file_formats = await all_file_formats_cache()
active_columns_dict, disabled_columns_dict = get_actived_and_disabled_columns(active_columns_str)
return Template(
"telegram_browser/index.html",
context={
"active_columns": active_columns_dict,
"disabled_columns": disabled_columns_dict,
},
# context={
# "channel_names": [i.channel_username for i in all_channels],
# "file_extensions": [i["file_extension_lower"] for i in all_file_formats],
Expand All @@ -109,21 +152,21 @@ async def query_search(
self,
request: HTMXRequest,
data: Annotated[SearchInput, Body(media_type=RequestEncodingType.URL_ENCODED)],
active_columns_str: Annotated[str | None, Parameter(cookie=COOKIES["active_columns"])] = None,
) -> Template:
logger.info(f"Search input: {data=}")
active_columns_dict, disabled_columns_dict = get_actived_and_disabled_columns(active_columns_str)
async with get_db() as db:
results = await db.telegrammessage.find_many(
# If "{}" then the filter will be ignored
where={
"AND": [
# SEARCH TEXT
{"message_text": {"contains": data.search_text, "mode": "insensitive"}}
if data.search_text
if data.search_text != ""
else {},
# CHANNEL NAME
{"channel": {"channel_username": {"contains": data.channel_name, "mode": "insensitive"}}}
if data.channel_name
else {},
{"channel": {"channel_username": data.channel_name}} if data.channel_name else {},
# DATE RANGE
{"message_date": {"gte": arrow.get(data.datetime_min).datetime}}
if data.datetime_min != ""
Expand Down Expand Up @@ -151,8 +194,8 @@ async def query_search(
if string_to_seconds(data.file_duration_max) > 0
else {},
# FILE SIZE
{"file_size_bytes": {"gte": data.file_size_min}} if data.file_size_min != "" else {},
{"file_size_bytes": {"lte": data.file_size_max}} if data.file_size_max != "" else {},
{"file_size_bytes": {"gte": data.file_size_min * 2**20}} if data.file_size_min != "" else {},
{"file_size_bytes": {"lte": data.file_size_max * 2**20}} if data.file_size_max != "" else {},
# IMAGE SIZE WIDTH
{"file_width": {"gte": data.file_image_width_min}} if data.file_image_width_min != "" else {},
{"file_width": {"lte": data.file_image_width_max}} if data.file_image_width_max != "" else {},
Expand All @@ -169,31 +212,53 @@ async def query_search(
# order={""},
include={"channel": True},
take=200,
skip=0, # TODO Pagination
# skip=0, # TODO Pagination
)
results_as_dict: list[dict] = [
{
"message_link": f"https://t.me/{row.channel.channel_username.lower()}/{row.message_id}",
"message_date": arrow.get(row.message_date).strftime("%Y-%m-%d %H:%M:%S"),
"message_text": row.message_text if row.message_text is not None else "",
"amount_of_reactions": row.amount_of_reactions,
"amount_of_comments": row.amount_of_comments,
"file_extension": row.file_extension if row.file_extension is not None else "-",
"file_size_bytes": humanize.naturalsize(row.file_size_bytes, format="%d")
if row.file_size_bytes is not None
else "-",
"file_duration_seconds": seconds_to_string(row.file_duration_seconds)
if row.file_duration_seconds is not None
else "-",
}
for row in results
]
if len(results_as_dict) > 0:
assert set(active_columns_dict) | set(disabled_columns_dict) == set(results_as_dict[0])
return HTMXTemplate(
template_name="telegram_browser/search_results.html",
context={"results": results},
context={
"table_headers": active_columns_dict,
"results": [{col_key: row[col_key] for col_key in active_columns_dict} for row in results_as_dict],
},
# TODO Change url to represent search params
# push_url="/asd",
)

@post("/queue-file")
async def queue_download_file(
self,
) -> Template:
) -> None:
"In database, set file to queued"

@post("/delete-file")
async def delete_file(
self,
) -> Template:
) -> None:
"Delete file in database and in minio"

@get("/view-file")
async def view_file(
self,
) -> Template:
) -> None:
"""
Allow the user to view the file in browser
Return <video> element for videos, <audio> for audio, <img> for image
Expand All @@ -202,5 +267,25 @@ async def view_file(
@get("/download-file")
async def download_file(
self,
) -> Template:
) -> None:
"Allow the user to download the file to file system"

@post("/save-active-columns")
async def save_active_columns(
self,
data: Annotated[dict, Body(media_type=RequestEncodingType.URL_ENCODED)],
) -> Response[str]:
"Allow the user to download the file to file system"

column_order = data["columns-order"].split(";")
return Response(
content="",
cookies=[
Cookie(
key=COOKIES["active_columns"],
value=json.dumps(column_order),
path="/telegram-browser",
expires=10**10,
),
],
)
2 changes: 2 additions & 0 deletions fastapi_server/src/templates/audiobook/epub_chapter.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
</div>
{% elif chapter.has_audio is defined and chapter.has_audio %}
<button class="flex border-4 border-black p-2 rounded-3xl hover:bg-green-300 items-center"
{% TODO Load audio directly with 'src' attribute but only load metadata, not the full audio %}
{% TODO The src may be another server endpoints or a minio presigned url with expire duration of 1h %}
hx-target="#chapter_audio_{{ chapter.chapter_number }}" hx-swap="outerHTML"
hx-post="/audiobook/load_generated_audio?book_id={{ book_id }}&chapter_number={{ chapter.chapter_number }}"
hx-indicator="#spinner2_{{ chapter.chapter_number }}">Load audio
Expand Down
Loading

0 comments on commit 0de2506

Please sign in to comment.