11import { RadarrClient } from '../../clients/radarr.js' ;
2- import { promptConfirm , promptSelect } from '../prompt.js' ;
2+ import { promptConfirm , promptIfMissing , promptSelect } from '../prompt.js' ;
33import type { ResourceDef } from './service.js' ;
44import { buildServiceCommand } from './service.js' ;
55
@@ -31,52 +31,95 @@ const resources: ResourceDef[] = [
3131 {
3232 name : 'add' ,
3333 description : 'Search and add a movie' ,
34- args : [ { name : 'term' , description : 'Search term' , required : true } ] ,
34+ args : [
35+ { name : 'term' , description : 'Search term' } ,
36+ { name : 'tmdb-id' , description : 'TMDB ID' , type : 'number' } ,
37+ { name : 'quality-profile-id' , description : 'Quality profile ID' , type : 'number' } ,
38+ { name : 'root-folder' , description : 'Root folder path' } ,
39+ { name : 'monitored' , description : 'Set monitored (true/false)' } ,
40+ ] ,
3541 run : async ( c : RadarrClient , a ) => {
36- const searchResult = await c . searchMovies ( a . term ) ;
37- const results = searchResult ?. data ?? searchResult ;
38- if ( ! Array . isArray ( results ) || results . length === 0 ) {
39- throw new Error ( 'No movies found.' ) ;
40- }
41- const movieId = await promptSelect (
42- 'Select a movie:' ,
43- results . map ( ( m : any ) => ( { label : `${ m . title } (${ m . year } )` , value : String ( m . tmdbId ) } ) )
44- ) ;
45- const movie = results . find ( ( m : any ) => String ( m . tmdbId ) === movieId ) ;
46- if ( ! movie ) {
47- throw new Error ( 'Selected movie was not found in the search results.' ) ;
42+ let movie : any ;
43+
44+ if ( a [ 'tmdb-id' ] !== undefined ) {
45+ const lookupResult = await c . lookupMovieByTmdbId ( a [ 'tmdb-id' ] ) ;
46+ const lookup = unwrapData < any [ ] | any > ( lookupResult ) ;
47+ const matches = Array . isArray ( lookup ) ? lookup : [ lookup ] ;
48+ movie = matches . find ( ( m : any ) => m ?. tmdbId === a [ 'tmdb-id' ] ) ?? matches [ 0 ] ;
49+
50+ if ( ! movie ) {
51+ throw new Error ( `No movie found for TMDB ID ${ a [ 'tmdb-id' ] } .` ) ;
52+ }
53+ } else {
54+ const term = await promptIfMissing ( a . term , 'Search term:' ) ;
55+ const searchResult = await c . searchMovies ( term ) ;
56+ const results = unwrapData < any [ ] > ( searchResult ) ;
57+ if ( ! Array . isArray ( results ) || results . length === 0 ) {
58+ throw new Error ( 'No movies found.' ) ;
59+ }
60+
61+ const movieId = await promptSelect (
62+ 'Select a movie:' ,
63+ results . map ( ( m : any ) => ( {
64+ label : `${ m . title } (${ m . year } )` ,
65+ value : String ( m . tmdbId ) ,
66+ } ) )
67+ ) ;
68+ movie = results . find ( ( m : any ) => String ( m . tmdbId ) === movieId ) ;
69+ if ( ! movie ) {
70+ throw new Error ( 'Selected movie was not found in the search results.' ) ;
71+ }
4872 }
4973
5074 const profilesResult = await c . getQualityProfiles ( ) ;
51- const profiles = profilesResult ?. data ?? profilesResult ;
75+ const profiles = unwrapData < any [ ] > ( profilesResult ) ;
5276 if ( ! Array . isArray ( profiles ) || profiles . length === 0 ) {
5377 throw new Error ( 'No quality profiles found. Configure one in Radarr first.' ) ;
5478 }
55- const profileId = await promptSelect (
56- 'Select quality profile:' ,
57- profiles . map ( ( p : any ) => ( { label : p . name , value : String ( p . id ) } ) )
58- ) ;
79+ const profileId =
80+ a [ 'quality-profile-id' ] !== undefined
81+ ? resolveQualityProfileId ( profiles , a [ 'quality-profile-id' ] )
82+ : Number (
83+ await promptSelect (
84+ 'Select quality profile:' ,
85+ profiles . map ( ( p : any ) => ( { label : p . name , value : String ( p . id ) } ) )
86+ )
87+ ) ;
5988
6089 const foldersResult = await c . getRootFolders ( ) ;
61- const folders = foldersResult ?. data ?? foldersResult ;
90+ const folders = unwrapData < any [ ] > ( foldersResult ) ;
6291 if ( ! Array . isArray ( folders ) || folders . length === 0 ) {
6392 throw new Error ( 'No root folders found. Configure one in Radarr first.' ) ;
6493 }
65- const rootFolderPath = await promptSelect (
66- 'Select root folder:' ,
67- folders . map ( ( f : any ) => ( { label : f . path , value : f . path } ) )
68- ) ;
94+ const rootFolderPath =
95+ a [ 'root-folder' ] !== undefined
96+ ? resolveRootFolderPath ( folders , a [ 'root-folder' ] )
97+ : await promptSelect (
98+ 'Select root folder:' ,
99+ folders . map ( ( f : any ) => ( { label : f . path , value : f . path } ) )
100+ ) ;
69101
70102 const confirmed = await promptConfirm ( `Add "${ movie . title } (${ movie . year } )"?` , ! ! a . yes ) ;
71103 if ( ! confirmed ) throw new Error ( 'Cancelled.' ) ;
72104
73- return c . addMovie ( {
105+ const addResult = await c . addMovie ( {
74106 ...movie ,
75- qualityProfileId : Number ( profileId ) ,
107+ qualityProfileId : profileId ,
76108 rootFolderPath,
77- monitored : true ,
109+ monitored : parseBooleanArg ( a . monitored , true ) ,
78110 addOptions : { searchForMovie : true } ,
79111 } ) ;
112+
113+ if ( addResult ?. error && getApiStatus ( addResult ) === 400 ) {
114+ const existingMovie = await findMovieByTmdbId ( c , movie . tmdbId ) ;
115+ if ( existingMovie ) {
116+ throw new Error (
117+ `${ existingMovie . title } is already in your library (ID: ${ existingMovie . id } )`
118+ ) ;
119+ }
120+ }
121+
122+ return addResult ;
80123 } ,
81124 } ,
82125 {
@@ -117,9 +160,30 @@ const resources: ResourceDef[] = [
117160 {
118161 name : 'delete' ,
119162 description : 'Delete a movie' ,
120- args : [ { name : 'id' , description : 'Movie ID' , required : true , type : 'number' } ] ,
163+ args : [
164+ { name : 'id' , description : 'Movie ID' , required : true , type : 'number' } ,
165+ { name : 'delete-files' , description : 'Delete movie files' , type : 'boolean' } ,
166+ {
167+ name : 'add-import-exclusion' ,
168+ description : 'Add import exclusion after delete' ,
169+ type : 'boolean' ,
170+ } ,
171+ ] ,
121172 confirmMessage : 'Are you sure you want to delete this movie?' ,
122- run : ( c : RadarrClient , a ) => c . deleteMovie ( a . id ) ,
173+ run : async ( c : RadarrClient , a ) => {
174+ const movieResult = await c . getMovie ( a . id ) ;
175+ if ( movieResult ?. error ) return movieResult ;
176+
177+ const movie = unwrapData < any > ( movieResult ) ;
178+ const deleteResult = await c . deleteMovie ( a . id , {
179+ deleteFiles : a [ 'delete-files' ] ,
180+ addImportExclusion : a [ 'add-import-exclusion' ] ,
181+ } ) ;
182+
183+ if ( deleteResult ?. error ) return deleteResult ;
184+
185+ return { message : `Deleted: ${ movie . title } (ID: ${ movie . id } )` } ;
186+ } ,
123187 } ,
124188 ] ,
125189 } ,
@@ -145,6 +209,28 @@ const resources: ResourceDef[] = [
145209 name : 'tag' ,
146210 description : 'Manage tags' ,
147211 actions : [
212+ {
213+ name : 'create' ,
214+ description : 'Create a tag' ,
215+ args : [ { name : 'label' , description : 'Tag label' , required : true } ] ,
216+ run : ( c : RadarrClient , a ) => c . addTag ( { label : a . label } as any ) ,
217+ } ,
218+ {
219+ name : 'delete' ,
220+ description : 'Delete a tag' ,
221+ args : [ { name : 'id' , description : 'Tag ID' , required : true , type : 'number' } ] ,
222+ confirmMessage : 'Are you sure you want to delete this tag?' ,
223+ run : async ( c : RadarrClient , a ) => {
224+ const tagResult = await c . getTag ( a . id ) ;
225+ if ( tagResult ?. error ) return tagResult ;
226+
227+ const tag = unwrapData < any > ( tagResult ) ;
228+ const deleteResult = await c . deleteTag ( a . id ) ;
229+ if ( deleteResult ?. error ) return deleteResult ;
230+
231+ return { message : `Deleted tag: ${ tag . label } (ID: ${ tag . id } )` } ;
232+ } ,
233+ } ,
148234 {
149235 name : 'list' ,
150236 description : 'List all tags' ,
@@ -231,3 +317,45 @@ export const radarr = buildServiceCommand(
231317 config => new RadarrClient ( config ) ,
232318 resources
233319) ;
320+
321+ function unwrapData < T > ( result : any ) : T {
322+ return ( result ?. data ?? result ) as T ;
323+ }
324+
325+ function parseBooleanArg ( value : unknown , fallback : boolean ) : boolean {
326+ if ( value === undefined ) return fallback ;
327+ if ( typeof value === 'boolean' ) return value ;
328+ if ( typeof value === 'string' ) {
329+ const normalized = value . trim ( ) . toLowerCase ( ) ;
330+ if ( normalized === 'true' ) return true ;
331+ if ( normalized === 'false' ) return false ;
332+ }
333+ return Boolean ( value ) ;
334+ }
335+
336+ function resolveQualityProfileId ( profiles : any [ ] , profileId : number ) : number {
337+ const profile = profiles . find ( ( item : any ) => item ?. id === profileId ) ;
338+ if ( ! profile ) {
339+ throw new Error ( `Quality profile ${ profileId } was not found.` ) ;
340+ }
341+ return profileId ;
342+ }
343+
344+ function resolveRootFolderPath ( folders : any [ ] , rootFolderPath : string ) : string {
345+ const folder = folders . find ( ( item : any ) => item ?. path === rootFolderPath ) ;
346+ if ( ! folder ) {
347+ throw new Error ( `Root folder "${ rootFolderPath } " was not found.` ) ;
348+ }
349+ return rootFolderPath ;
350+ }
351+
352+ async function findMovieByTmdbId ( client : RadarrClient , tmdbId : number | undefined ) {
353+ if ( tmdbId === undefined ) return undefined ;
354+
355+ const movies = unwrapData < any [ ] > ( await client . getMovies ( ) ) ;
356+ return movies . find ( ( movie : any ) => movie ?. tmdbId === tmdbId ) ;
357+ }
358+
359+ function getApiStatus ( result : any ) : number | undefined {
360+ return result ?. error ?. status ?? result ?. response ?. status ;
361+ }
0 commit comments