Advertisement
filebot

andreyy-amc

Apr 12th, 2013
657
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Groovy 12.08 KB | None | 0 0
  1. // 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"
  2. def input = []
  3. def failOnError = _args.conflict == 'fail'
  4.  
  5. def executeRename = { from, to ->
  6.     to.dir.mkdirs() // make sure to create parent folders
  7.     execute('ln', '-s', from, to)
  8. }
  9.  
  10. // print input parameters
  11. _args.bindings?.each{ _log.fine("Parameter: $it.key = $it.value") }
  12. args.each{ _log.fine("Argument: $it") }
  13. args.findAll{ !it.exists() }.each{ throw new Exception("File not found: $it") }
  14.  
  15. // check user-defined pre-condition
  16. if (tryQuietly{ ut_state ==~ ut_state_allow }) {
  17.     throw new Exception("Invalid state: ut_state = $ut_state (expected $ut_state_allow)")
  18. }
  19.  
  20. // enable/disable features as specified via --def parameters
  21. def music     = tryQuietly{ music.toBoolean() }
  22. def subtitles = tryQuietly{ subtitles.toBoolean() ? ['en'] : subtitles.split(/[ ,|]+/).findAll{ it.length() >= 2 } }
  23. def artwork   = tryQuietly{ artwork.toBoolean() }
  24. def backdrops = tryQuietly{ backdrops.toBoolean() }
  25. def clean     = tryQuietly{ clean.toBoolean() }
  26. def exec      = tryQuietly{ exec.toString() }
  27.  
  28. // array of xbmc/plex hosts
  29. def xbmc = tryQuietly{ xbmc.split(/[ ,|]+/) }
  30. def plex = tryQuietly{ plex.split(/[ ,|]+/) }
  31.  
  32. // myepisodes updates and email notifications
  33. def myepisodes = tryQuietly { myepisodes.split(':', 2) }
  34. def gmail = tryQuietly{ gmail.split(':', 2) }
  35. def pushover = tryQuietly{ pushover.toString() }
  36.  
  37.  
  38. // series/anime/movie format expressions
  39. def format = [
  40.     tvs:   '''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"}''',
  41.     anime: '''Anime/{n}/{n} - {sxe} - {t.replaceAll(/[!?.]+$/).replaceAll(/[`´‘’ʻ]/, "'").replacePart(', Part $1')}''',
  42.     mov:   '''Movies/{file.isFile() ? "$n ($y)/" : "./"}{n} ({y}){" CD$pi"}{".$lang"}''',
  43.     music: '''Music/{n}/{album+'/'}{pi.pad(2)+'. '}{artist} - {t}'''
  44. ]
  45.  
  46.  
  47. // force movie/series/anime logic
  48. def forceMovie(f) {
  49.     tryQuietly{ ut_label } =~ /^(?i:Movie|Couch.Potato)/
  50. }
  51.  
  52. def forceSeries(f) {
  53.     parseEpisodeNumber(f) || parseDate(f) || tryQuietly{ ut_label } =~ /^(?i:TV|Kids.Shows)/
  54. }
  55.  
  56. def forceAnime(f) {
  57.     tryQuietly{ ut_label } =~ /^(?i:Anime)/ || (f.isVideo() && (f.name =~ "[\\(\\[]\\p{XDigit}{8}[\\]\\)]" || getMediaInfo(file:f, format:'''{media.AudioLanguageList} {media.TextCodecList}''').tokenize().containsAll(['Japanese', 'ASS'])))
  58. }
  59.  
  60. def forceIgnore(f) {
  61.     tryQuietly{ ut_label } =~ /^(?i:ebook|other|ignore)/ || f.path =~ tryQuietly{ ignore }
  62. }
  63.  
  64.  
  65. // specify how to resolve input folders, e.g. grab files from all folders except disk folders
  66. def resolveInput(f) {
  67.     if (f.isDirectory() && !f.isDisk())
  68.         return f.listFiles().toList().findResults{ resolveInput(it) }
  69.     else
  70.         return f
  71. }
  72.  
  73. // collect input fileset as specified by the given --def parameters
  74. if (args.empty) {
  75.     // assume we're called with utorrent parameters (account for older and newer versions of uTorrents)
  76.     if (ut_kind == 'single' || (ut_kind != 'multi' && ut_dir && ut_file)) {
  77.         input += new File(ut_dir, ut_file) // single-file torrent
  78.     } else {
  79.         input += resolveInput(ut_dir as File) // multi-file torrent
  80.     }
  81. } else {
  82.     // assume we're called normally with arguments
  83.     input += args.findResults{ resolveInput(it) }
  84. }
  85.  
  86.  
  87. // flatten nested file structure
  88. input = input.flatten()
  89.  
  90. // extract archives (zip, rar, etc) that contain at least one video file
  91. def tempFiles = []
  92. input = input.flatten{ f ->
  93.     if (f.isArchive() || f.hasExtension('001')) {
  94.         def extractDir = new File(f.parentFile, f.nameWithoutExtension)
  95.         def extractFiles = extract(file: f, output: extractDir, conflict: 'override', filter: { it.isArchive() || it.isVideo() || it.isSubtitle() || (music && it.isAudio()) }, forceExtractAll: true) ?: []
  96.         tempFiles += extractDir
  97.         tempFiles += extractFiles
  98.         return extractFiles
  99.     }
  100.     return f
  101. }
  102.  
  103. // sanitize input
  104. input = input.findAll{ it?.exists() }.collect{ it.canonicalFile }.unique()
  105.  
  106. // process only media files
  107. input = input.findAll{ it.isVideo() || it.isSubtitle() || it.isDisk() || (music && it.isAudio()) }
  108.  
  109. // ignore clutter files
  110. input = input.findAll{ !(it.path =~ /\b(?i:sample|trailer|extras|deleted.scenes|music.video|scrapbook|behind.the.scenes)\b/) }
  111.  
  112. // print input fileset
  113. input.each{ f -> _log.finest("Input: $f") }
  114.  
  115. // artwork/nfo utility
  116. include('fn:lib/htpc')
  117.  
  118. // group episodes/movies and rename according to XBMC standards
  119. def groups = input.groupBy{ f ->
  120.     // skip auto-detection if possible
  121.     if (forceIgnore(f))
  122.         return []
  123.     if (f.isAudio() && !f.isVideo()) // PROCESS MUSIC FOLDER BY FOLDER
  124.         return [music: f.dir.name]
  125.     if (forceMovie(f))
  126.         return [mov:   detectMovie(f, false)]
  127.     if (forceSeries(f))
  128.         return [tvs:   detectSeriesName(f) ?: detectSeriesName(f.dir.listFiles{ it.isVideo() })]
  129.     if (forceAnime(f))
  130.         return [anime: detectSeriesName(f) ?: detectSeriesName(f.dir.listFiles{ it.isVideo() })]
  131.    
  132.    
  133.     def tvs = detectSeriesName(f)
  134.     def mov = detectMovie(f, false)
  135.     _log.fine("$f.name [series: $tvs, movie: $mov]")
  136.    
  137.     // DECIDE EPISODE VS MOVIE (IF NOT CLEAR)
  138.     if (tvs && mov) {
  139.         def norm = { s -> s.ascii().normalizePunctuation().lower().space(' ') }
  140.         def dn = norm(guessMovieFolder(f)?.name ?: '')
  141.         def fn = norm(f.nameWithoutExtension)
  142.         def sn = norm(tvs)
  143.         def mn = norm(mov.name)
  144.        
  145.         // S00E00 | 2012.07.21 | One Piece 217 | Firefly - Serenity | [Taken 1, Taken 2, Taken 3, Taken 4, ..., Taken 10]
  146.         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) {
  147.             _log.fine("Exclude Movie: $mov")
  148.             mov = null
  149.         } 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)) }) {
  150.             _log.fine("Exclude Series: $tvs")
  151.             tvs = null
  152.         }
  153.     }
  154.    
  155.     // CHECK CONFLICT
  156.     if (((mov && tvs) || (!mov && !tvs)) && failOnError) {
  157.         throw new Exception("Media detection failed")
  158.     }
  159.    
  160.     return [tvs: tvs, mov: mov, anime: null]
  161. }
  162.  
  163. // log movie/series/anime detection results
  164. groups.each{ group, files -> _log.finest("Group: $group => ${files*.name}") }
  165.  
  166. // process each batch
  167. groups.each{ group, files ->
  168.     // fetch subtitles (but not for anime)
  169.     if (subtitles && !group.anime) {
  170.         subtitles.each{ languageCode ->
  171.             def subtitleFiles = getMissingSubtitles(file:files, output:'srt', encoding:'UTF-8', lang:languageCode, strict:true) ?: []
  172.             files += subtitleFiles
  173.             tempFiles += subtitleFiles // if downloaded for temporarily extraced files delete later
  174.         }
  175.     }
  176.    
  177.     // EPISODE MODE
  178.     if ((group.tvs || group.anime) && !group.mov) {
  179.         // choose series / anime config
  180.         def config = group.tvs ? [name:group.tvs,   format:format.tvs,   db:'TheTVDB', seasonFolder:true ]
  181.                                : [name:group.anime, format:format.anime, db:'AniDB',   seasonFolder:false]
  182.         def dest = rename(file: files, format: config.format, db: config.db, action: executeRename)
  183.         if (dest && artwork) {
  184.             dest.mapByFolder().each{ dir, fs ->
  185.                 _log.finest "Fetching artwork for $dir from TheTVDB"
  186.                 def sxe = fs.findResult{ eps -> parseEpisodeNumber(eps) }
  187.                 def options = TheTVDB.search(config.name)
  188.                 if (options.isEmpty()) {
  189.                     _log.warning "TV Series not found: $config.name"
  190.                     return
  191.                 }
  192.                 options = options.sortBySimilarity(config.name, { s -> s.name })
  193.                 fetchSeriesArtworkAndNfo(config.seasonFolder ? dir.dir : dir, dir, options[0], sxe && sxe.season > 0 ? sxe.season : 1)
  194.             }
  195.         }
  196.         if (dest == null && failOnError) {
  197.             throw new Exception("Failed to rename series: $config.name")
  198.         }
  199.     }
  200.    
  201.     // MOVIE MODE
  202.     if (group.mov && !group.tvs && !group.anime) {
  203.         def dest = rename(file:files, format:format.mov, db:'TheMovieDB', action: executeRename)
  204.         if (dest && artwork) {
  205.             dest.mapByFolder().each{ dir, fs ->
  206.                 _log.finest "Fetching artwork for $dir from TheMovieDB"
  207.                 fetchMovieArtworkAndNfo(dir, group.mov, fs.findAll{ it.isVideo() }.sort{ it.length() }.reverse().findResult{ it }, backdrops)
  208.             }
  209.         }
  210.         if (dest == null && failOnError) {
  211.             throw new Exception("Failed to rename movie: $group.mov")
  212.         }
  213.     }
  214.    
  215.     // MUSIC MODE
  216.     if (group.music) {
  217.         def dest = rename(file:files, format:format.music, db:'AcoustID', action: executeRename)
  218.         if (dest == null && failOnError) {
  219.             throw new Exception("Failed to rename music: $group.music")
  220.         }
  221.     }
  222. }
  223.  
  224. input.each{ f ->
  225.     if ( !getRenameLog().containsKey(f) ) {
  226.        println "[REMAINING FILE] $f"
  227.        _guarded{ f.copyTo(new File("D:/path/to/remainder")) }
  228.     }
  229. }
  230.  
  231. // skip notifications if nothing was renamed anyway
  232. if (getRenameLog().isEmpty()) {
  233.     return
  234. }
  235.  
  236. // run program on newly processed files
  237. if (exec) {
  238.     getRenameLog().each{ from, to ->
  239.         def command = getMediaInfo(format: exec, file: to)
  240.         _log.finest("Execute: $command")
  241.         execute(command)
  242.     }
  243. }
  244.  
  245. // make XMBC scan for new content and display notification message
  246. if (xbmc) {
  247.     xbmc.each{ host ->
  248.         _log.info "Notify XBMC: $host"
  249.         _guarded{
  250.             showNotification(host, 9090, 'FileBot', "Finished processing ${tryQuietly { ut_title } ?: input*.dir.name.unique()} (${getRenameLog().size()} files).", 'http://www.filebot.net/images/icon.png')
  251.             scanVideoLibrary(host, 9090)
  252.         }
  253.     }
  254. }
  255.  
  256. // make Plex scan for new content
  257. if (plex) {
  258.     plex.each{
  259.         _log.info "Notify Plex: $it"
  260.         refreshPlexLibrary(it)
  261.     }
  262. }
  263.  
  264. // mark episodes as 'acquired'
  265. if (myepisodes) {
  266.     _log.info 'Update MyEpisodes'
  267.     include('fn:update-mes', [login:myepisodes.join(':'), addshows:true], getRenameLog().values())
  268. }
  269.  
  270. if (pushover) {
  271.     // include webservice utility
  272.     include('fn:lib/ws')
  273.    
  274.     _log.info 'Sending Pushover notification'
  275.     Pushover(pushover).send("Finished processing ${tryQuietly { ut_title } ?: input*.dir.name.unique()} (${getRenameLog().size()} files).")
  276. }
  277.  
  278. // send status email
  279. if (gmail) {
  280.     // ant/mail utility
  281.     include('fn:lib/ant')
  282.    
  283.     // send html mail
  284.     def renameLog = getRenameLog()
  285.     def emailTitle = tryQuietly { ut_title } ?: input*.dir.name.unique()
  286.    
  287.     sendGmail(
  288.         subject: "[FileBot] ${emailTitle}",
  289.         message: XML {
  290.             html {
  291.                 body {
  292.                     p("FileBot finished processing ${emailTitle} (${renameLog.size()} files).");
  293.                     hr(); table {
  294.                         th("Parameter"); th("Value")
  295.                         _args.bindings.findAll{ param -> param.key =~ /^ut_/ }.each{ param ->
  296.                             tr { [param.key, param.value].each{ td(it)} }
  297.                         }
  298.                     }
  299.                     hr(); table {
  300.                         th("Original Name"); th("New Name"); th("New Location")
  301.                         renameLog.each{ from, to ->
  302.                             tr { [from.name, to.name, to.parent].each{ cell -> td{ nobr{ code(cell) } } } }
  303.                         }
  304.                     }
  305.                     hr(); small("// Generated by ${net.sourceforge.filebot.Settings.applicationIdentifier} on ${new Date().dateString} at ${new Date().timeString}")
  306.                 }
  307.             }
  308.         },
  309.         messagemimetype: 'text/html',
  310.         to: tryQuietly{ mailto } ?: gmail[0] + '@gmail.com', // mail to self by default
  311.         user: gmail[0], password: gmail[1]
  312.     )
  313. }
  314.  
  315. // clean empty folders, clutter files, etc after move
  316. if (clean) {
  317.     if (['COPY', 'HARDLINK'].find{ it.equalsIgnoreCase(_args.action) } && tempFiles.size() > 0) {
  318.         _log.info 'Clean temporary extracted files'
  319.         // delete extracted files
  320.         tempFiles.findAll{ it.isFile() }.sort().each{
  321.             _log.finest "Delete $it"
  322.             it.delete()
  323.         }
  324.         // delete remaining empty folders
  325.         tempFiles.findAll{ it.isDirectory() }.sort().reverse().each{
  326.             _log.finest "Delete $it"
  327.             if (it.getFiles().isEmpty()) it.deleteDir()
  328.         }
  329.     }
  330.    
  331.     // deleting remaining files only makes sense after moving files
  332.     if ('MOVE'.equalsIgnoreCase(_args.action)) {
  333.         _log.info 'Clean clutter files and empty folders'
  334.         include('fn:cleaner', [:], !args.empty ? args : ut_kind == 'multi' && ut_dir ? [ut_dir as File] : [])
  335.     }
  336. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement