Commit Graph
1130 Commits
Author SHA1 Message Date
Dark-Alex-17 a70019b88f Merge remote-tracking branch 'origin/main' into readarr-support
# Conflicts:
#	src/utils_tests.rs
2026-09-29 12:46:54 -06:00
Alex Clarke fa02b70bf0 Merge pull request #64 from solareon/fix/servarr_name
fix: passes servarr-name global switch to both the tui and cli
2026-09-29 10:54:20 -06:00
Solareon 76f9f4da9a test: remove disable-spinner to improve test coverage 2026-09-29 11:05:37 +02:00
Solareon 49be500b8c style: cargo fmt and nested if fix 2026-09-29 10:44:39 +02:00
Solareon 78918b6d88 test: ensure no servarr-name select first configured instance 2026-09-29 10:40:11 +02:00
Solareon 9b98e53b55 test: servarr-name with whitespace is trimmed properly 2026-09-29 10:31:01 +02:00
Solareon b799fe3865 test: unknown servarr-name fails properly 2026-09-29 10:21:00 +02:00
Solareon 0e869e1e98 test: ensure duplicate servarr names select correct instance 2026-09-29 10:05:37 +02:00
Solareon 196bc55f35 test: validate selected servarr-name type mismatch 2026-09-29 10:00:24 +02:00
Solareon e29bd67a3e test: happy path test for servarr-name 2026-09-29 08:55:04 +02:00
Dark-Alex-17 424ed62229 chore: update dependencies ahead of release 2026-09-28 16:02:28 -06:00
Dark-Alex-17 ab7542651b chore: bump GitHub Actions to Node 24 runtimes 2026-09-28 15:53:26 -06:00
Dark-Alex-17 a222f40616 fmt: addressed linter complaints 2026-09-28 14:49:53 -06:00
solareon 699e929ed2 fix(app/mod): use correct api for tab when starting on non default tab
Further testing of the servarr_name switch revealed an issue where selecting a server that didn't match the first tab caused the wrong api to be used. Now when a non-default tab is selected the correct api is used
2026-09-25 07:44:51 -04:00
solareon 5279d37ea2 fix: passes servarr-name global switch to both the tui and cli
servarr-name global switch was documented but not fully implemented in commit fd6fcfc. this commit fully implements the feature
2026-09-25 07:30:01 -04:00
Dark-Alex-17 a4b8a23424 docs: list the Readarr features in the README 2026-09-19 13:57:49 -06:00
Dark-Alex-17 f2ee1e71f6 feat: show sizes under a gigabyte in megabytes
Every size cell converted bytes to gigabytes and formatted the float in
place, so anything below a gigabyte rendered as 0.00 GB. Books and
tracks are almost always that small, which made the Size columns in
Readarr and Lidarr useless.

Replace convert_to_gb and convert_f64_to_gb with one format_size helper
that renders gigabytes at or above 1 GiB and megabytes below it, at the
precision each site already used. Every render site across the four
servarr trees now calls it, including the two movie and episode detail
strings built on the network side. Values at or above a gigabyte render
byte for byte as before.

Give the Readarr book statistics and book file fixtures and the Lidarr
album statistics fixture realistic sub-gigabyte sizes so the megabyte
path is pinned by the committed snapshots.
2026-09-18 21:44:22 -06:00
Dark-Alex-17 bdeaf6ae35 test: prove the track, episode and movie row styles reach the rendered buffer
The row-style decorators were unit-tested as pure functions and the populated test apps do carry a downloading queue entry, but text snapshots discard colour, so nothing proved the draw functions actually call the decorator with the live downloads list. Passing an empty slice instead left every test green.

Render the lidarr tracks table, the sonarr episodes table and the radarr movies table into a Buffer and assert the probe row's foreground: downloading_style with the queue populated, missing_style once the queue is emptied. The probe is the second row so the selection highlight never masks its style.
2026-09-17 18:44:34 -06:00
Dark-Alex-17 35ed3f359d refactor: share one DownloadStatus enum across the servarr models
DownloadStatus was declared four times, byte for byte identically, in
radarr_models, sonarr_models, lidarr_models, and readarr_models: the same
variants, the same derive list, and the same serde, strum, and display_style
attributes. Every servarr's queue endpoint reports the same status
vocabulary, so there is nothing servarr specific about the type.

Move it into servarr_models, preserving that file's alphabetical ordering
(between DiskSpace and IndexerSettings), and drop the four copies. The
DownloadRecord types keep their per-servarr shapes and now reference the
shared enum.

The two enum tests, test_download_status_display and
test_download_status_to_display_str, were likewise duplicated in the four
per-servarr model test files. Keep one copy of each in servarr_models_tests
alongside the other shared enum display tests and delete the rest. The
per-servarr DownloadRecord fallback deserialization tests stay where they
are since they exercise each servarr's own record type.

Behavior is unchanged. Every remaining edit is an import path update.
2026-09-17 18:32:51 -06:00
Dark-Alex-17 040935178a test(sonarr): give the download record and episode fixtures distinct ids
The shared download record held id and episode id both equal to 1, and
the shared episode held id and episode file id both equal to 1, so
reading the wrong one of those fields could not fail any test that uses
them. Move the download record to id 9 and the episode to id 11, keeping
the download record's episode id equal to the episode's id so the
downloading episode still matches, and move the episode file to id 7
together with the episode's episode file id so the season view still
finds its quality and size. Series id, season number, and episode number
stay at 1. The handler tree keeps its own copy of these fixtures; update
it identically, along with the JSON twins in the network tests and the
seven handler assertions that named the old ids. The two get episodes
tests that asserted the sorted table now spell out the sorted order,
since the fixture episode no longer sorts first by id.
2026-09-17 18:22:31 -06:00
Dark-Alex-17 4f78a5940f test(lidarr): give the track and history fixtures distinct ids
The shared track held id, artist id, track file id, and album id all
equal to 1, and the shared history item held id, album id, artist id,
and track id all equal to 1, so reading the wrong one of those fields
could not fail any test that uses them. Move the track to id 5, artist
id 3, and track file id 7 (with the shared track file moved to id 7 so
the album view still finds its quality and audio info), and move the
history item to id 13, artist id 3, and track id 5. Both album ids stay
at 1 so the fully populated album view still shows its track as
downloading. Update the seven assertions that named the old ids.
2026-09-17 18:16:17 -06:00
Dark-Alex-17 9822597b56 fix(radarr): model the movie status as an enum
Movie.status and AddMovieSearchResult.status were bare Strings compared
against the literal "released" in the row-style decorator and in the
movie details style function. They now carry a MovieStatus enum with the
five values Radarr actually sends (tba, announced, inCinemas, released,
deleted), shaped like MinimumAvailability and DownloadStatus, and guarded
with from_json_or_default so an unrecognised status degrades to Tba
instead of failing the whole fetch.

Radarr never sends "Downloaded" as a movie status; its web UI derives
that label from released plus hasFile. Every fixture that carried
"Downloaded" now holds released, the value it was standing in for, so
none of them degrade to the default through the guard.

The status is never rendered as text in the Radarr tree, so no snapshot
changes; the CLI JSON output is unchanged since the enum serializes to
the same camelCase strings. The style_from_download_status tests now
exercise the unreleased arm, which previously had no case, in place of a
duplicated unmonitored-missing row.
2026-09-17 18:06:37 -06:00
Dark-Alex-17 c42a46c7ca fix: page up and down stop at the table edges instead of wrapping
A page is 20 rows and the index wrapped modulo one less than the row
count, so the last row was unreachable by paging, 21-row and 5-row tables
did nothing on page down from the first row, and 4-row tables cycled
0, 2, 1 and never reached row 3. Page down now clamps to the last row and
page up clamps to the first.

With no division left, a single-row table is safe, so the early return
only guards the empty table again, matching scroll_to_top and
scroll_to_bottom.
2026-09-17 17:38:37 -06:00
Dark-Alex-17 1d936c5cb5 fix(radarr): serialize the credit type under the same key it is read from
Radarr sends and accepts the credit type as "type" in both directions,
but the field was only renamed for deserialization, so serializing a
Credit emitted "creditType" and a to_value round trip could not be read
back. Credit is only deserialized in production, so this affects tests
and any future serialization of the record, not the live API traffic.
2026-09-17 15:49:12 -06:00
Dark-Alex-17 7b6d595397 fix(radarr): model the download status as a DownloadStatus enum
Radarr was the only tree still holding DownloadRecord.status as a bare
String and comparing it against literals. It now carries its own
DownloadStatus enum, shaped exactly like the Sonarr, Lidarr and Readarr
copies, and is guarded with from_json_or_default so an unrecognised
status degrades to Unknown instead of failing the whole queue fetch.

