mcp-arr-suite 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +198 -0
- package/dist/clients/arr-client.d.ts +681 -0
- package/dist/clients/arr-client.d.ts.map +1 -0
- package/dist/clients/arr-client.js +367 -0
- package/dist/clients/arr-client.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +93 -0
- package/dist/index.js.map +1 -0
- package/dist/registry.d.ts +16 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +32 -0
- package/dist/registry.js.map +1 -0
- package/dist/services/cross-service.d.ts +8 -0
- package/dist/services/cross-service.d.ts.map +1 -0
- package/dist/services/cross-service.js +113 -0
- package/dist/services/cross-service.js.map +1 -0
- package/dist/services/lidarr.d.ts +6 -0
- package/dist/services/lidarr.d.ts.map +1 -0
- package/dist/services/lidarr.js +263 -0
- package/dist/services/lidarr.js.map +1 -0
- package/dist/services/prowlarr.d.ts +10 -0
- package/dist/services/prowlarr.d.ts.map +1 -0
- package/dist/services/prowlarr.js +140 -0
- package/dist/services/prowlarr.js.map +1 -0
- package/dist/services/radarr.d.ts +9 -0
- package/dist/services/radarr.d.ts.map +1 -0
- package/dist/services/radarr.js +574 -0
- package/dist/services/radarr.js.map +1 -0
- package/dist/services/sonarr.d.ts +9 -0
- package/dist/services/sonarr.d.ts.map +1 -0
- package/dist/services/sonarr.js +696 -0
- package/dist/services/sonarr.js.map +1 -0
- package/dist/shared/config-tools.d.ts +15 -0
- package/dist/shared/config-tools.d.ts.map +1 -0
- package/dist/shared/config-tools.js +225 -0
- package/dist/shared/config-tools.js.map +1 -0
- package/dist/shared/formatting.d.ts +37 -0
- package/dist/shared/formatting.d.ts.map +1 -0
- package/dist/shared/formatting.js +57 -0
- package/dist/shared/formatting.js.map +1 -0
- package/dist/trash/client.d.ts +127 -0
- package/dist/trash/client.d.ts.map +1 -0
- package/dist/trash/client.js +289 -0
- package/dist/trash/client.js.map +1 -0
- package/dist/trash/tools.d.ts +13 -0
- package/dist/trash/tools.d.ts.map +1 -0
- package/dist/trash/tools.js +377 -0
- package/dist/trash/tools.js.map +1 -0
- package/dist/types.d.ts +33 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +15 -0
- package/dist/types.js.map +1 -0
- package/package.json +53 -0
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sonarr (TV Series) ToolModule
|
|
3
|
+
*
|
|
4
|
+
* Tool definitions and handlers live together — scroll down from any
|
|
5
|
+
* tool schema to find its implementation directly below it.
|
|
6
|
+
*/
|
|
7
|
+
import { ok } from '../types.js';
|
|
8
|
+
import { formatBytes, truncate, paginate, clampLimit, clampOffset, today, daysFromNow } from '../shared/formatting.js';
|
|
9
|
+
export const sonarrModule = {
|
|
10
|
+
tools: [
|
|
11
|
+
{
|
|
12
|
+
name: 'sonarr_get_series',
|
|
13
|
+
description: 'Get TV series from the Sonarr library. Returns summary fields only — use search to filter before paginating. Default limit=25 to keep context usage low.',
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
limit: { type: 'number', description: 'Results per page (default: 25, max: 100)' },
|
|
18
|
+
offset: { type: 'number', description: 'Skip N results (default: 0)' },
|
|
19
|
+
search: { type: 'string', description: 'Case-insensitive title filter' },
|
|
20
|
+
},
|
|
21
|
+
required: [],
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'sonarr_search',
|
|
26
|
+
description: 'Search for TV series by name. Returns top 10 results with tvdbId needed for sonarr_add_series.',
|
|
27
|
+
inputSchema: {
|
|
28
|
+
type: 'object',
|
|
29
|
+
properties: {
|
|
30
|
+
term: { type: 'string', description: 'Series name to search for' },
|
|
31
|
+
},
|
|
32
|
+
required: ['term'],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'sonarr_get_queue',
|
|
37
|
+
description: 'Get current Sonarr download queue. Shows active downloads and their progress.',
|
|
38
|
+
inputSchema: {
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: {
|
|
41
|
+
limit: { type: 'number', description: 'Max queue items to return (default: 10)' },
|
|
42
|
+
},
|
|
43
|
+
required: [],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'sonarr_get_calendar',
|
|
48
|
+
description: 'Get upcoming episode air dates from Sonarr.',
|
|
49
|
+
inputSchema: {
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: {
|
|
52
|
+
days: { type: 'number', description: 'Days to look ahead (default: 7)' },
|
|
53
|
+
},
|
|
54
|
+
required: [],
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'sonarr_get_episodes',
|
|
59
|
+
description: 'Get episodes for a series. Shows which episodes are available and which are missing. Filter by season to reduce response size.',
|
|
60
|
+
inputSchema: {
|
|
61
|
+
type: 'object',
|
|
62
|
+
properties: {
|
|
63
|
+
seriesId: { type: 'number', description: 'Series ID (from sonarr_get_series)' },
|
|
64
|
+
seasonNumber: { type: 'number', description: 'Filter to a specific season (optional)' },
|
|
65
|
+
},
|
|
66
|
+
required: ['seriesId'],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'sonarr_search_missing',
|
|
71
|
+
description: 'Trigger a search for all missing episodes in a series.',
|
|
72
|
+
inputSchema: {
|
|
73
|
+
type: 'object',
|
|
74
|
+
properties: {
|
|
75
|
+
seriesId: { type: 'number', description: 'Series ID (from sonarr_get_series)' },
|
|
76
|
+
},
|
|
77
|
+
required: ['seriesId'],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'sonarr_search_episode',
|
|
82
|
+
description: 'Trigger a download search for specific episode(s).',
|
|
83
|
+
inputSchema: {
|
|
84
|
+
type: 'object',
|
|
85
|
+
properties: {
|
|
86
|
+
episodeIds: {
|
|
87
|
+
type: 'array',
|
|
88
|
+
items: { type: 'number' },
|
|
89
|
+
description: 'Episode ID(s) (from sonarr_get_episodes)',
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
required: ['episodeIds'],
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'sonarr_refresh_series',
|
|
97
|
+
description: 'Trigger a metadata refresh for a specific series in Sonarr.',
|
|
98
|
+
inputSchema: {
|
|
99
|
+
type: 'object',
|
|
100
|
+
properties: {
|
|
101
|
+
seriesId: { type: 'number', description: 'Series ID (from sonarr_get_series)' },
|
|
102
|
+
},
|
|
103
|
+
required: ['seriesId'],
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'sonarr_add_series',
|
|
108
|
+
description: 'Add a TV series to Sonarr. Use sonarr_search first to get the tvdbId, sonarr_get_root_folders for rootFolderPath, sonarr_get_quality_profiles for qualityProfileId, and sonarr_get_tags for tag IDs.',
|
|
109
|
+
inputSchema: {
|
|
110
|
+
type: 'object',
|
|
111
|
+
properties: {
|
|
112
|
+
tvdbId: { type: 'number', description: 'TVDB ID from sonarr_search results' },
|
|
113
|
+
title: { type: 'string', description: 'Series title' },
|
|
114
|
+
qualityProfileId: { type: 'number', description: 'Quality profile ID' },
|
|
115
|
+
rootFolderPath: { type: 'string', description: 'Root folder path' },
|
|
116
|
+
monitored: { type: 'boolean', description: 'Monitor the series (default: true)' },
|
|
117
|
+
seasonFolder: { type: 'boolean', description: 'Use season folders (default: true)' },
|
|
118
|
+
tags: {
|
|
119
|
+
type: 'array',
|
|
120
|
+
items: { type: 'number' },
|
|
121
|
+
description: 'Tag IDs (optional)',
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
required: ['tvdbId', 'title', 'qualityProfileId', 'rootFolderPath'],
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'sonarr_delete_series',
|
|
129
|
+
description: 'Remove a series from Sonarr. Optionally delete files from disk and/or add to import exclusions.',
|
|
130
|
+
inputSchema: {
|
|
131
|
+
type: 'object',
|
|
132
|
+
properties: {
|
|
133
|
+
seriesId: { type: 'number', description: 'Series ID (from sonarr_get_series)' },
|
|
134
|
+
deleteFiles: { type: 'boolean', description: 'Delete episode files from disk (default: false)' },
|
|
135
|
+
addImportListExclusion: { type: 'boolean', description: 'Prevent re-importing this series (default: false)' },
|
|
136
|
+
},
|
|
137
|
+
required: ['seriesId'],
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: 'sonarr_update_series',
|
|
142
|
+
description: 'Update a series in Sonarr — change monitored status, quality profile, or tags.',
|
|
143
|
+
inputSchema: {
|
|
144
|
+
type: 'object',
|
|
145
|
+
properties: {
|
|
146
|
+
seriesId: { type: 'number', description: 'Series ID (from sonarr_get_series)' },
|
|
147
|
+
monitored: { type: 'boolean', description: 'Set monitored status' },
|
|
148
|
+
qualityProfileId: { type: 'number', description: 'Change quality profile ID' },
|
|
149
|
+
seasonFolder: { type: 'boolean', description: 'Use season folders' },
|
|
150
|
+
tags: { type: 'array', items: { type: 'number' }, description: 'Replace tag IDs' },
|
|
151
|
+
},
|
|
152
|
+
required: ['seriesId'],
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: 'sonarr_get_disk_space',
|
|
157
|
+
description: 'Get disk space usage for all Sonarr root folders and mounts.',
|
|
158
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: 'sonarr_get_history',
|
|
162
|
+
description: 'Get download history. Filter to a specific series with seriesId, or leave blank for recent activity across all series.',
|
|
163
|
+
inputSchema: {
|
|
164
|
+
type: 'object',
|
|
165
|
+
properties: {
|
|
166
|
+
seriesId: { type: 'number', description: 'Filter to a specific series (optional)' },
|
|
167
|
+
page: { type: 'number', description: 'Page number (default: 1)' },
|
|
168
|
+
pageSize: { type: 'number', description: 'Results per page (default: 20, max: 100)' },
|
|
169
|
+
},
|
|
170
|
+
required: [],
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: 'sonarr_remove_from_queue',
|
|
175
|
+
description: 'Remove one or more items from the Sonarr download queue. Optionally blocklist them to prevent re-grabbing.',
|
|
176
|
+
inputSchema: {
|
|
177
|
+
type: 'object',
|
|
178
|
+
properties: {
|
|
179
|
+
ids: {
|
|
180
|
+
type: 'array',
|
|
181
|
+
items: { type: 'number' },
|
|
182
|
+
description: 'Queue item ID(s) to remove (from sonarr_get_queue)',
|
|
183
|
+
},
|
|
184
|
+
blocklist: { type: 'boolean', description: 'Add to blocklist to prevent re-grab (default: false)' },
|
|
185
|
+
removeFromClient: { type: 'boolean', description: 'Also remove from download client (default: true)' },
|
|
186
|
+
},
|
|
187
|
+
required: ['ids'],
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: 'sonarr_get_wanted_missing',
|
|
192
|
+
description: 'Get paginated list of monitored episodes that are missing (not yet downloaded).',
|
|
193
|
+
inputSchema: {
|
|
194
|
+
type: 'object',
|
|
195
|
+
properties: {
|
|
196
|
+
page: { type: 'number', description: 'Page number (default: 1)' },
|
|
197
|
+
pageSize: { type: 'number', description: 'Results per page (default: 20, max: 100)' },
|
|
198
|
+
},
|
|
199
|
+
required: [],
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: 'sonarr_get_wanted_cutoff',
|
|
204
|
+
description: 'Get paginated list of episodes that have a file but have not met the quality cutoff (upgrade candidates).',
|
|
205
|
+
inputSchema: {
|
|
206
|
+
type: 'object',
|
|
207
|
+
properties: {
|
|
208
|
+
page: { type: 'number', description: 'Page number (default: 1)' },
|
|
209
|
+
pageSize: { type: 'number', description: 'Results per page (default: 20, max: 100)' },
|
|
210
|
+
},
|
|
211
|
+
required: [],
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: 'sonarr_get_episode_files',
|
|
216
|
+
description: 'Get file details for all episodes in a series — quality, size, codecs, languages.',
|
|
217
|
+
inputSchema: {
|
|
218
|
+
type: 'object',
|
|
219
|
+
properties: {
|
|
220
|
+
seriesId: { type: 'number', description: 'Series ID (from sonarr_get_series)' },
|
|
221
|
+
},
|
|
222
|
+
required: ['seriesId'],
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
name: 'sonarr_delete_episode_file',
|
|
227
|
+
description: 'Delete a specific episode file from disk (the episode remains in Sonarr as unmonitored).',
|
|
228
|
+
inputSchema: {
|
|
229
|
+
type: 'object',
|
|
230
|
+
properties: {
|
|
231
|
+
fileId: { type: 'number', description: 'Episode file ID (from sonarr_get_episode_files)' },
|
|
232
|
+
},
|
|
233
|
+
required: ['fileId'],
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: 'sonarr_get_blocklist',
|
|
238
|
+
description: 'Get the Sonarr blocklist — releases that were blocked/failed and won\'t be re-grabbed.',
|
|
239
|
+
inputSchema: {
|
|
240
|
+
type: 'object',
|
|
241
|
+
properties: {
|
|
242
|
+
page: { type: 'number', description: 'Page number (default: 1)' },
|
|
243
|
+
pageSize: { type: 'number', description: 'Results per page (default: 20, max: 100)' },
|
|
244
|
+
},
|
|
245
|
+
required: [],
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: 'sonarr_delete_from_blocklist',
|
|
250
|
+
description: 'Remove an entry from the Sonarr blocklist so that release can be grabbed again.',
|
|
251
|
+
inputSchema: {
|
|
252
|
+
type: 'object',
|
|
253
|
+
properties: {
|
|
254
|
+
blocklistId: { type: 'number', description: 'Blocklist entry ID (from sonarr_get_blocklist)' },
|
|
255
|
+
},
|
|
256
|
+
required: ['blocklistId'],
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
name: 'sonarr_monitor_episodes',
|
|
261
|
+
description: 'Bulk set monitored/unmonitored status for specific episodes.',
|
|
262
|
+
inputSchema: {
|
|
263
|
+
type: 'object',
|
|
264
|
+
properties: {
|
|
265
|
+
episodeIds: {
|
|
266
|
+
type: 'array',
|
|
267
|
+
items: { type: 'number' },
|
|
268
|
+
description: 'Episode IDs to update (from sonarr_get_episodes)',
|
|
269
|
+
},
|
|
270
|
+
monitored: { type: 'boolean', description: 'Set to true to monitor, false to unmonitor' },
|
|
271
|
+
},
|
|
272
|
+
required: ['episodeIds', 'monitored'],
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
name: 'sonarr_season_pass',
|
|
277
|
+
description: 'Bulk set monitored status for entire seasons across one or more series.',
|
|
278
|
+
inputSchema: {
|
|
279
|
+
type: 'object',
|
|
280
|
+
properties: {
|
|
281
|
+
seriesId: { type: 'number', description: 'Series ID to update' },
|
|
282
|
+
seasons: {
|
|
283
|
+
type: 'array',
|
|
284
|
+
description: 'Season numbers and their monitored state',
|
|
285
|
+
items: {
|
|
286
|
+
type: 'object',
|
|
287
|
+
properties: {
|
|
288
|
+
seasonNumber: { type: 'number', description: 'Season number (0 = specials)' },
|
|
289
|
+
monitored: { type: 'boolean', description: 'Monitor this season' },
|
|
290
|
+
},
|
|
291
|
+
required: ['seasonNumber', 'monitored'],
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
required: ['seriesId', 'seasons'],
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
],
|
|
299
|
+
handlers: {
|
|
300
|
+
sonarr_get_series: async (args, clients) => {
|
|
301
|
+
if (!clients.sonarr)
|
|
302
|
+
throw new Error('Sonarr is not configured');
|
|
303
|
+
const limit = clampLimit(args.limit ?? 25);
|
|
304
|
+
const offset = clampOffset(args.offset ?? 0);
|
|
305
|
+
const search = args.search?.trim().toLowerCase();
|
|
306
|
+
const all = await clients.sonarr.getSeries();
|
|
307
|
+
const filtered = search ? all.filter(s => s.title.toLowerCase().includes(search)) : all;
|
|
308
|
+
const page = filtered.slice(offset, offset + limit);
|
|
309
|
+
return ok({
|
|
310
|
+
...paginate(page.map(s => ({
|
|
311
|
+
id: s.id,
|
|
312
|
+
title: s.title,
|
|
313
|
+
year: s.year,
|
|
314
|
+
status: s.status,
|
|
315
|
+
network: s.network,
|
|
316
|
+
seasons: s.statistics?.seasonCount,
|
|
317
|
+
episodes: `${s.statistics?.episodeFileCount ?? 0}/${s.statistics?.totalEpisodeCount ?? 0}`,
|
|
318
|
+
sizeOnDisk: formatBytes(s.statistics?.sizeOnDisk ?? 0),
|
|
319
|
+
monitored: s.monitored,
|
|
320
|
+
qualityProfileId: s.qualityProfileId,
|
|
321
|
+
})), filtered.length, offset, limit),
|
|
322
|
+
...(search ? { search } : {}),
|
|
323
|
+
totalLibrary: all.length,
|
|
324
|
+
});
|
|
325
|
+
},
|
|
326
|
+
sonarr_search: async (args, clients) => {
|
|
327
|
+
if (!clients.sonarr)
|
|
328
|
+
throw new Error('Sonarr is not configured');
|
|
329
|
+
const term = args.term;
|
|
330
|
+
const results = await clients.sonarr.searchSeries(term);
|
|
331
|
+
return ok({
|
|
332
|
+
count: results.length,
|
|
333
|
+
results: results.slice(0, 10).map(r => ({
|
|
334
|
+
title: r.title,
|
|
335
|
+
year: r.year,
|
|
336
|
+
tvdbId: r.tvdbId,
|
|
337
|
+
status: r.status,
|
|
338
|
+
overview: truncate(r.overview, 200),
|
|
339
|
+
})),
|
|
340
|
+
});
|
|
341
|
+
},
|
|
342
|
+
sonarr_get_queue: async (args, clients) => {
|
|
343
|
+
if (!clients.sonarr)
|
|
344
|
+
throw new Error('Sonarr is not configured');
|
|
345
|
+
const limit = clampLimit(args.limit ?? 10);
|
|
346
|
+
const queue = await clients.sonarr.getQueue();
|
|
347
|
+
const items = queue.records.slice(0, limit);
|
|
348
|
+
return ok({
|
|
349
|
+
totalRecords: queue.totalRecords,
|
|
350
|
+
returned: items.length,
|
|
351
|
+
items: items.map(q => ({
|
|
352
|
+
title: q.title,
|
|
353
|
+
status: q.status,
|
|
354
|
+
progress: q.size > 0 ? `${((1 - q.sizeleft / q.size) * 100).toFixed(1)}%` : '0%',
|
|
355
|
+
timeLeft: q.timeleft,
|
|
356
|
+
downloadClient: q.downloadClient,
|
|
357
|
+
})),
|
|
358
|
+
});
|
|
359
|
+
},
|
|
360
|
+
sonarr_get_calendar: async (args, clients) => {
|
|
361
|
+
if (!clients.sonarr)
|
|
362
|
+
throw new Error('Sonarr is not configured');
|
|
363
|
+
const days = args.days ?? 7;
|
|
364
|
+
const calendar = await clients.sonarr.getCalendar(today(), daysFromNow(days));
|
|
365
|
+
const items = calendar;
|
|
366
|
+
return ok({
|
|
367
|
+
days,
|
|
368
|
+
count: items.length,
|
|
369
|
+
episodes: items.map(e => ({
|
|
370
|
+
id: e['id'],
|
|
371
|
+
seriesId: e['seriesId'],
|
|
372
|
+
seriesTitle: e['series'] ? e['series']['title'] : undefined,
|
|
373
|
+
seasonNumber: e['seasonNumber'],
|
|
374
|
+
episodeNumber: e['episodeNumber'],
|
|
375
|
+
title: e['title'],
|
|
376
|
+
airDate: e['airDate'],
|
|
377
|
+
hasFile: e['hasFile'],
|
|
378
|
+
})),
|
|
379
|
+
});
|
|
380
|
+
},
|
|
381
|
+
sonarr_get_episodes: async (args, clients) => {
|
|
382
|
+
if (!clients.sonarr)
|
|
383
|
+
throw new Error('Sonarr is not configured');
|
|
384
|
+
const seriesId = args.seriesId;
|
|
385
|
+
const seasonNumber = args.seasonNumber;
|
|
386
|
+
const episodes = await clients.sonarr.getEpisodes(seriesId, seasonNumber);
|
|
387
|
+
return ok({
|
|
388
|
+
count: episodes.length,
|
|
389
|
+
episodes: episodes.map(e => ({
|
|
390
|
+
id: e.id,
|
|
391
|
+
seasonNumber: e.seasonNumber,
|
|
392
|
+
episodeNumber: e.episodeNumber,
|
|
393
|
+
title: e.title,
|
|
394
|
+
airDate: e.airDate,
|
|
395
|
+
hasFile: e.hasFile,
|
|
396
|
+
monitored: e.monitored,
|
|
397
|
+
})),
|
|
398
|
+
});
|
|
399
|
+
},
|
|
400
|
+
sonarr_search_missing: async (args, clients) => {
|
|
401
|
+
if (!clients.sonarr)
|
|
402
|
+
throw new Error('Sonarr is not configured');
|
|
403
|
+
const seriesId = args.seriesId;
|
|
404
|
+
const result = await clients.sonarr.searchMissing(seriesId);
|
|
405
|
+
return ok({ success: true, message: 'Search triggered for missing episodes', commandId: result.id });
|
|
406
|
+
},
|
|
407
|
+
sonarr_search_episode: async (args, clients) => {
|
|
408
|
+
if (!clients.sonarr)
|
|
409
|
+
throw new Error('Sonarr is not configured');
|
|
410
|
+
const episodeIds = args.episodeIds;
|
|
411
|
+
const result = await clients.sonarr.searchEpisode(episodeIds);
|
|
412
|
+
return ok({
|
|
413
|
+
success: true,
|
|
414
|
+
message: `Search triggered for ${episodeIds.length} episode(s)`,
|
|
415
|
+
commandId: result.id,
|
|
416
|
+
});
|
|
417
|
+
},
|
|
418
|
+
sonarr_refresh_series: async (args, clients) => {
|
|
419
|
+
if (!clients.sonarr)
|
|
420
|
+
throw new Error('Sonarr is not configured');
|
|
421
|
+
const seriesId = args.seriesId;
|
|
422
|
+
const [series, result] = await Promise.all([
|
|
423
|
+
clients.sonarr.getSeriesById(seriesId),
|
|
424
|
+
clients.sonarr.refreshSeries(seriesId),
|
|
425
|
+
]);
|
|
426
|
+
return ok({
|
|
427
|
+
success: true,
|
|
428
|
+
message: 'Metadata refresh triggered',
|
|
429
|
+
series: { id: series.id, title: series.title, year: series.year },
|
|
430
|
+
commandId: result.id,
|
|
431
|
+
});
|
|
432
|
+
},
|
|
433
|
+
sonarr_add_series: async (args, clients) => {
|
|
434
|
+
if (!clients.sonarr)
|
|
435
|
+
throw new Error('Sonarr is not configured');
|
|
436
|
+
const { tvdbId, title, qualityProfileId, rootFolderPath, monitored, seasonFolder, tags } = args;
|
|
437
|
+
const added = await clients.sonarr.addSeries({
|
|
438
|
+
tvdbId,
|
|
439
|
+
title,
|
|
440
|
+
qualityProfileId,
|
|
441
|
+
rootFolderPath,
|
|
442
|
+
monitored,
|
|
443
|
+
seasonFolder,
|
|
444
|
+
tags: tags ?? [],
|
|
445
|
+
});
|
|
446
|
+
return ok({
|
|
447
|
+
success: true,
|
|
448
|
+
message: `Added "${added.title}" (${added.year}) to Sonarr`,
|
|
449
|
+
id: added.id,
|
|
450
|
+
path: added.path,
|
|
451
|
+
monitored: added.monitored,
|
|
452
|
+
});
|
|
453
|
+
},
|
|
454
|
+
sonarr_delete_series: async (args, clients) => {
|
|
455
|
+
if (!clients.sonarr)
|
|
456
|
+
throw new Error('Sonarr is not configured');
|
|
457
|
+
const { seriesId, deleteFiles = false, addImportListExclusion = false } = args;
|
|
458
|
+
const series = await clients.sonarr.getSeriesById(seriesId);
|
|
459
|
+
await clients.sonarr.deleteSeries(seriesId, deleteFiles, addImportListExclusion);
|
|
460
|
+
return ok({
|
|
461
|
+
success: true,
|
|
462
|
+
message: `Removed "${series.title}" (${series.year}) from Sonarr`,
|
|
463
|
+
deletedFiles: deleteFiles,
|
|
464
|
+
addedToExclusions: addImportListExclusion,
|
|
465
|
+
});
|
|
466
|
+
},
|
|
467
|
+
sonarr_update_series: async (args, clients) => {
|
|
468
|
+
if (!clients.sonarr)
|
|
469
|
+
throw new Error('Sonarr is not configured');
|
|
470
|
+
const { seriesId, monitored, qualityProfileId, seasonFolder, tags } = args;
|
|
471
|
+
const changes = {};
|
|
472
|
+
if (monitored !== undefined)
|
|
473
|
+
changes['monitored'] = monitored;
|
|
474
|
+
if (qualityProfileId !== undefined)
|
|
475
|
+
changes['qualityProfileId'] = qualityProfileId;
|
|
476
|
+
if (seasonFolder !== undefined)
|
|
477
|
+
changes['seasonFolder'] = seasonFolder;
|
|
478
|
+
if (tags !== undefined)
|
|
479
|
+
changes['tags'] = tags;
|
|
480
|
+
const updated = await clients.sonarr.updateSeries(seriesId, changes);
|
|
481
|
+
return ok({
|
|
482
|
+
success: true,
|
|
483
|
+
message: `Updated "${updated.title}" (${updated.year})`,
|
|
484
|
+
id: updated.id,
|
|
485
|
+
monitored: updated.monitored,
|
|
486
|
+
qualityProfileId: updated.qualityProfileId,
|
|
487
|
+
seasonFolder: updated.seasonFolder,
|
|
488
|
+
tags: updated.tags,
|
|
489
|
+
});
|
|
490
|
+
},
|
|
491
|
+
sonarr_get_disk_space: async (_args, clients) => {
|
|
492
|
+
if (!clients.sonarr)
|
|
493
|
+
throw new Error('Sonarr is not configured');
|
|
494
|
+
const diskSpace = await clients.sonarr.getDiskSpace();
|
|
495
|
+
return ok({
|
|
496
|
+
count: diskSpace.length,
|
|
497
|
+
disks: diskSpace.map(d => ({
|
|
498
|
+
path: d.path,
|
|
499
|
+
label: d.label,
|
|
500
|
+
freeSpace: formatBytes(d.freeSpace),
|
|
501
|
+
totalSpace: formatBytes(d.totalSpace),
|
|
502
|
+
usedSpace: formatBytes(d.totalSpace - d.freeSpace),
|
|
503
|
+
freePercent: d.totalSpace > 0
|
|
504
|
+
? `${((d.freeSpace / d.totalSpace) * 100).toFixed(1)}%`
|
|
505
|
+
: 'unknown',
|
|
506
|
+
})),
|
|
507
|
+
});
|
|
508
|
+
},
|
|
509
|
+
sonarr_get_history: async (args, clients) => {
|
|
510
|
+
if (!clients.sonarr)
|
|
511
|
+
throw new Error('Sonarr is not configured');
|
|
512
|
+
const { seriesId, page = 1, pageSize = 20 } = args;
|
|
513
|
+
const history = await clients.sonarr.getHistory(seriesId, page, clampLimit(pageSize));
|
|
514
|
+
const isArray = Array.isArray(history);
|
|
515
|
+
const records = isArray
|
|
516
|
+
? history
|
|
517
|
+
: (history.records);
|
|
518
|
+
const total = isArray ? records.length : history.totalRecords;
|
|
519
|
+
return ok({
|
|
520
|
+
totalRecords: total,
|
|
521
|
+
returned: records.length,
|
|
522
|
+
...(seriesId ? { seriesId } : {}),
|
|
523
|
+
records: records.map(r => ({
|
|
524
|
+
id: r['id'],
|
|
525
|
+
seriesId: r['seriesId'],
|
|
526
|
+
episodeId: r['episodeId'],
|
|
527
|
+
sourceTitle: r['sourceTitle'],
|
|
528
|
+
eventType: r['eventType'],
|
|
529
|
+
date: r['date'],
|
|
530
|
+
quality: r['quality']?.['quality'],
|
|
531
|
+
languages: r['languages'],
|
|
532
|
+
downloadId: r['downloadId'],
|
|
533
|
+
})),
|
|
534
|
+
});
|
|
535
|
+
},
|
|
536
|
+
sonarr_remove_from_queue: async (args, clients) => {
|
|
537
|
+
if (!clients.sonarr)
|
|
538
|
+
throw new Error('Sonarr is not configured');
|
|
539
|
+
const { ids, blocklist = false, removeFromClient = true } = args;
|
|
540
|
+
if (ids.length === 1) {
|
|
541
|
+
await clients.sonarr.removeFromQueue(ids[0], blocklist, removeFromClient);
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
544
|
+
await clients.sonarr.removeFromQueueBulk(ids, blocklist, removeFromClient);
|
|
545
|
+
}
|
|
546
|
+
return ok({
|
|
547
|
+
success: true,
|
|
548
|
+
message: `Removed ${ids.length} item(s) from queue`,
|
|
549
|
+
ids,
|
|
550
|
+
blocklisted: blocklist,
|
|
551
|
+
removedFromClient: removeFromClient,
|
|
552
|
+
});
|
|
553
|
+
},
|
|
554
|
+
sonarr_get_wanted_missing: async (args, clients) => {
|
|
555
|
+
if (!clients.sonarr)
|
|
556
|
+
throw new Error('Sonarr is not configured');
|
|
557
|
+
const { page = 1, pageSize = 20 } = args;
|
|
558
|
+
const wanted = await clients.sonarr.getWantedMissing(page, clampLimit(pageSize));
|
|
559
|
+
return ok({
|
|
560
|
+
totalRecords: wanted.totalRecords,
|
|
561
|
+
page: wanted.page,
|
|
562
|
+
pageSize: wanted.pageSize,
|
|
563
|
+
returned: wanted.records.length,
|
|
564
|
+
hasMore: wanted.page * wanted.pageSize < wanted.totalRecords,
|
|
565
|
+
series: wanted.records.map(s => ({
|
|
566
|
+
id: s.id,
|
|
567
|
+
title: s.title,
|
|
568
|
+
year: s.year,
|
|
569
|
+
status: s.status,
|
|
570
|
+
monitored: s.monitored,
|
|
571
|
+
qualityProfileId: s.qualityProfileId,
|
|
572
|
+
seasons: s.statistics?.seasonCount,
|
|
573
|
+
missingEpisodes: (s.statistics?.episodeCount ?? 0) - (s.statistics?.episodeFileCount ?? 0),
|
|
574
|
+
})),
|
|
575
|
+
});
|
|
576
|
+
},
|
|
577
|
+
sonarr_get_wanted_cutoff: async (args, clients) => {
|
|
578
|
+
if (!clients.sonarr)
|
|
579
|
+
throw new Error('Sonarr is not configured');
|
|
580
|
+
const { page = 1, pageSize = 20 } = args;
|
|
581
|
+
const wanted = await clients.sonarr.getWantedCutoff(page, clampLimit(pageSize));
|
|
582
|
+
return ok({
|
|
583
|
+
totalRecords: wanted.totalRecords,
|
|
584
|
+
page: wanted.page,
|
|
585
|
+
pageSize: wanted.pageSize,
|
|
586
|
+
returned: wanted.records.length,
|
|
587
|
+
hasMore: wanted.page * wanted.pageSize < wanted.totalRecords,
|
|
588
|
+
series: wanted.records.map(s => ({
|
|
589
|
+
id: s.id,
|
|
590
|
+
title: s.title,
|
|
591
|
+
year: s.year,
|
|
592
|
+
monitored: s.monitored,
|
|
593
|
+
qualityProfileId: s.qualityProfileId,
|
|
594
|
+
sizeOnDisk: formatBytes(s.statistics?.sizeOnDisk ?? 0),
|
|
595
|
+
episodes: `${s.statistics?.episodeFileCount ?? 0}/${s.statistics?.totalEpisodeCount ?? 0}`,
|
|
596
|
+
})),
|
|
597
|
+
});
|
|
598
|
+
},
|
|
599
|
+
sonarr_get_episode_files: async (args, clients) => {
|
|
600
|
+
if (!clients.sonarr)
|
|
601
|
+
throw new Error('Sonarr is not configured');
|
|
602
|
+
const seriesId = args.seriesId;
|
|
603
|
+
const files = await clients.sonarr.getEpisodeFiles(seriesId);
|
|
604
|
+
return ok({
|
|
605
|
+
count: files.length,
|
|
606
|
+
totalSize: formatBytes(files.reduce((sum, f) => sum + f.size, 0)),
|
|
607
|
+
files: files.map(f => ({
|
|
608
|
+
id: f.id,
|
|
609
|
+
seasonNumber: f.seasonNumber,
|
|
610
|
+
relativePath: f.relativePath,
|
|
611
|
+
size: formatBytes(f.size),
|
|
612
|
+
sizeBytes: f.size,
|
|
613
|
+
dateAdded: f.dateAdded,
|
|
614
|
+
quality: f.quality?.quality?.name,
|
|
615
|
+
qualityCutoffNotMet: f.qualityCutoffNotMet,
|
|
616
|
+
videoCodec: f.mediaInfo?.videoCodec,
|
|
617
|
+
audioCodec: f.mediaInfo?.audioCodec,
|
|
618
|
+
audioChannels: f.mediaInfo?.audioChannels,
|
|
619
|
+
languages: f.languages?.map(l => l.name),
|
|
620
|
+
dynamicRange: f.mediaInfo?.videoDynamicRangeType || undefined,
|
|
621
|
+
})),
|
|
622
|
+
});
|
|
623
|
+
},
|
|
624
|
+
sonarr_delete_episode_file: async (args, clients) => {
|
|
625
|
+
if (!clients.sonarr)
|
|
626
|
+
throw new Error('Sonarr is not configured');
|
|
627
|
+
const fileId = args.fileId;
|
|
628
|
+
await clients.sonarr.deleteEpisodeFile(fileId);
|
|
629
|
+
return ok({ success: true, message: `Deleted episode file ${fileId} from disk` });
|
|
630
|
+
},
|
|
631
|
+
sonarr_get_blocklist: async (args, clients) => {
|
|
632
|
+
if (!clients.sonarr)
|
|
633
|
+
throw new Error('Sonarr is not configured');
|
|
634
|
+
const { page = 1, pageSize = 20 } = args;
|
|
635
|
+
const blocklist = await clients.sonarr.getBlocklist(page, clampLimit(pageSize));
|
|
636
|
+
const records = Array.isArray(blocklist)
|
|
637
|
+
? blocklist
|
|
638
|
+
: blocklist.records;
|
|
639
|
+
const total = Array.isArray(blocklist)
|
|
640
|
+
? records.length
|
|
641
|
+
: blocklist.totalRecords;
|
|
642
|
+
return ok({
|
|
643
|
+
totalRecords: total,
|
|
644
|
+
returned: records.length,
|
|
645
|
+
entries: records.map(b => ({
|
|
646
|
+
id: b.id,
|
|
647
|
+
seriesId: b.seriesId,
|
|
648
|
+
episodeIds: b.episodeIds,
|
|
649
|
+
sourceTitle: b.sourceTitle,
|
|
650
|
+
quality: b.quality?.quality?.name,
|
|
651
|
+
date: b.date,
|
|
652
|
+
protocol: b.protocol,
|
|
653
|
+
indexer: b.indexer,
|
|
654
|
+
message: b.message,
|
|
655
|
+
})),
|
|
656
|
+
});
|
|
657
|
+
},
|
|
658
|
+
sonarr_delete_from_blocklist: async (args, clients) => {
|
|
659
|
+
if (!clients.sonarr)
|
|
660
|
+
throw new Error('Sonarr is not configured');
|
|
661
|
+
const blocklistId = args.blocklistId;
|
|
662
|
+
await clients.sonarr.deleteFromBlocklist(blocklistId);
|
|
663
|
+
return ok({ success: true, message: `Removed blocklist entry ${blocklistId}` });
|
|
664
|
+
},
|
|
665
|
+
sonarr_monitor_episodes: async (args, clients) => {
|
|
666
|
+
if (!clients.sonarr)
|
|
667
|
+
throw new Error('Sonarr is not configured');
|
|
668
|
+
const { episodeIds, monitored } = args;
|
|
669
|
+
await clients.sonarr.monitorEpisodes(episodeIds, monitored);
|
|
670
|
+
return ok({
|
|
671
|
+
success: true,
|
|
672
|
+
message: `${monitored ? 'Monitoring' : 'Unmonitoring'} ${episodeIds.length} episode(s)`,
|
|
673
|
+
episodeIds,
|
|
674
|
+
monitored,
|
|
675
|
+
});
|
|
676
|
+
},
|
|
677
|
+
sonarr_season_pass: async (args, clients) => {
|
|
678
|
+
if (!clients.sonarr)
|
|
679
|
+
throw new Error('Sonarr is not configured');
|
|
680
|
+
const { seriesId, seasons } = args;
|
|
681
|
+
const series = await clients.sonarr.getSeriesById(seriesId);
|
|
682
|
+
await clients.sonarr.setSeasonPass([{
|
|
683
|
+
id: seriesId,
|
|
684
|
+
monitored: series.monitored,
|
|
685
|
+
seasons,
|
|
686
|
+
}]);
|
|
687
|
+
return ok({
|
|
688
|
+
success: true,
|
|
689
|
+
message: `Updated season monitoring for "${series.title}"`,
|
|
690
|
+
seriesId,
|
|
691
|
+
seasons,
|
|
692
|
+
});
|
|
693
|
+
},
|
|
694
|
+
},
|
|
695
|
+
};
|
|
696
|
+
//# sourceMappingURL=sonarr.js.map
|