Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/env filebot -script
- // log input parameters
- log.fine("Run script [$_args.script] at [$now]")
- _def.each{ n, v -> log.finest('Parameter: ' + [n, n =~ /plex|kodi|pushover|pushbullet|mail|myepisodes/ ? '*****' : v].join(' = ')) }
- args.withIndex().each{ f, i -> if (f.exists()) { log.finest "Argument[$i]: $f" } else { log.warning "Argument[$i]: File does not exist: $f" } }
- // initialize variables
- failOnError = _args.conflict.equalsIgnoreCase('fail')
- testRun = _args.action.equalsIgnoreCase('test')
- // --output folder must be a valid folder
- outputFolder = tryLogCatch{ any{ _args.output }{ '.' }.toFile().getCanonicalFile() }
- // enable/disable features as specified via --def parameters
- unsorted = tryQuietly{ unsorted.toBoolean() }
- music = tryQuietly{ music.toBoolean() }
- subtitles = tryQuietly{ subtitles.split(/\W+/) as List }
- artwork = tryQuietly{ artwork.toBoolean() && !testRun }
- extras = tryQuietly{ extras.toBoolean() }
- clean = tryQuietly{ clean.toBoolean() }
- exec = tryQuietly{ exec.toString() }
- // array of kodi/plex/emby hosts
- kodi = tryQuietly{ any{kodi}{xbmc}.split(/[ ,;|]+/)*.split(/:(?=\d+$)/).collect{ it.length >= 2 ? [host: it[0], port: it[1] as int] : [host: it[0]] } }
- plex = tryQuietly{ plex.split(/[ ,;|]+/)*.split(/:/).collect{ it.length >= 2 ? [host: it[0], token: it[1]] : [host: it[0]] } }
- emby = tryQuietly{ emby.split(/[ ,;|]+/)*.split(/:/).collect{ it.length >= 2 ? [host: it[0], token: it[1]] : [host: it[0]] } }
- // extra options, myepisodes updates and email notifications
- extractFolder = tryQuietly{ extractFolder as File }
- skipExtract = tryQuietly{ skipExtract.toBoolean() }
- deleteAfterExtract = tryQuietly{ deleteAfterExtract.toBoolean() }
- excludeList = tryQuietly{ def f = excludeList as File; f.isAbsolute() ? f : outputFolder.resolve(f.path) }
- myepisodes = tryQuietly{ myepisodes.split(':', 2) as List }
- gmail = tryQuietly{ gmail.split(':', 2) as List }
- mail = tryQuietly{ mail.split(':', 5) as List }
- pushover = tryQuietly{ pushover.split(':', 2) as List }
- pushbullet = tryQuietly{ pushbullet.toString() }
- storeReport = tryQuietly{ storeReport.toBoolean() }
- reportError = tryQuietly{ reportError.toBoolean() }
- // user-defined filters
- label = any{ ut_label }{ null }
- ignore = any{ ignore }{ null }
- minFileSize = any{ minFileSize.toLong() }{ 50 * 1000L * 1000L }
- minLengthMS = any{ minLengthMS.toLong() }{ 10 * 60 * 1000L }
- // series/anime/movie format expressions
- seriesFormat = any{ seriesFormat }{ '{plex}' }
- animeFormat = any{ animeFormat }{ '{plex}' }
- movieFormat = any{ movieFormat }{ '{plex}' }
- musicFormat = any{ musicFormat }{ '{plex}' }
- unsortedFormat = any{ unsortedFormat }{ 'Unsorted/{file.structurePathTail}' }
- // force Movie / TV Series / Anime behaviour
- def forceMovie(f) {
- label =~ /^(?i:Movie|Film|Concert|UFC)/ || f.dir.listPath().any{ it.name ==~ /(?i:Movies|Movie)/ } || f.isMovie() || any{ (f.isVideo() || f.isSubtitle()) && !forceSeries(f) && getMediaInfo(f, '{minutes}').toInteger() >= 100 }{ false }
- }
- def forceSeries(f) {
- 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)
- }
- def forceAnime(f) {
- label =~ /^(?i:Anime)/ || f.dir.listPath().any{ it.name ==~ /(?i:Anime)/ } || ((f.isVideo() || f.isSubtitle()) && (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 }))
- }
- def forceAudio(f) {
- label =~ /^(?i:audio|music|music.video)/ || (f.isAudio() && !f.isVideo())
- }
- def forceIgnore(f) {
- label =~ /^(?i:games|ebook|other|ignore)/
- }
- // include artwork/nfo, pushover/pushbullet and ant utilities as required
- if (artwork || kodi || plex || emby) { include('lib/htpc') }
- if (pushover || pushbullet ) { include('lib/web') }
- def fail(message) {
- die(message)
- }
- // check input parameters
- def ut = _def.findAll{ k, v -> k.startsWith('ut_') }.collectEntries{ k, v ->
- if (v ==~ /[%$]\p{Alnum}|\p{Punct}+/) {
- log.warning "Bad $k value: $v"
- v = null
- }
- return [k.substring(3), v ? v : null]
- }
- roots = args
- if (args.size() == 0) {
- // 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)) {
- roots = [new File(ut.dir, ut.file).getCanonicalFile()] // single-file torrent
- } else {
- roots = [new File(ut.dir).getCanonicalFile()] // multi-file torrent
- }
- }
- // helper function to work with the structure relative path rather than the whole absolute path
- def relativeInputPath(f) {
- def r = roots.find{ r -> f.path.startsWith(r.path) && r.isDirectory() && f.isFile() }
- if (r != null) {
- return f.path.substring(r.path.length() + 1)
- }
- return f.name
- }
- // define and load exclude list (e.g. to make sure files are only processed once)
- excludePathSet = new FileSet()
- if (excludeList) {
- if (excludeList.exists()) {
- try {
- excludePathSet.load(excludeList)
- } catch(Exception e) {
- fail "Failed to load excludeList: $e"
- }
- log.fine "Use excludes: $excludeList (${excludePathSet.size()})"
- } else {
- log.fine "Use excludes: $excludeList"
- if ((!excludeList.parentFile.isDirectory() && !excludeList.parentFile.mkdirs()) || (!excludeList.isFile() && !excludeList.createNewFile())) {
- fail "Failed to create excludeList: $excludeList"
- }
- }
- }
- extractedArchives = []
- temporaryFiles = []
- def extract(f) {
- def folder = new File(extractFolder ?: f.dir, f.nameWithoutExtension)
- 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) ?: []
- extractedArchives += f
- temporaryFiles += folder
- temporaryFiles += files
- return files
- }
- def acceptFile(f) {
- if (f.isHidden()) {
- log.finest "Ignore hidden: $f"
- return false
- }
- 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/) {
- log.finest "Ignore system path: $f"
- return false
- }
- 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|_)/) {
- log.finest "Ignore extra: $f"
- return false
- }
- // ignore if the user-defined ignore pattern matches
- if (f.path.findMatch(ignore)) {
- log.finest "Ignore pattern: $f"
- return false
- }
- // ignore archives that are on the exclude path list
- if (excludePathSet.contains(f)) {
- return false
- }
- // accept folders right away and skip file sanity checks
- if (f.isDirectory()) {
- return true
- }
- // accept archives if the extract feature is enabled
- if (f.isArchive() || f.hasExtension('001')) {
- return !skipExtract
- }
- // ignore iso images that do not contain a video disk structure
- if (f.hasExtension('iso') && !f.isDisk()) {
- log.fine "Ignore disk image: $f"
- return false
- }
- // ignore small video files
- if (minFileSize > 0 && f.isVideo() && f.length() < minFileSize) {
- log.fine "Skip small video file: $f"
- return false
- }
- // ignore short videos
- if (minLengthMS > 0 && f.isVideo() && any{ getMediaInfo(f, '{minutes}').toLong() * 60 * 1000L < minLengthMS }{ false /* default if MediaInfo fails */ }) {
- log.fine "Skip short video: $f"
- return false
- }
- // ignore subtitle files without matching video file in the same or parent folder
- if (f.isSubtitle() && ![f, f.dir].findResults{ it.dir }.any{ it.listFiles{ it.isVideo() && f.isDerived(it) }}) {
- log.fine "Ignore orphaned subtitles: $f"
- return false
- }
- // process only media files (accept audio files only if music mode is enabled)
- return f.isVideo() || f.isSubtitle() || (music && f.isAudio())
- }
- // 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)
- def resolveInput(f) {
- // resolve folder recursively, except disk folders
- if (f.isDirectory()) {
- if (f.isDisk()) {
- return f
- }
- return f.listFiles{ acceptFile(it) }.collect{ resolveInput(it) }
- }
- if (f.isArchive() || f.hasExtension('001')) {
- return extract(f).findAll{ acceptFile(it) }.collect{ resolveInput(it) }
- }
- return f
- }
- // flatten nested file structure
- def input = roots.findAll{ acceptFile(it) }.flatten{ resolveInput(it) }.toSorted()
- // update exclude list with all input that will be processed during this run
- if (excludeList && !testRun) {
- excludePathSet.append(excludeList, extractedArchives, input)
- }
- // print exclude and input sets for logging
- input.each{ log.fine "Input: $it" }
- // early abort if there is nothing to do
- if (input.size() == 0) {
- log.warning "No files selected for processing"
- return
- }
- if (exec) {
- input.collect{ getMediaInfo(it, exec) }.unique().each{ command ->
- //log.fine "Execute: $command"
- execute(command)
- }
- }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement