{ "$schema": "homarr-widget-package-v3", "manifest": { "id": "homarr.reference.sonarr", "version": "1.2.0", "sdkVersion": "1", "name": "Sonarr operations", "description": "Calendar, missing episodes and queue across any number of Sonarr integrations, with per-server search controls.", "entrypoints": { "tile": "widget.tsx", "advanced": "widget.tsx", "server": "server.ts" }, "handlers": { "overview": { "kind": "query", "permission": "view" }, "searchEpisodes": { "kind": "action", "permission": "modify", "inputSchema": { "type": "object", "required": [ "connection", "episodeIds" ], "additionalProperties": false, "properties": { "connection": { "type": "string", "minLength": 1, "maxLength": 256 }, "episodeIds": { "type": "array", "minItems": 1, "maxItems": 100, "items": { "type": "integer", "minimum": 1 } } } } } } }, "files": { "widget.tsx": "import { Alert, Badge, Button, Group, Loader, Progress, SegmentedControl, Select, Stack, Text, TextInput } from \"@mantine/core\";\nimport { useWidgetAction, useWidgetHost, useWidgetOptions, useWidgetQuery, useWidgetServices, useWidgetState } from \"@homarr/widget-sdk\";\nimport type { Connection, Overview, SonarrOptions } from \"./types\";\nexport default function Sonarr() {\n const host = useWidgetHost(); const services = useWidgetServices(); const options = useWidgetOptions();\n const [tab,setTab] = useWidgetState(\"tab\",\"calendar\");\n const [source,setSource] = useWidgetState(\"source\",\"all\");\n const [filter,setFilter] = useWidgetState(\"filter\",\"\");\n const query = useWidgetQuery(\"overview\",{}, {refetchInterval:60000});\n const search = useWidgetAction<{id:number;status:string},{connection:Connection;episodeIds:number[]}>(\"searchEpisodes\");\n const label = (connection:Connection) => sources.find((source) => source.connection === connection)?.label ?? connection;\n const sources = query.data?.sources ?? [];\n const rows = sources.filter((entry) => source === \"all\" || source === entry.connection).flatMap((entry) => {\n let items = entry.calendar;\n if (tab === \"missing\") items = entry.missing.items;\n if (tab === \"queue\") items = entry.queue.items;\n return items.map((item,index) => ({...item,connection:entry.connection,rowKey:`${entry.connection}:${tab}:${item.id ?? index}`}));\n }).filter((item) => `${item.title} ${item.subTitle ?? item.seriesTitle ?? \"\"}`.toLowerCase().includes(filter.toLowerCase()));\n const visible = host.displayMode === \"advanced\" ? rows : rows.slice(0,4);\n const requestSearch = async (connection:Connection,id:number,title:string) => {\n if (await services.confirm({title:\"Search for this episode?\",message:`Ask ${label(connection)} to find and download an eligible release for ${title}?`})) search.mutate({connection,episodeIds:[id]});\n };\n return \n {options.title}{sources.reduce((sum,entry) => sum+entry.queue.totalCount,0)} queued\n \n {sources.length > 1 &&