protocol: download unknown mime types
This commit is contained in:
parent
b2fdabea71
commit
cf48818c24
|
@ -10,15 +10,17 @@ TODO DONE
|
||||||
encodings
|
encodings
|
||||||
bookmarks
|
bookmarks
|
||||||
view/edit sources
|
view/edit sources
|
||||||
|
downloads
|
||||||
|
open last download
|
||||||
non shit command-line
|
non shit command-line
|
||||||
home page
|
home page
|
||||||
downloads
|
|
||||||
media files
|
media files
|
||||||
view history
|
view history
|
||||||
identity management
|
identity management
|
||||||
configuration
|
configuration
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
BACKLOG
|
BACKLOG
|
||||||
|
download to disk, not in memory
|
||||||
margins / centering
|
margins / centering
|
||||||
pre blocks folding
|
pre blocks folding
|
||||||
buffers (tabs)
|
buffers (tabs)
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
"""Gemini-related features of the browser."""
|
"""Gemini-related features of the browser."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from bebop.browser.browser import Browser
|
from bebop.browser.browser import Browser
|
||||||
|
from bebop.fs import get_downloads_path
|
||||||
from bebop.navigation import set_parameter
|
from bebop.navigation import set_parameter
|
||||||
from bebop.page import Page
|
from bebop.page import Page
|
||||||
from bebop.protocol import Request, Response
|
from bebop.protocol import Request, Response
|
||||||
|
@ -55,17 +58,10 @@ def open_gemini_url(browser: Browser, url, redirects=0, history=True,
|
||||||
if not response:
|
if not response:
|
||||||
browser.set_status_error(f"Server response parsing failed ({url}).")
|
browser.set_status_error(f"Server response parsing failed ({url}).")
|
||||||
return
|
return
|
||||||
|
response.url = url
|
||||||
|
|
||||||
if response.code == 20:
|
if response.code == 20:
|
||||||
handle_code = handle_response_content(browser, response)
|
handle_response_content(browser, url, response, history)
|
||||||
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}.")
|
|
||||||
elif response.generic_code == 30 and response.meta:
|
elif response.generic_code == 30 and response.meta:
|
||||||
browser.open_url(response.meta, base_url=url, redirects=redirects + 1)
|
browser.open_url(response.meta, base_url=url, redirects=redirects + 1)
|
||||||
elif response.generic_code in (40, 50):
|
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)
|
browser.set_status_error(error)
|
||||||
|
|
||||||
|
|
||||||
def handle_response_content(browser: Browser, response: Response) -> int:
|
def handle_response_content(browser: Browser, url: str, response: Response,
|
||||||
"""Handle a response's content from a Gemini server.
|
history: bool):
|
||||||
|
"""Handle a successful response content from a Gemini server.
|
||||||
|
|
||||||
According to the MIME type received or inferred, render or download the
|
According to the MIME type received or inferred, the response is either
|
||||||
response's content.
|
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
|
Only text content is rendered. For Gemini, the encoding specified in the
|
||||||
in the response is used, if available on the Python distribution. For other
|
response is used, if available on the Python distribution. For other text
|
||||||
text formats, only UTF-8 is attempted.
|
formats, only UTF-8 is attempted.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
|
- browser: Browser instance that made the initial request.
|
||||||
|
- url: original URL.
|
||||||
- response: a successful Response.
|
- response: a successful Response.
|
||||||
|
- history: whether to modify history on a page load.
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
mime_type = response.get_mime_type()
|
mime_type = response.get_mime_type()
|
||||||
|
page = None
|
||||||
|
error = None
|
||||||
|
filepath = None
|
||||||
if mime_type.main_type == "text":
|
if mime_type.main_type == "text":
|
||||||
if mime_type.sub_type == "gemini":
|
if mime_type.sub_type == "gemini":
|
||||||
encoding = mime_type.charset
|
encoding = mime_type.charset
|
||||||
try:
|
try:
|
||||||
text = response.content.decode(encoding, errors="replace")
|
text = response.content.decode(encoding, errors="replace")
|
||||||
except LookupError:
|
except LookupError:
|
||||||
browser.set_status_error("Unknown encoding {encoding}.")
|
error = f"Unknown encoding {encoding}."
|
||||||
return 2
|
else:
|
||||||
browser.load_page(Page.from_gemtext(text))
|
page = Page.from_gemtext(text)
|
||||||
return 0
|
|
||||||
else:
|
else:
|
||||||
text = response.content.decode("utf-8", errors="replace")
|
text = response.content.decode("utf-8", errors="replace")
|
||||||
browser.load_page(Page.from_text(text))
|
page = Page.from_text(text)
|
||||||
return 0
|
|
||||||
else:
|
else:
|
||||||
pass # TODO
|
filepath = get_download_path(url)
|
||||||
return 1
|
|
||||||
|
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):
|
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
|
https://github.com/ActiveState/appdirs/blob/master/appdirs.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from functools import lru_cache
|
||||||
from os import getenv
|
from os import getenv
|
||||||
from os.path import expanduser
|
from os.path import expanduser
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
@ -12,7 +13,28 @@ from pathlib import Path
|
||||||
APP_NAME = "bebop"
|
APP_NAME = "bebop"
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(None)
|
||||||
def get_user_data_path() -> Path:
|
def get_user_data_path() -> Path:
|
||||||
"""Return the user data directory path."""
|
"""Return the user data directory path."""
|
||||||
path = Path(getenv("XDG_DATA_HOME", expanduser("~/.local/share")))
|
path = Path(getenv("XDG_DATA_HOME", expanduser("~/.local/share")))
|
||||||
return path / APP_NAME
|
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
|
sub_type: str
|
||||||
parameters: dict
|
parameters: dict
|
||||||
|
|
||||||
|
@property
|
||||||
|
def short(self):
|
||||||
|
return f"{self.main_type or '*'}/{self.sub_type or '*'}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def charset(self):
|
def charset(self):
|
||||||
return self.parameters.get("charset", DEFAULT_CHARSET)
|
return self.parameters.get("charset", DEFAULT_CHARSET)
|
||||||
|
|
|
@ -182,7 +182,16 @@ class StatusCode(IntEnum):
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Response:
|
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
|
code: StatusCode
|
||||||
meta: str = ""
|
meta: str = ""
|
||||||
|
|
|
@ -18,7 +18,7 @@ def render_lines(metalines, window, max_width):
|
||||||
- window: window that will be resized as filled with rendered lines.
|
- window: window that will be resized as filled with rendered lines.
|
||||||
- max_width: line length limit for the pad.
|
- max_width: line length limit for the pad.
|
||||||
|
|
||||||
Return:
|
Returns:
|
||||||
The tuple of integers (error, height, width), error being a non-zero value
|
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
|
if an error occured during rendering, and height and width being the new
|
||||||
dimensions of the resized window.
|
dimensions of the resized window.
|
||||||
|
@ -53,7 +53,7 @@ def render_line(metaline, window, max_width):
|
||||||
window.addstr(url_text, attributes)
|
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.
|
"""Return the base attributes for this line type.
|
||||||
|
|
||||||
Other attributes may be freely used later for this line type but this is
|
Other attributes may be freely used later for this line type but this is
|
||||||
|
|
Reference in a new issue