AMC advanced script options rename cover.jpg

Running FileBot from the console, Groovy scripting, shell scripts, etc
Post Reply
jtb2586
Posts: 3
Joined: 28 Oct 2015, 16:56

AMC advanced script options rename cover.jpg

Post by jtb2586 »

I'm running an edited version of the AMC and htpc scripts and would like to know how ik can save the downloaded cover with the name of the movie.

The line in htpc script

Code: Select all

fetchMovieArtwork(movieDir.resolve('cover.jpg'), movieInfo, 'posters', override, locale)
saves it as cover.jpg, what argument should i use so it saves it with the movie name?

My amc script:

Code: Select all

// 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"


// log input parameters
log.fine("Run script [${_args.script}] at [${now}]")
_def.each{ n, v -> log.finer('Parameter: ' + [n, n =~ /pushover|pushbullet|mail|myepisodes/ ? '*****' : v].join(' = ')) }
args.each{ log.finer("Argument: $it") }


// initialize variables
def input = []
def failOnError = _args.conflict.equalsIgnoreCase('fail')
def isTest = _args.action.equalsIgnoreCase('test')

// enable/disable features as specified via --def parameters
def unsorted  = tryQuietly{ unsorted.toBoolean() }
def music     = tryQuietly{ music.toBoolean() }
def subtitles = tryQuietly{ subtitles.split(/\W+/) as List }
def artwork   = tryQuietly{ artwork.toBoolean() && !isTest }
def extras    = tryQuietly{ extras.toBoolean() }
def clean     = tryQuietly{ clean.toBoolean() }
def exec      = tryQuietly{ exec.toString() }

// array of xbmc/plex hosts
def xbmc = tryQuietly{ xbmc.split(/[ ,|]+/) }
def plex = tryQuietly{ plex.split(/[ ,|]+/)*.split(/:/).collect{ it.length >= 2 ? [host:it[0], token:it[1]] : [host:it[0]] } }

// extra options, myepisodes updates and email notifications
def storeReport = tryQuietly{ storeReport.toBoolean() }
def skipExtract = tryQuietly{ skipExtract.toBoolean() }
def deleteAfterExtract = tryQuietly{ deleteAfterExtract.toBoolean() }
def excludeList = tryQuietly{ (excludeList as File).isAbsolute() ? (excludeList as File) : new File(_args.output ?: '.', excludeList).getCanonicalFile() }
def myepisodes = tryQuietly{ myepisodes.split(':', 2) }
def gmail = tryQuietly{ gmail.split(':', 2) }
def mail = tryQuietly{ mail.split(':', 3) }
def pushover = tryQuietly{ pushover.split(':', 2) }
def pushbullet = tryQuietly{ pushbullet.toString() }
def reportError = tryQuietly{ reportError.toBoolean() }

// user-defined filters
def label = tryQuietly{ ut_label } ?: null
def ignore = tryQuietly{ ignore } ?: null
def minFileSize = tryQuietly{ minFileSize.toLong() }; if (minFileSize == null) { minFileSize = 50 * 1000L * 1000L }
def minLengthMS = tryQuietly{ minLengthMS.toLong() }; if (minLengthMS == null) { minLengthMS = 10 * 60 * 1000L }

// series/anime/movie format expressions
def format = [
	tvs:   any{ seriesFormat }{ '''TV Shows/{n}/{episode.special ? 'Special' : 'Season '+s.pad(2)}/{n} {s00e00} {t.replaceAll(/[`´‘’ʻ]/, /'/).replaceAll(/[!?.]+$/).replacePart(', Part $1')}''' },
	anime: any{ animeFormat  }{ '''Anime/{primaryTitle}/{primaryTitle} {sxe} {t.replaceAll(/[!?.]+$/).replaceAll(/[`´‘’ʻ]/, /'/).replacePart(', Part $1')}''' },
	mov:   any{ movieFormat  }{ '''Movies/{n} ({y})''' },
	music: any{ musicFormat  }{ '''Music/{n}/{album+'/'}{pi.pad(2)+' - '} {t}''' },
	unsorted: any{ unsortedFormat }{ '''Unsorted/{file.structurePathTail}''' }
]


// force movie/series/anime logic
def forceMovie = { f ->
	label =~ /^(?i:Movie|Couch.Potato)/ || f.dir.listPath().any{ it.name ==~ /(?i:Movies)/ }  || f.path =~ /(?<=tt)\\d{7}/
}

def forceSeries = { f ->
	label =~ /^(?i:TV|Kids.Shows)/ || f.dir.listPath().any{ it.name ==~ /(?i:TV.Shows)/ } || parseEpisodeNumber(f.path) || parseDate(f.path) || f.path =~ /(?i:tvs-|tvp-|EP[0-9]{2,3}|Season\D?[0-9]{1,2}\D|(19|20)\d{2}.S\d{2})/
}

def forceAnime = { f ->
	label =~ /^(?i:Anime)/ || f.dir.listPath().any{ it.name ==~ /(?i:Anime)/ } || (f.isVideo() && (f.name =~ /(?i:HorribleSubs)/ || f.name =~ "[\\(\\[]\\p{XDigit}{8}[\\]\\)]" || (getMediaInfo(file:f, format:'''{media.AudioLanguageList} {media.TextCodecList}''').tokenize().containsAll(['Japanese', 'ASS']) && (parseEpisodeNumber(f.name, false) != null || getMediaInfo(file:f, format:'{minutes}').toInteger() < 60))))
}

def forceAudio = { f ->
	label =~ /^(?i:audio|music|music.video)/ || (f.isAudio() && !f.isVideo())
}

def forceIgnore = { f ->
	label =~ /^(?i:games|ebook|other|ignore|seeding)/ || f.path.findMatch(ignore) != null
}



// include artwork/nfo, pushover/pushbullet and ant utilities as required
if (artwork || xbmc || plex) { include('lib/htpc') }
if (pushover || pushbullet ) { include('lib/web') }
if (gmail || mail) { include('lib/ant') }



// error reporting functions
def sendEmailReport = { title, message, messagetype ->
	if (gmail) {
		sendGmail(
			subject: title,
			message: message,
			messagemimetype: messagetype,
			to: any{ mailto } { gmail[0] + '@gmail.com' }, // mail to self by default
			user: gmail[0],
			password: gmail[1]
		)
	}
	if (mail) {
		sendmail(
			mailhost: mail[0],
			mailport: mail[1],
			from: mail[2],
			to: mailto,
			subject: title,
			message: message,
			messagemimetype: messagetype
		)
	}
}

def fail = { message ->
	if (reportError) {
		sendEmailReport('[FileBot] Failure', message, 'text/plain')
	}
	die(message)
}



// sanity checks
args.findAll{ !it.exists() }.each{ fail("File not found: $it") }

// check user-defined pre-condition
if (tryQuietly{ !(ut_state ==~ ut_state_allow) }) {
	fail("Invalid state: ut_state = $ut_state (expected $ut_state_allow)")
}

// check ut mode vs standalone mode
if ((tryQuietly{ ut_dir } == '/') || (args.size() > 0 && (tryQuietly{ ut_dir }?.size() > 0 || tryQuietly{ ut_file }?.size() > 0)) || (args.size() == 0 && (tryQuietly{ ut_dir } == null && tryQuietly{ ut_file } == null))) {
	fail("Invalid arguments: pass in either file arguments or ut_dir/ut_file parameters but not both")
}



// define and load exclude list (e.g. to make sure files are only processed once)
def excludePathSet = new FileSet()
if (excludeList) {
	if (excludeList.exists()) {
		excludePathSet.feed(Files.lines(excludeList.toPath(), StandardCharsets.UTF_8))
		log.finest "Using excludes: ${excludeList} (${excludePathSet.size()})"
	} else {
		log.finest "Creating excludes: ${excludeList}"
		if ((!excludeList.parentFile.isDirectory() && !excludeList.parentFile.mkdirs()) || (!excludeList.isFile() && !excludeList.createNewFile())) {
			die("Failed to create excludeList: ${excludeList}")
		}
	}
}


// 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) {
	// ignore system and hidden folders
	if (f.isHidden()) {
		if (f.isDirectory()) log.finest "Ignore hidden: $f" // ignore all hidden files but only log hidden folders
		return []
	}

	if (f.isDirectory()) {
		def files = f.listFiles() as List ?: []

		// ignore already processed folders
		if (files.any{ it.name ==~ /movie.nfo|tvshow.nfo/ }) {
			log.finest "Ignore processed folder: $f"
			return []
		}

		// resolve folder recursively, except disk folders
		if (f.isDisk()) {
			return f
		}

		// resolve folder recursively
		return files.findResults{ resolveInput(it) }
	}
	
	return f
}

// collect input fileset as specified by the given --def parameters
def roots = []
if (args.empty) {
	// 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) // single-file torrent
	} else {
		roots += new File(ut_dir) // multi-file torrent
	}
} else {
	// assume we're called normally with arguments
	roots += args
}

// sanitize input
roots = roots.findAll{ it?.exists() }.collect{ it.canonicalFile }.unique() // roots could be folders as well as files

// flatten nested file structure
input = roots.flatten{ f -> resolveInput(f) }

// ignore archives that are on the exclude path list
input = input.findAll{ f -> !excludePathSet.contains(f.path) }

// extract archives (zip, rar, etc) that contain at least one video file
def extractedArchives = []
def tempFiles = []
input = input.flatten{ f ->
	if (!skipExtract && (f.isArchive() || f.hasExtension('001'))) {
		def extractDir = new File(f.dir, f.nameWithoutExtension)
		def extractFiles = extract(file: f, output: new File(extractDir, f.dir.name), conflict: 'auto', filter: { it.isArchive() || it.isVideo() || it.isSubtitle() || (music && it.isAudio()) }, forceExtractAll: true) ?: []

		if (extractFiles.size() > 0) {
			extractedArchives += f
			tempFiles += extractDir
			tempFiles += extractFiles
		}
		return extractFiles
	}
	return f
}


// ignore files that are on the exclude path list
input = input.findAll{ f -> !excludePathSet.contains(f.path) }

// update exclude list with all input that will be processed during this run
if (excludeList && !isTest) {
	excludeList.withWriterAppend('UTF-8') { out ->
		extractedArchives.path.each{ out.println(it) }
		input.path.each{ out.println(it) }
	}
}


// 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
}


// keep original input around so we can print excluded files later
def originalInputSet = input as LinkedHashSet

// process only media files
input = input.findAll{ f -> (f.isVideo() && !tryQuietly{ f.hasExtension('iso') && !f.isDisk() }) || f.isSubtitle() || (f.isDirectory() && f.isDisk()) || (music && f.isAudio()) }

// ignore clutter files
input = input.findAll{ f -> !(relativeInputPath(f) =~ /(?<=\b|_)(?i:sample|trailer|extras|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|_)/) }

// ignore video files that don't conform with the file-size and video-length limits
input = input.findAll{ f -> !(f.isVideo() && ((minFileSize > 0 && f.length() < minFileSize) || (minLengthMS > 0 && tryQuietly{ getMediaInfo(file:f, format:'{duration}').toLong() < minLengthMS }))) }

// ignore subtitles files that are not stored in the same folder as the movie
input = input.findAll{ f -> !(f.isSubtitle() && !input.findAll{ it.isVideo() }.any{ f.isDerived(it) }) }

// ensure that the final input set is sorted
input = input.sort()

// print exclude and input sets for logging
input.each{ f -> log.finer("Input: $f") }
(originalInputSet - input).each{ f -> log.finest("Exclude: $f") }

// early abort if there is nothing to do
if (input.size() == 0) die("No files selected for processing")



