merge branch 'develop' into 'main' - Release v1.7.19
Create Release / release (push) Successful in 33s
Build and Push Docker Image / build (push) Successful in 2m19s
CI / Security audit (push) Successful in 2m14s
CI / Tests & coverage (push) Successful in 2m21s
CI / Swagger Validation & Coverage (push) Successful in 2m24s
Create Release / release (push) Successful in 33s
Build and Push Docker Image / build (push) Successful in 2m19s
CI / Security audit (push) Successful in 2m14s
CI / Tests & coverage (push) Successful in 2m21s
CI / Swagger Validation & Coverage (push) Successful in 2m24s
This commit is contained in:
@@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
|
|||||||
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.7.19] - 2026-05-25
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Requests Mobile CSS Overflow Fix (Issue #49)** — Resolved the remaining viewport overflow on narrow screens by adding `min-width: 0` to `.request-card` to allow proper flexbox and grid shrinking, reducing mobile `.main-tabs` padding to `0 8px`, and tightening `.requests-container` mobile padding to `8px`.
|
||||||
|
- **Admin *arr Badge Links on Active Downloads (Issue #50)** — Fixed the missing Sonarr/Radarr badges and links on active downloads for admin users. Enabled robust `_instanceUrl` propagation during queue/history metadata compilation (`buildMetadataMaps`) and active matching (`DownloadMatcher.js`) to ensure link generation succeeds. Added complete schema documentation in OpenAPI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [1.7.18] - 2026-05-24
|
## [1.7.18] - 2026-05-24
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "sofarr",
|
"name": "sofarr",
|
||||||
"version": "1.7.18",
|
"version": "1.7.19",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "sofarr",
|
"name": "sofarr",
|
||||||
"version": "1.7.18",
|
"version": "1.7.19",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sofarr",
|
"name": "sofarr",
|
||||||
"version": "1.7.18",
|
"version": "1.7.19",
|
||||||
"description": "A personal media download dashboard that shows your downloads 'so far' while you relax on the sofa waiting for your *arr services to finish",
|
"description": "A personal media download dashboard that shows your downloads 'so far' while you relax on the sofa waiting for your *arr services to finish",
|
||||||
"main": "server/index.js",
|
"main": "server/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+7
-1
@@ -1888,12 +1888,17 @@ body {
|
|||||||
|
|
||||||
/* ===== Mobile ===== */
|
/* ===== Mobile ===== */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
.main-tabs {
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.requests-container {
|
.requests-container {
|
||||||
padding: 12px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.request-card {
|
.request-card {
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.request-meta {
|
.request-meta {
|
||||||
@@ -2258,6 +2263,7 @@ body {
|
|||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
transition: box-shadow 0.2s ease, border-color 0.2s ease;
|
transition: box-shadow 0.2s ease, border-color 0.2s ease;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.request-card:hover {
|
.request-card:hover {
|
||||||
|
|||||||
+1
-1
@@ -132,7 +132,7 @@ function createApp({ skipRateLimits = false } = {}) {
|
|||||||
* version:
|
* version:
|
||||||
* type: string
|
* type: string
|
||||||
* description: sofarr version
|
* description: sofarr version
|
||||||
* example: "1.7.16"
|
* example: "1.7.19"
|
||||||
* x-code-samples:
|
* x-code-samples:
|
||||||
* - lang: curl
|
* - lang: curl
|
||||||
* label: cURL
|
* label: cURL
|
||||||
|
|||||||
+1
-1
@@ -249,7 +249,7 @@ app.use(express.json({ limit: '64kb' })); // prevent oversized JSON payloads
|
|||||||
* version:
|
* version:
|
||||||
* type: string
|
* type: string
|
||||||
* description: sofarr version
|
* description: sofarr version
|
||||||
* example: "1.7.16"
|
* example: "1.7.19"
|
||||||
*/
|
*/
|
||||||
app.get('/health', (req, res) => {
|
app.get('/health', (req, res) => {
|
||||||
res.json({ status: 'ok', uptime: process.uptime(), version });
|
res.json({ status: 'ok', uptime: process.uptime(), version });
|
||||||
|
|||||||
+22
-1
@@ -22,7 +22,7 @@ info:
|
|||||||
|
|
||||||
## SSE Streaming
|
## SSE Streaming
|
||||||
Real-time updates are available via Server-Sent Events at GET /api/dashboard/stream.
|
Real-time updates are available via Server-Sent Events at GET /api/dashboard/stream.
|
||||||
version: 1.7.18
|
version: 1.7.19
|
||||||
contact:
|
contact:
|
||||||
name: sofarr
|
name: sofarr
|
||||||
license:
|
license:
|
||||||
@@ -176,6 +176,27 @@ components:
|
|||||||
nullable: true
|
nullable: true
|
||||||
description: Tooltip text for Ombi icon ("Request" or "Search")
|
description: Tooltip text for Ombi icon ("Request" or "Search")
|
||||||
example: "Request"
|
example: "Request"
|
||||||
|
arrLink:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
format: uri
|
||||||
|
description: Sonarr/Radarr show/movie web UI link (admin-only)
|
||||||
|
example: "http://sonarr:8989/series/show-slug"
|
||||||
|
downloadPath:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
description: Save path in download client (admin-only)
|
||||||
|
example: "/downloads/series/show-slug"
|
||||||
|
targetPath:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
description: Target path in library (admin-only)
|
||||||
|
example: "/tv/show-slug"
|
||||||
|
arrInstanceKey:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
description: Sonarr/Radarr instance API key (admin-only)
|
||||||
|
example: "api-key-here"
|
||||||
|
|
||||||
DashboardPayload:
|
DashboardPayload:
|
||||||
type: object
|
type: object
|
||||||
|
|||||||
@@ -51,17 +51,43 @@ function readCacheSnapshot() {
|
|||||||
function buildMetadataMaps(snapshot) {
|
function buildMetadataMaps(snapshot) {
|
||||||
const seriesMap = new Map();
|
const seriesMap = new Map();
|
||||||
for (const r of snapshot.sonarrQueue.data.records) {
|
for (const r of snapshot.sonarrQueue.data.records) {
|
||||||
if (r.series && r.seriesId) seriesMap.set(r.seriesId, r.series);
|
if (r.series && r.seriesId) {
|
||||||
|
if (!r.series._instanceUrl && r._instanceUrl) {
|
||||||
|
r.series._instanceUrl = r._instanceUrl;
|
||||||
|
}
|
||||||
|
seriesMap.set(r.seriesId, r.series);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (const r of snapshot.sonarrHistory.data.records) {
|
for (const r of snapshot.sonarrHistory.data.records) {
|
||||||
if (r.series && r.seriesId && !seriesMap.has(r.seriesId)) seriesMap.set(r.seriesId, r.series);
|
if (r.series && r.seriesId) {
|
||||||
|
if (!r.series._instanceUrl && r._instanceUrl) {
|
||||||
|
r.series._instanceUrl = r._instanceUrl;
|
||||||
|
}
|
||||||
|
const existing = seriesMap.get(r.seriesId);
|
||||||
|
if (!existing || (!existing._instanceUrl && r.series._instanceUrl)) {
|
||||||
|
seriesMap.set(r.seriesId, r.series);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const moviesMap = new Map();
|
const moviesMap = new Map();
|
||||||
for (const r of snapshot.radarrQueue.data.records) {
|
for (const r of snapshot.radarrQueue.data.records) {
|
||||||
if (r.movie && r.movieId) moviesMap.set(r.movieId, r.movie);
|
if (r.movie && r.movieId) {
|
||||||
|
if (!r.movie._instanceUrl && r._instanceUrl) {
|
||||||
|
r.movie._instanceUrl = r._instanceUrl;
|
||||||
|
}
|
||||||
|
moviesMap.set(r.movieId, r.movie);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (const r of snapshot.radarrHistory.data.records) {
|
for (const r of snapshot.radarrHistory.data.records) {
|
||||||
if (r.movie && r.movieId && !moviesMap.has(r.movieId)) moviesMap.set(r.movieId, r.movie);
|
if (r.movie && r.movieId) {
|
||||||
|
if (!r.movie._instanceUrl && r._instanceUrl) {
|
||||||
|
r.movie._instanceUrl = r._instanceUrl;
|
||||||
|
}
|
||||||
|
const existing = moviesMap.get(r.movieId);
|
||||||
|
if (!existing || (!existing._instanceUrl && r.movie._instanceUrl)) {
|
||||||
|
moviesMap.set(r.movieId, r.movie);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const sonarrTagMap = new Map(snapshot.sonarrTagsResults.flatMap(t => t.data || []).map(t => [t.id, t.label]));
|
const sonarrTagMap = new Map(snapshot.sonarrTagsResults.flatMap(t => t.data || []).map(t => [t.id, t.label]));
|
||||||
const radarrTagMap = new Map(snapshot.radarrTags.data.map(t => [t.id, t.label]));
|
const radarrTagMap = new Map(snapshot.radarrTags.data.map(t => [t.id, t.label]));
|
||||||
|
|||||||
@@ -215,6 +215,9 @@ async function matchSabSlots(slots, context) {
|
|||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
dlObj.downloadPath = slot.storage || null;
|
dlObj.downloadPath = slot.storage || null;
|
||||||
dlObj.targetPath = series.path || null;
|
dlObj.targetPath = series.path || null;
|
||||||
|
if (series && !series._instanceUrl && sonarrMatch._instanceUrl) {
|
||||||
|
series._instanceUrl = sonarrMatch._instanceUrl;
|
||||||
|
}
|
||||||
dlObj.arrLink = DownloadAssembler.getSonarrLink(series);
|
dlObj.arrLink = DownloadAssembler.getSonarrLink(series);
|
||||||
dlObj.arrInstanceKey = sonarrMatch._instanceKey || null;
|
dlObj.arrInstanceKey = sonarrMatch._instanceKey || null;
|
||||||
}
|
}
|
||||||
@@ -269,6 +272,9 @@ async function matchSabSlots(slots, context) {
|
|||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
dlObj.downloadPath = slot.storage || null;
|
dlObj.downloadPath = slot.storage || null;
|
||||||
dlObj.targetPath = movie.path || null;
|
dlObj.targetPath = movie.path || null;
|
||||||
|
if (movie && !movie._instanceUrl && radarrMatch._instanceUrl) {
|
||||||
|
movie._instanceUrl = radarrMatch._instanceUrl;
|
||||||
|
}
|
||||||
dlObj.arrLink = DownloadAssembler.getRadarrLink(movie);
|
dlObj.arrLink = DownloadAssembler.getRadarrLink(movie);
|
||||||
dlObj.arrInstanceKey = radarrMatch._instanceKey || null;
|
dlObj.arrInstanceKey = radarrMatch._instanceKey || null;
|
||||||
}
|
}
|
||||||
@@ -459,6 +465,9 @@ async function matchTorrents(torrents, context) {
|
|||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
download.downloadPath = download.savePath || null;
|
download.downloadPath = download.savePath || null;
|
||||||
download.targetPath = series.path || null;
|
download.targetPath = series.path || null;
|
||||||
|
if (series && !series._instanceUrl && sonarrMatch._instanceUrl) {
|
||||||
|
series._instanceUrl = sonarrMatch._instanceUrl;
|
||||||
|
}
|
||||||
download.arrLink = DownloadAssembler.getSonarrLink(series);
|
download.arrLink = DownloadAssembler.getSonarrLink(series);
|
||||||
download.arrInstanceKey = sonarrMatch._instanceKey || null;
|
download.arrInstanceKey = sonarrMatch._instanceKey || null;
|
||||||
}
|
}
|
||||||
@@ -505,6 +514,9 @@ async function matchTorrents(torrents, context) {
|
|||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
download.downloadPath = download.savePath || null;
|
download.downloadPath = download.savePath || null;
|
||||||
download.targetPath = movie.path || null;
|
download.targetPath = movie.path || null;
|
||||||
|
if (movie && !movie._instanceUrl && radarrMatch._instanceUrl) {
|
||||||
|
movie._instanceUrl = radarrMatch._instanceUrl;
|
||||||
|
}
|
||||||
download.arrLink = DownloadAssembler.getRadarrLink(movie);
|
download.arrLink = DownloadAssembler.getRadarrLink(movie);
|
||||||
download.arrInstanceKey = radarrMatch._instanceKey || null;
|
download.arrInstanceKey = radarrMatch._instanceKey || null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -349,6 +349,7 @@ describe('GET /api/dashboard/user-downloads', () => {
|
|||||||
expect(dl.arrQueueId).toBe(1002);
|
expect(dl.arrQueueId).toBe(1002);
|
||||||
expect(dl.arrType).toBe('sonarr');
|
expect(dl.arrType).toBe('sonarr');
|
||||||
expect(dl.arrInstanceUrl).toBe(SONARR_BASE);
|
expect(dl.arrInstanceUrl).toBe(SONARR_BASE);
|
||||||
|
expect(dl.arrLink).toBe(SONARR_BASE + '/series/admin-show');
|
||||||
expect(dl.downloadPath).toBeDefined();
|
expect(dl.downloadPath).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user