Compare commits
No commits in common. "7d55a43119c2898dfd9b4f8ff87b4298977dcca9" and "b1763a610e868dc0317a6d4a3fa04648183f6ef1" have entirely different histories.
7d55a43119
...
b1763a610e
@ -6,7 +6,7 @@ Cista takes its name from the ancient *cistae*, metal containers used by Greeks
|
||||
|
||||
This is a cutting-edge **file and document server** designed for speed, efficiency, and unparalleled ease of use. Experience **lightning-fast browsing**, thanks to the file list maintained directly in your browser and updated from server filesystem events, coupled with our highly optimized code. Fully **keyboard-navigable** and with a responsive layout, Cista flawlessly adapts to your devices, providing a seamless experience wherever you are. Our powerful **instant search** means you're always just a few keystrokes away from finding exactly what you need. Press **1/2/3** to switch ordering, navigate with all four arrow keys (+Shift to select). Or click your way around on **breadcrumbs that remember where you were**.
|
||||
|
||||
**Built-in document and media previews** let you quickly view files without downloading them. Cista shows PDF and other documents, video and image thumbnails, with **HDR10 support** video previews and image formats, including HEIC and AVIF. It also has a player for music and video files.
|
||||
**Built-in document and media previews** let you quickly view files without downloading them. Cista shows PDF, video and image thumbnails, with **HDR support** for HEIC and AVIF images. It also has a player for music and video files. Enable Gallery mode to see previews.
|
||||
|
||||
The Cista project started as an inevitable remake of [Droppy](https://github.com/droppyjs/droppy) which we used and loved despite its numerous bugs. Cista Storage stands out in handling even the most exotic filenames, ensuring a smooth experience where others falter.
|
||||
|
||||
|
@ -6,6 +6,7 @@ import urllib.parse
|
||||
from pathlib import PurePosixPath
|
||||
from urllib.parse import unquote
|
||||
from wsgiref.handlers import format_date_time
|
||||
|
||||
import av
|
||||
import av.datasets
|
||||
import fitz # PyMuPDF
|
||||
@ -35,13 +36,13 @@ async def preview(req, path):
|
||||
etag = config.derived_secret(
|
||||
"preview", rel, stat.st_mtime_ns, quality, maxsize, maxzoom
|
||||
).hex()
|
||||
savename = PurePosixPath(path.name).with_suffix(".avif")
|
||||
savename = PurePosixPath(path.name).with_suffix(".webp")
|
||||
headers = {
|
||||
"etag": etag,
|
||||
"last-modified": format_date_time(stat.st_mtime),
|
||||
"cache-control": "max-age=604800, immutable"
|
||||
+ ("" if config.config.public else ", private"),
|
||||
"content-type": "image/avif",
|
||||
"content-type": "image/webp",
|
||||
"content-disposition": f"inline; filename*=UTF-8''{urllib.parse.quote(savename.as_posix())}",
|
||||
}
|
||||
if req.headers.if_none_match == etag:
|
||||
@ -82,7 +83,7 @@ def process_image(path, *, maxsize, quality):
|
||||
logger.error(f"Error rotating preview image: {e}")
|
||||
# Save as webp
|
||||
imgdata = io.BytesIO()
|
||||
img.save(imgdata, format="avif", quality=quality, method=4)
|
||||
img.save(imgdata, format="webp", quality=quality, method=4)
|
||||
return imgdata.getvalue()
|
||||
|
||||
|
||||
@ -93,48 +94,48 @@ def process_pdf(path, *, maxsize, maxzoom, quality, page_number=0):
|
||||
zoom = min(maxsize / w, maxsize / h, maxzoom)
|
||||
mat = fitz.Matrix(zoom, zoom)
|
||||
pix = page.get_pixmap(matrix=mat)
|
||||
return pix.pil_tobytes(format="avif", quality=quality, method=4)
|
||||
return pix.pil_tobytes(format="webp", quality=quality, method=4)
|
||||
|
||||
|
||||
def process_video(path, *, maxsize, quality):
|
||||
frame = None
|
||||
with av.open(str(path)) as container:
|
||||
stream = container.streams.video[0]
|
||||
stream.codec_context.skip_frame = "NONKEY"
|
||||
|
||||
# Updated side data access for newer av versions
|
||||
rot = 0
|
||||
try:
|
||||
# Try newer API first
|
||||
if hasattr(stream, "side_data") and stream.side_data:
|
||||
display_matrix = stream.side_data.get("DISPLAYMATRIX")
|
||||
if display_matrix:
|
||||
rot = (
|
||||
display_matrix.rotation
|
||||
if hasattr(display_matrix, "rotation")
|
||||
else 0
|
||||
)
|
||||
except (AttributeError, KeyError):
|
||||
# Fallback for older API or missing side data
|
||||
rot = 0
|
||||
|
||||
container.seek(container.duration // 8)
|
||||
try:
|
||||
frame = next(container.decode(stream))
|
||||
img = frame.to_image()
|
||||
except StopIteration:
|
||||
# If no frame found, try from beginning
|
||||
container.seek(0)
|
||||
frame = next(container.decode(stream))
|
||||
img = frame.to_image()
|
||||
|
||||
del stream
|
||||
|
||||
img.thumbnail((maxsize, maxsize))
|
||||
imgdata = io.BytesIO()
|
||||
with (
|
||||
av.open(str(path)) as container,
|
||||
av.open(imgdata, "w", format="avif") as ocontainer,
|
||||
):
|
||||
istream = container.streams.video[0]
|
||||
istream.codec_context.skip_frame = "NONKEY"
|
||||
container.seek((container.duration or 0) // 8)
|
||||
for frame in container.decode(istream):
|
||||
if frame.dts is not None:
|
||||
break
|
||||
else:
|
||||
raise RuntimeError("No frames found in video")
|
||||
|
||||
# Resize frame to thumbnail size
|
||||
if frame.width > maxsize or frame.height > maxsize:
|
||||
scale_factor = min(maxsize / frame.width, maxsize / frame.height)
|
||||
new_width = int(frame.width * scale_factor)
|
||||
new_height = int(frame.height * scale_factor)
|
||||
frame = frame.reformat(width=new_width, height=new_height)
|
||||
|
||||
ostream = ocontainer.add_stream("av1", options={"quality": str(quality)})
|
||||
assert isinstance(ostream, av.VideoStream)
|
||||
ostream.width = frame.width
|
||||
ostream.height = frame.height
|
||||
icc = istream.codec_context
|
||||
occ = ostream.codec_context
|
||||
|
||||
# Copy HDR metadata from input video stream
|
||||
occ.color_primaries = icc.color_primaries
|
||||
occ.color_trc = icc.color_trc
|
||||
occ.colorspace = icc.colorspace
|
||||
occ.color_range = icc.color_range
|
||||
|
||||
ocontainer.mux(ostream.encode(frame))
|
||||
ocontainer.mux(ostream.encode(None)) # Flush the stream
|
||||
|
||||
if rot and rot != 0:
|
||||
img = img.rotate(-rot, expand=True) # Negative rotation for correct orientation
|
||||
img.save(imgdata, format="webp", quality=quality, method=4)
|
||||
del img
|
||||
ret = imgdata.getvalue()
|
||||
del imgdata
|
||||
gc.collect()
|
||||
|
@ -29,8 +29,6 @@ async def handle_sanic_exception(request, e):
|
||||
if not message or not request.app.debug and code == 500:
|
||||
message = "Internal Server Error"
|
||||
message = f"⚠️ {message}" if code < 500 else f"🛑 {message}"
|
||||
if code == 500:
|
||||
logger.exception(e)
|
||||
# Non-browsers get JSON errors
|
||||
if "text/html" not in request.headers.accept:
|
||||
return jres(
|
||||
|
@ -71,6 +71,32 @@ testpaths = [
|
||||
"tests",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
select = ["ALL"]
|
||||
ignore = [
|
||||
"A0",
|
||||
"ARG001",
|
||||
"ANN",
|
||||
"B018",
|
||||
"BLE001",
|
||||
"C901",
|
||||
"COM812", # conflicts with ruff format
|
||||
"D",
|
||||
"E501",
|
||||
"EM1",
|
||||
"FIX002",
|
||||
"ISC001", # conflicts with ruff format
|
||||
"PGH003",
|
||||
"PLR0912",
|
||||
"PLR2004",
|
||||
"PLW0603",
|
||||
"S101",
|
||||
"SLF001",
|
||||
"T201",
|
||||
"TD0",
|
||||
"TRY",
|
||||
]
|
||||
|
||||
[tool.ruff.isort]
|
||||
known-first-party = ["cista"]
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user