// group episodes/movies and rename according to XBMC standards
def groups = input.groupBy{ f ->
	// skip auto-detection if possible
	if (forceIgnore(f))
		return []
	if (music && forceAudio(f)) // process audio only if music mode is enabled
		return [music: f.dir.name]
	if (forceMovie(f))
		return [mov:   detectMovie(f, false)]
	if (forceSeries(f))
		return [tvs:   detectSeriesName(f, true, false) ?: detectSeriesName(input.findAll{ s -> f.dir == s.dir && s.isVideo() }, true, false)]
	if (forceAnime(f))
		return [anime: detectSeriesName(f, false, true) ?: detectSeriesName(input.findAll{ s -> f.dir == s.dir && s.isVideo() }, false, true)]
	
	
	def tvs = detectSeriesName(f, true, false)
	def mov = detectMovie(f, false)
	log.fine("$f.name [series: $tvs, movie: $mov]")
	
	// DECIDE EPISODE VS MOVIE (IF NOT CLEAR)
	if (tvs && mov) {
		def norm = { s -> s.ascii().normalizePunctuation().lower().space(' ') }
		def dn = norm(guessMovieFolder(f)?.name ?: '')
		def fn = norm(f.nameWithoutExtension)
		def sn = norm(tvs)
		def mn = norm(mov.name)
		def my = mov.year as String
		
		/*
		println '--- EPISODE FILTER (POS) ---'
		println parseEpisodeNumber(fn, true) || parseDate(fn)
		println ([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)
		println (fn.after(sn) ==~ /.{0,3} - .+/ && matchMovie(fn) == null)
		println 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
		println '--- EPISODE FILTER (NEG) ---'
		println (mn == fn)
		println (mov.year >= 1950 && f.listPath().reverse().take(3).find{ it.name.contains(my) && parseEpisodeNumber(it.name.after(my), false) == null })
		println (mn =~ sn && [dn, fn].find{ it =~ /\b(19|20)\d{2}\b/ && parseEpisodeNumber(it.after(/\b(19|20)\d{2}\b/), false) == null })
		println '--- MOVIE FILTER (POS) ---'
		println (fn.contains(mn) && parseEpisodeNumber(fn.after(mn), false) == null)
		println (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)
		println ([dn, fn].find{ it =~ mn && !(it.after(mn) =~ /\b\d{1,3}\b/) && (it.getSimilarity(mn) > 0.2 + it.getSimilarity(sn)) } != null)
		println (detectMovie(f, true) && [dn, fn].find{ it =~ /(19|20)\d{2}/ } != null)
		*/
		
		// S00E00 | 2012.07.21 | One Piece 217 | Firefly - Serenity | [Taken 1, Taken 2, Taken 3, Taken 4, ..., Taken 10]
		if ((parseEpisodeNumber(fn, true) || parseDate(fn) || ([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) || (fn.after(sn) ==~ /.{0,3} - .+/ && matchMovie(fn) == null) || 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 || mov.year < 1900) && !( (mn == fn) || (mov.year >= 1950 && f.listPath().reverse().take(3).find{ it.name.contains(my) && parseEpisodeNumber(it.name.after(my), false) == null }) || (mn =~ sn && [dn, fn].find{ it =~ /\b(19|20)\d{2}\b/ && parseEpisodeNumber(it.after(/\b(19|20)\d{2}\b/), false) == null }) ) ) {
			log.fine("Exclude Movie: $mov")
			mov = null
		} else if ((fn.contains(mn) && parseEpisodeNumber(fn.after(mn), false) == null) || (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) || ([dn, fn].find{ it =~ mn && !(it.after(mn) =~ /\b\d{1,3}\b/) && (it.getSimilarity(mn) > 0.2 + it.getSimilarity(sn)) } != null) || (detectMovie(f, false) && [dn, fn].find{ it =~ /(19|20)\d{2}|(?i:CD)[1-9]/ } != null)) {
			log.fine("Exclude Series: $tvs")
			tvs = null
		}
	}
	
	// CHECK CONFLICT
	if (((mov && tvs) || (!mov && !tvs))) {
		if (failOnError) {
			fail("Media detection failed")
		} else {
			log.fine("Unable to differentiate: [$f.name] => [$tvs] VS [$mov]")
			return [tvs: null, mov: null, anime: null]
		}
	}
	
	return [tvs: tvs, mov: mov, anime: null]
}

// group entries by unique tvs/mov descriptor
groups = groups.groupBy{ group, files -> group.collectEntries{ type, query -> [type, query ? query.toString().ascii().normalizePunctuation().lower() : null] } }.collectEntries{ group, maps -> [group, maps.values().flatten()] }

// log movie/series/anime detection results
groups.each{ group, files -> log.finest("Group: $group => ${files*.name}") }

// process each batch
groups.each{ group, files ->
	// fetch subtitles (but not for anime)
	if (group.anime == null && subtitles != null && files.findAll{ it.isVideo() }.size() > 0) {
		subtitles.each{ languageCode ->
			def subtitleFiles = getMissingSubtitles(file:files, lang:languageCode, strict:true, output:'srt', encoding:'UTF-8', format:'MATCH_VIDEO_ADD_LANGUAGE_TAG') ?: []
			files += subtitleFiles
			input += subtitleFiles // make sure subtitles are added to the exclude list and other post processing operations
			tempFiles += subtitleFiles // if downloaded for temporarily extraced files delete later
		}
	}
	
	// EPISODE MODE
	if ((group.tvs || group.anime) && !group.mov) {
		// choose series / anime config
		def config = group.tvs ? [name:group.tvs,   format:format.tvs,   db:'TheTVDB']
		                       : [name:group.anime, format:format.anime, db:'AniDB']
		def dest = rename(file: files, format: config.format, db: config.db)
		if (dest && artwork) {
			dest.mapByFolder().each{ dir, fs ->
				def hasSeasonFolder = (config.format =~ /(?i)Season/)
				def sxe = fs.findResult{ eps -> parseEpisodeNumber(eps) }
				def seriesName = detectSeriesName(fs, true, false)
				def options = TheTVDB.search(seriesName, _args.locale)
				if (options.isEmpty()) {
					log.warning "TV Series not found: $config.name"
					return
				}
				def series = options.sortBySimilarity(seriesName, { s -> s.name }).get(0)
				log.fine "Fetching series artwork for [$series] to [$dir]"
				fetchSeriesArtworkAndNfo(hasSeasonFolder ? dir.dir : dir, dir, series, sxe && sxe.season > 0 ? sxe.season : 1, false, _args.locale)
			}
		}
		if (dest == null && failOnError) {
			fail("Failed to rename series: $config.name")
		}
	}
	
	// MOVIE MODE
	else if (group.mov && !group.tvs && !group.anime) {
		def dest = rename(file:files, format:format.mov, db:'TheMovieDB')
		if (dest && artwork) {
			dest.mapByFolder().each{ dir, fs ->
				def movieFile = fs.findAll{ it.isVideo() || it.isDisk() }.sort{ it.length() }.reverse().findResult{ it }
				if (movieFile != null) {
					def movie = detectMovie(movieFile, false)
					log.fine "Fetching movie artwork for [$movie] to [$dir]"
					fetchMovieArtworkAndNfo(dir, movie, movieFile, extras, false, _args.locale)
				}
			}
		}
		if (dest == null && failOnError) {
			fail("Failed to rename movie: $group.mov")
		}
	}
	
	// MUSIC MODE
	else if (group.music) {
		def dest = rename(file:files, format:format.music, db:'ID3 Tags')
		if (dest == null && failOnError) {
			fail("Failed to rename music: $group.music")
		}
	}
}


// ---------- POST PROCESSING ---------- //

// deal with remaining files that cannot be sorted automatically
if (unsorted) {
	def unsortedFiles = (input - getRenameLog().keySet())
	if (unsortedFiles.size() > 0) {
		log.info "Processing ${unsortedFiles.size()} unsorted files"
		rename(map: unsortedFiles.collectEntries{ original ->
			[original, new File(_args.output, getMediaInfo(file: original, format: format.unsorted))]
		})
	}
}

// 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)
	}
}


// ---------- REPORTING ---------- //


if (getRenameLog().size() > 0) {
	
	// messages used for xbmc / plex / pushover notifications
	def getNotificationTitle = { "FileBot finished processing ${getRenameLog().values().findAll{ !it.isSubtitle() }.size()} files" }.memoize()
	def getNotificationMessage = { prefix = '• ', postfix = '\n' -> tryQuietly{ ut_title } ?: (input.any{ !it.isSubtitle() } ? input.findAll{ !it.isSubtitle() } : input).collect{ relativeInputPath(it) as File }*.getRoot()*.getNameWithoutExtension().unique().sort{ it.toLowerCase() }.collect{ prefix + it }.join(postfix).trim() }.memoize()
	
	// make XMBC scan for new content and display notification message
	if (xbmc) {
		xbmc.each{ host ->
			log.info "Notify XBMC: $host"
			tryLogCatch{
				showNotification(host, 9090, getNotificationTitle(), getNotificationMessage(), 'http://app.filebot.net/icon.png')
				scanVideoLibrary(host, 9090)
			}
		}
	}
	
	// make Plex scan for new content
	if (plex) {
		plex.each{ instance ->
			log.info "Notify Plex: ${instance.host}"
			tryLogCatch {
				refreshPlexLibrary(instance.host, 32400, instance.token)
			}
		}
	}
	
	// mark episodes as 'acquired'
	if (myepisodes) {
		log.info 'Update MyEpisodes'
		tryLogCatch {
			executeScript('update-mes', [login:myepisodes.join(':'), addshows:true], getRenameLog().values())
		}
	}
	
	if (pushover) {
		log.info 'Sending Pushover notification'
		tryLogCatch {
			Pushover(pushover[0], pushover.length == 1 ? 'wcckDz3oygHSU2SdIptvnHxJ92SQKK' : pushover[1]).send(getNotificationTitle(), getNotificationMessage())
		}
	}
	
	// messages used for email / pushbullet reports
	def getReportSubject = { getNotificationMessage('', ', ') }
	def getReportTitle = { '[FileBot] ' + getReportSubject() }
	def getReportMessage = { 
		def renameLog = getRenameLog()
		'''<!DOCTYPE html>\n''' + XML {
			html {
				head {
					meta(charset:'UTF-8')
					style('''
						p{font-family:Arial,Helvetica,sans-serif}
						p b{color:#07a}
						hr{border-style:dashed;border-width:1px 0 0 0;border-color:lightgray}
						small{color:#d3d3d3;font-size:xx-small;font-weight:normal;font-family:Arial,Helvetica,sans-serif}
						table a:link{color:#666;font-weight:bold;text-decoration:none}
						table a:visited{color:#999;font-weight:bold;text-decoration:none}
						table a:active,table a:hover{color:#bd5a35;text-decoration:underline}
						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}
						table th{padding:15px;border-top:1px solid #fafafa;border-bottom:1px solid #e0e0e0;background:#ededed}
						table th{text-align:center;padding-left:20px}
						table tr:first-child th:first-child{border-top-left-radius:3px}
						table tr:first-child th:last-child{border-top-right-radius:3px}
						table tr{text-align:left;padding-left:20px}
						table td:first-child{text-align:left;padding-left:20px;border-left:0}
						table td{padding:15px;border-top:1px solid #fff;border-bottom:1px solid #e0e0e0;border-left:1px solid #e0e0e0;background:#fafafa;white-space:nowrap}
						table tr.even td{background:#f6f6f6}
						table tr:last-child td{border-bottom:0}
						table tr:last-child td:first-child{border-bottom-left-radius:3px}
						table tr:last-child td:last-child{border-bottom-right-radius:3px}
						table tr:hover td{background:#f2f2f2}
					''')
					title(getReportTitle())
				}
				body {
					p {
						mkp.yield("FileBot finished processing ")
						b(getReportSubject())
						mkp.yield(" (${renameLog.size()} files).")
					}
					hr(); table {
						tr { th('Original Name'); th('New Name'); th('New Location') }
						renameLog.each{ from, to ->
							tr { [from.name, to.name, to.parent].each{ cell -> td(cell) } }
						}
					}
					hr(); small("// Generated by ${Settings.getApplicationIdentifier()} on ${InetAddress.localHost.hostName} at ${now.dateTimeString}")
				}
			}
		}
	}
	
	// store processing report
	if (storeReport) {
		def reportFolder = new File(Settings.getApplicationFolder(), 'reports').getCanonicalFile()
		def reportFile = getReportMessage().saveAs(new File(reportFolder, "AMC ${now.format('''[yyyy-MM-dd HH'h'mm'm']''')} ${getReportSubject().take(50).trim()}.html".validateFileName()))
		log.finest("Saving report as ${reportFile}")
	}
	
	// send pushbullet report
	if (pushbullet) {
		log.info 'Sending PushBullet report'
		tryLogCatch {
			PushBullet(pushbullet).sendFile(getNotificationTitle(), getReportMessage(), 'text/html', getNotificationMessage(), tryQuietly{ mailto })
		}
	}
	
	// send email report
	if (gmail || mail) {
		tryLogCatch {
			sendEmailReport(getReportTitle(), getReportMessage(), 'text/html')
		}
	}
}


// ---------- CLEAN UP ---------- //


