protocol: download unknown mime types
This commit is contained in:
parent
b2fdabea71
commit
cf48818c24
|
@ -10,15 +10,17 @@ TODO DONE
|
|||
encodings
|
||||
bookmarks
|
||||
view/edit sources
|
||||
downloads
|
||||
open last download
|
||||
non shit command-line
|
||||
home page
|
||||
downloads
|
||||
media files
|
||||
view history
|
||||
identity management
|
||||
configuration
|
||||
--------------------------------------------------------------------------------
|
||||
BACKLOG
|
||||
download to disk, not in memory
|
||||
margins / centering
|
||||
pre blocks folding
|
||||
buffers (tabs)
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
"""Gemini-related features of the browser."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from bebop.browser.browser import Browser
|
||||
from bebop.fs import get_downloads_path
|
||||
from bebop.navigation import set_parameter
|
||||
from bebop.page import Page
|
||||
from bebop.protocol import Request, Response
|
||||
|
@ -55,17 +58,10 @@ def open_gemini_url(browser: Browser, url, redirects=0, history=True,
|
|||
if not response:
|
||||
browser.set_status_error(f"Server response parsing failed ({url}).")
|
||||
return
|
||||
response.url = url
|
||||
|
||||
if response.code == 20:
|
||||
handle_code = handle_response_content(browser, response)
|
||||
if handle_code == 0:
|
||||
if browser.current_url and history:
|
||||
browser.history.push(browser.current_url)
|
||||
browser.current_url = url
|
||||
browser.cache[url] = browser.page_pad.current_page
|
||||
browser.set_status(url)
|
||||
elif handle_code == 1:
|
||||
browser.set_status(f"Downloaded {url}.")
|
||||
handle_response_content(browser, url, response, history)
|
||||
elif response.generic_code == 30 and response.meta:
|
||||
browser.open_url(response.meta, base_url=url, redirects=redirects + 1)
|
||||
elif response.generic_code in (40, 50):
|
||||
|
@ -78,44 +74,72 @@ def open_gemini_url(browser: Browser, url, redirects=0, history=True,
|
|||
browser.set_status_error(error)
|
||||
|
||||
|
||||
def handle_response_content(browser: Browser, response: Response) -> int:
|
||||
"""Handle a response's content from a Gemini server.
|
||||
def handle_response_content(browser: Browser, url: str, response: Response,
|
||||
history: bool):
|
||||
"""Handle a successful response content from a Gemini server.
|
||||
|
||||
According to the MIME type received or inferred, render or download the
|
||||
response's content.
|
||||
According to the MIME type received or inferred, the response is either
|
||||
rendered by the browser, or saved to disk. If an error occurs, the browser
|
||||
displays it.
|
||||
|
||||
Currently only text content is rendered. For Gemini, the encoding specified
|
||||
in the response is used, if available on the Python distribution. For other
|
||||
text formats, only UTF-8 is attempted.
|
||||
Only text content is rendered. For Gemini, the encoding specified in the
|
||||
response is used, if available on the Python distribution. For other text
|
||||
formats, only UTF-8 is attempted.
|
||||
|
||||
Arguments:
|
||||
- browser: Browser instance that made the initial request.
|
||||
- url: original URL.
|
||||
- response: a successful Response.
|
||||
|
||||
Returns:
|
||||
An error code: 0 means a page has been loaded, so any book-keeping such
|
||||
as history management can be applied; 1 means a content has been
|
||||
successfully retrieved but has not been displayed (e.g. non-text
|
||||
content) nor saved as a page; 2 means that the content could not be
|
||||
handled, either due to bogus MIME type or MIME parameters.
|
||||
- history: whether to modify history on a page load.
|
||||
"""
|
||||
mime_type = response.get_mime_type()
|
||||
page = None
|
||||
error = None
|
||||
filepath = None
|
||||
if mime_type.main_type == "text":
|
||||
if mime_type.sub_type == "gemini":
|
||||
encoding = mime_type.charset
|
||||
try:
|
||||
text = response.content.decode(encoding, errors="replace")
|
||||
except LookupError:
|
||||
browser.set_status_error("Unknown encoding {encoding}.")
|
||||
return 2
|
||||
browser.load_page(Page.from_gemtext(text))
|
||||
return 0
|
||||
error = f"Unknown encoding {encoding}."
|
||||
else:
|
||||
page = Page.from_gemtext(text)
|
||||
else:
|
||||
text = response.content.decode("utf-8", errors="replace")
|
||||
browser.load_page(Page.from_text(text))
|
||||
return 0
|
||||
page = Page.from_text(text)
|
||||
else:
|
||||
pass # TODO
|
||||
return 1
|
||||
filepath = get_download_path(url)
|
||||
|
||||
if page:
|
||||
browser.load_page(page)
|
||||
if browser.current_url and history:
|
||||
browser.history.push(browser.current_url)
|
||||
browser.current_url = url
|
||||
browser.cache[url] = page
|
||||
browser.set_status(url)
|
||||
elif filepath:
|
||||
try:
|
||||
with open(filepath, "wb") as download_file:
|
||||
download_file.write(response.content)
|
||||
except OSError as exc:
|
||||
browser.set_status_error(f"Failed to save {url} ({exc})")
|
||||
else:
|
||||
browser.set_status(f"Downloaded {url} ({mime_type.short}).")
|
||||
elif error:
|
||||
browser.set_status_error(error)
|
||||
|
||||
|
||||
def get_download_path(url: str) -> Path:
|
||||
"""Try to find the best download file path possible from this URL."""
|
||||
download_dir = get_downloads_path()
|
||||
url_parts = url.rsplit("/", maxsplit=1)
|
||||
if url_parts:
|
||||
filename = url_parts[-1]
|
||||
else:
|
||||
filename = url.split("://")[1] if "://" in url else url
|
||||
filename = filename.replace("/", "_")
|
||||
return download_dir / filename
|
||||
|
||||
|
||||
def handle_input_request(browser: Browser, from_url: str, message: str =None):
|
||||
|
|
22
bebop/fs.py
22
bebop/fs.py
|
@ -4,6 +4,7 @@ A lot of logic comes from `appdirs`:
|
|||
https://github.com/ActiveState/appdirs/blob/master/appdirs.py
|
||||
"""
|
||||
|
||||
from functools import lru_cache
|
||||
from os import getenv
|
||||
from os.path import expanduser
|
||||
from pathlib import Path
|
||||
|
@ -12,7 +13,28 @@ from pathlib import Path
|
|||
APP_NAME = "bebop"
|
||||
|
||||
|
||||
@lru_cache(None)
|
||||
def get_user_data_path() -> Path:
|
||||
"""Return the user data directory path."""
|
||||
path = Path(getenv("XDG_DATA_HOME", expanduser("~/.local/share")))
|
||||
return path / APP_NAME
|
||||
|
||||
|
||||
@lru_cache(None)
|
||||
def get_downloads_path() -> Path:
|
||||
"""Return the user downloads directory path."""
|
||||
xdg_config_path = Path(getenv("XDG_CONFIG_HOME", expanduser("~/.config")))
|
||||
download_path = ""
|
||||
try:
|
||||
with open(xdg_config_path / "user-dirs.dirs", "rt") as user_dirs_file:
|
||||
for line in user_dirs_file:
|
||||
if line.startswith("XDG_DOWNLOAD_DIR="):
|
||||
download_path = line.rstrip().split("=", maxsplit=1)[1]
|
||||
download_path = download_path.strip('"')
|
||||
download_path = download_path.replace("$HOME", expanduser("~"))
|
||||
break
|
||||
except OSError:
|
||||
pass
|
||||
if download_path:
|
||||
return Path(download_path)
|
||||
return Path.home()
|
||||
|
|
|
@ -13,6 +13,10 @@ class MimeType:
|
|||
sub_type: str
|
||||
parameters: dict
|
||||
|
||||
@property
|
||||
def short(self):
|
||||
return f"{self.main_type or '*'}/{self.sub_type or '*'}"
|
||||
|
||||
@property
|
||||
def charset(self):
|
||||
return self.parameters.get("charset", DEFAULT_CHARSET)
|
||||
|
|
|
@ -182,7 +182,16 @@ class StatusCode(IntEnum):
|
|||
|
||||
@dataclass
|
||||
class Response:
|
||||
"""A Gemini response."""
|
||||
"""A Gemini response.
|
||||
|
||||
Response objects can be created only by parsing a Gemini response using the
|
||||
static `parse` method, so you're guaranteed to have a valid object.
|
||||
|
||||
Attributes:
|
||||
- code: the status code returned by the server.
|
||||
- meta: optional meta content.
|
||||
- content: bytes as returned by the server, only in successful requests.
|
||||
"""
|
||||
|
||||
code: StatusCode
|
||||
meta: str = ""
|
||||
|
|
|
@ -18,7 +18,7 @@ def render_lines(metalines, window, max_width):
|
|||
- window: window that will be resized as filled with rendered lines.
|
||||
- max_width: line length limit for the pad.
|
||||
|
||||
Return:
|
||||
Returns:
|
||||
The tuple of integers (error, height, width), error being a non-zero value
|
||||
if an error occured during rendering, and height and width being the new
|
||||
dimensions of the resized window.
|
||||
|
@ -53,7 +53,7 @@ def render_line(metaline, window, max_width):
|
|||
window.addstr(url_text, attributes)
|
||||
|
||||
|
||||
def get_base_line_attributes(line_type):
|
||||
def get_base_line_attributes(line_type) -> int:
|
||||
"""Return the base attributes for this line type.
|
||||
|
||||
Other attributes may be freely used later for this line type but this is
|
||||
|
|
Reference in a new issue