The row-style decorator and get_movie_status compare against the enum
variants. The status cell is never rendered in the Radarr tree, so no
snapshot changes; the CLI JSON output is unchanged for every real value
since the enum serializes to the same camelCase strings.
2026-09-17 15:46:49 -06:00
Dark-Alex-17 25da032f02 test(lidarr): give the download record fixture distinct album and artist ids
The shared download record held id, album id, and artist id all equal to
1, so reading the wrong one of the three could not fail any test that
uses the fixture. Move artist id to 3 and id to 9, keeping album id at 1
so the fully populated album view still shows its track as downloading,
and update the three handler assertions that named the old id.
2026-09-17 15:36:27 -06:00
Dark-Alex-17 e9c323c4e8 test(sonarr): cover the episode row style branches
decorate_with_row_style had no unit tests, so every one of its seven
return sites could be swapped for another style without a failure. Add
one test per branch, including the ordering cases where the download
match precedes the monitored guard and the monitored guard precedes the
air date check, using a local download fixture with distinct episode,
other-episode, and download ids.
2026-09-17 15:30:16 -06:00
Dark-Alex-17 289abbd8eb test(readarr): populate the book fixture with statistics
The shared Book fixture left statistics as None, so every author details
snapshot rendered the missing-statistics N/A cells and the populated
branch of the Files and Size columns was only exercised by one test.
Give the fixture distinct, non-default counts and a size that formats to
a value different from the dedicated statistics test.
2026-09-17 15:19:16 -06:00
Dark-Alex-17 a68de41025 fix: fall back to the default when a servarr returns an unknown enum value
A Servarr build can emit an enum value this app has no name for, either
as a string it does not recognise or as a bare integer for a value the
build itself cannot name. Until now any such value in a response failed
the whole payload, so one unexpected artist status, download status,
task name, series type, minimum availability, credit type, or security
setting emptied the entire view with "Failed to parse response!".

Every enum-typed field on a struct the app only ever deserializes and
displays now uses from_json_or_default, so an unrecognised value
degrades to that enum's default and the rest of the record is kept.
Known values still map exactly as before and serialization is untouched.

Fields on request bodies (Add*Body, Add*Options, Edit*Params, the root
folder bodies) are deliberately left alone: those structs are only ever
serialized, so the attribute would be meaningless there. No candidate
struct is round-tripped: every edit and toggle in the app re-fetches
the raw JSON value and patches it by key, so the fallback can never be
written back to a server as a real setting.

The one Option field, the security config's authentication_required,
also carries serde(default), because deserialize_with on an Option turns
a missing key into a hard error. An unknown value there yields None.

Verified by mutation. Removing the attribute from one field per tree
fails exactly that field's test and nothing else. Dropping default from
the Option field fails only the missing-key test with "missing field
authenticationRequired"; dropping deserialize_with from it fails only
the unknown-value test. All 21 new fallback tests fail against the
unguarded code with "invalid type: integer 2, expected string or map".
2026-09-17 15:08:52 -06:00
Dark-Alex-17 9d0973352f feat(lidarr): highlight tracks whose album is downloading or awaiting import
Lidarr queues whole albums rather than individual tracks, so a track row
is styled from the queue entry whose albumId matches the track's albumId,
mirroring the Sonarr episode row logic. Tracks that already have a file
keep the downloaded style regardless of the queue.
2026-09-17 14:50:27 -06:00
Dark-Alex-17 76c490f45b fix: show N/A instead of zeros when a season, album or book has no statistics
The season, album and book tables rendered 0/0 and 0.00 GB whenever the
API omitted the statistics object for a row, presenting fabricated counts
as real. The row colour already treated that case as indeterminate; the
cell text now does too, rendering N/A for both cells, matching what these
tables already do for a missing release date.

Sonarr's season row is restructured from a destructure-with-default to the
same Option-aware map_or_else shape lidarr and readarr use.

The readarr test fixture book has no statistics, so its existing author
details snapshots change from the fabricated zeros to N/A on exactly that
row.
2026-09-17 14:42:54 -06:00
Dark-Alex-17 9b4b3003b5 test: give the string input and table filter property tests real assertions 2026-09-17 14:33:16 -06:00
Dark-Alex-17 d678f5777a test: cover the status style branches for track and episode details 2026-09-17 14:22:47 -06:00
Dark-Alex-17 87edf1fb9e test: wire the handler property tests and make the proptest recipe run them
handler_proptest.rs was committed without ever being declared as a module, and it imported a list_size strategy that did not exist, so it never compiled. The proptest recipe filtered on a test name substring that matched no test.

