diff --git a/shelfmark/release_sources/__init__.py b/shelfmark/release_sources/__init__.py
index 34e3f9fc..e5e39de6 100644
--- a/shelfmark/release_sources/__init__.py
+++ b/shelfmark/release_sources/__init__.py
@@ -58,6 +58,7 @@ class ColumnRenderType(str, Enum):
"""How the frontend should render the column value."""
TEXT = "text" # Plain text
BADGE = "badge" # Colored badge (format, language)
+ TAGS = "tags" # List of colored badges
SIZE = "size" # File size formatting
NUMBER = "number" # Numeric value
PEERS = "peers" # Peers display: "S/L" with color based on seeder count
diff --git a/shelfmark/release_sources/prowlarr/source.py b/shelfmark/release_sources/prowlarr/source.py
index 19da899a..f597716d 100644
--- a/shelfmark/release_sources/prowlarr/source.py
+++ b/shelfmark/release_sources/prowlarr/source.py
@@ -199,6 +199,7 @@ def _prowlarr_result_to_release(result: dict, search_content_type: str = "ebook"
"indexer_id": result.get("indexerId"),
"files": result.get("files"),
"grabs": result.get("grabs"),
+ "indexer_flags": result.get("indexerFlags", []),
},
)
@@ -248,6 +249,17 @@ class ProwlarrSource(ReleaseSource):
sortable=True,
sort_key="seeders",
),
+ ColumnSchema(
+ key="extra.indexer_flags",
+ label="Flags",
+ render_type=ColumnRenderType.TAGS,
+ align=ColumnAlign.CENTER,
+ width="minmax(80px, 1.5fr)",
+ hide_mobile=False,
+ color_hint=ColumnColorHint(type="map", value="flags"),
+ fallback="-",
+ uppercase=True,
+ ),
ColumnSchema(
key="content_type",
label="Type",
@@ -270,7 +282,7 @@ class ProwlarrSource(ReleaseSource):
sort_key="size_bytes",
),
],
- grid_template="minmax(0,2fr) minmax(80px,1fr) 60px 70px 90px 80px",
+ grid_template="minmax(0,2fr) minmax(80px,1fr) 60px 70px 80px 90px 80px",
leading_cell=LeadingCellConfig(type=LeadingCellType.NONE), # No leading cell for Prowlarr
supported_filters=["language"], # Enables multi-language query expansion; Prowlarr language metadata is unreliable
)
diff --git a/src/frontend/src/components/ReleaseCell.tsx b/src/frontend/src/components/ReleaseCell.tsx
index b9173b4e..f56001cb 100644
--- a/src/frontend/src/components/ReleaseCell.tsx
+++ b/src/frontend/src/components/ReleaseCell.tsx
@@ -50,6 +50,46 @@ export const ReleaseCell = ({ column, release, compact = false, onlineServers }:
);
}
+ case 'tags': {
+ // Tags display: render list of strings as distinct badges
+ // Treat non-array values as single-item array
+ const tags = (Array.isArray(rawValue) ? rawValue : (value ? [value] : [])) as any[];
+
+ // Compact mode: render as comma-separated text
+ if (compact) {
+ if (tags.length === 0) return {column.fallback};
+ const displayTags = column.uppercase ? tags.map(t => String(t).toUpperCase()) : tags;
+ return {displayTags.join(', ')};
+ }
+
+ if (!tags.length) {
+ return (
+
+ {column.fallback}
+
+ );
+ }
+
+ return (
+
+ {tags.map((tag, idx) => {
+ const tagStr = String(tag);
+ const displayTag = column.uppercase ? tagStr.toUpperCase() : tagStr;
+ const colorStyle = getColorStyleFromHint(tagStr, column.color_hint);
+
+ return (
+
+ {displayTag}
+
+ );
+ })}
+
+ );
+ }
+
case 'size':
if (compact) {
return {displayValue};
diff --git a/src/frontend/src/components/ReleaseModal.tsx b/src/frontend/src/components/ReleaseModal.tsx
index ac98c4a5..cc826aba 100644
--- a/src/frontend/src/components/ReleaseModal.tsx
+++ b/src/frontend/src/components/ReleaseModal.tsx
@@ -1248,8 +1248,8 @@ export const ReleaseModal = ({
- {/* Scrollable content */}
-
+ {/* Scrollable content */}
+
{/* Book summary - scrolls with content */}
{book.preview ? (
@@ -1391,18 +1391,17 @@ export const ReleaseModal = ({
}}
/>
{allTabs.map((tab) => (
-
))}
@@ -1423,9 +1422,8 @@ export const ReleaseModal = ({
return next;
});
}}
- className={`p-2.5 rounded-full transition-colors hover-surface text-gray-500 dark:text-gray-400 ${
- manualQuery.trim() ? 'text-emerald-600 dark:text-emerald-400' : ''
- }`}
+ className={`p-2.5 rounded-full transition-colors hover-surface text-gray-500 dark:text-gray-400 ${manualQuery.trim() ? 'text-emerald-600 dark:text-emerald-400' : ''
+ }`}
aria-label="Manual search query"
title="Manual query"
>
@@ -1444,9 +1442,8 @@ export const ReleaseModal = ({
)}
@@ -1691,11 +1685,10 @@ export const ReleaseModal = ({
{currentTabLoading ? 'Searching…' : 'Search'}
@@ -1731,16 +1724,16 @@ export const ReleaseModal = ({
releasesBySource[activeTab]?.search_info?.[activeTab]?.search_type ?? ''
)
)) && (
-
-
- {columnConfig.action_button?.label ?? 'Expand search'}
-
-
- )}
+
+
+ {columnConfig.action_button?.label ?? 'Expand search'}
+
+
+ )}
>
) : (
<>
@@ -1768,22 +1761,22 @@ export const ReleaseModal = ({
releasesBySource[activeTab]?.search_info?.[activeTab]?.search_type ?? ''
)
)) && (
-
-
- {columnConfig.action_button?.label ?? 'Expand search'}
-
-
- )}
+
+ {columnConfig.action_button?.label ?? 'Expand search'}
+
+
+ )}
{/* Expanding search - show skeleton below existing results */}
{currentTabLoading && filteredReleases.length > 0 && (
diff --git a/src/frontend/src/types/index.ts b/src/frontend/src/types/index.ts
index 15eae8d8..770ad5b4 100644
--- a/src/frontend/src/types/index.ts
+++ b/src/frontend/src/types/index.ts
@@ -204,7 +204,7 @@ export interface ReleaseSource {
}
// Column schema types for plugin-driven release list UI
-export type ColumnRenderType = 'text' | 'badge' | 'size' | 'number' | 'peers';
+export type ColumnRenderType = 'text' | 'badge' | 'tags' | 'size' | 'number' | 'peers';
export type ColumnAlign = 'left' | 'center' | 'right';
export interface ColumnColorHint {
diff --git a/src/frontend/src/utils/colorMaps.ts b/src/frontend/src/utils/colorMaps.ts
index 9e92d6b3..7f7fdc90 100644
--- a/src/frontend/src/utils/colorMaps.ts
+++ b/src/frontend/src/utils/colorMaps.ts
@@ -51,10 +51,18 @@ const CONTENT_TYPE_COLORS: Record = {
audiobook: { bg: 'bg-violet-500/20', text: 'text-violet-700 dark:text-violet-300' },
};
+const FLAG_COLORS: Record = {
+ freeleech: { bg: 'bg-green-500/20', text: 'text-green-700 dark:text-green-300' },
+ 'double upload': { bg: 'bg-blue-500/20', text: 'text-blue-700 dark:text-blue-300' },
+ vip: { bg: 'bg-amber-500/20', text: 'text-amber-700 dark:text-amber-300' },
+ sticky: { bg: 'bg-yellow-500/20', text: 'text-yellow-700 dark:text-yellow-300' },
+};
+
const DEFAULT_FORMAT_COLOR: ColorStyle = { bg: 'bg-cyan-500/20', text: 'text-cyan-700 dark:text-cyan-300' };
const DEFAULT_LANGUAGE_COLOR: ColorStyle = { bg: 'bg-indigo-500/20', text: 'text-indigo-700 dark:text-indigo-300' };
const DEFAULT_DOWNLOAD_TYPE_COLOR: ColorStyle = { bg: 'bg-violet-500/20', text: 'text-violet-700 dark:text-violet-300' };
const DEFAULT_CONTENT_TYPE_COLOR: ColorStyle = { bg: 'bg-gray-500/20', text: 'text-gray-700 dark:text-gray-300' };
+const DEFAULT_FLAG_COLOR: ColorStyle = { bg: 'bg-gray-500/20', text: 'text-gray-700 dark:text-gray-300' };
const FALLBACK_COLOR: ColorStyle = { bg: 'bg-gray-500/20', text: 'text-gray-700 dark:text-gray-300' };
export function getFormatColor(format?: string): ColorStyle {
@@ -77,6 +85,11 @@ export function getContentTypeColor(contentType?: string): ColorStyle {
return CONTENT_TYPE_COLORS[contentType.toLowerCase()] || DEFAULT_CONTENT_TYPE_COLOR;
}
+export function getFlagColor(flag?: string): ColorStyle {
+ if (!flag || flag === '-') return FALLBACK_COLOR;
+ return FLAG_COLORS[flag.toLowerCase()] || DEFAULT_FLAG_COLOR;
+}
+
/**
* Color hint type for dynamic column coloring.
*/
@@ -106,6 +119,8 @@ export function getColorStyleFromHint(value: string, colorHint?: ColumnColorHint
return getDownloadTypeColor(value);
case 'content_type':
return getContentTypeColor(value);
+ case 'flags':
+ return getFlagColor(value);
default:
return FALLBACK_COLOR;
}