// clean up temporary files that may be left behind after extraction
if (deleteAfterExtract) {
	extractedArchives.each{ a ->
		log.finest("Delete archive $a")
		a.delete()
		a.dir.listFiles().toList().findAll{ v -> v.name.startsWith(a.nameWithoutExtension) && v.extension ==~ /r\d+/ }.each{ v ->
			log.finest("Delete archive volume $v")
			v.delete()
		}
	}
}

// clean empty folders, clutter files, etc after move
if (clean) {
	if (['COPY', 'HARDLINK'].find{ it.equalsIgnoreCase(_args.action) } && tempFiles.size() > 0) {
		log.info 'Clean temporary extracted files'
		// delete extracted files
		tempFiles.findAll{ it.isFile() }.sort().each{
			log.finest "Delete $it"
			it.delete()
		}
		// delete remaining empty folders
		tempFiles.findAll{ it.isDirectory() }.sort().reverse().each{
			log.finest "Delete $it"
			if (it.getFiles().isEmpty()) it.deleteDir()
		}
	}
	
	// deleting remaining files only makes sense after moving files
	if ('MOVE'.equalsIgnoreCase(_args.action)) {
		def cleanerInput = !args.empty ? args : ut_kind == 'multi' && ut_dir ? [ut_dir as File] : []
		cleanerInput = cleanerInput.findAll{ f -> f.exists() }
		if (cleanerInput.size() > 0) {
			log.info 'Clean clutter files and empty folders'
			executeScript('cleaner', args.empty ? [root:true, ignore: ignore] : [root:false, ignore: ignore], cleanerInput)
		}
	}
}


if (getRenameLog().size() == 0) fail("Finished without processing any files")
My htpc script:

Code: Select all

import static groovy.json.StringEscapeUtils.*


/**
 * XBMC helper functions
 */
def scanVideoLibrary(host, port) {
	tryLogCatch {
		telnet(host, port) { writer, reader ->
			writer.println("""{"jsonrpc":"2.0","method":"VideoLibrary.Scan","id":1}""")
		}
	}
}

def showNotification(host, port, title, message, image) {
	tryLogCatch {
		telnet(host, port) { writer, reader ->
			writer.println("""{"jsonrpc":"2.0","method":"GUI.ShowNotification","params":{"title":"${escapeJavaScript(title)}","message":"${escapeJavaScript(message)}", "image":"${escapeJavaScript(image)}"},"id":1}""")
		}
	}
}



/**
 * Plex helpers
 */
def refreshPlexLibrary(server, port = 32400, token = null) {
	tryLogCatch {
		// use HTTPS if hostname is specified, use HTTP if IP is specified
		def protocol = server.split('[.]').length == 4 ? 'http' : 'https'
		def url = "$protocol://$server:$port/library/sections/all/refresh"
		if (token) {
			url += "?X-Plex-Token=$token"
		}
		log.finest "GET: $url"
		new URL(url).get()
	}
}



/**
 * TheTVDB artwork/nfo helpers
 */
def fetchSeriesBanner(outputFile, series, bannerType, bannerType2, season, override, locale) {
	if (outputFile.exists() && !override) {
		log.finest "Banner already exists: $outputFile"
		return outputFile
	}

	// select and fetch banner
	def banner = [locale, null].findResult { TheTVDB.getBanner(series, [BannerType:bannerType, BannerType2:bannerType2, Season:season, Language:it]) }
	if (banner == null) {
		log.finest "Banner not found: $outputFile / $bannerType:$bannerType2"
		return null
	}
	log.finest "Fetching $outputFile => $banner"
	return banner.url.saveAs(outputFile)
}

def fetchSeriesFanart(outputFile, series, type, season, override, locale) {
	if (outputFile.exists() && !override) {
		log.finest "Fanart already exists: $outputFile"
		return outputFile
	}

	def fanart = [locale, null].findResult{ lang -> FanartTV.getSeriesArtwork(series.seriesId).find{ type == it.type && (season == null || season == it.season) && (lang == null || lang == it.language) }}
	if (fanart == null) {
		log.finest "Fanart not found: $outputFile / $type"
		return null
	}
	log.finest "Fetching $outputFile => $fanart"
	return fanart.url.saveAs(outputFile)
}

def fetchSeriesNfo(outputFile, seriesInfo, override, locale) {
	def i = seriesInfo
	XML {
		tvshow {
			title(i.name)
			sorttitle([i.name, i.firstAired as String].findAll{ it?.length() > 0 }.findResults{ it.sortName('$2') }.join(' :: '))
			year(i.firstAired?.year)
			rating(i.rating)
			votes(i.ratingCount)
			plot(i.overview)
			runtime(i.runtime)
			mpaa(i.contentRating)
			id(i.id)
			episodeguide {
				url(cache:"${i.id}.xml", "http://www.thetvdb.com/api/1D62F2F90030C444/series/${i.id}/all/${locale.language}.zip")
			}
			i.genres?.each{
				genre(it)
			}
			thumb(i.bannerUrl)
			premiered(i.firstAired)
			status(i.status)
			studio(i.network)
			i.actors?.each{ n ->
				actor {
					name(n)
				}
			}
			tvdb(id:i.id, "http://www.thetvdb.com/?tab=series&id=${i.id}")
		}
	}
	.saveAs(outputFile)
}

def fetchSeriesArtworkAndNfo(seriesDir, seasonDir, series, season, override = false, locale = Locale.ENGLISH) {
	tryLogCatch {
		// fetch nfo
		//def seriesInfo = TheTVDB.getSeriesInfo(series, locale)
		//fetchSeriesNfo(seriesDir.resolve('tvshow.nfo'), seriesInfo, override, locale)

		// fetch series banner, fanart, posters, etc
		['680x1000', null].findResult{ fetchSeriesBanner(seriesDir.resolve('cover.jpg'), series, 'poster', it, null, override, locale) }
		//['graphical', null].findResult{ fetchSeriesBanner(seriesDir.resolve('banner.jpg'), series, 'series', it, null, override, locale) }

		// fetch highest resolution fanart
		//['1920x1080', '1280x720', null].findResult{ fetchSeriesBanner(seriesDir.resolve('fanart.jpg'), series, 'fanart', it, null, override, locale) }

		// fetch season banners
		if (seasonDir != seriesDir) {
			fetchSeriesBanner(seasonDir.resolve('cover.jpg'), series, 'season', 'season', season, override, locale)
			//fetchSeriesBanner(seasonDir.resolve('banner.jpg'), series, 'season', 'seasonwide', season, override, locale)
			
			// folder image (resuse series poster if possible)
			//copyIfPossible(seasonDir.resolve('poster.jpg'), seasonDir.resolve('folder.jpg'))
		}

		// fetch fanart
		//fetchSeriesFanart(seriesDir.resolve('clearart.png'), series, 'clearart', null, override, locale)
		//fetchSeriesFanart(seriesDir.resolve('logo.png'), series, 'clearlogo', null, override, locale)
		//fetchSeriesFanart(seriesDir.resolve('landscape.jpg'), series, 'tvthumb', null, override, locale)

		// fetch season fanart
		//if (seasonDir != seriesDir) {
		//	fetchSeriesFanart(seasonDir.resolve('landscape.jpg'), series, 'seasonthumb', season, override, locale)
		//}

		// folder image (reuse series poster if possible)
		//copyIfPossible(seriesDir.resolve('cover.jpg'), seriesDir.resolve('folder.jpg'))
	}
}



/**
 * TheMovieDB artwork/nfo helpers
 */
def fetchMovieArtwork(outputFile, movieInfo, category, override, locale) {
	if (outputFile.exists() && !override) {
		log.finest "Artwork already exists: $outputFile"
		return outputFile
	}

	// select and fetch artwork
	def artwork = TheMovieDB.getArtwork(movieInfo.id as String)
	def selection = [locale.language, 'en', null].findResult{ l -> artwork.find{ (l == it.language || l == null) && it.category == category } }
	if (selection == null) {
		log.finest "Artwork not found: $outputFile"
		return null
	}
	log.finest "Fetching $outputFile => $selection"
	return selection.url.saveAs(outputFile)
}

def fetchAllMovieArtwork(outputFolder, movieInfo, category, override, locale) {
	// select and fetch artwork
	def artwork = TheMovieDB.getArtwork(movieInfo.id as String)
	def selection = [locale.language, 'en', null].findResults{ l -> artwork.findAll{ (l == it.language || l == null) && it.category == category } }.flatten().findAll{ it?.url }.unique()
	if (selection == null) {
		log.finest "Artwork not found: $outputFolder"
		return null
	}
	selection.eachWithIndex{ s, i ->
		def outputFile = new File(outputFolder, "$category-${(i+1).pad(2)}.jpg")
		if (outputFile.exists() && !override) {
			log.finest "Artwork already exists: $outputFile"
		} else {
			log.finest "Fetching $outputFile => $s"
			s.url.saveAs(outputFile)
		}
	}
}

def fetchMovieFanart(outputFile, movieInfo, type, diskType, override, locale) {
	if (outputFile.exists() && !override) {
		log.finest "Fanart already exists: $outputFile"
		return outputFile
	}

	def fanart = [locale, null].findResult{ lang -> FanartTV.getMovieArtwork(movieInfo.id).find{ type == it.type && (diskType == null || diskType == it.diskType) && (lang == null || lang == it.language) }}
	if (fanart == null) {
		log.finest "Fanart not found: $outputFile / $type"
		return null
	}
	log.finest "Fetching $outputFile => $fanart"
	return fanart.url.saveAs(outputFile)
}