Declare the module, add the two strategies it needs, and pass both property test filters to libtest so the recipe runs the ui and handler property tests.
2026-09-16 14:28:30 -06:00
Dark-Alex-17 d3a959d2dd fix: stop paging up or down on a single row table from crashing
page_up and page_down wrap the selection with a modulus of items_len - 1,
which is a remainder by zero when the table holds exactly one row. Ctrl-U
or Ctrl-D on such a table panicked the TUI. Both now return early for
tables of one row or fewer, mirroring the existing empty-table guard;
tables of two or more rows keep their existing paging arithmetic.
2026-09-16 12:25:00 -06:00
Dark-Alex-17 1857d5ba15 feat(readarr): show the author overview in the add author prompt
The add author confirmation popup previously left the space above the
root folder field blank. It now renders the selected search result's
overview there, in a fixed six line area matching the Radarr add movie
prompt, so the user can confirm they picked the right author before
adding. An author without an overview keeps the same layout.

The fully populated search result fixture gains a long overview so the
snapshot proves the paragraph is clipped to six lines.
2026-09-16 12:00:27 -06:00
Dark-Alex-17 86b91ce6dd fix: keep history readable when an event type is unrecognised
A single history record whose event type the Servarr build has no name
for made the entire history response fail to decode, so the view died
with "Failed to parse response!" and showed nothing at all.

The event type is stored as an integer and is only serialized as a
camelCase string when the build can name it. For values it cannot name,
it is emitted as a bare JSON number instead, which is a type mismatch
rather than merely an unknown name. Serde's other attribute does not
help: it is rejected outright unless it sits on the last variant, and
even there it only catches unrecognised strings, leaving a number to
fail exactly as before.

from_json_or_default buffers the field and falls back to the type's
default for any input the enum does not recognise, whether that is an
unknown string, a number, or any other JSON value. It is applied to the
event type of all four history item types. Known values still map
exactly as they did, and serialization is untouched.

Verified against a live Readarr carrying such a record: the history
listing failed to decode before the change, and afterwards lists every
record, rendering the unnamed one as Unknown.

Verified by mutation. Seven mutations all fail the new suite: dropping
the fallback, discarding the decoded value, removing the attribute from
each of the four history items, and renaming a variant on serialize
only. The five per-tree and serialize-only mutations were each invisible
to every pre-existing test.
2026-09-11 12:27:53 -06:00
Dark-Alex-17 9dbd985506 fix(readarr): attach editions when toggling book monitoring
Toggling book monitoring failed with an HTTP 500, "Value cannot be null.
(Parameter 'source')", every time, in both the CLI and the TUI.

GET /book/{id} omits the editions field entirely, but PUT /book/{id}
requires it, so round-tripping Readarr's own response back as an update
was always rejected. Editions are served separately from
GET /edition?bookId=, so fetch them and attach them to the body before
the PUT.
2026-09-11 11:27:27 -06:00
Dark-Alex-17 fbf711f428 test(lidarr): extract and cover the track row style branch
The snapshot harness renders to a plain string, so colour and text modifiers never reach the assertion. The track row's downloaded and missing arms were therefore invisible to the suite: swapping either arm for the other, or inverting the has_file condition, all left the full run green.

Move the branch into decorate_track_row_with_style, mirroring the row decorators already extracted in the other library detail views, and unit test both arms directly against Row equality. All three mutations now fail the suite.
2026-09-10 16:19:25 -06:00
Dark-Alex-17 db8e97a700 fix(sonarr): stop marking seasons fully downloaded when no episodes are known
A monitored season rendered in the fully downloaded colour whenever Sonarr returned no statistics for it, and whenever it genuinely had zero known episodes. Both cases compared zero against zero and short circuited to the downloaded style, claiming the user had every episode of a season that was only announced, or that had never been scanned.

Seasons whose statistics are absent now render as indeterminate, matching the series row and the equivalent album and book rows in the other trees, and the downloaded style now requires a positive episode count.
2026-09-10 15:09:40 -06:00
Dark-Alex-17 c069ed4a9b test(sonarr): extract and cover the season row style branches
Row styling is invisible to the snapshot harness, because render_to_string_with_app returns a plain String and discards colour and modifiers. The season row style branch was therefore entirely uncovered, and any arm could be swapped for another without a single test noticing.

decorate_season_row_with_style now holds that branch, matching decorate_series_row_with_style in the library view. The extraction is behaviour preserving: the branch is token identical to the one it replaces, the statistics fallback resolves the same way, and no snapshot changed. The season row deliberately has no positive episode count guard, so a season without statistics falls back to zero of zero episodes and reads as downloaded; a test now pins that.

