2013-02-13 00:58:13 -05:00
// filebot -script "fn:amc" --output "X:/media" --action copy --conflict override --def subtitles=en music=y artwork=y "ut_dir=%D" "ut_file=%F" "ut_kind=%K" "ut_title=%N" "ut_label=%L" "ut_state=%S"
2012-04-28 14:15:15 -04:00
def input = [ ]
2012-07-08 02:29:07 -04:00
def failOnError = _args . conflict = = 'fail'
2012-04-26 07:25:58 -04:00
2012-04-28 14:15:15 -04:00
// print input parameters
2013-02-15 04:50:23 -05:00
_args . bindings ? . each { _log . fine ( "Parameter: $it.key = $it.value" ) }
args . each { _log . fine ( "Argument: $it" ) }
2013-02-12 12:07:59 -05:00
args . findAll { ! it . exists ( ) } . each { throw new Exception ( "File not found: $it" ) }
2012-04-28 14:15:15 -04:00
2012-12-10 13:07:03 -05:00
// check user-defined pre-condition
2013-02-20 05:16:21 -05:00
if ( tryQuietly { ut_state = = ~ ut_state_allow } ) {
2012-12-10 13:07:03 -05:00
throw new Exception ( "Invalid state: ut_state = $ut_state (expected $ut_state_allow)" )
}
2012-08-01 02:04:47 -04:00
// enable/disable features as specified via --def parameters
2013-02-01 22:21:56 -05:00
def music = tryQuietly { music . toBoolean ( ) }
2013-01-19 12:04:15 -05:00
def subtitles = tryQuietly { subtitles . toBoolean ( ) ? [ 'en' ] : subtitles . split ( /[ ,|]+/ ) . findAll { it . length ( ) > = 2 } }
2012-07-30 06:01:03 -04:00
def artwork = tryQuietly { artwork . toBoolean ( ) }
2012-12-16 08:26:39 -05:00
def backdrops = tryQuietly { backdrops . toBoolean ( ) }
2013-01-04 23:25:21 -05:00
def clean = tryQuietly { clean . toBoolean ( ) }
2013-03-01 02:40:50 -05:00
def exec = tryQuietly { exec . toString ( ) }
2012-07-30 06:01:03 -04:00
// array of xbmc/plex hosts
2012-08-01 02:04:47 -04:00
def xbmc = tryQuietly { xbmc . split ( /[ ,|]+/ ) }
def plex = tryQuietly { plex . split ( /[ ,|]+/ ) }
2012-07-30 06:01:03 -04:00
2012-12-09 08:37:27 -05:00
// myepisodes updates and email notifications
def myepisodes = tryQuietly { myepisodes . split ( ':' , 2 ) }
2012-07-30 12:59:09 -04:00
def gmail = tryQuietly { gmail . split ( ':' , 2 ) }
2013-03-01 02:40:50 -05:00
def pushover = tryQuietly { pushover . toString ( ) }
2012-07-30 12:59:09 -04:00
2013-05-04 08:09:51 -04:00
// user-defined filters
def minFileSize = tryQuietly { minFileSize . toLong ( ) } ; if ( minFileSize = = null ) { minFileSize = 104857600 } ; // files smaller than 100 MB will be considered clutter by default
2012-10-14 01:19:35 -04:00
// series/anime/movie format expressions
def format = [
2013-04-02 11:34:25 -04:00
tvs: tryQuietly { seriesFormat } ? : '''TV Shows/{n}/{episode.special ? "Special" : "Season "+s.pad(2)}/{n} - {episode.special ? "S00E"+special.pad(2) : s00e00} - {t.replaceAll(/[`´ ‘ ’ ʻ ]/, "'").replaceAll(/[!?.]+$/).replacePart(', Part $1')}{".$lang"}''' ,
2013-02-22 06:37:08 -05:00
anime: tryQuietly { animeFormat } ? : '''Anime/{n}/{n} - {sxe} - {t.replaceAll(/[!?.]+$/).replaceAll(/[`´ ‘ ’ ʻ ]/, "'").replacePart(', Part $1')}''' ,
2013-01-10 14:30:16 -05:00
mov: tryQuietly { movieFormat } ? : '''Movies/{n} ({y})/{n} ({y}){" CD$pi"}{".$lang"}''' ,
2013-02-15 04:50:23 -05:00
music: tryQuietly { musicFormat } ? : '''Music/{n}/{album+'/'}{pi.pad(2)+'. '}{artist} - {t}'''
2012-10-14 01:19:35 -04:00
]
2012-07-30 12:59:09 -04:00
// force movie/series/anime logic
def forceMovie ( f ) {
2013-04-22 00:06:21 -04:00
tryQuietly { ut_label } = ~ /^(?i:Movie|Couch.Potato)/ | | f . path = ~ /(?<=tt)\\d{7}/ | | tryQuietly { f . metadata ? . object ? . class . name = ~ /Movie/ }
2012-07-30 12:59:09 -04:00
}
def forceSeries ( f ) {
2013-04-19 05:28:55 -04:00
tryQuietly { ut_label } = ~ /^(?i:TV|Kids.Shows)/ | | parseEpisodeNumber ( f ) | | parseDate ( f ) | | f . path = ~ /(?i:Season)\D?[0-9]{1,2}/ | | tryQuietly { f . metadata ? . object ? . class . name = ~ /Episode/ }
2012-07-30 12:59:09 -04:00
}
def forceAnime ( f ) {
2013-04-02 11:34:25 -04:00
tryQuietly { ut_label } = ~ /^(?i:Anime)/ | | ( f . isVideo ( ) & & ( f . name = ~ "[\\(\\[]\\p{XDigit}{8}[\\]\\)]" | | getMediaInfo ( file: f , format: '''{media.AudioLanguageList} {media.TextCodecList}''' ) . tokenize ( ) . containsAll ( [ 'Japanese' , 'ASS' ] ) ) )
2012-07-30 12:59:09 -04:00
}
def forceIgnore ( f ) {
2013-02-01 22:21:56 -05:00
tryQuietly { ut_label } = ~ /^(?i:ebook|other|ignore)/ | | f . path = ~ tryQuietly { ignore }
2012-07-30 12:59:09 -04:00
}
2012-07-30 06:01:03 -04:00
2012-10-30 05:48:34 -04:00
// specify how to resolve input folders, e.g. grab files from all folders except disk folders
def resolveInput ( f ) {
if ( f . isDirectory ( ) & & ! f . isDisk ( ) )
return f . listFiles ( ) . toList ( ) . findResults { resolveInput ( it ) }
else
return f
}
2012-07-30 06:01:03 -04:00
// collect input fileset as specified by the given --def parameters
2012-10-18 12:34:38 -04:00
if ( args . empty ) {
2012-11-09 06:19:35 -05:00
// assume we're called with utorrent parameters (account for older and newer versions of uTorrents)
if ( ut_kind = = 'single' | | ( ut_kind ! = 'multi' & & ut_dir & & ut_file ) ) {
2012-07-13 10:33:39 -04:00
input + = new File ( ut_dir , ut_file ) // single-file torrent
2012-07-16 08:19:13 -04:00
} else {
2012-10-30 05:48:34 -04:00
input + = resolveInput ( ut_dir as File ) // multi-file torrent
2012-07-13 10:33:39 -04:00
}
2012-04-28 14:15:15 -04:00
} else {
2012-07-13 10:33:39 -04:00
// assume we're called normally with arguments
2012-10-30 05:48:34 -04:00
input + = args . findResults { resolveInput ( it ) }
2012-04-28 14:15:15 -04:00
}
2012-04-26 07:25:58 -04:00
2012-07-31 12:17:15 -04:00
2012-10-30 05:48:34 -04:00
// flatten nested file structure
input = input . flatten ( )
2012-07-31 12:17:15 -04:00
// extract archives (zip, rar, etc) that contain at least one video file
2013-04-09 05:12:20 -04:00
def tempFiles = [ ]
input = input . flatten { f - >
if ( f . isArchive ( ) | | f . hasExtension ( '001' ) ) {
2013-04-13 02:47:28 -04:00
def extractDir = new File ( f . dir , f . nameWithoutExtension )
def extractFiles = extract ( file: f , output: new File ( extractDir , f . dir . name ) , conflict: 'override' , filter: { it . isArchive ( ) | | it . isVideo ( ) | | it . isSubtitle ( ) | | ( music & & it . isAudio ( ) ) } , forceExtractAll: true ) ? : [ ]
2013-04-09 05:12:20 -04:00
tempFiles + = extractDir
tempFiles + = extractFiles
return extractFiles
}
return f
}
2012-04-26 07:25:58 -04:00
2012-08-13 04:03:11 -04:00
// sanitize input
input = input . findAll { it ? . exists ( ) } . collect { it . canonicalFile } . unique ( )
2012-04-26 07:25:58 -04:00
// process only media files
2013-03-25 05:01:56 -04:00
input = input . findAll { it . isVideo ( ) | | it . isSubtitle ( ) | | it . isDisk ( ) | | ( music & & it . isAudio ( ) ) }
2012-04-26 07:25:58 -04:00
2012-05-31 08:10:50 -04:00
// ignore clutter files
2013-05-04 08:09:51 -04:00
input = input . findAll { ! ( it . path = ~ /\b(?i:sample|trailer|extras|deleted.scenes|music.video|scrapbook|behind.the.scenes)\b/ | | ( it . isFile ( ) & & it . length ( ) < minFileSize ) ) }
2012-05-31 08:10:50 -04:00
2012-04-28 14:15:15 -04:00
// print input fileset
2012-07-30 12:59:09 -04:00
input . each { f - > _log . finest ( "Input: $f" ) }
2012-04-26 07:25:58 -04:00
2012-07-28 06:21:30 -04:00
// artwork/nfo utility
2012-11-09 06:19:35 -05:00
include ( 'fn:lib/htpc' )
2012-04-29 01:28:38 -04:00
2012-04-26 07:25:58 -04:00
// group episodes/movies and rename according to XBMC standards
2012-07-08 08:01:19 -04:00
def groups = input . groupBy { f - >
2012-07-30 12:59:09 -04:00
// skip auto-detection if possible
if ( forceIgnore ( f ) )
return [ ]
2013-01-14 12:26:59 -05:00
if ( f . isAudio ( ) & & ! f . isVideo ( ) ) // PROCESS MUSIC FOLDER BY FOLDER
2013-01-10 14:30:16 -05:00
return [ music: f . dir . name ]
2012-07-30 12:59:09 -04:00
if ( forceMovie ( f ) )
2012-08-09 01:06:28 -04:00
return [ mov: detectMovie ( f , false ) ]
2012-07-30 12:59:09 -04:00
if ( forceSeries ( f ) )
2012-08-09 01:06:28 -04:00
return [ tvs: detectSeriesName ( f ) ? : detectSeriesName ( f . dir . listFiles { it . isVideo ( ) } ) ]
2012-07-30 12:59:09 -04:00
if ( forceAnime ( f ) )
2012-08-09 01:06:28 -04:00
return [ anime: detectSeriesName ( f ) ? : detectSeriesName ( f . dir . listFiles { it . isVideo ( ) } ) ]
2012-07-30 12:59:09 -04:00
2012-07-08 08:01:19 -04:00
def tvs = detectSeriesName ( f )
2012-07-30 12:59:09 -04:00
def mov = detectMovie ( f , false )
2013-02-15 04:50:23 -05:00
_log . fine ( "$f.name [series: $tvs, movie: $mov]" )
2012-04-26 07:25:58 -04:00
// DECIDE EPISODE VS MOVIE (IF NOT CLEAR)
if ( tvs & & mov ) {
2013-03-16 12:13:50 -04:00
def norm = { s - > s . ascii ( ) . normalizePunctuation ( ) . lower ( ) . space ( ' ' ) }
2012-07-26 22:14:49 -04:00
def dn = norm ( guessMovieFolder ( f ) ? . name ? : '' )
2012-07-21 10:32:55 -04:00
def fn = norm ( f . nameWithoutExtension )
def sn = norm ( tvs )
def mn = norm ( mov . name )
// S00E00 | 2012.07.21 | One Piece 217 | Firefly - Serenity | [Taken 1, Taken 2, Taken 3, Taken 4, ..., Taken 10]
2013-04-22 00:06:21 -04:00
if ( ( parseEpisodeNumber ( fn , true ) | | parseDate ( fn ) | | ( [ dn , fn ] . find { it = ~ sn & & matchMovie ( it , true ) = = null } & & ( parseEpisodeNumber ( fn . after ( sn ) , false ) | | fn . after ( sn ) = ~ /\d{1,2}\D+\d{1,2}/ ) & & matchMovie ( fn , true ) = = null ) | | ( fn . after ( sn ) = = ~ /.{0,3} - .+/ & & matchMovie ( fn , true ) = = null ) | | f . dir . listFiles { it . isVideo ( ) & & norm ( it . name ) = ~ sn & & it . name = ~ /\b\d{1,3}\b/ } . size ( ) > = 10 ) & & ! tryQuietly { def m = detectMovie ( f , true ) ; m . year > = 1950 & & f . listPath ( ) . reverse ( ) . take ( 3 ) . find { it . name = ~ m . year } } ) {
2013-02-15 04:50:23 -05:00
_log . fine ( "Exclude Movie: $mov" )
2012-04-26 07:25:58 -04:00
mov = null
2013-03-28 05:04:35 -04:00
} else if ( mn = = ~ fn | | ( detectMovie ( f , true ) & & [ dn , fn ] . find { it = ~ /(19|20)\d{2}/ } ) | | [ dn , fn ] . find { it = ~ mn & & ! ( it . after ( mn ) = ~ /\b\d{1,3}\b/ ) & & ! ( it . before ( mn ) . contains ( sn ) ) } ) {
2013-02-15 04:50:23 -05:00
_log . fine ( "Exclude Series: $tvs" )
2012-07-08 09:13:18 -04:00
tvs = null
2012-04-26 07:25:58 -04:00
}
}
2012-07-08 02:29:07 -04:00
// CHECK CONFLICT
if ( ( ( mov & & tvs ) | | ( ! mov & & ! tvs ) ) & & failOnError ) {
throw new Exception ( "Media detection failed" )
}
2012-07-30 12:59:09 -04:00
return [ tvs: tvs , mov: mov , anime: null ]
2012-04-26 07:25:58 -04:00
}
2012-07-30 12:59:09 -04:00
// log movie/series/anime detection results
2012-07-30 20:00:46 -04:00
groups . each { group , files - > _log . finest ( "Group: $group => ${files*.name}" ) }
2012-07-30 12:59:09 -04:00
// process each batch
2012-04-26 07:25:58 -04:00
groups . each { group , files - >
2013-02-01 22:21:56 -05:00
// fetch subtitles (but not for anime)
if ( subtitles & & ! group . anime ) {
2013-01-19 12:04:15 -05:00
subtitles . each { languageCode - >
2013-04-08 15:04:01 -04:00
def subtitleFiles = getMissingSubtitles ( file: files , output: 'srt' , encoding: 'UTF-8' , lang: languageCode , strict: true ) ? : [ ]
2013-02-26 11:36:53 -05:00
files + = subtitleFiles
tempFiles + = subtitleFiles // if downloaded for temporarily extraced files delete later
2013-01-19 12:04:15 -05:00
}
2012-07-30 06:01:03 -04:00
}
2012-04-29 01:28:38 -04:00
2012-04-26 07:25:58 -04:00
// EPISODE MODE
2012-07-30 12:59:09 -04:00
if ( ( group . tvs | | group . anime ) & & ! group . mov ) {
// choose series / anime config
2012-11-23 19:45:47 -05:00
def config = group . tvs ? [ name: group . tvs , format: format . tvs , db: 'TheTVDB' , seasonFolder: true ]
2012-11-24 16:56:09 -05:00
: [ name: group . anime , format: format . anime , db: 'AniDB' , seasonFolder: false ]
2012-07-30 12:59:09 -04:00
def dest = rename ( file: files , format: config . format , db: config . db )
2012-07-30 06:01:03 -04:00
if ( dest & & artwork ) {
2012-07-08 02:29:07 -04:00
dest . mapByFolder ( ) . each { dir , fs - >
2013-03-06 03:34:43 -05:00
_log . finest "Fetching artwork for $dir from TheTVDB"
2012-07-27 02:29:28 -04:00
def sxe = fs . findResult { eps - > parseEpisodeNumber ( eps ) }
2012-07-30 12:59:09 -04:00
def options = TheTVDB . search ( config . name )
2012-05-31 08:10:50 -04:00
if ( options . isEmpty ( ) ) {
2013-03-06 03:34:43 -05:00
_log . warning "TV Series not found: $config.name"
2012-05-31 08:10:50 -04:00
return
}
2012-07-30 12:59:09 -04:00
options = options . sortBySimilarity ( config . name , { s - > s . name } )
2012-11-23 19:45:47 -05:00
fetchSeriesArtworkAndNfo ( config . seasonFolder ? dir . dir : dir , dir , options [ 0 ] , sxe & & sxe . season > 0 ? sxe . season : 1 )
2012-04-28 14:15:15 -04:00
}
}
2012-07-30 06:01:03 -04:00
if ( dest = = null & & failOnError ) {
2012-07-30 12:59:09 -04:00
throw new Exception ( "Failed to rename series: $config.name" )
2012-07-30 06:01:03 -04:00
}
2012-04-26 07:25:58 -04:00
}
// MOVIE MODE
2012-07-30 12:59:09 -04:00
if ( group . mov & & ! group . tvs & & ! group . anime ) {
2012-10-14 01:19:35 -04:00
def dest = rename ( file: files , format: format . mov , db: 'TheMovieDB' )
2012-07-30 06:01:03 -04:00
if ( dest & & artwork ) {
2012-07-08 02:29:07 -04:00
dest . mapByFolder ( ) . each { dir , fs - >
2013-03-06 03:34:43 -05:00
_log . finest "Fetching artwork for $dir from TheMovieDB"
2012-12-16 08:26:39 -05:00
fetchMovieArtworkAndNfo ( dir , group . mov , fs . findAll { it . isVideo ( ) } . sort { it . length ( ) } . reverse ( ) . findResult { it } , backdrops )
2012-05-31 08:10:50 -04:00
}
2012-04-28 14:15:15 -04:00
}
2012-07-30 06:01:03 -04:00
if ( dest = = null & & failOnError ) {
throw new Exception ( "Failed to rename movie: $group.mov" )
}
2012-04-26 07:25:58 -04:00
}
2013-01-10 14:30:16 -05:00
// MUSIC MODE
if ( group . music ) {
def dest = rename ( file: files , format: format . music , db: 'AcoustID' )
if ( dest = = null & & failOnError ) {
throw new Exception ( "Failed to rename music: $group.music" )
}
}
2012-04-26 07:25:58 -04:00
}
2013-01-05 23:03:59 -05:00
// skip notifications if nothing was renamed anyway
if ( getRenameLog ( ) . isEmpty ( ) ) {
return
}
2012-04-26 07:25:58 -04:00
2013-03-01 02:40:50 -05:00
// run program on newly processed files
if ( exec ) {
getRenameLog ( ) . each { from , to - >
def command = getMediaInfo ( format: exec , file: to )
_log . finest ( "Execute: $command" )
execute ( command )
2013-02-26 14:40:24 -05:00
}
2012-07-30 06:01:03 -04:00
}
2013-03-01 02:40:50 -05:00
// make XMBC scan for new content and display notification message
if ( xbmc ) {
xbmc . each { host - >
2013-03-06 03:34:43 -05:00
_log . info "Notify XBMC: $host"
2013-03-01 02:40:50 -05:00
_guarded {
2013-03-09 12:15:46 -05:00
showNotification ( host , 9090 , 'FileBot' , "Finished processing ${tryQuietly { ut_title } ?: input*.dir.name.unique()} (${getRenameLog().size()} files)." , 'http://www.filebot.net/images/icon.png' )
2013-03-01 02:40:50 -05:00
scanVideoLibrary ( host , 9090 )
}
}
}
// make Plex scan for new content
if ( plex ) {
plex . each {
2013-03-06 03:34:43 -05:00
_log . info "Notify Plex: $it"
2013-03-01 02:40:50 -05:00
refreshPlexLibrary ( it )
}
2012-04-28 14:15:15 -04:00
}
2012-07-30 12:59:09 -04:00
2012-12-07 11:47:54 -05:00
// mark episodes as 'acquired'
if ( myepisodes ) {
2013-03-06 03:34:43 -05:00
_log . info 'Update MyEpisodes'
2013-02-11 04:02:17 -05:00
include ( 'fn:update-mes' , [ login: myepisodes . join ( ':' ) , addshows: true ] , getRenameLog ( ) . values ( ) )
2012-12-07 11:47:54 -05:00
}
2013-01-22 03:14:21 -05:00
if ( pushover ) {
// include webservice utility
include ( 'fn:lib/ws' )
2013-03-06 03:34:43 -05:00
_log . info 'Sending Pushover notification'
2013-01-22 03:14:21 -05:00
Pushover ( pushover ) . send ( "Finished processing ${tryQuietly { ut_title } ?: input*.dir.name.unique()} (${getRenameLog().size()} files)." )
}
2012-07-30 12:59:09 -04:00
// send status email
2013-01-05 23:03:59 -05:00
if ( gmail ) {
2012-07-30 12:59:09 -04:00
// ant/mail utility
2012-11-09 06:19:35 -05:00
include ( 'fn:lib/ant' )
2012-07-30 12:59:09 -04:00
// send html mail
def renameLog = getRenameLog ( )
2012-10-29 10:46:25 -04:00
def emailTitle = tryQuietly { ut_title } ? : input * . dir . name . unique ( )
2012-07-30 12:59:09 -04:00
sendGmail (
2012-10-29 05:56:17 -04:00
subject: "[FileBot] ${emailTitle}" ,
2012-07-30 12:59:09 -04:00
message: XML {
html {
body {
2012-10-29 05:56:17 -04:00
p ( "FileBot finished processing ${emailTitle} (${renameLog.size()} files)." ) ;
2012-07-30 12:59:09 -04:00
hr ( ) ; table {
th ( "Parameter" ) ; th ( "Value" )
_args . bindings . findAll { param - > param . key = ~ /^ut_/ } . each { param - >
tr { [ param . key , param . value ] . each { td ( it ) } }
}
}
hr ( ) ; table {
th ( "Original Name" ) ; th ( "New Name" ) ; th ( "New Location" )
renameLog . each { from , to - >
2012-08-12 00:38:14 -04:00
tr { [ from . name , to . name , to . parent ] . each { cell - > td { nobr { code ( cell ) } } } }
2012-07-30 12:59:09 -04:00
}
}
hr ( ) ; small ( "// Generated by ${net.sourceforge.filebot.Settings.applicationIdentifier} on ${new Date().dateString} at ${new Date().timeString}" )
}
}
} ,
2013-02-05 10:44:18 -05:00
messagemimetype: 'text/html' ,
2012-07-31 03:46:33 -04:00
to: tryQuietly { mailto } ? : gmail [ 0 ] + '@gmail.com' , // mail to self by default
2012-07-30 12:59:09 -04:00
user: gmail [ 0 ] , password: gmail [ 1 ]
)
}
2013-01-04 23:25:21 -05:00
// clean empty folders, clutter files, etc after move
if ( clean ) {
2013-03-06 03:34:43 -05:00
if ( [ 'COPY' , 'HARDLINK' ] . find { it . equalsIgnoreCase ( _args . action ) } & & tempFiles . size ( ) > 0 ) {
_log . info 'Clean temporary extracted files'
2013-01-29 05:32:48 -05:00
// delete extracted files
2013-04-09 05:12:20 -04:00
tempFiles . findAll { it . isFile ( ) } . sort ( ) . each {
_log . finest "Delete $it"
it . delete ( )
2013-01-29 05:32:48 -05:00
}
// delete remaining empty folders
2013-04-09 05:12:20 -04:00
tempFiles . findAll { it . isDirectory ( ) } . sort ( ) . reverse ( ) . each {
_log . finest "Delete $it"
if ( it . getFiles ( ) . isEmpty ( ) ) it . deleteDir ( )
2013-01-29 05:32:48 -05:00
}
}
// deleting remaining files only makes sense after moving files
if ( 'MOVE' . equalsIgnoreCase ( _args . action ) ) {
2013-03-06 03:34:43 -05:00
_log . info 'Clean clutter files and empty folders'
2013-01-29 05:32:48 -05:00
include ( 'fn:cleaner' , [ : ] , ! args . empty ? args : ut_kind = = 'multi' & & ut_dir ? [ ut_dir as File ] : [ ] )
}
2013-01-04 23:25:21 -05:00
}