645 Commits

Author SHA1 Message Date
pax
deec81fc12 db: remove unused Favorite alias
Zero callers in source (rg 'Favorite\b' returns only this line).
The rename from favorite -> bookmark landed; the alias existed as
a fall-back while callers migrated, and nothing still needs it.
2026-04-15 17:50:14 -05:00
pax
585979a0d1 window_state: annotate silent excepts
Both hyprctl-path guards in window_state (hyprctl_main_window()
JSON parse, save_main_window_state() full flow) now explain why
the failure is absorbed instead of raised. No behavior change.
2026-04-15 17:49:54 -05:00
pax
b63341fec1 video_player: annotate silent excepts
Four mpv-state transition guards (letterbox color apply, hwdec
re-arm on play_file, hwdec drop on stop, replay-on-end seek) each
gained a one-line comment naming the absorbed failure and the
graceful fallback. No behavior change.
2026-04-15 17:49:28 -05:00
pax
873dcd8998 popout/window: annotate silent excepts
Four silent except-pass sites now either explain the absorbed
failure (mpv mid-transition, close-path cleanup, post-shutdown
video_params access) or downgrade to log.debug with exc_info so
the next debugger has breadcrumbs.

No behavior change.
2026-04-15 17:48:44 -05:00
pax
cec93545ad popout: drop in-flight-refactor language from docstrings
During the state machine extraction every comment that referenced
a specific commit in the plan (skeleton / 14a / 14b / 'future
commit') was useful — it told you which commit a line appeared
in and what was about to change. Once the refactor landed those
notes became noise: they describe history nobody needs while
reading the current code.

Rewrites keep the rationale (no-op handlers still explain WHY
they're no-ops, Loop=Next / video auto-fit still have their
explanations) and preserves the load-bearing commit 14b reference
in _dispatch_and_apply's docstring — that one actually does
protect future-you from reintroducing the bug-by-typo pattern.
2026-04-15 17:47:36 -05:00
pax
9ec034f7ef api/base: retry RemoteProtocolError and ReadError
Both surface when an overloaded booru drops the TCP connection
after sending headers but before the body completes. The existing
retry tuple (TimeoutException, ConnectError, NetworkError) missed
these even though they're the same shape of transient server-side
failure.

Keeps the existing single-retry-at-1s cadence; no retry-count
bump in this pass.
2026-04-15 17:44:15 -05:00
pax
ab44735f28 http: consolidate httpx.AsyncClient construction into make_client
Three call sites built near-identical httpx.AsyncClient instances:
the cache download pool, BooruClient's shared API pool, and
detect_site_type's reach into that same pool. They differed only
in timeout (60s vs 20s), Accept header (cache pool only), and
which extra request hooks to attach.

core/http.py:make_client is the single constructor now. Each call
site still keeps its own singleton + lock (separate connection
pools for large transfers vs short JSON), so this is a constructor
consolidation, not a pool consolidation.

No behavior change. Drops now-unused USER_AGENT imports from
cache.py and base.py; make_client pulls it from core.config.
2026-04-15 17:43:49 -05:00
pax
90b27fe36a info_panel: render uncategorized tags under Other bucket
behavior change: tags that weren't in any section of
post.tag_categories (partial batch-API response, HTML scrape
returned empty, stale cache) used to silently disappear from the
info panel — the categorized loop only iterated categories, so
any tag without a cached label just didn't render.

Now after the known category sections, any remaining tags from
post.tag_list are collected into an 'Other:' section with a
neutral header. The tag is visible and clickable even when its
type code never made it into the cache.

Reported against Gelbooru posts with long character tag names
where the batch tag API was returning partial results and the
missing tags were just gone from the UI.
2026-04-15 17:42:38 -05:00
pax
730b2a7b7e settings: add Clear Tag Category Cache button
behavior change: Settings > Cache now has a 'Clear Tag Category
Cache' action that wipes the per-site tag_types table via the
existing db.clear_tag_cache() hook. This also drops the
__batch_api_probe__ sentinel so Gelbooru/Moebooru sites re-probe
the batch tag API on next use and repopulate the cache from a
fresh response.

Use case: category types like Character/Copyright/Meta appear
missing when the local tag cache was populated by an older build
that didn't map all of Gelbooru's type codes. Clearing lets the
current _GELBOORU_TYPE_MAP re-label tags cleanly instead of
inheriting whatever the old rows said.
2026-04-15 17:39:57 -05:00
pax
0f26475f52 detect: remove leftover if-True indent marker
Dead syntax left over from a prior refactor. No behavior change.
2026-04-15 17:34:27 -05:00
pax
cf8bc0ad89 library_save: require category_fetcher to prevent silent category drop
behavior change: save_post_file's category_fetcher argument is now
keyword-only with no default, so every call site has to pass something
explicit (fetcher instance or None). Previously the =None default let
bookmark→library save and bookmark Save As slip through without a
fetcher at all, silently rendering %artist%/%character% tokens as
empty strings and producing filenames like '_12345.jpg' instead of
'greatartist_12345.jpg'.

BookmarksView now takes a category_fetcher_factory callable in its
constructor (wired to BooruApp._get_category_fetcher), called at save
time so it picks up the fetcher for whatever site is currently active.

tests/core/test_library_save.py pins the signature shape and the
three relevant paths: fetcher populates empty categories, None
accepted when categories are pre-populated (Danbooru/e621 inline),
fetcher skipped when template has no category tokens.
2026-04-15 17:32:25 -05:00
pax
bbf0d3107b category_fetcher: stop flipping _batch_api_works=False on transient errors in single-post path
behavior change: a single mid-call network drop could previously
poison _batch_api_works=False for the whole site, forcing every
future ensure_categories onto the slower HTML scrape path. _do_ensure
now routes the unprobed case through _probe_batch_api, which only
flips the flag on a clean HTTP 200 with zero matching names; timeout
and non-200 responses leave the flag None so the next call retries
the probe.

The bug surfaced because fetch_via_tag_api swallows per-chunk
failures with 'except Exception: continue', so the previous code
path couldn't distinguish 'API returned zero matches' from 'the
network dropped halfway through.' _probe_batch_api already made
that distinction for prefetch_batch; _do_ensure now reuses it.

Tests in tests/core/api/test_category_fetcher.py pin the three
routes (transient raise, clean-200-zero-matches, non-200).
2026-04-15 17:29:01 -05:00
pax
ec9e44efbe category_fetcher: extract shared tag-API params builder
Both fetch_via_tag_api and _probe_batch_api built the same params
dict (with identical lstrip/startswith credential quirks) inline.
Pulled into _build_tag_api_params so future credential-format tweaks
have one site, not two.
2026-04-15 17:27:10 -05:00
pax
24f398795b changelog: drag-start threshold bump 2026-04-14 23:27:32 -05:00
pax
3b3de35689 grid: raise drag-start threshold to 30px to match rubber band
Thumbnail file drag kicked off after only 10px of movement, which made
it too easy to start a drag when the user meant to rubber-band select
or just click-and-micro-wobble. Bumped to 30px so the gate matches the
rubber band's own threshold in `_maybe_start_rb`.

behavior change: tiny mouse movement on a thumbnail no longer starts a
file drag; you now need to drag ~30px before the OS drag kicks in.
2026-04-14 23:25:56 -05:00
pax
21bb3aa979 CHANGELOG: add [Unreleased] section for changes since v0.2.7 2026-04-14 19:05:23 -05:00
pax
289e4c2fdb release: v0.2.7 v0.2.7 2026-04-14 19:03:22 -05:00
pax
3c2aa5820d popout: remember tiled state across open/close
Popout was always reopening as floating even when it had been tiled at
close. closeEvent already persisted geometry + fullscreen, but nothing
captured the Hyprland floating/tiled bit, so the windowrule's
`float = yes` rule always won on reopen.

Now closeEvent records `_saved_tiled` from hyprctl, popout_controller
persists it as `slideshow_tiled`, and FullscreenPreview's restore path
calls the new `hyprland.settiled` helper shortly after show() to push
the window back into the layout. Saved geometry is ignored for tiled
reopens since the tile extent is the layout's concern.

behavior change: popout reopens tiled if it was tiled at close.
2026-04-14 19:01:34 -05:00
pax
a2609199bd changelog: tiled grid repaint + video thumb 10% seek 2026-04-14 15:58:36 -05:00
pax
c3efcf9f89 library: seek to 10% before capturing video thumbnail
Videos that open on a black frame (fade-in, title card, codec warmup)
produced black library thumbnails. mpv now starts at 10% with hr_seek
so the first decoded frame is past the opening. mpv clamps `start`
to valid range so very short clips still land on a real frame.
2026-04-14 15:58:33 -05:00
pax
22f09c3cdb grid: force viewport repaint on resize to fix tiled blank-out
Qt Wayland buffer goes stale after compositor-driven resize events
(Hyprland tiled geometry change). FlowLayout reflowed thumbs but the
viewport skipped paint until a scroll or click invalidated it, leaving
the grid blank. ThumbnailGrid.resizeEvent now calls viewport().update()
after reflowing so the buffer stays in sync.
2026-04-14 15:58:28 -05:00
pax
70a7903f85 changelog: VRAM fixes and popout open animation 2026-04-13 21:49:38 -05:00
pax
e004add28f popout: let open animation play on first fit
resize() and resize_and_move() gain an animate flag — when True, skip
the no_anim setprop so Hyprland's windowsIn/popin animation plays
through. Popout passes animate=_first_fit_pending so the first fit
after open animates; subsequent navigation fits still suppress anim
to avoid resize flicker.

behavior change: popout now animates in on open instead of snapping.
2026-04-13 21:49:35 -05:00
pax
9713794633 popout: explicit mpv cleanup on close to free VRAM
FullscreenPreview has no WA_DeleteOnClose and Qt's C++ dtor does
not reliably call Python-side destroy() overrides once
popout_controller drops its reference, so the popout's separate
mpv instance + NVDEC surface pool leaked until the next full
Python GC cycle. closeEvent now calls _gl_widget.cleanup()
explicitly after the state machine's CloseRequested dispatch.

behavior change from v0.2.6: popout open/close cycles no longer
stair-step VRAM upward; the popout's mpv is torn down immediately
on close instead of waiting on GC.
2026-04-13 21:04:59 -05:00
pax
860c8dcd50 video_player: drop hwdec surface pool on stop
On NVIDIA the NVDEC surface pool is the bulk of mpv's idle VRAM
footprint, and keep_open=yes plus the live GL render context pin
it for the widget lifetime. stop() now sets hwdec='no' to release
the pool while idle; play_file() re-arms hwdec='auto' before the
next loadfile so GPU decode is restored on playback.

behavior change from v0.2.6: video VRAM now releases when switching
from a video post to an image post in the same preview pane, instead
of staying pinned for the widget lifetime.
2026-04-13 21:04:54 -05:00
pax
0d75b8a3c8 changelog: 9 commits since last Unreleased update
Fixed: GL context leak on Mesa/Intel, popout teardown None guards,
category_fetcher XXE/billion-laughs rejection.
Changed: dark Fusion palette fallback, popout aspect refit on untile
(behavior change).
Removed: rolled in latest dead-var and unused-import cleanups.
2026-04-13 19:02:40 -05:00
pax
94a64dcd25 mpv_gl: make GL current before freeing mpv render context
Drivers that enforce per-context GPU resource ownership (Mesa, Intel)
leak textures and FBOs when mpv_render_context_free runs without the
owning GL context current. NVIDIA tolerates this but others do not.
2026-04-13 18:40:23 -05:00
pax
3d26e40e0f popout: guard against None centralWidget and QApplication during teardown
resizeEvent, installEventFilter, and removeEventFilter all
dereference return values that can be None during init/shutdown,
causing AttributeError crashes on edge-case lifecycle timing.
2026-04-13 18:35:22 -05:00
pax
2cdab574ca popout: refit window with correct aspect when leaving tiled layout
behavior change: navigating to a different-aspect image/video while
tiled then un-tiling now resizes the floating window to the current
content's aspect and resets the image viewer zoom. Previously the
window restored to the old floating geometry with the wrong aspect
locked.

Stash content dims on the tiled early-return in _fit_to_content, then
detect the tiled-to-floating transition via a debounced resizeEvent
check that re-runs the fit.
2026-04-12 22:18:21 -05:00
pax
57108cd0b5 info_panel: remove unnecessary f-prefix on plain string 2026-04-12 14:55:35 -05:00
pax
667ee87641 settings: remove dead get_connection_log import in _build_network_tab 2026-04-12 14:55:35 -05:00
pax
2e436af4e8 video_player: remove unused QBrush and QApplication imports 2026-04-12 14:55:34 -05:00
pax
a7586a9e43 grid: remove dead mid variable from paintEvent 2026-04-12 14:55:33 -05:00
pax
ad6f876f40 category_fetcher: reject XML responses with DOCTYPE/ENTITY declarations
User-configurable sites could send XXE or billion-laughs payloads
via tag category API responses. Reject any XML body containing
<!DOCTYPE or <!ENTITY before passing to ET.fromstring.
2026-04-12 14:55:30 -05:00
pax
56c5eac870 app_runtime: dark Fusion fallback when no system theme is detected
Systems without Trolltech.conf (bare Arch, fresh installs without a
DE) were landing on Qt's default light palette. Apply a neutral dark
Fusion palette when no system theme file exists and the palette is
still light. KDE/GNOME users keep their own palette untouched.
2026-04-12 14:43:59 -05:00
pax
11cc26479b changelog: parallel video caching, mpv library thumbnails 2026-04-12 14:31:33 -05:00
pax
14c81484c9 replace ffmpeg with mpv for library video thumbnails
behavior change: video thumbnails in the Library tab are now generated
by a headless mpv instance (vo=null, pause=True, screenshot-to-file)
instead of shelling out to ffmpeg. Resized to LIBRARY_THUMB_SIZE with
PIL. Falls back to the same placeholder on failure. ffmpeg removed
from README install commands — no longer a dependency.
2026-04-12 14:31:30 -05:00
pax
0d72b0ec8a replace stream-record with parallel httpx download for uncached videos
behavior change: clicking an uncached video now starts a full httpx
download in the background alongside mpv streaming. The cached file
is available for copy/paste as soon as the download completes, without
waiting for playback to finish. stream-record machinery removed from
video_player.py (~60 lines); on_image_done detects the streaming case
and updates path references without restarting playback.
2026-04-12 14:31:23 -05:00
pax
445d3c7a0f CHANGELOG: add [Unreleased] section for changes since v0.2.6 2026-04-12 07:23:46 -05:00
pax
0583f962d1 main_window: set minimum width on thumbnail grid
Prevents the splitter from collapsing the grid to zero width.
The minimum is one column of thumbnails (THUMB_SIZE + margins).
2026-04-12 01:15:31 -05:00
pax
3868858811 media_controller: set _cached_path for streaming videos
Streaming videos skip on_image_done (the _load coroutine returns
early), so the thumbnail's _cached_path was never set. Drag-to-copy
failed until the user navigated away and back (which went through
the cached path and hit on_image_done).

Now on_video_stream sets _cached_path to the expected cache location
immediately. Once the stream-record promotes the .part file on EOF,
drag-to-copy works without needing to change posts first.
2026-04-12 01:10:53 -05:00
pax
7ef517235f revert audio normalization feature
Neither loudnorm (EBU R128) nor dynaudnorm work well for this
use case — both are designed for continuous playback, not rapidly
switching between random short clips with wildly different levels.
2026-04-11 23:30:09 -05:00
pax
2824840b07 post_actions: refresh bookmarks grid on unsave
unsave_from_preview only refreshed the library grid when on the
library tab. Now also refreshes the bookmarks grid when on the
bookmarks tab so the saved dot clears immediately.
2026-04-11 23:24:35 -05:00
pax
61403c8acc main_window: wire loudnorm setting to video players
Read the setting at startup and apply it to the embedded preview's
video player. On settings change, toggle af=loudnorm live on all
active mpv instances (embedded + popout).

Also adds _get_all_video_players() helper for iterating both.
2026-04-11 23:22:33 -05:00
pax
2e9b99e4b8 video_player: apply loudnorm audio filter on mpv init
Reads the _loudnorm flag (set by main_window from the DB setting)
and applies af=loudnorm when mpv is first initialized.
2026-04-11 23:22:30 -05:00
pax
73206994ec settings: add audio normalization checkbox 2026-04-11 23:22:28 -05:00
pax
738e1329b8 main_window: read image dimensions for bookmark popout aspect lock
Bookmark Post objects have no width/height (the DB doesn't store
them). When opening a bookmark in the popout, read the actual
dimensions from the cached file via QImageReader so the popout
can set keep_aspect_ratio correctly. Previously images from the
bookmarks tab always got 0x0, skipping the aspect lock.
2026-04-11 23:18:06 -05:00
pax
a3cb563ae0 grid: shorten thumbnail fade-in from 200ms to 80ms 2026-04-11 23:18:06 -05:00
pax
60cf4e0beb grid: fix fade animation cleanup crashing FlowLayout.clear
The previous deleteLater on the QPropertyAnimation left a dangling
self._fade_anim reference to a dead C++ object. When the next
search called FlowLayout.clear(), calling .stop() on the dead
animation threw RuntimeError and aborted widget cleanup, leaving
stale thumbnails in the grid.

Now the finished callback nulls self._fade_anim before scheduling
deletion, so clear() never touches a dead object.
2026-04-11 23:10:54 -05:00
pax
692a0c1569 grid: clean up QPropertyAnimation after fade completes
Connect finished signal to deleteLater so the animation object
is freed instead of being retained on the widget indefinitely.
2026-04-11 23:01:45 -05:00