Verified by mutation. Against the previous tree all seven mutations left the suite green: replacing each of the unmonitored, downloaded, unreleased and both missing arms, flipping the episode file count comparison, and inverting the monitored check. Against the new tests every one of those seven fails.
2026-09-10 13:52:17 -06:00
Dark-Alex-17 3227495947 test(lidarr): extract and cover the album row style branches
Row styling is invisible to the snapshot harness, because render_to_string_with_app returns a plain String and discards colour and modifiers. The album row style branch was therefore entirely uncovered, and any arm could be swapped for another without a single test noticing.

decorate_album_row_with_style now holds that branch, matching decorate_artist_row_with_style in the library view and decorate_book_row_with_style in the Readarr author details view. The extraction is behaviour preserving: the branch is token identical to the one it replaces, and no snapshot changed.

Verified by mutation. Against the previous tree all eight mutations left the suite green: replacing each of the unmonitored, downloaded, unreleased, both missing and indeterminate arms, flipping the track file count comparison, and inverting the monitored check. Against the new tests every one of those eight fails.
2026-09-10 13:51:25 -06:00
Dark-Alex-17 243b22a426 fix: keep a space between overview words that span a line break
The collapsed one-line overview on the Lidarr artist, Sonarr series, and Readarr author details views replaced every line break and tab with an empty string. Words on either side of a paragraph break fused together, rendering as "20th century.She was born in Ohio".

Collapse each run of whitespace to a single space instead, and trim the result. Handling a whole run at a time keeps a Windows line ending, an indented paragraph, or a space that already precedes a line break from producing two spaces.

The overview popups are unaffected: they receive the raw text and keep their paragraph breaks.
2026-09-10 13:27:02 -06:00
Dark-Alex-17 caafe1602e test(radarr): name the updates loading test after its neighbours
The Radarr system details tests prefix their loading cases with renders_. This one was copied verbatim from the other trees and did not match. Rename it and its snapshot to fit, leaving the sibling trees alone.
2026-09-10 13:07:54 -06:00
Dark-Alex-17 65ba37c09a fix: parse the log level from the unscrolled log text
draw_logs read the level from the log line's Display output, which drops the first offset characters. Once a log line is scrolled, the field at index 1 is no longer the level: past the timestamp it yields the logger name and colours the row wrong, and past the final delimiter the index is out of bounds and panics, taking the TUI down.

Parse log.text instead, matching what draw_logs_popup already does. The displayed text is unchanged.
2026-09-10 13:06:04 -06:00
Dark-Alex-17 c525fb9a4e feat(radarr): render the movie overview popup at the large size
The movie overview was the only overview popup drawn at the small size;
the sonarr, lidarr and readarr overview popups all use the large size.
The small size wrapped longer overviews sooner and gave the text roughly
a quarter of the space the other trees allow.

Draw it at the large size to match them. The wider popup now covers the
collection details behind it, as the other overview popups do.
2026-09-10 12:40:59 -06:00
Dark-Alex-17 e79cc7a12d fix(radarr): show a loading block while the updates popup refreshes
The updates popup only checked whether its text was empty, so during a
refresh it kept rendering the previously fetched text. Opening the system
updates view dispatches a request for the update notes, so the popup could
sit on stale content until the response landed.

Guard on the loading flag as well, matching the sonarr, lidarr and readarr
updates popups, and add the loading-state test those trees have so both
halves of the condition are exercised. The pre-existing empty-updates test
never reaches the second half on its own.
2026-09-10 12:35:56 -06:00
Dark-Alex-17 12dbbf0b1b test: give the statistics fixtures distinct file and total counts
The artist, album and season statistics fixtures set the file count equal to the total count, so the columns rendering the pair showed the same number twice and swapping the two fields changed nothing. The JSON fixtures move with them so the round trip assertions still hold.
2026-09-10 01:10:41 -06:00
Dark-Alex-17 1a5e5c3145 test: give the delete flags distinct values where params are asserted
The delete params tests set the delete files flag and the list exclusion flag both true, so swapping the two fields in the params builder produced identical params and no test noticed. The tests that assert a reset still set both true, since that is what proves both flags are cleared.
2026-09-10 01:04:07 -06:00
Dark-Alex-17 5fa521ba7f test: cover indexer settings drawn over the indexers table
IndexersUi dispatches to IndexerSettingsUi, but no test rendered that route through the parent, so deleting the dispatch arm in radarr, sonarr and lidarr left the whole suite green.
2026-09-10 00:57:55 -06:00