AMC advanced script options rename cover.jpg
Posted: 28 Oct 2015, 17:07
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
saves it as cover.jpg, what argument should i use so it saves it with the movie name?
My amc script:
My htpc script:
The line in htpc script
Code: Select all
fetchMovieArtwork(movieDir.resolve('cover.jpg'), movieInfo, 'posters', override, locale)
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")
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)
}
}