def fetchMovieNfo(outputFile, movieInfo, movieFile, override) {
	def i = movieInfo
	def mi = tryLogCatch{ movieFile?.isFile() ? MediaInfo.snapshot(movieFile) : null }
	XML {
		movie {
			title(i.name)
			originaltitle(i.originalName)
			sorttitle([i.collection, i.name, i.released as String].findAll{ it?.length() > 0 }.findResults{ it.sortName('$2') }.join(' :: '))
			set(i.collection)
			year(i.released?.year)
			rating(i.rating)
			votes(i.votes)
			mpaa(i.certification)
			id('tt' + (i.imdbId ?: 0).pad(7))
			plot(i.overview)
			tagline(i.tagline)
			runtime(i.runtime)
			i.genres.each{
				genre(it)
			}
			i.productionCountries.each{
				country(it)
			}
			i.productionCompanies.each{
				studio(it)
			}
			i.people.each{ p ->
				if (p.director) {
					director(p.name)
				} else if (p.writer) {
					writer(p.name)
				} else if (p.actor) { 
					actor {
						name(p.name)
						role(p.character)
					}
				} else if (p.job ==~ /Writer|Screenplay|Story|Novel/) {
					credits("$p.name ($p.job)")
				}
			}
			/** <trailer> element not supported due to lack of specification on acceptable values for both Plex and Kodi
			i.trailers.each{ t ->
				t.sources.each { s, v ->
					trailer(type:t.type, name:t.name, size:s, v)
				}
			}
			**/
			fileinfo {
				streamdetails {
					mi?.each { kind, streams ->
						def section = kind.toString().toLowerCase()
						streams.each { s ->
							if (section == 'video') {
								video {
									codec((s.'Encoded_Library/Name' ?: s.'CodecID/Hint' ?: s.'Format').replaceAll(/[ ].+/, '').trim())
									aspect(s.'DisplayAspectRatio')
									width(s.'Width')
									height(s.'Height')
								}
							}
							if (section == 'audio') {
								audio {
									codec((s.'CodecID/Hint' ?: s.'Format').replaceAll(/\p{Punct}/, '').trim())
									language(s.'Language/String3')
									channels(s.'Channel(s)')
								}
							}
							if (section == 'text') {
								subtitle { language(s.'Language/String3') }
							}
						}
					}
				}
			}
			imdb(id:"tt" + (i.imdbId ?: 0).pad(7), "http://www.imdb.com/title/tt" + (i.imdbId ?: 0).pad(7))
			tmdb(id:i.id, "http://www.themoviedb.org/movie/${i.id}")
		}
	}
	.saveAs(outputFile)
}

def fetchMovieArtworkAndNfo(movieDir, movie, movieFile = null, extras = false, override = false, locale = Locale.ENGLISH) {
	tryLogCatch {
		def movieInfo = TheMovieDB.getMovieInfo(movie, locale, true)

		// fetch nfo
		//fetchMovieNfo(movieDir.resolve('movie.nfo'), movieInfo, movieFile, override)

		// generate url files
		//if (extras) {
		//	[[db:'imdb', id:movieInfo.imdbId, url:'http://www.imdb.com/title/tt' + (movieInfo.imdbId ?: 0).pad(7)], [db:'tmdb', id:movieInfo.id, url:"http://www.themoviedb.org/movie/${movieInfo.id}"]].each{
		//		if (it.id > 0) {
		//			def content = "[InternetShortcut]\nURL=${it.url}\n"
		//			content.saveAs(movieDir.resolve("${it.db}.url"))
		//		}
		//	}
		//}

		// fetch series banner, fanart, posters, etc
		fetchMovieArtwork(movieDir.resolve('cover.jpg'), movieInfo, 'posters', override, locale)
		//fetchMovieArtwork(movieDir.resolve('fanart.jpg'), movieInfo, 'backdrops', override, locale)

		//fetchMovieFanart(movieDir.resolve('clearart.png'), movieInfo, 'movieart', null, override, locale)
		//fetchMovieFanart(movieDir.resolve('logo.png'), movieInfo, 'movielogo', null, override, locale)
		//['bluray', 'dvd', null].findResult { diskType -> fetchMovieFanart(movieDir.resolve('disc.png'), movieInfo, 'moviedisc', diskType, override, locale) }

		//if (extras) {
		//	fetchAllMovieArtwork(movieDir.resolve('backdrops'), movieInfo, 'backdrops', override, locale)
		//}

		// folder image (reuse movie poster if possible)
		//copyIfPossible(movieDir.resolve('cover.jpg'), movieDir.resolve($movie'.jpg'))

	}
}

def copyIfPossible(File src, File dst) {
	if (src.exists() && !dst.exists()) {
		src.copyAs(dst)
	}
}
Last edited by jtb2586 on 09 Apr 2016, 11:36, edited 1 time in total.
jtb2586
Posts: 3
Joined: 28 Oct 2015, 16:56

Re: AMC HTPC advanced CLI options

Post by jtb2586 »

Is there nobody that can help me rename the cover.jpg to the movie file name?
kim
Power User
Posts: 1251
Joined: 15 May 2014, 16:17

Re: AMC advanced script options rename cover.jpg

Post by kim »

Just made this work ;)

put in LOCAL htpc file

Code: Select all

		// fetch series banner, fanart, posters, etc
		fetchMovieArtwork(movieDir.resolve('movieFile.nameWithoutExtension+'.jpg'), movieInfo, 'posters', override, locale)
		fetchMovieArtwork(movieDir.resolve('fanart.jpg'), movieInfo, 'backdrops', override, locale)
jtb2586
Posts: 3
Joined: 28 Oct 2015, 16:56

Re: AMC advanced script options rename cover.jpg

Post by jtb2586 »

Thank you, it was driving me crazy, finaly got it to work,
Post Reply