Page 1 of 1

Confused on the very basics

Posted: 16 Nov 2013, 16:35
by brown3218
I'm attempting to following the Automated Media Center sticky post but I'm having trouble with the basics.

I type this into "Run this program when a torrent finishes"

filebot -script fn:amc --output "D:/Downloads" --log-file amc.log --action copy --conflict override -non-strict --def music=y subtitles=en artwork=y "ut_dir=%D" "ut_file=%F" "ut_kind=%K" "ut_title=%N" "ut_label=%L" "ut_state=%S"

The only thing I've changed in the above text is the output path to my media folder. Now what do I do? I've downloaded filebot but I'm not sure what to do with the amc script that was attached to the sticky post. Do I just save it as a text file? Where do I put it? I feel dumb because I know I'm confused about the very basics. Is there a more detailed tutorial?

Re: Confused on the very basics

Posted: 16 Nov 2013, 16:43
by rednoah
Paste the filebot call into utorrent... obviously:

Code: Select all

µTorrent -> Run Program -> Run this program when torrent finishes:
Look at the pictures! Right after Step 3 there's a picture!~~

Re: Confused on the very basics

Posted: 16 Nov 2013, 16:46
by brown3218
I did that. Thats as far as I got. I don't know where to put the attached script file. Where do I save it and do I need to save it as a certain file type or name?

Re: Confused on the very basics

Posted: 16 Nov 2013, 16:50
by rednoah
You're done.

If it's not working look at the troubleshooting tips. I figure you haven't restarted utorrent since you installed filebot and thus utorrent can't filebot in the PATH yet. Restart everything.

Re: Confused on the very basics

Posted: 16 Nov 2013, 16:57
by brown3218
I appreciate your help. I restarted everything and nothing happens when I finish a new download. Don't I need to put the amc.groovy script somewhere?

Re: Confused on the very basics

Posted: 16 Nov 2013, 18:00
by brown3218
I feel like I've missed an important first step. I'm running windows with the latest utorrent and the latest filebot.
Literally all I have to do is type this into utorrent right?

filebot -script fn:amc --output "D:/Downloads" --log-file amc.log --action copy --conflict override -non-strict --def music=y subtitles=en artwork=y "ut_dir=%D" "ut_file=%F" "ut_kind=%K" "ut_title=%N" "ut_label=%L" "ut_state=%S"

This text is in µTorrent -> Run Program -> Run this program when torrent finishes
I've edited the above text only in the output section to change it to my downloads folder.
I've restarted filebot and utorrent and tried downloading a new file, nothing happens when the file finishes.
Whats with this whole amc.groocy script file? Do I need to put that somewhere on my computer? Or literally just the above text?

Re: Confused on the very basics

Posted: 16 Nov 2013, 18:08
by rednoah
Just the above.

If it doesn't work check the Logger tab. And try running filebot from console. Plenty of tips in the troubleshooting section.

Re: Confused on the very basics

Posted: 16 Nov 2013, 19:39
by brown3218
Where is the logger tab?

Re: Confused on the very basics

Posted: 16 Nov 2013, 19:45
by brown3218
Isn't there supposed to be a script fn:amc somewhere on my computer for this to run?
Where does all this code below go?

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"
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(/[ ,|]+/) }

// myepisodes updates and email notifications
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.path =~ /(?<=tt)\\d{7}/ || tryQuietly{ f.metadata?.object?.class.name =~ /Movie/ }
}

def forceSeries(f) {
	tryQuietly{ ut_label } =~ /^(?i:TV|Kids.Shows)/ || 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.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 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: 'override', filter: { it.isArchive() || it.isVideo() || it.isSubtitle() || (music && it.isAudio()) }, forceExtractAll: true) ?: []
		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('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}/ })
		**/
		
		// 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 =~ /\b/+mov.year+/\b/ } || [dn, fn].find{ it =~ mn && !(it.after(mn) =~ /\b\d{1,3}\b/) && !(it.before(mn).contains(sn)) } || (detectMovie(f, true) && [dn, fn].find{ it =~ /(19|20)\d{2}/ })) {
			_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('update-mes', [login:myepisodes.join(':'), addshows:true], getRenameLog().values())
}

if (pushover) {
	// include webservice utility
	include('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('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]
	)
}

// 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] : [root:false], cleanerInput)
		}
	}
}

Re: Confused on the very basics

Posted: 17 Nov 2013, 04:40
by rednoah
How can I be more clear? NO YOU DO NOT NEED TO COPY THE SCRIPT


The text in red is probably what u wanna focus on:
  • The folder paths to Location of Downloaded Files (--output) must not end with backslash \ because the value gets passed through as the %D token. So the final backslash \ will escape the double-quote " and mess up everything
  • It is recommended to use / as separator and not \ since it's also used to escape characters, e.g. --output "X:\" will mess up the cmdline in unexpected ways
  • If you have PATH issues (e.g. 'filebot' command not found) try using the absolute path to the executable, e.g. "C:/Program Files/Filebot/filebot.exe" or "./Applications/Filebot.app/Contents/MacOS/filebot.sh"
  • If you have installed 32-bit Java you will need a 32-bit FileBot install for 7zip and MediaInfo integration to work. If you're on 64-bit, then only install 64-bit Java and 64-bit FileBot.
  • You can force Movie/TV Show/Anime mode or force ignore files via torrent labels, e.g. label movie to force TMDB, tv to force TheTVDB, anime to force AniDB or other to not process this torrent
  • If you use more than one XBMC/Plex you can specify multiple hosts or ips like this xbmc=host1,host2,host3,etc
  • Use --conflict fail to make the script terminate on any conflicts or failures rather than gracefully ignore these errors and continue
  • Using --action symlink|keeplink|hardlink requires at least Java 7
  • Using --action keeplink won't work because µTorrent has a known bug that prevents it from opening and seeding symbolic links
  • fn:amc will automatically fetch the script and auto-update but you can also specify a local path to run your own customized script

Google "utorrent logger", 1st result => http://forum.utorrent.com/viewtopic.php?pid=606045

Re: Confused on the very basics

Posted: 17 Nov 2013, 16:56
by brown3218
Thanks for the help. It looks like i had problems because I wasnt using an absolute path

I just have one last question. The AMC post says that filebot can email your gmail account when something is processed, where do I enter my gmsil credentials? Is there a file that needs to be edited?

Re: Confused on the very basics

Posted: 17 Nov 2013, 17:01
by rednoah
RTFM wrote:--def gmail=username:password Use the following gmail account to send and recieve reports (username is the username only, without the @gmail.com part)

Re: Confused on the very basics

Posted: 17 Nov 2013, 17:43
by brown3218
I'm just not sure where I'm supposed to enter this info. I realize I will have to out my username and password into the script, but how do I pull up the script to that email field?

Re: Confused on the very basics

Posted: 20 Nov 2013, 06:13
by festol
just add it to you existing entry in utorrent along with the other settings you are passing to the AMC script

filebot -script fn:amc --output "D:/Downloads" --log-file amc.log --action copy --conflict override -non-strict --def music=y subtitles=en artwork=y "ut_dir=%D" "ut_file=%F" "ut_kind=%K" "ut_title=%N" "ut_label=%L" "ut_state=%S" --def gmail=username:password

^^^^^^