Compare commits
4 Commits
b1763a610e
...
7d55a43119
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7d55a43119 | ||
![]() |
a266479027 | ||
![]() |
16c1dcd7f9 | ||
![]() |
2bce21a5ab |
@ -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**.
|
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, 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.
|
**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.
|
||||||
|
|
||||||
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.
|
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,7 +6,6 @@ import urllib.parse
|
|||||||
from pathlib import PurePosixPath
|
from pathlib import PurePosixPath
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
from wsgiref.handlers import format_date_time
|
from wsgiref.handlers import format_date_time
|
||||||
|
|
||||||
import av
|
import av
|
||||||
import av.datasets
|
import av.datasets
|
||||||
import fitz # PyMuPDF
|
import fitz # PyMuPDF
|
||||||
@ -36,13 +35,13 @@ async def preview(req, path):
|
|||||||
etag = config.derived_secret(
|
etag = config.derived_secret(
|
||||||
"preview", rel, stat.st_mtime_ns, quality, maxsize, maxzoom
|
"preview", rel, stat.st_mtime_ns, quality, maxsize, maxzoom
|
||||||
).hex()
|
).hex()
|
||||||
savename = PurePosixPath(path.name).with_suffix(".webp")
|
savename = PurePosixPath(path.name).with_suffix(".avif")
|
||||||
headers = {
|
headers = {
|
||||||
"etag": etag,
|
"etag": etag,
|
||||||
"last-modified": format_date_time(stat.st_mtime),
|
"last-modified": format_date_time(stat.st_mtime),
|
||||||
"cache-control": "max-age=604800, immutable"
|
"cache-control": "max-age=604800, immutable"
|
||||||
+ ("" if config.config.public else ", private"),
|
+ ("" if config.config.public else ", private"),
|
||||||
"content-type": "image/webp",
|
"content-type": "image/avif",
|
||||||
"content-disposition": f"inline; filename*=UTF-8''{urllib.parse.quote(savename.as_posix())}",
|
"content-disposition": f"inline; filename*=UTF-8''{urllib.parse.quote(savename.as_posix())}",
|
||||||
}
|
}
|
||||||
if req.headers.if_none_match == etag:
|
if req.headers.if_none_match == etag:
|
||||||
@ -83,7 +82,7 @@ def process_image(path, *, maxsize, quality):
|
|||||||
logger.error(f"Error rotating preview image: {e}")
|
logger.error(f"Error rotating preview image: {e}")
|
||||||
# Save as webp
|
# Save as webp
|
||||||
imgdata = io.BytesIO()
|
imgdata = io.BytesIO()
|
||||||
img.save(imgdata, format="webp", quality=quality, method=4)
|
img.save(imgdata, format="avif", quality=quality, method=4)
|
||||||
return imgdata.getvalue()
|
return imgdata.getvalue()
|
||||||
|
|
||||||
|
|
||||||
@ -94,48 +93,48 @@ def process_pdf(path, *, maxsize, maxzoom, quality, page_number=0):
|
|||||||
zoom = min(maxsize / w, maxsize / h, maxzoom)
|
zoom = min(maxsize / w, maxsize / h, maxzoom)
|
||||||
mat = fitz.Matrix(zoom, zoom)
|
mat = fitz.Matrix(zoom, zoom)
|
||||||
pix = page.get_pixmap(matrix=mat)
|
pix = page.get_pixmap(matrix=mat)
|
||||||
return pix.pil_tobytes(format="webp", quality=quality, method=4)
|
return pix.pil_tobytes(format="avif", quality=quality, method=4)
|
||||||
|
|
||||||
|
|
||||||
def process_video(path, *, maxsize, quality):
|
def process_video(path, *, maxsize, quality):
|
||||||
with av.open(str(path)) as container:
|
frame = None
|
||||||
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()
|
imgdata = io.BytesIO()
|
||||||
if rot and rot != 0:
|
with (
|
||||||
img = img.rotate(-rot, expand=True) # Negative rotation for correct orientation
|
av.open(str(path)) as container,
|
||||||
img.save(imgdata, format="webp", quality=quality, method=4)
|
av.open(imgdata, "w", format="avif") as ocontainer,
|
||||||
del img
|
):
|
||||||
|
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
|
||||||
|
|
||||||
ret = imgdata.getvalue()
|
ret = imgdata.getvalue()
|
||||||
del imgdata
|
del imgdata
|
||||||
gc.collect()
|
gc.collect()
|
||||||
|
@ -29,6 +29,8 @@ async def handle_sanic_exception(request, e):
|
|||||||
if not message or not request.app.debug and code == 500:
|
if not message or not request.app.debug and code == 500:
|
||||||
message = "Internal Server Error"
|
message = "Internal Server Error"
|
||||||
message = f"⚠️ {message}" if code < 500 else f"🛑 {message}"
|
message = f"⚠️ {message}" if code < 500 else f"🛑 {message}"
|
||||||
|
if code == 500:
|
||||||
|
logger.exception(e)
|
||||||
# Non-browsers get JSON errors
|
# Non-browsers get JSON errors
|
||||||
if "text/html" not in request.headers.accept:
|
if "text/html" not in request.headers.accept:
|
||||||
return jres(
|
return jres(
|
||||||
|
@ -71,32 +71,6 @@ testpaths = [
|
|||||||
"tests",
|
"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]
|
[tool.ruff.isort]
|
||||||
known-first-party = ["cista"]
|
known-first-party = ["cista"]
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user