mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-10-03 22:07:04 +01:00
Prowlarr non-category search fallback, bypass optimizations, and code cleanup (#379)
- Prowlarr: Added automatic fallback to search without category filter when indexers return no results with book category (7000), improving compatibility with indexers that don't support category filtering - Prowlarr: Hide language filter in UI since Prowlarr has unreliable language metadata - Bypass: Refactored internal bypasser with code cleanup, extracted helper functions, and added health check capability - Bypass: Added fingerprint module for screen size handling - qBittorrent: Fixed connection test to use web API version instead of app version - Frontend: Added supported_filters config to control which filters display per source - Auth: Improved CWA database path validation (now uses Path object properly)
This commit is contained in:
@@ -9,7 +9,7 @@ import { ReleaseCell } from './ReleaseCell';
|
||||
import { getColorStyleFromHint } from '../utils/colorMaps';
|
||||
import { getNestedValue } from '../utils/objectHelpers';
|
||||
import { LanguageMultiSelect } from './LanguageMultiSelect';
|
||||
import { LANGUAGE_OPTION_ALL, LANGUAGE_OPTION_DEFAULT, getLanguageFilterValues } from '../utils/languageFilters';
|
||||
import { LANGUAGE_OPTION_ALL, LANGUAGE_OPTION_DEFAULT, getLanguageFilterValues, releaseLanguageMatchesFilter } from '../utils/languageFilters';
|
||||
|
||||
// Module-level cache for release search results
|
||||
// Key format: `${provider}:${provider_id}:${source}`
|
||||
@@ -92,6 +92,7 @@ const DEFAULT_COLUMN_CONFIG: ReleaseColumnConfig = {
|
||||
},
|
||||
],
|
||||
grid_template: 'minmax(0,2fr) 60px 80px 80px',
|
||||
supported_filters: ['format', 'language'], // Default: both filters available
|
||||
};
|
||||
|
||||
interface ReleaseModalProps {
|
||||
@@ -916,32 +917,27 @@ export const ReleaseModal = ({
|
||||
const supportedLower = supportedFormats.map((f) => f.toLowerCase());
|
||||
|
||||
return releases.filter((r) => {
|
||||
// Format filtering: always filter by supported formats
|
||||
if (r.format) {
|
||||
const fmt = r.format.toLowerCase();
|
||||
// If user selected a specific format, filter to that
|
||||
if (formatFilter) {
|
||||
if (fmt !== formatFilter.toLowerCase()) return false;
|
||||
} else {
|
||||
// Otherwise, only show supported formats
|
||||
if (!supportedLower.includes(fmt)) return false;
|
||||
}
|
||||
}
|
||||
// Format filtering
|
||||
const fmt = r.format?.toLowerCase();
|
||||
|
||||
// Language filtering using resolved language codes
|
||||
// null or includes 'all' means show all languages
|
||||
// Otherwise filter to the specific language codes
|
||||
if (formatFilter) {
|
||||
// User selected a specific format - must match exactly
|
||||
if (!fmt || fmt !== formatFilter.toLowerCase()) return false;
|
||||
} else if (fmt) {
|
||||
// No specific filter - show only supported formats
|
||||
if (!supportedLower.includes(fmt)) return false;
|
||||
}
|
||||
// Releases with no format pass through when no filter is set (show all)
|
||||
|
||||
// Language filtering
|
||||
const releaseLang = r.extra?.language as string | undefined;
|
||||
if (releaseLang && resolvedLanguageCodes && !resolvedLanguageCodes.includes(LANGUAGE_OPTION_ALL)) {
|
||||
const releaseLangLower = releaseLang.toLowerCase();
|
||||
if (!resolvedLanguageCodes.some(code => code.toLowerCase() === releaseLangLower)) {
|
||||
return false;
|
||||
}
|
||||
if (!releaseLanguageMatchesFilter(releaseLang, resolvedLanguageCodes ?? defaultLanguages)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}, [releasesBySource, activeTab, formatFilter, resolvedLanguageCodes, supportedFormats]);
|
||||
}, [releasesBySource, activeTab, formatFilter, resolvedLanguageCodes, supportedFormats, defaultLanguages]);
|
||||
|
||||
// Get column config from response or use default
|
||||
const columnConfig = useMemo((): ReleaseColumnConfig => {
|
||||
@@ -1254,7 +1250,9 @@ export const ReleaseModal = ({
|
||||
</div>
|
||||
|
||||
{/* Filter funnel button - stays fixed */}
|
||||
{(availableFormats.length > 0 || bookLanguages.length > 0) && (
|
||||
{/* Only show filter button if source supports at least one filter type */}
|
||||
{((columnConfig.supported_filters?.includes('format') && availableFormats.length > 0) ||
|
||||
(columnConfig.supported_filters?.includes('language') && bookLanguages.length > 0)) && (
|
||||
<Dropdown
|
||||
align="right"
|
||||
widthClassName="w-auto flex-shrink-0"
|
||||
@@ -1287,7 +1285,7 @@ export const ReleaseModal = ({
|
||||
>
|
||||
{({ close }) => (
|
||||
<div className="p-4 space-y-4">
|
||||
{availableFormats.length > 0 && (
|
||||
{columnConfig.supported_filters?.includes('format') && availableFormats.length > 0 && (
|
||||
<DropdownList
|
||||
label="Format"
|
||||
options={formatOptions}
|
||||
@@ -1296,13 +1294,15 @@ export const ReleaseModal = ({
|
||||
placeholder="All Formats"
|
||||
/>
|
||||
)}
|
||||
<LanguageMultiSelect
|
||||
label="Language"
|
||||
options={bookLanguages}
|
||||
value={languageFilter}
|
||||
onChange={setLanguageFilter}
|
||||
defaultLanguageCodes={defaultLanguages}
|
||||
/>
|
||||
{columnConfig.supported_filters?.includes('language') && (
|
||||
<LanguageMultiSelect
|
||||
label="Language"
|
||||
options={bookLanguages}
|
||||
value={languageFilter}
|
||||
onChange={setLanguageFilter}
|
||||
defaultLanguageCodes={defaultLanguages}
|
||||
/>
|
||||
)}
|
||||
{/* Apply button - for AA, re-fetches with language filter; for others, just closes */}
|
||||
{activeTab === 'direct_download' && (
|
||||
<button
|
||||
@@ -1386,7 +1386,8 @@ export const ReleaseModal = ({
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className="divide-y divide-gray-200/60 dark:divide-gray-800/60">
|
||||
{/* Key includes filter to force remount when filter changes */}
|
||||
<div key={`releases-${formatFilter}-${languageFilter.join(',')}`} className="divide-y divide-gray-200/60 dark:divide-gray-800/60">
|
||||
{filteredReleases.map((release, index) => (
|
||||
<ReleaseRow
|
||||
key={`${release.source}-${release.source_id}`}
|
||||
@@ -1401,11 +1402,13 @@ export const ReleaseModal = ({
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{/* Expand search button - only show if ISBN search was used (otherwise we already did title+author) */}
|
||||
{activeTab === 'direct_download' &&
|
||||
releasesBySource[activeTab]?.search_info?.direct_download?.search_type === 'isbn' &&
|
||||
!expandedBySource[activeTab] &&
|
||||
!currentTabLoading && (
|
||||
{/* Expand search button */}
|
||||
{!expandedBySource[activeTab] &&
|
||||
!currentTabLoading &&
|
||||
releasesBySource[activeTab]?.search_info?.[activeTab]?.search_type &&
|
||||
!['title_author', 'expanded'].includes(
|
||||
releasesBySource[activeTab]?.search_info?.[activeTab]?.search_type ?? ''
|
||||
) && (
|
||||
<div
|
||||
className="py-3 text-center animate-slide-up will-change-transform"
|
||||
style={{
|
||||
|
||||
@@ -228,6 +228,7 @@ export interface ReleaseColumnConfig {
|
||||
leading_cell?: LeadingCellConfig; // Defaults to thumbnail from extra.preview
|
||||
online_servers?: string[]; // For IRC: list of currently online server nicks
|
||||
cache_ttl_seconds?: number; // How long to cache results (default: 300 = 5 min)
|
||||
supported_filters?: string[]; // Which filters this source supports: ["format", "language"]
|
||||
}
|
||||
|
||||
// A downloadable release from any source
|
||||
@@ -250,7 +251,7 @@ export interface Release {
|
||||
|
||||
// Search info returned by release sources
|
||||
export interface SourceSearchInfo {
|
||||
search_type: 'isbn' | 'title_author';
|
||||
search_type: 'isbn' | 'title_author' | 'categories' | 'expanded';
|
||||
}
|
||||
|
||||
// Response from /api/releases endpoint
|
||||
|
||||
@@ -87,3 +87,21 @@ export const formatDefaultLanguageLabel = (
|
||||
return `Default (${joined}${suffix})`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if ALL languages in a multi-language release match the selected filter.
|
||||
*/
|
||||
export const releaseLanguageMatchesFilter = (
|
||||
releaseLang: string | undefined,
|
||||
selectedCodes: string[] | null,
|
||||
): boolean => {
|
||||
if (!releaseLang || !selectedCodes) {
|
||||
return true;
|
||||
}
|
||||
if (selectedCodes.includes(LANGUAGE_OPTION_ALL)) {
|
||||
return true;
|
||||
}
|
||||
const releaseCodes = releaseLang.split(/[,/]/).map(l => l.trim().toLowerCase()).filter(Boolean);
|
||||
const selectedSet = new Set(selectedCodes.map(c => c.toLowerCase()));
|
||||
return releaseCodes.every(code => selectedSet.has(code));
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user