Advertisement
Guest User

Untitled

a guest
Mar 1st, 2017
265
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 25.13 KB | None | 0 0
  1. #!/usr/bin/env filebot -script
  2.  
  3.  
  4. // log input parameters
  5. log.fine("Run script [$_args.script] at [$now]")
  6. _def.each{ n, v -> log.finest('Parameter: ' + [n, n =~ /plex|kodi|pushover|pushbullet|mail|myepisodes/ ? '*****' : v].join(' = ')) }
  7. args.withIndex().each{ f, i -> if (f.exists()) { log.finest "Argument[$i]: $f" } else { log.warning "Argument[$i]: File does not exist: $f" } }
  8.  
  9.  
  10. // initialize variables
  11. failOnError = _args.conflict.equalsIgnoreCase('fail')
  12. testRun = _args.action.equalsIgnoreCase('test')
  13.  
  14. // --output folder must be a valid folder
  15. outputFolder = tryLogCatch{ any{ _args.output }{ '.' }.toFile().getCanonicalFile() }
  16.  
  17. // enable/disable features as specified via --def parameters
  18. unsorted = tryQuietly{ unsorted.toBoolean() }
  19. music = tryQuietly{ music.toBoolean() }
  20. subtitles = tryQuietly{ subtitles.split(/\W+/) as List }
  21. artwork = tryQuietly{ artwork.toBoolean() && !testRun }
  22. extras = tryQuietly{ extras.toBoolean() }
  23. clean = tryQuietly{ clean.toBoolean() }
  24. exec = tryQuietly{ exec.toString() }
  25.  
  26. // array of kodi/plex/emby hosts
  27. kodi = tryQuietly{ any{kodi}{xbmc}.split(/[ ,;|]+/)*.split(/:(?=\d+$)/).collect{ it.length >= 2 ? [host: it[0], port: it[1] as int] : [host: it[0]] } }
  28. plex = tryQuietly{ plex.split(/[ ,;|]+/)*.split(/:/).collect{ it.length >= 2 ? [host: it[0], token: it[1]] : [host: it[0]] } }
  29. emby = tryQuietly{ emby.split(/[ ,;|]+/)*.split(/:/).collect{ it.length >= 2 ? [host: it[0], token: it[1]] : [host: it[0]] } }
  30.  
  31. // extra options, myepisodes updates and email notifications
  32. extractFolder = tryQuietly{ extractFolder as File }
  33. skipExtract = tryQuietly{ skipExtract.toBoolean() }
  34. deleteAfterExtract = tryQuietly{ deleteAfterExtract.toBoolean() }
  35. excludeList = tryQuietly{ def f = excludeList as File; f.isAbsolute() ? f : outputFolder.resolve(f.path) }
  36. myepisodes = tryQuietly{ myepisodes.split(':', 2) as List }
  37. gmail = tryQuietly{ gmail.split(':', 2) as List }
  38. mail = tryQuietly{ mail.split(':', 5) as List }
  39. pushover = tryQuietly{ pushover.split(':', 2) as List }
  40. pushbullet = tryQuietly{ pushbullet.toString() }
  41. storeReport = tryQuietly{ storeReport.toBoolean() }
  42. reportError = tryQuietly{ reportError.toBoolean() }
  43.  
  44. // user-defined filters
  45. label = any{ _args.mode }{ ut_label }{ null }
  46. ignore = any{ ignore }{ null }
  47. minFileSize = any{ minFileSize.toLong() }{ 50 * 1000L * 1000L }
  48. minLengthMS = any{ minLengthMS.toLong() }{ 10 * 60 * 1000L }
  49.  
  50. // series/anime/movie format expressions
  51. seriesFormat = any{ seriesFormat }{ '{plex}' }
  52. animeFormat = any{ animeFormat }{ '{plex}' }
  53. movieFormat = any{ movieFormat }{ '{plex}' }
  54. musicFormat = any{ musicFormat }{ '{plex}' }
  55. unsortedFormat = any{ unsortedFormat }{ 'Unsorted/{file.structurePathTail}' }
  56.  
  57.  
  58.  
  59. // force Movie / TV Series / Anime behaviour
  60. def forceMovie(f) {
  61. label =~ /^(?i:Movie|Film|Concert|UFC)/ || f.dir.listPath().any{ it.name ==~ /(?i:Movies|Movie)/ } || f.isMovie()
  62. }
  63.  
  64. def forceSeries(f) {
  65. label =~ /^(?i:TV|Show|Series|Documentary)/ || f.dir.listPath().any{ it.name ==~ /(?i:TV.Shows|TV.Series)/ } || f.path =~ /(?<=\b|_)(?i:tv[sp]-|Season\D?\d{1,2}|\d{4}.S\d{2})(?=\b|_)/ || parseEpisodeNumber(f.path, true) || parseDate(f.path)
  66. }
  67.  
  68. def forceAnime(f) {
  69. label =~ /^(?i:Anime)/ || f.dir.listPath().any{ it.name ==~ /(?i:Anime)/ } || (f.isVideo() && (f.name =~ /(?i:HorribleSubs)/ || f.name =~ /[\(\[]\p{XDigit}{8}[\]\)]/ || any{ getMediaInfo(f, '{media.AudioLanguageList} {media.TextCodecList}').tokenize().containsAll(['Japanese', 'ASS']) && (parseEpisodeNumber(f.name, false) != null || getMediaInfo(f, '{minutes}').toInteger() < 60) }{ false }))
  70. }
  71.  
  72. def forceAudio(f) {
  73. label =~ /^(?i:audio|music|music.video)/ || (f.isAudio() && !f.isVideo())
  74. }
  75.  
  76. def forceIgnore(f) {
  77. label =~ /^(?i:games|ebook|other|ignore)/
  78. }
  79.  
  80.  
  81.  
  82. // include artwork/nfo, pushover/pushbullet and ant utilities as required
  83. if (artwork || kodi || plex || emby) { include('lib/htpc') }
  84. if (pushover || pushbullet ) { include('lib/web') }
  85. if (gmail || mail) { include('lib/ant') }
  86.  
  87.  
  88.  
  89. // error reporting functions
  90. def sendEmailReport(title, message, messagetype) {
  91. if (gmail) {
  92. sendGmail(
  93. subject: title, message: message, messagemimetype: messagetype,
  94. to: any{ mailto } { gmail[0].contains('@') ? gmail[0] : gmail[0] + '@gmail.com' }, // mail to self by default
  95. user: gmail[0].contains('@') ? gmail[0] : gmail[0] + '@gmail.com', password: gmail[1]
  96. )
  97. }
  98. if (mail) {
  99. sendmail(
  100. subject: title, message: message, messagemimetype: messagetype,
  101. mailhost: mail[0], mailport: mail[1], from: mail[2], to: mailto,
  102. user: mail[3], password: mail[4]
  103. )
  104. }
  105. }
  106.  
  107. def fail(message) {
  108. if (reportError) {
  109. sendEmailReport('[FileBot] Failure', message as String, 'text/plain')
  110. }
  111. die(message)
  112. }
  113.  
  114.  
  115.  
  116. // check input parameters
  117. def ut = _def.findAll{ k, v -> k.startsWith('ut_') }.collectEntries{ k, v ->
  118. if (v ==~ /[%$]\p{Alnum}|\p{Punct}+/) {
  119. log.warning "Bad $k value: $v"
  120. v = null
  121. }
  122. return [k.substring(3), v ? v : null]
  123. }
  124.  
  125.  
  126.  
  127. // sanity checks
  128. if (outputFolder == null || !outputFolder.isDirectory()) {
  129. fail "Illegal usage: output folder must exist and must be a directory: $outputFolder"
  130. }
  131.  
  132. if (ut.dir) {
  133. if (ut.state_allow && !(ut.state ==~ ut.state_allow)) {
  134. fail "Illegal state: $ut.state != $ut.state_allow"
  135. }
  136. if (args.size() > 0) {
  137. fail "Illegal usage: use either script parameters $ut or file arguments $args but not both"
  138. }
  139. if (ut.dir == '/') {
  140. fail "Illegal usage: No! Are you insane? You can't just pass in the entire filesystem. Think long and hard about what you just tried to do."
  141. }
  142. if (outputFolder.path.startsWith(ut.dir)) {
  143. fail "Illegal usage: output folder [$outputFolder] must be separate from input folder $ut"
  144. }
  145. } else if (args.size() == 0) {
  146. fail "Illegal usage: no input"
  147. } else if (args.any{ f -> outputFolder.path.startsWith(f.path) }) {
  148. fail "Illegal usage: output folder [$outputFolder] must be separate from input arguments $args"
  149. } else if (args.any{ f -> f in File.listRoots() }) {
  150. fail "Illegal usage: input $args must not include a filesystem root"
  151. }
  152.  
  153.  
  154.  
  155. // collect input fileset as specified by the given --def parameters
  156. roots = args
  157.  
  158. if (args.size() == 0) {
  159. // assume we're called with utorrent parameters (account for older and newer versions of uTorrents)
  160. if (ut.kind == 'single' || (ut.kind != 'multi' && ut.dir && ut.file)) {
  161. roots = [new File(ut.dir, ut.file).getCanonicalFile()] // single-file torrent
  162. } else {
  163. roots = [new File(ut.dir).getCanonicalFile()] // multi-file torrent
  164. }
  165. }
  166.  
  167. // helper function to work with the structure relative path rather than the whole absolute path
  168. def relativeInputPath(f) {
  169. def r = roots.find{ r -> f.path.startsWith(r.path) && r.isDirectory() && f.isFile() }
  170. if (r != null) {
  171. return f.path.substring(r.path.length() + 1)
  172. }
  173. return f.name
  174. }
  175.  
  176.  
  177.  
  178. // define and load exclude list (e.g. to make sure files are only processed once)
  179. excludePathSet = new FileSet()
  180.  
  181. if (excludeList) {
  182. if (excludeList.exists()) {
  183. try {
  184. excludePathSet.load(excludeList)
  185. } catch(Exception e) {
  186. fail "Failed to load excludeList: $e"
  187. }
  188. log.fine "Use excludes: $excludeList (${excludePathSet.size()})"
  189. } else {
  190. log.fine "Use excludes: $excludeList"
  191. if ((!excludeList.parentFile.isDirectory() && !excludeList.parentFile.mkdirs()) || (!excludeList.isFile() && !excludeList.createNewFile())) {
  192. fail "Failed to create excludeList: $excludeList"
  193. }
  194. }
  195. }
  196.  
  197.  
  198. extractedArchives = []
  199. temporaryFiles = []
  200.  
  201. def extract(f) {
  202. def folder = new File(extractFolder ?: f.dir, f.nameWithoutExtension)
  203. def files = extract(file: f, output: folder.resolve(f.dir.name), conflict: 'auto', filter: { it.isArchive() || it.isVideo() || it.isSubtitle() || (music && it.isAudio()) }, forceExtractAll: true) ?: []
  204.  
  205. extractedArchives += f
  206. temporaryFiles += folder
  207. temporaryFiles += files
  208.  
  209. return files
  210. }
  211.  
  212.  
  213. def acceptFile(f) {
  214. if (f.isHidden()) {
  215. log.finest "Ignore hidden: $f"
  216. return false
  217. }
  218.  
  219. if (f.isDirectory() && f.name ==~ /[.@].+|bin|initrd|opt|sbin|var|dev|lib|proc|sys|var.defaults|etc|lost.found|root|tmp|etc.defaults|mnt|run|usr|System.Volume.Information/) {
  220. log.finest "Ignore system path: $f"
  221. return false
  222. }
  223.  
  224. if (f.name =~ /(?<=\b|_)(?i:Sample|Trailer|Extras|Extra.Episodes|Bonus.Features|Music.Video|Scrapbook|Behind.the.Scenes|Extended.Scenes|Deleted.Scenes|Mini.Series|s\d{2}c\d{2}|S\d+EXTRA|\d+xEXTRA|NCED|NCOP|(OP|ED)\d+|Formula.1.\d{4})(?=\b|_)/) {
  225. log.finest "Ignore extra: $f"
  226. return false
  227. }
  228.  
  229. // ignore if the user-defined ignore pattern matches
  230. if (f.path.findMatch(ignore)) {
  231. log.finest "Ignore pattern: $f"
  232. return false
  233. }
  234.  
  235. // ignore archives that are on the exclude path list
  236. if (excludePathSet.contains(f)) {
  237. return false
  238. }
  239.  
  240. // accept folders right away and skip file sanity checks
  241. if (f.isDirectory()) {
  242. return true
  243. }
  244.  
  245. // accept archives if the extract feature is enabled
  246. if (f.isArchive() || f.hasExtension('001')) {
  247. return !skipExtract
  248. }
  249.  
  250. // ignore iso images that do not contain a video disk structure
  251. if (f.hasExtension('iso') && !f.isDisk()) {
  252. log.fine "Ignore disk image: $f"
  253. return false
  254. }
  255.  
  256. // ignore small video files
  257. if (minFileSize > 0 && f.isVideo() && f.length() < minFileSize) {
  258. log.fine "Skip small video file: $f"
  259. return false
  260. }
  261.  
  262. // ignore short videos
  263. if (minLengthMS > 0 && f.isVideo() && any{ getMediaInfo(f, '{duration}').toLong() < minLengthMS }{ false /* default if MediaInfo fails */ }) {
  264. log.fine "Skip short video: $f"
  265. return false
  266. }
  267.  
  268. // ignore subtitle files without matching video file in the same or parent folder
  269. if (f.isSubtitle() && ![f, f.dir].findResults{ it.dir }.any{ it.listFiles{ it.isVideo() && f.isDerived(it) }}) {
  270. log.fine "Ignore orphaned subtitles: $f"
  271. return false
  272. }
  273.  
  274. // process only media files (accept audio files only if music mode is enabled)
  275. return f.isVideo() || f.isSubtitle() || (music && f.isAudio())
  276. }
  277.  
  278.  
  279. // specify how to resolve input folders, e.g. grab files from all folders except disk folders and already processed folders (i.e. folders with movie/tvshow nfo files)
  280. def resolveInput(f) {
  281. // resolve folder recursively, except disk folders
  282. if (f.isDirectory()) {
  283. if (f.isDisk()) {
  284. return f
  285. }
  286. return f.listFiles{ acceptFile(it) }.collect{ resolveInput(it) }
  287. }
  288.  
  289. if (f.isArchive() || f.hasExtension('001')) {
  290. return extract(f).findAll{ acceptFile(it) }.collect{ resolveInput(it) }
  291. }
  292.  
  293. return f
  294. }
  295.  
  296.  
  297.  
  298. // flatten nested file structure
  299. def input = roots.findAll{ acceptFile(it) }.flatten{ resolveInput(it) }.toSorted()
  300.  
  301. // update exclude list with all input that will be processed during this run
  302. if (excludeList && !testRun) {
  303. excludePathSet.append(excludeList, extractedArchives, input)
  304. }
  305.  
  306. // print exclude and input sets for logging
  307. input.each{ log.fine "Input: $it" }
  308.  
  309. // early abort if there is nothing to do
  310. if (input.size() == 0) {
  311. log.warning "No files selected for processing"
  312. return
  313. }
  314.  
  315.  
  316.  
  317. // group episodes/movies and rename according to Plex standards
  318. def groups = input.groupBy{ f ->
  319. // print xattr metadata
  320. if (f.metadata) {
  321. log.finest "xattr: [$f.name] => [$f.metadata]"
  322. }
  323.  
  324. // skip auto-detection if possible
  325. if (forceIgnore(f))
  326. return []
  327. if (music && forceAudio(f)) // process audio only if music mode is enabled
  328. return [music: f.dir.name]
  329. if (forceMovie(f))
  330. return [mov: detectMovie(f, false)]
  331. if (forceSeries(f))
  332. return [tvs: detectSeriesName(f) ?: detectSeriesName(input.findAll{ s -> f.dir == s.dir && s.isVideo() })]
  333. if (forceAnime(f))
  334. return [anime: detectAnimeName(f) ?: detectAnimeName(input.findAll{ s -> f.dir == s.dir && s.isVideo() })]
  335.  
  336.  
  337. def tvs = detectSeriesName(f)
  338. def mov = detectMovie(f, false)
  339. log.fine "$f.name [series: $tvs, movie: $mov]"
  340.  
  341. // DECIDE EPISODE VS MOVIE (IF NOT CLEAR)
  342. if (tvs && mov) {
  343. def norm = { s -> s.ascii().normalizePunctuation().lower().space(' ') }
  344. def dn = norm(guessMovieFolder(f)?.name ?: '')
  345. def fn = norm(f.nameWithoutExtension)
  346. def sn = norm(tvs)
  347. def mn = norm(mov.name)
  348. def my = mov.year as String
  349.  
  350. // S00E00 | 2012.07.21 | One Piece 217 | Firefly - Serenity | [Taken 1, Taken 2, Taken 3, Taken 4, ..., Taken 10]
  351. def metrics = [
  352. [tvs: -1, mov: 0, fun: { mn == fn } ],
  353. [tvs: -1, mov: 0, fun: { mov.year >= 1950 && f.listPath().reverse().take(3).find{ it.name.contains(my) && parseEpisodeNumber(it.name.after(my), false) == null } } ],
  354. [tvs: -1, mov: 0, fun: { mn =~ sn && [dn, fn].find{ it =~ /\b(19|20)\d{2}\b/ && parseEpisodeNumber(it.after(/\b(19|20)\d{2}\b/), false) == null } } ],
  355. [tvs: 5, mov: -1, fun: { parseEpisodeNumber(fn, true) || parseDate(fn) } ],
  356. [tvs: 5, mov: -1, fun: { f.dir.listFiles{ it.isVideo() && (dn =~ sn || norm(it.name) =~ sn) && it.name =~ /\d{1,3}/}.findResults{ it.name.matchAll(/\d{1,3}/) as Set }.unique().size() >= 10 } ],
  357. [tvs: 1, mov: -1, fun: { fn.after(sn) ==~ /.{0,3}\s-\s.+/ && matchMovie(fn) == null } ],
  358. [tvs: 1, mov: -1, fun: { [dn, fn].find{ it =~ sn && matchMovie(it) == null } && (parseEpisodeNumber(stripReleaseInfo(fn.after(sn), false), false) || stripReleaseInfo(fn.after(sn), false) =~ /\D\d{1,2}\D{1,3}\d{1,2}\D/) && matchMovie(fn) == null } ],
  359. [tvs: -1, mov: 1, fun: { fn =~ /tt\d{7}/ } ],
  360. [tvs: -1, mov: 1, fun: { f.nameWithoutExtension ==~ /[\D\s_.]+/ } ],
  361. [tvs: -1, mov: 5, fun: { detectMovie(f, true) && [dn, fn].find{ it =~ /(19|20)\d{2}/ } != null } ],
  362. [tvs: -1, mov: 1, fun: { fn.contains(mn) && parseEpisodeNumber(fn.after(mn), false) == null } ],
  363. [tvs: -1, mov: 1, fun: { mn.getSimilarity(fn) >= 0.8 || [dn, fn].find{ it.findAll( ~/\d{4}/ ).findAll{ y -> [mov.year-1, mov.year, mov.year+1].contains(y.toInteger()) }.size() > 0 } != null } ],
  364. [tvs: -1, mov: 1, fun: { [dn, fn].find{ it =~ mn && !(it.after(mn) =~ /\b\d{1,3}\b/) && (it.getSimilarity(mn) > 0.2 + it.getSimilarity(sn)) } != null } ],
  365. [tvs: -1, mov: 1, fun: { detectMovie(f, false).aliasNames.find{ fn.contains(norm(it)) } } ]
  366. ]
  367.  
  368. def score = [tvs: 0, mov: 0]
  369. metrics.each{
  370. if (tvs && mov && it.fun()) {
  371. score.tvs += it.tvs
  372. score.mov += it.mov
  373.  
  374. if (score.tvs >= 1 && score.mov <= -1) {
  375. log.fine "Exclude Movie: $mov"
  376. mov = null
  377. } else if (score.mov >= 1 && score.tvs <= -1) {
  378. log.fine "Exclude Series: $tvs"
  379. tvs = null
  380. }
  381. }
  382. }
  383.  
  384. }
  385.  
  386. // CHECK CONFLICT
  387. if (((mov && tvs) || (!mov && !tvs))) {
  388. if (failOnError) {
  389. fail 'Media detection failed'
  390. } else {
  391. log.fine "Unable to differentiate: [$f.name] => [$tvs] VS [$mov]"
  392. return [:]
  393. }
  394. }
  395.  
  396. return [tvs: tvs, mov: mov]
  397. }
  398.  
  399. // group entries by unique tvs/mov descriptor
  400. groups = groups.groupBy{ group, files -> group.collectEntries{ type, query -> [type, query ? query.toString().ascii().normalizePunctuation().lower() : null] } }.collectEntries{ group, maps -> [group, maps.values().flatten()] }
  401.  
  402. // log movie/series/anime detection results
  403. groups.each{ group, files -> log.finest "Group: $group => ${files*.name}" }
  404.  
  405. // keep track of unsorted files or files that could not be processed for some reason
  406. def unsortedFiles = []
  407.  
  408. // process each batch
  409. groups.each{ group, files ->
  410. // fetch subtitles (but not for anime)
  411. if (group.anime == null && subtitles != null && files.findAll{ it.isVideo() }.size() > 0) {
  412. subtitles.each{ languageCode ->
  413. def subtitleFiles = getMissingSubtitles(file: files, lang: languageCode, strict: true, output: 'srt', encoding: 'UTF-8', format: 'MATCH_VIDEO_ADD_LANGUAGE_TAG') ?: []
  414. files += subtitleFiles
  415. input += subtitleFiles // make sure subtitles are added to the exclude list and other post processing operations
  416. temporaryFiles += subtitleFiles // if downloaded for temporarily extraced files delete later
  417. }
  418. }
  419.  
  420. // EPISODE MODE
  421. if ((group.tvs || group.anime) && !group.mov) {
  422. // choose series / anime
  423. def dest = group.tvs ? rename(file: files, format: seriesFormat, db: 'TheTVDB') : rename(file: files, format: animeFormat, db: 'AniDB')
  424.  
  425. if (dest != null) {
  426. if (artwork) {
  427. dest.mapByFolder().each{ dir, fs ->
  428. def hasSeasonFolder = any{ dir =~ /Specials|Season.\d+/ || dir.parentFile.structurePathTail.listPath().size() > 0 }{ false } // MAY NOT WORK FOR CERTAIN FORMATS
  429.  
  430. fs.findResults{ it.metadata }.findAll{ it.seriesInfo.database == 'TheTVDB' }.collect{ [name: it.seriesName, season: it.special ? 0 : it.season, id: it.seriesInfo.id] }.unique().each{
  431. log.fine "Fetching series artwork for [$it.name / Season $it.season] to [$dir]"
  432. fetchSeriesArtworkAndNfo(hasSeasonFolder ? dir.parentFile : dir, dir, it.id, it.season, false, _args.language.locale)
  433. }
  434. }
  435. }
  436. } else if (failOnError) {
  437. fail "Failed to process group: $group"
  438. } else {
  439. unsortedFiles += files
  440. }
  441. }
  442.  
  443. // MOVIE MODE
  444. else if (group.mov && !group.tvs && !group.anime) {
  445. def dest = rename(file: files, format: movieFormat, db: 'TheMovieDB')
  446.  
  447. if (dest != null) {
  448. if (artwork) {
  449. dest.mapByFolder().each{ dir, fs ->
  450. def movieFile = fs.findAll{ it.isVideo() || it.isDisk() }.sort{ it.length() }.reverse().findResult{ it }
  451. if (movieFile) {
  452. def movieInfo = movieFile.metadata
  453. log.fine "Fetching movie artwork for [$movieInfo] to [$dir]"
  454. fetchMovieArtworkAndNfo(dir, movieInfo, movieFile, extras, false, _args.language.locale)
  455. }
  456. }
  457. }
  458. } else if (failOnError) {
  459. fail "Failed to process group: $group"
  460. } else {
  461. unsortedFiles += files
  462. }
  463. }
  464.  
  465. // MUSIC MODE
  466. else if (group.music) {
  467. def dest = rename(file: files, format: musicFormat, db: 'ID3')
  468.  
  469. if (dest != null) {
  470. // music artwork not supported
  471. } else if (failOnError) {
  472. fail "Failed to process group: $group"
  473. } else {
  474. unsortedFiles += files
  475. }
  476. }
  477.  
  478. // UNSORTED
  479. else {
  480. unsortedFiles += files
  481. }
  482. }
  483.  
  484.  
  485. // ---------- POST PROCESSING ---------- //
  486.  
  487. // deal with remaining files that cannot be sorted automatically
  488. if (unsorted) {
  489. if (unsortedFiles.size() > 0) {
  490. log.fine "Processing ${unsortedFiles.size()} unsorted files"
  491. rename(map: unsortedFiles.collectEntries{ original ->
  492. def destination = getMediaInfo(original, unsortedFormat) as File
  493.  
  494. // sanity check user-defined unsorted format
  495. if (destination == null) {
  496. fail("Illegal usage: unsorted format must yield valid file path")
  497. }
  498.  
  499. // resolve relative paths
  500. if (!destination.isAbsolute()) {
  501. destination = outputFolder.resolve(destination.path)
  502. }
  503.  
  504. return [original, destination]
  505. })
  506. }
  507. }
  508.  
  509. // run program on newly processed files
  510. if (exec) {
  511. getRenameLog().collect{ from, to -> getMediaInfo(to, exec) }.unique().each{ command ->
  512. log.fine "Execute: $command"
  513. execute(command)
  514. }
  515. }
  516.  
  517.  
  518. // ---------- REPORTING ---------- //
  519.  
  520.  
  521. if (getRenameLog().size() > 0) {
  522. // messages used for kodi / plex / emby pushover notifications
  523. def getNotificationTitle = {
  524. def count = getRenameLog().count{ k, v -> !v.isSubtitle() }
  525. return "FileBot finished processing $count files"
  526. }.memoize()
  527.  
  528. def getNotificationMessage = { prefix = '• ', postfix = '\n' ->
  529. return ut.title ?: (input.findAll{ !it.isSubtitle() } ?: input).collect{ relativeInputPath(it) as File }.root.nameWithoutExtension.unique().collect{ prefix + it }.join(postfix).trim()
  530. }.memoize()
  531.  
  532. // make Kodi scan for new content and display notification message
  533. if (kodi) {
  534. kodi.each{ instance ->
  535. log.fine "Notify Kodi: $instance"
  536. tryLogCatch {
  537. showNotification(instance.host, instance.port ?: 8080, getNotificationTitle(), getNotificationMessage(), 'http://app.filebot.net/icon.png')
  538. scanVideoLibrary(instance.host, instance.port ?: 8080)
  539. }
  540. }
  541. }
  542.  
  543. // make Plex scan for new content
  544. if (plex) {
  545. plex.each{ instance ->
  546. log.fine "Notify Plex: $instance"
  547. tryLogCatch {
  548. refreshPlexLibrary(instance.host, 32400, instance.token)
  549. }
  550. }
  551. }
  552.  
  553. // make Emby scan for new content
  554. if (emby) {
  555. emby.each{ instance ->
  556. log.fine "Notify Emby: $instance"
  557. tryLogCatch {
  558. refreshEmbyLibrary(instance.host, 8096, instance.token)
  559. }
  560. }
  561. }
  562.  
  563. // mark episodes as 'acquired'
  564. if (myepisodes) {
  565. log.fine 'Update MyEpisodes'
  566. tryLogCatch {
  567. executeScript('update-mes', [login:myepisodes.join(':'), addshows:true], getRenameLog().values())
  568. }
  569. }
  570.  
  571. if (pushover) {
  572. log.fine 'Sending Pushover notification'
  573. tryLogCatch {
  574. Pushover(pushover[0], pushover[1] ?: 'wcckDz3oygHSU2SdIptvnHxJ92SQKK').send(getNotificationTitle(), getNotificationMessage())
  575. }
  576. }
  577.  
  578. // messages used for email / pushbullet reports
  579. def getReportSubject = { getNotificationMessage('', ' | ') }
  580. def getReportTitle = { '[FileBot] ' + getReportSubject() }
  581. def getReportMessage = {
  582. def renameLog = getRenameLog()
  583. '''<!DOCTYPE html>\n''' + XML {
  584. html {
  585. head {
  586. meta(charset:'UTF-8')
  587. style('''
  588. p{font-family:Arial,Helvetica,sans-serif}
  589. p b{color:#07a}
  590. hr{border-style:dashed;border-width:1px 0 0 0;border-color:lightgray}
  591. small{color:#d3d3d3;font-size:xx-small;font-weight:normal;font-family:Arial,Helvetica,sans-serif}
  592. table a:link{color:#666;font-weight:bold;text-decoration:none}
  593. table a:visited{color:#999;font-weight:bold;text-decoration:none}
  594. table a:active,table a:hover{color:#bd5a35;text-decoration:underline}
  595. table{font-family:Arial,Helvetica,sans-serif;color:#666;background:#eaebec;margin:15px;border:#ccc 1px solid;border-radius:3px;box-shadow:0 1px 2px #d1d1d1}
  596. table th{padding:15px;border-top:1px solid #fafafa;border-bottom:1px solid #e0e0e0;background:#ededed}
  597. table th{text-align:center;padding-left:20px}
  598. table tr:first-child th:first-child{border-top-left-radius:3px}
  599. table tr:first-child th:last-child{border-top-right-radius:3px}
  600. table tr{text-align:left;padding-left:20px}
  601. table td:first-child{text-align:left;padding-left:20px;border-left:0}
  602. table td{padding:15px;border-top:1px solid #fff;border-bottom:1px solid #e0e0e0;border-left:1px solid #e0e0e0;background:#fafafa;white-space:nowrap}
  603. table tr.even td{background:#f6f6f6}
  604. table tr:last-child td{border-bottom:0}
  605. table tr:last-child td:first-child{border-bottom-left-radius:3px}
  606. table tr:last-child td:last-child{border-bottom-right-radius:3px}
  607. table tr:hover td{background:#f2f2f2}
  608. ''')
  609. title(getReportTitle())
  610. }
  611. body {
  612. p {
  613. mkp.yield("FileBot finished processing ")
  614. b(getReportSubject())
  615. mkp.yield(" (${renameLog.size()} files).")
  616. }
  617. hr(); table {
  618. tr { th('Original Name'); th('New Name'); th('New Location') }
  619. renameLog.each{ from, to ->
  620. tr { [from.name, to.name, to.parent].each{ cell -> td(cell) } }
  621. }
  622. }
  623. hr(); small("// Generated by ${Settings.applicationIdentifier} on ${InetAddress.localHost.hostName} at ${now}")
  624. }
  625. }
  626. }
  627. }
  628.  
  629. // store processing report
  630. if (storeReport) {
  631. def reportFolder = ApplicationFolder.AppData.resolve('reports')
  632. def reportName = [now.format(/[yyyy-MM-dd HH mm]/), getReportSubject().take(50)].join(' ').validateFileName().space('_')
  633. def reportFile = getReportMessage().saveAs(reportFolder.resolve(reportName + '.html'))
  634. log.finest "Saving report as ${reportFile}"
  635. }
  636.  
  637. // send pushbullet report
  638. if (pushbullet) {
  639. log.fine 'Sending PushBullet report'
  640. tryLogCatch {
  641. PushBullet(pushbullet).sendFile(getNotificationTitle(), getReportMessage(), 'text/html', getNotificationMessage(), any{ mailto }{ null })
  642. }
  643. }
  644.  
  645. // send email report
  646. if (gmail || mail) {
  647. tryLogCatch {
  648. sendEmailReport(getReportTitle(), getReportMessage(), 'text/html')
  649. }
  650. }
  651. }
  652.  
  653.  
  654. // ---------- CLEAN UP ---------- //
  655.  
  656.  
  657. // clean up temporary files that may be left behind after extraction
  658. if (deleteAfterExtract) {
  659. extractedArchives.each{ a ->
  660. log.finest "Delete archive $a"
  661. a.delete()
  662. a.dir.listFiles().toList().findAll{ v -> v.name.startsWith(a.nameWithoutExtension) && v.extension ==~ /r\d+/ }.each{ v ->
  663. log.finest "Delete archive volume $v"
  664. v.delete()
  665. }
  666. }
  667. }
  668.  
  669. // clean empty folders, clutter files, etc after move
  670. if (clean) {
  671. if (['DUPLICATE', 'COPY', 'HARDLINK'].any{ it.equalsIgnoreCase(_args.action) } && temporaryFiles.size() > 0) {
  672. log.fine 'Clean temporary extracted files'
  673. // delete extracted files
  674. temporaryFiles.findAll{ it.isFile() }.sort().each{
  675. log.finest "Delete $it"
  676. it.delete()
  677. }
  678. // delete remaining empty folders
  679. temporaryFiles.findAll{ it.isDirectory() }.sort().reverse().each{
  680. log.finest "Delete $it"
  681. if (it.getFiles().size() == 0) {
  682. it.deleteDir()
  683. }
  684. }
  685. }
  686.  
  687. // deleting remaining files only makes sense after moving files
  688. if ('MOVE'.equalsIgnoreCase(_args.action)) {
  689. def cleanerInput = args.size() > 0 ? args : ut.kind == 'multi' && ut.dir ? [ut.dir as File] : []
  690. cleanerInput = cleanerInput.findAll{ f -> f.exists() }
  691. if (cleanerInput.size() > 0) {
  692. log.fine 'Clean clutter files and empty folders'
  693. executeScript('cleaner', args.size() == 0 ? [root:true, ignore: ignore] : [root:false, ignore: ignore], cleanerInput)
  694. }
  695. }
  696. }
  697.  
  698.  
  699.  
  700. if (getRenameLog().size() == 0) {
  701. fail "Finished without processing any files"
  702. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement