+ move shared scripts to github
This commit is contained in:
parent
4843bb55d2
commit
ad3d34eeba
|
@ -656,15 +656,6 @@
|
|||
</target>
|
||||
|
||||
|
||||
<target name="upload-scripts" depends="login">
|
||||
<scp todir="${sf.user}:${sf.password}@${deploy.website}" trust="yes" verbose="true" sftp="true">
|
||||
<fileset dir="${dir.website}">
|
||||
<include name="scripts/**" />
|
||||
</fileset>
|
||||
</scp>
|
||||
</target>
|
||||
|
||||
|
||||
<target name="deploy-test-package" depends="svn-update, fatjar, login">
|
||||
<!-- deploy fatjar -->
|
||||
<input message="Mark:" addproperty="mark" />
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
options +indexes
|
||||
|
||||
redirect 301 /scripts/utorrent-postprocess.groovy /scripts/amc.groovy
|
|
@ -1,385 +0,0 @@
|
|||
// 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"
|
||||
def input = []
|
||||
def failOnError = _args.conflict == 'fail'
|
||||
|
||||
// print input parameters
|
||||
_args.bindings?.each{ _log.fine("Parameter: $it.key = $it.value") }
|
||||
args.each{ _log.fine("Argument: $it") }
|
||||
args.findAll{ !it.exists() }.each{ throw new Exception("File not found: $it") }
|
||||
|
||||
// check user-defined pre-condition
|
||||
if (tryQuietly{ !(ut_state ==~ ut_state_allow) }) {
|
||||
throw new Exception("Invalid state: ut_state = $ut_state (expected $ut_state_allow)")
|
||||
}
|
||||
|
||||
// check ut mode vs standalone mode
|
||||
if ((args.size() > 0 && (tryQuietly{ ut_dir }?.size() > 0 || tryQuietly{ ut_file }?.size() > 0)) || (args.size() == 0 && (tryQuietly{ ut_dir } == null && tryQuietly{ ut_file } == null))) {
|
||||
throw new Exception("Conflicting arguments: pass in either file arguments or ut_dir/ut_file parameters but not both")
|
||||
}
|
||||
|
||||
// enable/disable features as specified via --def parameters
|
||||
def music = tryQuietly{ music.toBoolean() }
|
||||
def subtitles = tryQuietly{ subtitles.toBoolean() ? ['en'] : subtitles.split(/[ ,|]+/).findAll{ it.length() >= 2 } }
|
||||
def artwork = tryQuietly{ artwork.toBoolean() }
|
||||
def backdrops = tryQuietly{ backdrops.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(/[ ,|]+/) }
|
||||
|
||||
// extra options, myepisodes updates and email notifications
|
||||
def deleteAfterExtract = tryQuietly{ deleteAfterExtract.toBoolean() }
|
||||
def excludeList = tryQuietly{ new File(_args.output, excludeList) }
|
||||
def myepisodes = tryQuietly{ myepisodes.split(':', 2) }
|
||||
def gmail = tryQuietly{ gmail.split(':', 2) }
|
||||
def pushover = tryQuietly{ pushover.toString() }
|
||||
|
||||
// user-defined filters
|
||||
def minFileSize = tryQuietly{ minFileSize.toLong() }; if (minFileSize == null) { minFileSize = 0 };
|
||||
|
||||
// series/anime/movie format expressions
|
||||
def format = [
|
||||
tvs: tryQuietly{ seriesFormat } ?: '''TV Shows/{n}/{episode.special ? "Special" : "Season "+s.pad(2)}/{n} - {episode.special ? "S00E"+special.pad(2) : s00e00} - {t.replaceAll(/[`´‘’ʻ]/, "'").replaceAll(/[!?.]+$/).replacePart(', Part $1')}{".$lang"}''',
|
||||
anime: tryQuietly{ animeFormat } ?: '''Anime/{n}/{n} - {sxe} - {t.replaceAll(/[!?.]+$/).replaceAll(/[`´‘’ʻ]/, "'").replacePart(', Part $1')}''',
|
||||
mov: tryQuietly{ movieFormat } ?: '''Movies/{n} ({y})/{n} ({y}){" CD$pi"}{".$lang"}''',
|
||||
music: tryQuietly{ musicFormat } ?: '''Music/{n}/{album+'/'}{pi.pad(2)+'. '}{artist} - {t}'''
|
||||
]
|
||||
|
||||
|
||||
// force movie/series/anime logic
|
||||
def forceMovie(f) {
|
||||
tryQuietly{ ut_label } =~ /^(?i:Movie|Couch.Potato)/ || f.dir.path =~ /\b(?i:Movies)\b/ || f.path =~ /(?<=tt)\\d{7}/ || tryQuietly{ f.metadata?.object?.class.name =~ /Movie/ }
|
||||
}
|
||||
|
||||
def forceSeries(f) {
|
||||
tryQuietly{ ut_label } =~ /^(?i:TV|Kids.Shows)/ || f.dir.path =~ /\b(?i:TV.Shows)\b/ || parseEpisodeNumber(f.path) || parseDate(f.path) || f.path =~ /(?i:Season)\D?[0-9]{1,2}\D/ || tryQuietly{ f.metadata?.object?.class.name =~ /Episode/ }
|
||||
}
|
||||
|
||||
def forceAnime(f) {
|
||||
tryQuietly{ ut_label } =~ /^(?i:Anime)/ || f.dir.path =~ /\b(?i:Anime)\b/ || (f.isVideo() && (f.name =~ "[\\(\\[]\\p{XDigit}{8}[\\]\\)]" || getMediaInfo(file:f, format:'''{media.AudioLanguageList} {media.TextCodecList}''').tokenize().containsAll(['Japanese', 'ASS'])))
|
||||
}
|
||||
|
||||
def forceIgnore(f) {
|
||||
tryQuietly{ ut_label } =~ /^(?i:ebook|other|ignore)/ || f.path =~ tryQuietly{ ignore }
|
||||
}
|
||||
|
||||
|
||||
// specify how to resolve input folders, e.g. grab files from all folders except disk folders
|
||||
def resolveInput(f) {
|
||||
if (f.isDirectory() && !f.isDisk())
|
||||
return f.listFiles().toList().findResults{ resolveInput(it) }
|
||||
else
|
||||
return f
|
||||
}
|
||||
|
||||
// collect input fileset as specified by the given --def parameters
|
||||
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)) {
|
||||
input += new File(ut_dir, ut_file) // single-file torrent
|
||||
} else {
|
||||
input += resolveInput(ut_dir as File) // multi-file torrent
|
||||
}
|
||||
} else {
|
||||
// assume we're called normally with arguments
|
||||
input += args.findResults{ resolveInput(it) }
|
||||
}
|
||||
|
||||
|
||||
// flatten nested file structure
|
||||
input = input.flatten()
|
||||
|
||||
// extract archives (zip, rar, etc) that contain at least one video file
|
||||
def extractedArchives = []
|
||||
def tempFiles = []
|
||||
input = input.flatten{ f ->
|
||||
if (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: 'skip', 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
|
||||
}
|
||||
|
||||
// sanitize input
|
||||
input = input.findAll{ it?.exists() }.collect{ it.canonicalFile }.unique()
|
||||
|
||||
// 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 -> !(f.path =~ /\b(?i:sample|trailer|extras|music.video|scrapbook|behind.the.scenes|extended.scenes|deleted.scenes|s\d{2}c\d{2}|mini.series)\b/ || (f.isFile() && f.length() < minFileSize)) }
|
||||
|
||||
// check and update exclude list (e.g. to make sure files are only processed once)
|
||||
if (excludeList) {
|
||||
// check excludes from previous runs
|
||||
def excludePathSet = excludeList.exists() ? excludeList.text.split('\n') as HashSet : []
|
||||
input = input.findAll{ f -> !excludePathSet.contains(f.path) }
|
||||
|
||||
// update excludes with input of this run
|
||||
excludePathSet += input
|
||||
excludePathSet.join('\n').saveAs(excludeList)
|
||||
}
|
||||
|
||||
// print input fileset
|
||||
input.each{ f -> _log.finest("Input: $f") }
|
||||
|
||||
// artwork/nfo utility
|
||||
if (artwork || xbmc || plex) { include('fn:lib/htpc') }
|
||||
|
||||
// group episodes/movies and rename according to XBMC standards
|
||||
def groups = input.groupBy{ f ->
|
||||
// skip auto-detection if possible
|
||||
if (forceIgnore(f))
|
||||
return []
|
||||
if (f.isAudio() && !f.isVideo()) // PROCESS MUSIC FOLDER BY FOLDER
|
||||
return [music: f.dir.name]
|
||||
if (forceMovie(f))
|
||||
return [mov: detectMovie(f, false)]
|
||||
if (forceSeries(f))
|
||||
return [tvs: detectSeriesName(f) ?: detectSeriesName(f.dir.listFiles{ it.isVideo() })]
|
||||
if (forceAnime(f))
|
||||
return [anime: detectSeriesName(f) ?: detectSeriesName(f.dir.listFiles{ it.isVideo() })]
|
||||
|
||||
|
||||
def tvs = detectSeriesName(f)
|
||||
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)
|
||||
|
||||
/**
|
||||
println '--- EPISODE FILTER (POS) ---'
|
||||
println parseEpisodeNumber(fn, true) || parseDate(fn)
|
||||
println ([dn, fn].find{ it =~ sn && matchMovie(it, true) == null } && (parseEpisodeNumber(stripReleaseInfo(fn.after(sn), false), false) || fn.after(sn) =~ /\D\d{1,2}\D{1,3}\d{1,2}\D/) && matchMovie(fn, true) == null)
|
||||
println (fn.after(sn) ==~ /.{0,3} - .+/ && matchMovie(fn, true) == 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 (mov.year >= 1950 && f.listPath().reverse().take(3).find{ it.name =~ mov.year })
|
||||
println (mn =~ sn && [dn, fn].find{ it =~ /(19|20)\d{2}/ })
|
||||
println '--- MOVIE FILTER (POS) ---'
|
||||
println (similarity(mn, 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/) && (similarity(it, mn) > 0.2 + similarity(it, 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, true) == null } && (parseEpisodeNumber(stripReleaseInfo(fn.after(sn), false), false) || fn.after(sn) =~ /\D\d{1,2}\D{1,3}\d{1,2}\D/) && matchMovie(fn, true) == null) || (fn.after(sn) ==~ /.{0,3} - .+/ && matchMovie(fn, true) == 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) && !( (mov.year >= 1950 && f.listPath().reverse().take(3).find{ it.name =~ mov.year }) || (mn =~ sn && [dn, fn].find{ it =~ /(19|20)\d{2}/ }) ) ) {
|
||||
_log.fine("Exclude Movie: $mov")
|
||||
mov = null
|
||||
} else if ((similarity(mn, 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/) && (similarity(it, mn) > 0.2 + similarity(it, sn)) } != null) || (detectMovie(f, true) && [dn, fn].find{ it =~ /(19|20)\d{2}/ } != null)) {
|
||||
_log.fine("Exclude Series: $tvs")
|
||||
tvs = null
|
||||
}
|
||||
}
|
||||
|
||||
// CHECK CONFLICT
|
||||
if (((mov && tvs) || (!mov && !tvs))) {
|
||||
if (failOnError) {
|
||||
throw new Exception("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]
|
||||
}
|
||||
|
||||
// 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 (subtitles && !group.anime && files.findAll{ it.isVideo() }.size() > 0) {
|
||||
subtitles.each{ languageCode ->
|
||||
def subtitleFiles = getMissingSubtitles(file:files, output:'srt', encoding:'UTF-8', lang:languageCode, strict:true) ?: []
|
||||
files += subtitleFiles
|
||||
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', seasonFolder:true ]
|
||||
: [name:group.anime, format:format.anime, db:'AniDB', seasonFolder:false]
|
||||
def dest = rename(file: files, format: config.format, db: config.db)
|
||||
if (dest && artwork) {
|
||||
dest.mapByFolder().each{ dir, fs ->
|
||||
_log.finest "Fetching artwork for $dir from TheTVDB"
|
||||
def sxe = fs.findResult{ eps -> parseEpisodeNumber(eps) }
|
||||
def options = TheTVDB.search(detectSeriesName(fs), _args.locale)
|
||||
if (options.isEmpty()) {
|
||||
_log.warning "TV Series not found: $config.name"
|
||||
return
|
||||
}
|
||||
options = options.sortBySimilarity(config.name, { s -> s.name })
|
||||
fetchSeriesArtworkAndNfo(config.seasonFolder ? dir.dir : dir, dir, options[0], sxe && sxe.season > 0 ? sxe.season : 1)
|
||||
}
|
||||
}
|
||||
if (dest == null && failOnError) {
|
||||
throw new Exception("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 ->
|
||||
_log.finest "Fetching artwork for $dir from TheMovieDB"
|
||||
def movieFile = fs.findAll{ it.isVideo() }.sort{ it.length() }.reverse().findResult{ it }
|
||||
fetchMovieArtworkAndNfo(dir, detectMovie(movieFile), movieFile, backdrops)
|
||||
}
|
||||
}
|
||||
if (dest == null && failOnError) {
|
||||
throw new Exception("Failed to rename movie: $group.mov")
|
||||
}
|
||||
}
|
||||
|
||||
// MUSIC MODE
|
||||
else if (group.music) {
|
||||
def dest = rename(file:files, format:format.music, db:'AcoustID')
|
||||
if (dest == null && failOnError) {
|
||||
throw new Exception("Failed to rename music: $group.music")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// skip notifications if nothing was renamed anyway
|
||||
if (getRenameLog().isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
// make XMBC scan for new content and display notification message
|
||||
if (xbmc) {
|
||||
xbmc.each{ host ->
|
||||
_log.info "Notify XBMC: $host"
|
||||
_guarded{
|
||||
showNotification(host, 9090, 'FileBot', "Finished processing ${tryQuietly { ut_title } ?: input*.dir.name.unique()} (${getRenameLog().size()} files).", 'http://www.filebot.net/images/icon.png')
|
||||
scanVideoLibrary(host, 9090)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// make Plex scan for new content
|
||||
if (plex) {
|
||||
plex.each{
|
||||
_log.info "Notify Plex: $it"
|
||||
refreshPlexLibrary(it)
|
||||
}
|
||||
}
|
||||
|
||||
// mark episodes as 'acquired'
|
||||
if (myepisodes) {
|
||||
_log.info 'Update MyEpisodes'
|
||||
executeScript('fn:update-mes', [login:myepisodes.join(':'), addshows:true], getRenameLog().values())
|
||||
}
|
||||
|
||||
if (pushover) {
|
||||
// include webservice utility
|
||||
include('fn:lib/ws')
|
||||
|
||||
_log.info 'Sending Pushover notification'
|
||||
Pushover(pushover).send("Finished processing ${tryQuietly { ut_title } ?: input*.dir.name.unique()} (${getRenameLog().size()} files).")
|
||||
}
|
||||
|
||||
// send status email
|
||||
if (gmail) {
|
||||
// ant/mail utility
|
||||
include('fn:lib/ant')
|
||||
|
||||
// send html mail
|
||||
def renameLog = getRenameLog()
|
||||
def emailTitle = tryQuietly { ut_title } ?: input*.dir.name.unique()
|
||||
|
||||
sendGmail(
|
||||
subject: "[FileBot] ${emailTitle}",
|
||||
message: XML {
|
||||
html {
|
||||
body {
|
||||
p("FileBot finished processing ${emailTitle} (${renameLog.size()} files).");
|
||||
hr(); table {
|
||||
th("Parameter"); th("Value")
|
||||
_args.bindings.findAll{ param -> param.key =~ /^ut_/ }.each{ param ->
|
||||
tr { [param.key, param.value].each{ td(it)} }
|
||||
}
|
||||
}
|
||||
hr(); table {
|
||||
th("Original Name"); th("New Name"); th("New Location")
|
||||
renameLog.each{ from, to ->
|
||||
tr { [from.name, to.name, to.parent].each{ cell -> td{ nobr{ code(cell) } } } }
|
||||
}
|
||||
}
|
||||
hr(); small("// Generated by ${net.sourceforge.filebot.Settings.applicationIdentifier} on ${new Date().dateString} at ${new Date().timeString}")
|
||||
}
|
||||
}
|
||||
},
|
||||
messagemimetype: 'text/html',
|
||||
to: tryQuietly{ mailto } ?: gmail[0] + '@gmail.com', // mail to self by default
|
||||
user: gmail[0], password: gmail[1]
|
||||
)
|
||||
}
|
||||
|
||||
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('fn:cleaner', args.empty ? [root:true] : [root:false], cleanerInput)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
// filebot -script fn:artwork.tmdb /path/to/movies/
|
||||
def override = _args.conflict == 'override'
|
||||
|
||||
/*
|
||||
* Fetch movie artwork. The movie is determined using the parent folders name.
|
||||
*/
|
||||
|
||||
// artwork/nfo helpers
|
||||
include('lib/htpc')
|
||||
|
||||
|
||||
args.eachMediaFolder{ dir ->
|
||||
// fetch only missing artwork by default
|
||||
if (!override && dir.hasFile{it.name == 'movie.nfo'} && dir.hasFile{it.name == 'poster.jpg'} && dir.hasFile{it.name == 'fanart.jpg'}) {
|
||||
println "Skipping $dir"
|
||||
return
|
||||
}
|
||||
|
||||
def videos = dir.listFiles{ it.isVideo() }
|
||||
def query = _args.query
|
||||
def options = []
|
||||
|
||||
if (query) {
|
||||
// manual search
|
||||
options = TheMovieDB.searchMovie(query, _args.locale)
|
||||
// sort by relevance
|
||||
options = options.sortBySimilarity(query, { it.name })
|
||||
} else {
|
||||
// auto-detection
|
||||
options = net.sourceforge.filebot.media.MediaDetection.detectMovie(videos[0], null, TheMovieDB, _args.locale, true)
|
||||
}
|
||||
|
||||
if (options.isEmpty()) {
|
||||
println "Movie not found: $query"
|
||||
return
|
||||
}
|
||||
|
||||
// auto-select movie
|
||||
def movie = options[0]
|
||||
|
||||
// maybe require user input
|
||||
if (options.size() != 1 && !_args.nonStrict && !java.awt.GraphicsEnvironment.headless) {
|
||||
movie = javax.swing.JOptionPane.showInputDialog(null, 'Please select Movie:', dir.path, 3, null, options.toArray(), movie)
|
||||
if (movie == null) return null
|
||||
}
|
||||
|
||||
println "$dir => $movie"
|
||||
try {
|
||||
fetchMovieArtworkAndNfo(dir, movie, dir.getFiles{ it.isVideo() }.sort{ it.length() }.reverse().findResult{ it }, true, override)
|
||||
} catch(e) {
|
||||
println "${e.class.simpleName}: ${e.message}"
|
||||
}
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
// filebot -script fn:artwork.tvdb /path/to/media/
|
||||
def override = _args.conflict == 'override'
|
||||
|
||||
/*
|
||||
* Fetch series and season banners for all tv shows. Series name is auto-detected if possible or the folder name is used.
|
||||
*/
|
||||
|
||||
// artwork/nfo helpers
|
||||
include('lib/htpc')
|
||||
|
||||
|
||||
args.eachMediaFolder{ dir ->
|
||||
// fetch only missing artwork by default
|
||||
if (!override && dir.hasFile{it.name == 'banner.jpg'}) {
|
||||
println "Skipping $dir"
|
||||
return
|
||||
}
|
||||
|
||||
def videos = dir.listFiles{ it.isVideo() }
|
||||
def query = _args.query ?: detectSeriesName(videos, _args.locale)
|
||||
def sxe = videos.findResult{ parseEpisodeNumber(it) }
|
||||
|
||||
if (query == null) {
|
||||
query = dir.dir.hasFile{ it.name =~ /Season/ && it.isDirectory() } ? dir.dir.name : dir.name
|
||||
}
|
||||
|
||||
println "$dir => Search by $query"
|
||||
def options = TheTVDB.search(query, _args.locale)
|
||||
if (options.isEmpty()) {
|
||||
println "TV Series not found: $query"
|
||||
return
|
||||
}
|
||||
|
||||
// sort by relevance
|
||||
options = options.sortBySimilarity(query, { it.name })
|
||||
|
||||
// auto-select series
|
||||
def series = options[0]
|
||||
|
||||
// maybe require user input
|
||||
if (options.size() != 1 && !_args.nonStrict && !java.awt.GraphicsEnvironment.headless) {
|
||||
series = javax.swing.JOptionPane.showInputDialog(null, 'Please select TV Show:', dir.path, 3, null, options.toArray(), series)
|
||||
if (series == null) return
|
||||
}
|
||||
|
||||
// auto-detect structure
|
||||
def seriesDir = [dir.dir, dir].sortBySimilarity(series.name, { it.name })[0]
|
||||
def season = sxe && sxe.season > 0 ? sxe.season : 1
|
||||
|
||||
println "$dir => $series"
|
||||
fetchSeriesArtworkAndNfo(seriesDir, dir, series, season, override)
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
// filebot -script fn:chkall <folder>
|
||||
|
||||
/*
|
||||
* Check all sfv/md5/sha1 files and stop if a conflict is found
|
||||
*/
|
||||
args.getFiles().findAll { it.isVerification() }.each {
|
||||
if (!check(file:it))
|
||||
throw new Exception("*ERROR*")
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
// filebot -script fn:cleaner [--action test] /path/to/media/
|
||||
def deleteRootFolder = tryQuietly{ root.toBoolean() }
|
||||
|
||||
/*
|
||||
* Delete orphaned "clutter" files like nfo, jpg, etc and sample files
|
||||
*/
|
||||
def isClutter(f) {
|
||||
// white list
|
||||
def ignore = tryQuietly{ ignore } ?: /extrathumbs/
|
||||
if (f.path =~ "(?i)\\b($ignore)\\b") return false
|
||||
|
||||
// black list
|
||||
def exts = tryQuietly{ exts } ?: /jpg|jpeg|png|gif|nfo|xml|htm|html|log|srt|sub|idx|md5|sfv|txt|rtf|url|db|dna|log/
|
||||
def terms = tryQuietly{ terms } ?: /sample|trailer|extras|deleted.scenes|music.video|scrapbook/
|
||||
def maxsize = tryQuietly{ maxsize as Long } ?: 100 * 1024 * 1024
|
||||
|
||||
// path contains blacklisted terms or extension is blacklisted
|
||||
return (f.extension ==~ "(?i)($exts)" || f.path =~ "(?i)\\b($terms)\\b") && f.length() < maxsize
|
||||
}
|
||||
|
||||
|
||||
def clean(f) {
|
||||
println "Delete $f"
|
||||
|
||||
// do a dry run via --action test
|
||||
if (_args.action == 'test') {
|
||||
return false
|
||||
}
|
||||
|
||||
return f.isDirectory() ? f.deleteDir() : f.delete()
|
||||
}
|
||||
|
||||
|
||||
// memoize media folder status for performance
|
||||
def hasMediaFiles = { dir -> dir.getFiles().find{ (it.isVideo() || it.isAudio()) && !isClutter(it) } }.memoize()
|
||||
|
||||
// delete clutter files in orphaned media folders
|
||||
args.getFiles{ isClutter(it) && !hasMediaFiles(it.dir) }.each { clean(it) }
|
||||
|
||||
// delete empty folders but exclude given args
|
||||
args.getFolders().sort().reverse().each { if (it.listFiles().length == 0) { if (deleteRootFolder || !args.contains(it)) clean(it) } }
|
|
@ -1,26 +0,0 @@
|
|||
// OpenSubtitles
|
||||
console.print('Enter OpenSubtitles username: ')
|
||||
def osdbUser = console.readLine()
|
||||
console.print('Enter OpenSubtitles password: ')
|
||||
def osdbPwd = console.readLine()
|
||||
|
||||
|
||||
setLogin('osdb.user', osdbUser, osdbPwd)
|
||||
|
||||
|
||||
/* --------------------------------------------------------------------- */
|
||||
|
||||
import net.sourceforge.filebot.*
|
||||
|
||||
if (osdbUser) {
|
||||
console.print('Testing OpenSubtitles... ')
|
||||
WebServices.OpenSubtitles.setUser(osdbUser, osdbPwd)
|
||||
WebServices.OpenSubtitles.login()
|
||||
console.println('OK')
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------- */
|
||||
|
||||
def setLogin(key, user, pwd) {
|
||||
Settings.forPackage(WebServices.class).put(key, [user, pwd].join(':'))
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
|
||||
|
||||
|
||||
def escapeShell(String arg) {
|
||||
return '"' + arg.replaceAll(/["$`<>^\\"]/, /\\$0/) + '"'
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (java.awt.GraphicsEnvironment.headless && console != null) {
|
||||
// CLI mode
|
||||
console.printf('Enter: ')
|
||||
def s = console.readLine()
|
||||
console.println('\n' + escapeShell(s) + '\n')
|
||||
System.exit(0)
|
||||
} else {
|
||||
// GUI mode
|
||||
new groovy.swing.SwingBuilder().edt{
|
||||
frame(title: 'Escape Tool', size: [350, 230], show: true, defaultCloseOperation: javax.swing.JFrame.EXIT_ON_CLOSE) {
|
||||
gridLayout(cols: 1, rows: 2)
|
||||
scrollPane{
|
||||
textArea id: 'value', lineWrap: true, font: new java.awt.Font('Monospaced', 0, 16)
|
||||
}
|
||||
scrollPane{
|
||||
textArea id: 'escape', lineWrap: true, text: bind(source:value, sourceProperty:'text', converter: { escapeShell(it) }), font: new java.awt.Font('Monospaced', 0, 16)
|
||||
}
|
||||
}
|
||||
}
|
||||
System.in.read() // wait for GUI to close
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
// filebot -script fn:extract <folder>
|
||||
|
||||
/*
|
||||
* Auto-extract all zip and rar archives.
|
||||
*/
|
||||
args.getFiles{ it.isArchive() }.each {
|
||||
def output = extract(file:it)
|
||||
|
||||
output.each{ println "Extracted: " + it.path }
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
// filebot -script fn:history --format "$from => $to"
|
||||
|
||||
// use --format parameter to specify your own output format
|
||||
def format = _args.format ?: '[$from.name] => [$to.name]'
|
||||
def template = new groovy.text.GStringTemplateEngine().createTemplate(format)
|
||||
|
||||
// use args to list history only for the given folders if desired
|
||||
def accept(from, to) {
|
||||
args.empty ? true : args.find{ to.absolutePath.startsWith(it.absolutePath) } && to.exists()
|
||||
}
|
||||
|
||||
|
||||
getRenameLog(true).each { from, to ->
|
||||
if (accept(from, to))
|
||||
println template.make(from:from, to:to)
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
// filebot -script fn:housekeeping /path/to/folder/ --output /output/folder/ --format <expression>
|
||||
|
||||
/*
|
||||
* Watch folder for new tv shows and automatically move/rename new episodes
|
||||
*/
|
||||
|
||||
// check for new media files once every 5 minutes
|
||||
def updateFrequency = 5 * 60 * 1000
|
||||
|
||||
// spawn daemon thread
|
||||
Thread.startDaemon {
|
||||
while (sleep(updateFrequency) || true) {
|
||||
// extract all
|
||||
if (_args.extract) {
|
||||
extract(file:args.getFiles{ it.isArchive() }, output:'.')
|
||||
}
|
||||
|
||||
// subtitles for all
|
||||
if (_args.getSubtitles) {
|
||||
getMissingSubtitles(file:args.getFiles{ it.isVideo() }, output:'srt')
|
||||
}
|
||||
|
||||
// rename all
|
||||
if (_args.rename) {
|
||||
args.eachMediaFolder {
|
||||
rename(folder:it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println "Press ENTER to abort"
|
||||
console.readLine() // keep script running until aborted by user
|
|
@ -1,53 +0,0 @@
|
|||
|
||||
/**
|
||||
* Log into a remote host and run a given command.
|
||||
*
|
||||
* e.g.
|
||||
* sshexec(command: "ps", host: "filebot.sf.net", username: "rednoah", password: "correcthorsebatterystaple")
|
||||
*/
|
||||
def sshexec(param) {
|
||||
param << [trust: true] // auto-trust remote hosts
|
||||
|
||||
_guarded {
|
||||
ant().sshexec(param)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send email via smtp.
|
||||
*
|
||||
* e.g.
|
||||
* sendmail(mailhost:'smtp.gmail.com', mailport:'587', ssl:'no', enableStartTLS:'yes', user:'rednoah@gmail.com', password:'correcthorsebatterystaple', from:'rednoah@gmail.com', to:'someone@gmail.com', subject:'Hello Ant World', message:'Dear Ant, ...')
|
||||
*/
|
||||
def sendmail(param) {
|
||||
def sender = param.remove('from')
|
||||
def recipient = param.remove('to')
|
||||
|
||||
_guarded {
|
||||
ant().mail(param) {
|
||||
from(address:sender)
|
||||
to(address:recipient)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send email using gmail default settings.
|
||||
*
|
||||
* e.g.
|
||||
* sendGmail(subject:'Hello Ant World', message:'Dear Ant, ...', to:'someone@gmail.com', user:'rednoah', password:'correcthorsebatterystaple')
|
||||
*/
|
||||
def sendGmail(param) {
|
||||
param << [mailhost:'smtp.gmail.com', mailport:'587', ssl:'no', enableStartTLS:'yes']
|
||||
param << [user:param.username ? param.remove('username') + '@gmail.com' : param.user]
|
||||
param << [from: param.from ?: param.user]
|
||||
|
||||
sendmail(param)
|
||||
}
|
||||
|
||||
|
||||
def ant() {
|
||||
return new AntBuilder()
|
||||
}
|
|
@ -1,286 +0,0 @@
|
|||
import static net.sourceforge.filebot.WebServices.*
|
||||
import static groovy.json.StringEscapeUtils.*
|
||||
|
||||
import groovy.xml.*
|
||||
import net.sourceforge.filebot.mediainfo.*
|
||||
|
||||
|
||||
/**
|
||||
* XBMC helper functions
|
||||
*/
|
||||
def scanVideoLibrary(host, port) {
|
||||
_guarded {
|
||||
telnet(host, port) { writer, reader ->
|
||||
writer.println("""{"jsonrpc":"2.0","method":"VideoLibrary.Scan","id":1}""")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def showNotification(host, port, title, message, image) {
|
||||
_guarded {
|
||||
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) {
|
||||
_guarded {
|
||||
new URL("http://$server:$port/library/sections/all/refresh").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 }.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")
|
||||
}
|
||||
genre(i.genres?.size() > 0 ? i.genres[0] : null)
|
||||
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 = _args.locale) {
|
||||
_guarded {
|
||||
// fetch nfo
|
||||
def seriesInfo = TheTVDB.getSeriesInfo(series, locale)
|
||||
fetchSeriesNfo(seriesDir['tvshow.nfo'], seriesInfo, override, locale)
|
||||
|
||||
// fetch series banner, fanart, posters, etc
|
||||
["680x1000", null].findResult{ fetchSeriesBanner(seriesDir['poster.jpg'], series, "poster", it, null, override, locale) }
|
||||
["graphical", null].findResult{ fetchSeriesBanner(seriesDir['banner.jpg'], series, "series", it, null, override, locale) }
|
||||
|
||||
// fetch highest resolution fanart
|
||||
["1920x1080", "1280x720", null].findResult{ fetchSeriesBanner(seriesDir["fanart.jpg"], series, "fanart", it, null, override, locale) }
|
||||
|
||||
// fetch season banners
|
||||
if (seasonDir != seriesDir) {
|
||||
fetchSeriesBanner(seasonDir["poster.jpg"], series, "season", "season", season, override, locale)
|
||||
fetchSeriesBanner(seasonDir["banner.jpg"], series, "season", "seasonwide", season, override, locale)
|
||||
}
|
||||
|
||||
// fetch fanart
|
||||
fetchSeriesFanart(seriesDir['clearart.png'], series, 'clearart', null, override, locale)
|
||||
fetchSeriesFanart(seriesDir['logo.png'], series, 'clearlogo', null, override, locale)
|
||||
fetchSeriesFanart(seriesDir['landscape.jpg'], series, 'tvthumb', null, override, locale)
|
||||
|
||||
// fetch season fanart
|
||||
if (seasonDir != seriesDir) {
|
||||
fetchSeriesFanart(seasonDir['landscape.jpg'], series, 'seasonthumb', season, override, locale)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 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 = _guarded{ movieFile ? 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 }.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)
|
||||
genre(i.genres?.size() > 0 ? i.genres[0] : null)
|
||||
director(i.director)
|
||||
i.cast?.each{ a ->
|
||||
actor {
|
||||
name(a.name)
|
||||
role(a.character)
|
||||
}
|
||||
}
|
||||
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, fetchAll = false, override = false, locale = _args.locale) {
|
||||
_guarded {
|
||||
def movieInfo = TheMovieDB.getMovieInfo(movie, locale)
|
||||
|
||||
// fetch nfo
|
||||
fetchMovieNfo(movieDir['movie.nfo'], movieInfo, movieFile, override)
|
||||
|
||||
// generate url files
|
||||
[[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(new File(movieDir, "${it.db}.url"))
|
||||
}
|
||||
}
|
||||
|
||||
// fetch series banner, fanart, posters, etc
|
||||
fetchMovieArtwork(movieDir['poster.jpg'], movieInfo, 'posters', override, locale)
|
||||
fetchMovieArtwork(movieDir['fanart.jpg'], movieInfo, 'backdrops', override, locale)
|
||||
|
||||
fetchMovieFanart(movieDir['clearart.png'], movieInfo, 'movieart', null, override, locale)
|
||||
fetchMovieFanart(movieDir['logo.png'], movieInfo, 'movielogo', null, override, locale)
|
||||
['bluray', 'dvd', null].findResult { diskType -> fetchMovieFanart(movieDir['disc.png'], movieInfo, 'moviedisc', diskType, override, locale) }
|
||||
|
||||
if (fetchAll) {
|
||||
fetchAllMovieArtwork(movieDir['backdrops'], movieInfo, 'backdrops', override, locale)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
/****************************************************************************
|
||||
* MyEpisodes
|
||||
* http://www.myepisodes.com
|
||||
****************************************************************************/
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.Connection.Method
|
||||
import net.sourceforge.filebot.Cache
|
||||
|
||||
def MyEpisodes(username, password) {
|
||||
return new MyEpisodesScraper(username:username, password:password)
|
||||
}
|
||||
|
||||
class MyEpisodesScraper {
|
||||
def username
|
||||
def password
|
||||
|
||||
def cache = Cache.getCache('web-datasource-lv2')
|
||||
def session = [:]
|
||||
|
||||
def login = {
|
||||
def response = Jsoup.connect('http://www.myepisodes.com/login.php').data('username', username, 'password', password, 'action', 'Login', 'u', '').method(Method.POST).execute()
|
||||
session << response.cookies()
|
||||
return response.parse()
|
||||
}
|
||||
|
||||
def get = { url ->
|
||||
if (session.isEmpty()) {
|
||||
login()
|
||||
}
|
||||
|
||||
def response = Jsoup.connect(url).cookies(session).method(Method.GET).execute()
|
||||
session << response.cookies()
|
||||
def html = response.parse()
|
||||
|
||||
if (html.select('#frmLogin')) {
|
||||
session.clear()
|
||||
throw new Exception('Login failed')
|
||||
}
|
||||
|
||||
return html
|
||||
}
|
||||
|
||||
def getShows = {
|
||||
def shows = cache.get('MyEpisodes.Shows')
|
||||
if (shows == null) {
|
||||
shows = ['other', 'A'..'Z'].flatten().findResults{ section ->
|
||||
get("http://myepisodes.com/shows.php?list=${section}").select('a').findResults{ a ->
|
||||
try {
|
||||
return [id:a.absUrl('href').match(/showid=(\d+)/).toInteger(), name:a.text().trim()]
|
||||
} catch(e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}.flatten().sort{ it.name }
|
||||
cache.put('MyEpisodes.Shows', shows)
|
||||
}
|
||||
return shows
|
||||
}
|
||||
|
||||
def getShowList = {
|
||||
get("http://www.myepisodes.com/shows.php?type=manage").select('option').findResults{ option ->
|
||||
try {
|
||||
return [id:option.attr('value').toInteger(), name:option.text().trim()]
|
||||
} catch(e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def addShow = { showid ->
|
||||
get("http://www.myepisodes.com/views.php?type=manageshow&mode=add&showid=${showid}")
|
||||
}
|
||||
|
||||
def update = { showid, season, episode, tick = 'acquired', value = '1' ->
|
||||
get("http://www.myepisodes.com/myshows.php?action=Update&showid=${showid}&season=${season}&episode=${episode}&${tick}=${value}")
|
||||
}
|
||||
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
/****************************************************************************
|
||||
* Pushover
|
||||
* https://pushover.net
|
||||
****************************************************************************/
|
||||
def Pushover(user, token = 'wcckDz3oygHSU2SdIptvnHxJ92SQKK') {
|
||||
new PushoverClient(user:user, token:token)
|
||||
}
|
||||
|
||||
class PushoverClient {
|
||||
def user
|
||||
def token
|
||||
|
||||
def endpoint = new URL('https://api.pushover.net/1/messages.xml')
|
||||
|
||||
def send = { text, parameters = [:] ->
|
||||
// inject default post parameters
|
||||
parameters << [token:token, user:user, message:text as String]
|
||||
|
||||
// post and process response
|
||||
endpoint.post(parameters).text.xml
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
// filebot -script fn:mi /path/to/media/ "MediaIndex.csv"
|
||||
|
||||
/*
|
||||
* Print media info of all video files to CSV file
|
||||
*/
|
||||
def model = '''Name;Container;Resolution;Video Codec;Video Format;Audio Codec;Audio Format;Audio Language(s);Duration;File Size;Folder Size;Folder Count;Path'''
|
||||
def template = '''{fn};{cf};{resolution};{vc};{vf};{ac};{af};{media.AudioLanguageList};{media.DurationString3};{file.length()};{folder.listFiles().sum{ it.length() }};{folder.listFiles().sum{ it.isFile() ? 1 : 0 }};{file.getCanonicalPath()}'''
|
||||
|
||||
// sanity check
|
||||
if (args.size() != 2) throw new Exception('Invalid arguments:' + args)
|
||||
|
||||
// open destination file (writing files requires -trust-script)
|
||||
args[1].withWriter{ output ->
|
||||
// print header
|
||||
output.writeLine(model)
|
||||
|
||||
// print info for each video file (sorted by filename)
|
||||
args[0].getFiles{ it.isVideo() }.sort{ a, b -> a.name.compareToIgnoreCase(b.name) }.each{
|
||||
def mi = getMediaInfo(file:it, format:template)
|
||||
|
||||
// print to console
|
||||
println mi
|
||||
|
||||
// append to file
|
||||
output.writeLine(mi)
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
// filebot -script fn:renall <options> <folder> --def target=[file|folder]
|
||||
|
||||
/*
|
||||
* Rename all tv shows, anime or movies folder by folder using given or default options.
|
||||
*/
|
||||
def target = tryQuietly{ target } ?: 'file' // target files by default
|
||||
|
||||
args.eachMediaFolder {
|
||||
if (it.isDisk())
|
||||
return rename(file:it) // rename disk folders instead of files regardless of mode
|
||||
|
||||
switch(target) {
|
||||
case 'file' : return rename(folder:it) // rename files within each folder
|
||||
case 'folder' : return rename(file:it) // rename folders as if they were files
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
// filebot -script fn:replace --action copy --filter "[.]srt$" --def "e=[.](eng|english)" "r=.en"
|
||||
|
||||
// imports
|
||||
import net.sourceforge.filebot.StandardRenameAction
|
||||
|
||||
// parameters
|
||||
def action = StandardRenameAction.forName(_args.action)
|
||||
def accept = { f -> _args.filter ? f.path =~ _args.filter : true }
|
||||
|
||||
// rename
|
||||
args.getFiles{ accept(it) }.each{
|
||||
if (it.path =~ e) {
|
||||
def nfile = new File(it.path.replaceAll(e, r))
|
||||
|
||||
// override files only when --conflict override is set
|
||||
if (!it.equals(nfile)) {
|
||||
if (nfile.exists() && _args.conflict == 'override' && action != StandardRenameAction.TEST) {
|
||||
nfile.delete() // resolve conflict
|
||||
}
|
||||
|
||||
if (!nfile.exists()) {
|
||||
println action.rename(it, nfile)
|
||||
} else {
|
||||
println "Skipped $nfile"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
// filebot -script fn:revert <file or folder>
|
||||
|
||||
|
||||
def accept(from, to) {
|
||||
args.find{ to.absolutePath.startsWith(it.absolutePath) } && to.exists()
|
||||
}
|
||||
|
||||
def revert(from, to) {
|
||||
def action = net.sourceforge.filebot.StandardRenameAction.forName(_args.action)
|
||||
|
||||
println "[$action] Revert [$from] to [$to]"
|
||||
if (!from.canonicalFile.equals(to.canonicalFile)) {
|
||||
action.rename(from, to) // reverse-rename only if path has changed
|
||||
}
|
||||
|
||||
// reset extended attributes
|
||||
tryQuietly{ to.xattr.clear() }
|
||||
}
|
||||
|
||||
|
||||
getRenameLog(true).reverseEach { from, to ->
|
||||
if (accept(from, to))
|
||||
revert(to, from)
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
// filebot -script fn:sortivo <folder> --output path/to/folder [-non-strict]
|
||||
|
||||
// process only media files
|
||||
def input = args.getFiles{ it.isVideo() || it.isSubtitle() }
|
||||
|
||||
// ignore clutter files
|
||||
input = input.findAll{ !(it.path =~ /\b(?i:sample|trailer|extras|deleted.scenes|music.video|scrapbook)\b/) }
|
||||
|
||||
// print input fileset
|
||||
input.each{ println "Input: $it" }
|
||||
|
||||
/*
|
||||
* Move/Rename a mix of episodes and movies that are all in the same folder.
|
||||
*/
|
||||
def groups = input.groupBy{ f ->
|
||||
def tvs = detectSeriesName(f)
|
||||
def mov = (parseEpisodeNumber(f) || parseDate(f)) ? null : detectMovie(f, false) // skip movie detection if we can already tell it's an episode
|
||||
println "$f.name [series: $tvs, movie: $mov]"
|
||||
|
||||
// DECIDE EPISODE VS MOVIE (IF NOT CLEAR)
|
||||
if (tvs && mov) {
|
||||
def norm = { s -> s.lower().space(' ') }
|
||||
def fn = norm(f.nameWithoutExtension)
|
||||
def sn = norm(tvs)
|
||||
def mn = norm(mov.name)
|
||||
|
||||
// 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) || (fn =~ sn && parseEpisodeNumber(fn.after(sn), false)) || fn.after(sn) =~ / - .+/ || f.dir.listFiles{ it.isVideo() && norm(it.name) =~ sn && it.name =~ /\b\d{1,3}\b/}.size() >= 10) {
|
||||
println "Exclude Movie: $mov"
|
||||
mov = null
|
||||
} else if ((detectMovie(f, true) && fn =~ /(19|20)\d{2}/) || (fn =~ mn && !(fn.after(mn) =~ /\b\d{1,3}\b/))) {
|
||||
println "Exclude Series: $tvs"
|
||||
tvs = null
|
||||
}
|
||||
}
|
||||
|
||||
return [tvs:tvs, mov:mov]
|
||||
}
|
||||
|
||||
groups.each{ group, files ->
|
||||
// EPISODE MODE
|
||||
if (group.tvs && !group.mov) {
|
||||
rename(file:files, format:'TV Shows/{n}/{episode.special ? "Special" : "Season "+s}/{n} - {episode.special ? "S00E"+special.pad(2) : s00e00} - {t}', db:'TheTVDB')
|
||||
}
|
||||
|
||||
// MOVIE MODE
|
||||
if (group.mov && !group.tvs) {
|
||||
rename(file:files, format:'Movies/{n} ({y})/{n} ({y}){" CD$pi"}', db:'TheMovieDB')
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
// PERSONALIZED SETTINGS
|
||||
def episodeDir = '''/in/TV'''
|
||||
def episodeFormat = '''/out/TV/{n}/{"Season ${s.pad(2)}"}/{n} - {s00e00} - {t}'''
|
||||
def movieDir = '''/in/Movies'''
|
||||
def movieFormat = '''/out/Movies/{n} ({y})/{n} ({y}){" CD$pi"}'''
|
||||
|
||||
// XBMC ON LOCAL MACHINE
|
||||
def xbmc = ['localhost'] // (use [] to not notify any XBMC instances about updates)
|
||||
|
||||
|
||||
|
||||
// ignore chunk, part, par and hidden files
|
||||
def incomplete(f) { f.name =~ /[.]incomplete|[.]chunk|[.]par$|[.]dat$/ }
|
||||
|
||||
|
||||
// extract completed multi-volume rar files
|
||||
[episodeDir, movieDir].getFolders{ !it.hasFile{ incomplete(it) } && it.hasFile{ it =~ /[.]rar$/ } }.each{ dir ->
|
||||
// extract all archives found in this folder
|
||||
def paths = extract(folder:dir)
|
||||
|
||||
// delete original archive volumes after successful extraction
|
||||
if (paths != null && !paths.isEmpty()) {
|
||||
dir.listFiles{ it =~ /[.]rar$|[.]r[\d]+$/ }*.delete()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Fetch subtitles and sort into folders
|
||||
*/
|
||||
episodeDir.getFolders{ !it.hasFile{ incomplete(it) } && it.hasFile{ it.isVideo() } }.each{ dir ->
|
||||
println "Processing $dir"
|
||||
def files = dir.listFiles{ it.isVideo() }
|
||||
|
||||
// fetch subtitles
|
||||
files += getSubtitles(file:files)
|
||||
|
||||
// sort episodes / subtitles
|
||||
rename(file:files, db:'TheTVDB', format:episodeFormat)
|
||||
}
|
||||
|
||||
movieDir.getFolders{ !it.hasFile{ incomplete(it) } && it.hasFile{ it.isVideo() } }.each{ dir ->
|
||||
println "Processing $dir"
|
||||
def files = dir.listFiles{ it.isVideo() }
|
||||
|
||||
// fetch subtitles
|
||||
files += getSubtitles(file:files)
|
||||
|
||||
// sort movies / subtitles
|
||||
rename(file:files, db:'TheMovieDB', format:movieFormat)
|
||||
}
|
||||
|
||||
|
||||
// make XBMC scan for new content
|
||||
xbmc.each { host ->
|
||||
telnet(host, 9090) { writer, reader ->
|
||||
// API call for latest XBMC release
|
||||
def msg = '{"id":1,"method":"VideoLibrary.Scan","params":[],"jsonrpc":"2.0"}'
|
||||
|
||||
// API call for XBMC Dharma-Release or older
|
||||
// def msg = '{"id":1,"method":"VideoLibrary.ScanForContent","params":[],"jsonrpc":"2.0"}'
|
||||
|
||||
writer.println(msg)
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
// filebot -script fn:src <folder>
|
||||
|
||||
/*
|
||||
* Fetch subtitles, rename and calculate checksums for all video files
|
||||
*/
|
||||
args.eachMediaFolder {
|
||||
|
||||
getMissingSubtitles(folder:it)
|
||||
|
||||
def renamedFiles = rename(folder:it)
|
||||
|
||||
compute(file:renamedFiles.findAll{ it.isVideo() })
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
// filebot -script fn:suball <options> <folder>
|
||||
|
||||
/*
|
||||
* Get subtitles for all your media files
|
||||
*/
|
||||
args.eachMediaFolder {
|
||||
getMissingSubtitles(folder:it)
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
// filebot -script fn:sysenv
|
||||
|
||||
println '# Java System Properties #'
|
||||
_system.each{
|
||||
println "$it.key: $it.value"
|
||||
}
|
||||
|
||||
println '# Environment Variables #'
|
||||
_environment.each{
|
||||
println "$it.key: $it.value"
|
||||
}
|
|
@ -1,98 +0,0 @@
|
|||
// filebot -script fn:sysinfo
|
||||
|
||||
// FileBot 2.62 (r993)
|
||||
println net.sourceforge.filebot.Settings.applicationIdentifier
|
||||
|
||||
// JNA Native: 3.5.0
|
||||
try {
|
||||
print 'JNA Native: '
|
||||
println com.sun.jna.Native.nativeVersion
|
||||
} catch(Throwable error) {
|
||||
println error.cause
|
||||
}
|
||||
|
||||
// MediaInfo: MediaInfoLib - v0.7.48
|
||||
try {
|
||||
print 'MediaInfo: '
|
||||
println net.sourceforge.filebot.mediainfo.MediaInfo.version()
|
||||
} catch(Throwable error) {
|
||||
println error.cause
|
||||
}
|
||||
|
||||
// 7-Zip-JBinding: OK
|
||||
try {
|
||||
print '7-Zip-JBinding: '
|
||||
net.sourceforge.filebot.archive.SevenZipLoader.requireNativeLibraries() // load 7-Zip-JBinding native libs
|
||||
println 'OK'
|
||||
} catch(Throwable error) {
|
||||
println error
|
||||
}
|
||||
|
||||
// chromaprint-tools
|
||||
try {
|
||||
print 'chromaprint-tools: '
|
||||
def fpcalc = System.getProperty('net.sourceforge.filebot.AcoustID.fpcalc', 'fpcalc')
|
||||
def version = [fpcalc, '-version'].execute().text.trim() ?: 'fpcalc -version failed'
|
||||
println "$version ($fpcalc)"
|
||||
} catch(Throwable error) {
|
||||
println error
|
||||
}
|
||||
|
||||
// Extended File Attributes
|
||||
try {
|
||||
print 'Extended Attributes: '
|
||||
if (net.sourceforge.filebot.Settings.useExtendedFileAttributes()){
|
||||
// create new temp file
|
||||
def f = new File(net.sourceforge.filebot.Settings.applicationFolder, '.xattr-test')
|
||||
f.createNewFile() && f.deleteOnExit()
|
||||
|
||||
// xattr write, read and verify
|
||||
def xattr = new net.sourceforge.filebot.media.MetaAttributes(f)
|
||||
def payload = new Date()
|
||||
xattr.setObject(payload)
|
||||
assert xattr.getObject() == payload
|
||||
println 'OK'
|
||||
} else {
|
||||
println 'DISABLED'
|
||||
}
|
||||
} catch(Throwable error) {
|
||||
println error
|
||||
}
|
||||
|
||||
// GIO and GVFS
|
||||
try {
|
||||
if (net.sourceforge.filebot.Settings.useGVFS()) {
|
||||
print 'GVFS: '
|
||||
assert net.sourceforge.filebot.gio.GVFS.defaultVFS != null
|
||||
println 'OK'
|
||||
}
|
||||
} catch(Throwable error) {
|
||||
println error
|
||||
}
|
||||
|
||||
// Groovy Engine: 2.1.7
|
||||
println 'Groovy Engine: ' + groovy.lang.GroovySystem.version
|
||||
|
||||
// Java(TM) SE Runtime Environment 1.6.0_30 (headless)
|
||||
println net.sourceforge.filebot.Settings.javaRuntimeIdentifier
|
||||
|
||||
// 32-bit Java HotSpot(TM) Client VM
|
||||
println String.format('%d-bit %s', com.sun.jna.Platform.is64Bit() ? 64 : 32, _system['java.vm.name'])
|
||||
|
||||
// Windows 7 (x86)
|
||||
println String.format('%s (%s)', _system['os.name'], _system['os.arch'])
|
||||
|
||||
|
||||
|
||||
// check for updates
|
||||
try {
|
||||
def update = new XmlSlurper().parse('http://filebot.net/update.xml')
|
||||
def latestRev = update.revision.text() as int
|
||||
def latestApp = update.name.text()
|
||||
|
||||
if (latestRev > net.sourceforge.filebot.Settings.applicationRevisionNumber) {
|
||||
println "\n--- UPDATE AVAILABLE: $latestApp (r$latestRev) ---\n"
|
||||
}
|
||||
} catch(Throwable error) {
|
||||
// ignore
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
// filebot -script fn:update-mes "X:/path/to/episodes" --def login=user:pwd addshows=y tick=acquired
|
||||
|
||||
def mesacc = login.split(':')
|
||||
def mesadd = tryQuietly{ addshows.toBoolean() }
|
||||
def mesupdate = tryQuietly { tick } ?: 'acquired'
|
||||
def mesvalue = tryQuietly { value } ?: '1'
|
||||
|
||||
// import myepisodes scraper
|
||||
include('lib/scraper')
|
||||
|
||||
def mes = MyEpisodes(mesacc[0], mesacc[1])
|
||||
def myshows = mes.getShowList()
|
||||
|
||||
// series name => series key (e.g. Doctor Who (2005) => doctorwho)
|
||||
def collationKey = { s -> s == null ? '' : s.replaceAll(/^(?i)(The|A)\b/, '').replaceAll(/(?<!\d)\d{4}$/).replaceAll(/\W/).lower() }
|
||||
|
||||
args.getFiles().findAll{ it.isVideo() && parseEpisodeNumber(it) && detectSeriesName(it) }.groupBy{ detectSeriesName(it) }.each{ series, files ->
|
||||
def show = myshows.find{ collationKey(it.name) == collationKey(series) }
|
||||
if (show == null && mesadd) {
|
||||
show = mes.getShows().find{ collationKey(it.name) == collationKey(series) }
|
||||
if (show == null) {
|
||||
println "[failure] '$series' not found"
|
||||
return
|
||||
}
|
||||
mes.addShow(show.id)
|
||||
println "[added] $show.name"
|
||||
}
|
||||
|
||||
files.each{
|
||||
if (show != null) {
|
||||
def sxe = parseEpisodeNumber(it)
|
||||
mes.update(show.id, sxe.season, sxe.episode, mesupdate, mesvalue)
|
||||
println "[$mesupdate] $show.name $sxe [$it.name]"
|
||||
} else {
|
||||
println "[failure] '$series' has not been added [$it.name]"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
// filebot -script fn:watcher /path/to/folder/ --output /output/folder/ --format <expression>
|
||||
|
||||
// watch folders and print files that were added/modified
|
||||
def watchman = args.watch { changes ->
|
||||
// extract all
|
||||
if (_args.extract)
|
||||
changes += extract(file:changes.findAll{ it.isArchive() }, output:'.')
|
||||
|
||||
// subtitles for all
|
||||
if (_args.getSubtitles)
|
||||
changes += getMissingSubtitles(file:changes.findAll{ it.isVideo() }, output:'srt')
|
||||
|
||||
// rename all
|
||||
if (_args.rename)
|
||||
rename(file:changes)
|
||||
}
|
||||
|
||||
watchman.commitDelay = 5 * 1000 // default = 5s
|
||||
watchman.commitPerFolder = true // default = true
|
||||
|
||||
println "Waiting for events"
|
||||
if (console) { console.readLine() } else { sleep(Long.MAX_VALUE) } // keep running and watch for changes
|
|
@ -1,13 +0,0 @@
|
|||
// filebot -script fn:xattr --action clear /path/to/files
|
||||
|
||||
args.getFiles{ it.xattr.size() > 0 }.each{
|
||||
println it
|
||||
it.xattr.each{ k, v ->
|
||||
println "\t$k: $v"
|
||||
}
|
||||
// clear xattr mode
|
||||
if (_args.action == 'clear') {
|
||||
it.xattr.clear()
|
||||
println '*** CLEARED ***'
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue