showrobot 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Scott Rabin
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,19 @@
1
+ = showrobot
2
+
3
+ Description goes here.
4
+
5
+ == Contributing to showrobot
6
+
7
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
8
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
9
+ * Fork the project.
10
+ * Start a feature/bugfix branch.
11
+ * Commit and push until you are happy with your contribution.
12
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
13
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
14
+
15
+ == Copyright
16
+
17
+ Copyright (c) 2012 Scott Rabin. See LICENSE.txt for
18
+ further details.
19
+
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # ensure ShowRobot's lib directory is in Ruby's load path
4
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../lib') unless $LOAD_PATH.include?(File.dirname(__FILE__) + '/../lib')
5
+
6
+ # load the cli tools
7
+ Dir[File.dirname(__FILE__) + '/../lib/showrobot/cli/*.rb'].each { |file| require File.expand_path(file) }
8
+
9
+ SUBCOMMANDS = %w(rename identify config)
10
+ command = ARGV.shift
11
+ abort "#{BASIC_USAGE}\nShowRobot expects subcommand [#{SUBCOMMANDS.join('|')}]\nRun `showrobot [command] -h` to see specific usage options" if not SUBCOMMANDS.include? command
12
+
13
+ # dependencies for parsing options
14
+ require 'trollop'
15
+
16
+ # load up ShowRobot
17
+ require 'showrobot'
18
+
19
+ # dispatch on command
20
+ case command.to_sym
21
+ when :config
22
+ # going to need YAML
23
+ require 'yaml'
24
+
25
+ # configuration options
26
+ options = Trollop::options {
27
+ banner "Configure default options for ShowRobot\n"
28
+ opt :global, "Modify the global options for ShowRobot", :default => false, :short => :g
29
+ instance_eval &OptionSet[:config]
30
+ }
31
+
32
+ apply_options options
33
+
34
+ file = opts[:global] ? File.dirname(__FILE__) + '/../config.yml' : ShowRobot.config[:config_file]
35
+
36
+ conf = YAML::load(IO.read(file)) rescue {}
37
+
38
+ # extend the loaded configuration
39
+ while not ARGV.empty?
40
+ prop, val = ARGV.shift 2
41
+ conf[prop] = val
42
+
43
+ printf "Setting [ %-12s ] to [ %-12s ]\n", prop, val if ShowRobot.config[:verbose]
44
+ end
45
+
46
+ # write to the file
47
+ File.open(file, 'w') do |file|
48
+ file.write conf.to_yaml
49
+ end
50
+
51
+ when :rename
52
+ # mkdir_p and mv
53
+ require 'fileutils'
54
+
55
+ options = Trollop::options do
56
+ banner "Rename a file according to the specified format\n"
57
+
58
+ # Configuration options
59
+ instance_eval &OptionSet[:config]
60
+
61
+ # Cache options
62
+ instance_eval &OptionSet[:cache]
63
+
64
+ # Database options
65
+ instance_eval &OptionSet[:database]
66
+
67
+ # File options
68
+ instance_eval &OptionSet[:file]
69
+
70
+ # Rename-specific options
71
+ opt :movie_format, "Define the output filename for movies", :type => String, :short => :m
72
+ opt :tv_format, "Define the output filename for TV shows", :type => String, :short => :t
73
+ opt :dry_run, "Dry run; don't touch the files", :default => false, :short => :D
74
+ opt :space_replace, "Replace spaces in the output file name with the specified character", :default => " ", :short => :r
75
+ end
76
+
77
+ apply_options options
78
+
79
+ # loop through the video files
80
+ ARGV.select { |file| ShowRobot::MediaFile.isvideo? file }.each do |file|
81
+ media = ShowRobot::MediaFile.load file
82
+
83
+ if options[:force_movie] or (media.is_movie? and not options[:force_tv])
84
+ # TODO
85
+ else options[:force_tv] or (media.is_tv? and not options[:force_movie])
86
+ identity = identify media, ShowRobot.config[:tv_database], options[:prompt]
87
+ filename_format = translate options[:tv_format], {
88
+ '{s}' => '%<season>02d',
89
+ '{e}' => '%<episode>02d',
90
+ '{n}' => '%<series>s',
91
+ '{t}' => '%<title>s',
92
+ '{ext}' => File.extname(file).sub(/^\./, '')
93
+ }
94
+ end
95
+
96
+ new_filename = File.expand_path(sprintf(filename_format, identity).gsub(/\s/, options[:space_replace]))
97
+
98
+ puts "Would have performed the following operations:" if options[:dry_run]
99
+
100
+ # ensure the target directory exists
101
+ FileUtils.mkdir_p File.dirname(new_filename), :noop => options[:dry_run], :verbose => options[:dry_run]
102
+
103
+ # if the file already exists, log an error
104
+ if File.exists? new_filename
105
+ ShowRobot.log :error, "mv failed (destination exists): [ #{file} ] --> [ #{new_filename} ]" if not options[:dry_run]
106
+ else
107
+ ShowRobot.log :info, "mv [ #{file} ] --> [ #{new_filename} ]" if not options[:dry_run]
108
+ FileUtils.mv file, new_filename, :noop => options[:dry_run], :verbose => options[:dry_run] # verbosity here is covered by the logging statement
109
+ end
110
+ end
111
+ when :identify
112
+ else
113
+ puts "Unknown subcommand: #{command.inspect}"
114
+ end
@@ -0,0 +1,12 @@
1
+ require 'showrobot/log'
2
+ require 'showrobot/config'
3
+ require 'showrobot/media_file'
4
+ require 'showrobot/db'
5
+ (
6
+ # load all utility files
7
+ Dir[File.dirname(__FILE__) + '/showrobot/utility/*.rb'] +
8
+ # load all media file parsers
9
+ Dir[File.dirname(__FILE__) + '/showrobot/video/*.rb'] +
10
+ # load all database shims
11
+ Dir[File.dirname(__FILE__) + '/showrobot/db/*.rb']
12
+ ).each { |file| require file }
@@ -0,0 +1,25 @@
1
+ # helper function for identifying a file
2
+ def identify media, database, prompt
3
+ if media.is_movie?
4
+
5
+ else
6
+ db = ShowRobot.create_datasource database
7
+ db.mediaFile = media
8
+
9
+ db.series = if prompt
10
+ select db.series_list, "Select a series for [ #{media.fileName} ]" do |i, item|
11
+ sprintf " %3d) %s", i, item[:name]
12
+ end
13
+ else
14
+ db.series_list.first
15
+ end
16
+
17
+ if prompt
18
+ select db.episode_list, "Select an episode for [ #{media.fileName} ]" do |i, item|
19
+ sprintf " %3d) [%02d.%02d] %s", i, item[:season], item[:episode], item[:title]
20
+ end
21
+ else
22
+ db.episode media.season, media.episode
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,59 @@
1
+ # Define various command line option sets here
2
+
3
+ BASIC_USAGE = <<EOS
4
+ ShowRobot is a utility for matching media files against
5
+ TV and movie databases to categorize your media collection.
6
+ EOS
7
+
8
+ OptionSet = {}
9
+ OptionSet[:config] = Proc.new do
10
+ # Use the specified configuration file
11
+ opt :config_file, "Use the specified configuration file for ShowRobot", :type => String, :short => :f
12
+
13
+ # Causes ShowRobot to display additional debugging information
14
+ opt :verbose, "Show detailed information messages", :default => false
15
+ end
16
+
17
+ OptionSet[:cache] = Proc.new do
18
+ # Cache control
19
+ opt :cache_dir, "Use the specified directory for caching server queries", :type => String, :short => :c
20
+ opt :no_cache, "Do not use caching for any queries", :default => false, :short => nil
21
+ opt :clear_cache, "Clear the cache and start fresh with queries", :default => false, :short => :C
22
+ end
23
+
24
+ OptionSet[:database] = Proc.new do
25
+ # Media database identification options
26
+ opt :movie_database, "Use the specified database for movie matching", :type => String, :short => nil
27
+ opt :tv_database, "Use the specified database for TV show matching", :type => String, :short => nil
28
+
29
+ # prompts user to choose amongst various options instead of deferring to the first
30
+ opt :prompt, "Prompt for media file matches", :default => false, :short => :p
31
+ end
32
+
33
+ OptionSet[:file] = Proc.new do
34
+ # File type control
35
+ opt :force_movie, "Force the specified file(s) to be matched against the movie database", :default => false, :short => :M
36
+ opt :force_tv, "Force the specified file(s) to be matched against the tv database", :default => false, :short => :T
37
+ end
38
+
39
+ def apply_options options={}
40
+ require 'showrobot'
41
+
42
+ options.each do |key, value|
43
+ # if the value is nil, skip
44
+ next if not options.include?((key.to_s + '_given').to_sym) or value.nil?
45
+
46
+ case key
47
+ when :config_file
48
+ ShowRobot.configure :config_file => options[:config_file]
49
+ ShowRobot.load_config
50
+ when :cache_dir, :tv_database, :movie_database, :verbose
51
+ ShowRobot.configure key, value
52
+ when :no_cache
53
+ ShowRobot.configure :use_cache, false
54
+ when :clear_cache
55
+ puts "Clearing cache in [ #{ShowRobot.config[:cache_dir]} ]" if ShowRobot.config[:verbose]
56
+ File.delete(*Dir[ShowRobot.config[:cache_dir] + '/*.cache'])
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,25 @@
1
+ # helper function for prompts
2
+ def select list, prompt, &format
3
+ range = 0...10
4
+ begin
5
+ puts prompt
6
+ range.each do |i|
7
+ puts format.call i, list[i]
8
+ end
9
+ print " > "
10
+ input = $stdin.gets.chomp
11
+
12
+ case input
13
+ when /^[\d]+$/
14
+ # it's a number, make sure it's in the range
15
+ selected = list[input.to_i] if input.to_i < list.length
16
+ when 'n' # next range
17
+ range = Range.new(range.end, [range.end + 10, list.length].min, true) if range.end < list.length
18
+ when 'p' # prev range
19
+ range = Range.new([0, range.first - 10].max, range.first, true) if range.first > 1
20
+ when 'q'
21
+ exit
22
+ end
23
+ end while selected.nil?
24
+ selected
25
+ end
@@ -0,0 +1,3 @@
1
+ def translate str, mapping
2
+ str.gsub(/(?<!\\){[^}]+}/, mapping).gsub(/\\({[^}]+})/, '\1')
3
+ end
@@ -0,0 +1,53 @@
1
+ module ShowRobot
2
+
3
+ require 'yaml'
4
+
5
+ # configuration defaults
6
+ @config = {
7
+ # base directory against which other files are loaded (i.e. cache/config/logs)
8
+ :basepath => ENV['HOME'] + '/.showrobot',
9
+ # verbosity of output
10
+ :verbose => false,
11
+ # whether or not to use the cache store for data fetching
12
+ :use_cache => true,
13
+ # where the cache directory is located
14
+ :cache_dir => ENV['HOME'] + '/.showrobot/cache',
15
+ # default configuration file
16
+ :config_file => ENV['HOME'] + '/.showrobot/config.yml'
17
+ }
18
+ def self.config
19
+ @config
20
+ end
21
+
22
+ def self.configure *args
23
+ if args.length == 2
24
+ @config[args[0].to_sym] = args[1]
25
+ elsif args[0].instance_of? Hash
26
+ args[0].each { |k, v| @config[k.to_sym] = v }
27
+ else
28
+ raise "Invalid arguments to ShowRobot.configure: #{args}"
29
+ end
30
+ end
31
+ # Configure via hash
32
+ #def self.configure(opts = {})
33
+ # opts.each { |k, v| @config[k.to_sym] = v } if not opts.nil?
34
+ #end
35
+
36
+ def self.load_config(file = @config[:config_file])
37
+ begin
38
+ config = YAML::load(IO.read(file))
39
+ rescue Errno::ENOENT
40
+ puts :warning, "YAML configuration file could not be found. Using defaults."
41
+ rescue Psych::SyntaxError
42
+ puts :warning, "YAML configuration file contains invalid syntax. Using defaults"
43
+ end
44
+
45
+ configure config
46
+ end
47
+
48
+ # load the default configuration file
49
+ load_config(File.dirname(__FILE__) + '/../../config.yml')
50
+ # and the environment configuration file
51
+ load_config
52
+
53
+ end
@@ -0,0 +1,52 @@
1
+ module ShowRobot
2
+
3
+ class Datasource
4
+ def initialize
5
+ end
6
+
7
+ # returns a list of series matching the given file
8
+ def series_list
9
+ puts "Fetching series data for [ #{@mediaFile.name_guess} ] from #{self.class::DB_NAME} (#{match_query})" if ShowRobot.config[:verbose] and @series_list.nil?
10
+
11
+ @series_list ||= yield ShowRobot.fetch(self.class::DATA_TYPE, match_query)
12
+ end
13
+
14
+ def episode_list
15
+ puts "Fetching episode data for [ #{series[:name]} ] from #{self.class::DB_NAME} (#{episode_query})" if ShowRobot.config[:verbose] and @episode_list.nil?
16
+
17
+ @episode_list ||= yield ShowRobot.fetch(self.class::DATA_TYPE, episode_query)
18
+ end
19
+
20
+ attr_accessor :mediaFile
21
+
22
+ attr_writer :series
23
+ def series
24
+ @series ||= series_list.first
25
+ end
26
+
27
+ # Returns the episode data for the specified episode
28
+ def episode(seasonnum = @mediaFile.season, episodenum = @mediaFile.episode)
29
+ episode_list.find { |ep| ep[:season] == seasonnum and ep[:episode] == episodenum }
30
+ end
31
+ end
32
+
33
+ class << self
34
+ DATASOURCES = {}
35
+ def add_datasource sym, klass
36
+ DATASOURCES[sym] = klass
37
+ end
38
+
39
+ def create_datasource sym
40
+ datasource_for(sym).new
41
+ end
42
+
43
+ def datasource_for sym
44
+ DATASOURCES[sym.to_sym]
45
+ end
46
+
47
+ def url_encode(s)
48
+ s.to_s.gsub(/[^a-zA-Z0-9_\-.]/n){ sprintf("%%%02X", $&.unpack("C")[0]) }
49
+ end
50
+ end
51
+
52
+ end
@@ -0,0 +1,41 @@
1
+ module ShowRobot
2
+
3
+ class TheTVDB < Datasource
4
+ DB_NAME = "The TVDB"
5
+ DATA_TYPE = :xml
6
+
7
+ def match_query
8
+ "http://www.thetvdb.com/api/GetSeries.php?seriesname=#{ShowRobot.url_encode @mediaFile.name_guess}&language=en"
9
+ end
10
+
11
+ def episode_query
12
+ lang = 'en' # TODO
13
+ "http://www.thetvdb.com/api/#{ShowRobot.config[:tvdb_api_key]}/series/#{series[:source].find('seriesid').first.content}/all/#{lang}.xml"
14
+ end
15
+
16
+ # Returns a list of series related to the media file
17
+ def series_list
18
+ super do |xml|
19
+ xml.find('//Series').collect { |series| {:name => series.find('SeriesName').first.content, :source => series} }
20
+ end
21
+ end
22
+
23
+ # Returns a list of episodes related to the media file from a given series
24
+ def episode_list
25
+ super do |xml|
26
+ xml.find('//Episode').collect do |episode|
27
+ {
28
+ :series => series[:name],
29
+ :title => episode.find('EpisodeName').first.content,
30
+ :season => episode.find('SeasonNumber').first.content.to_i,
31
+ :episode => episode.find('EpisodeNumber').first.content.to_i,
32
+ :episode_ct => episode.find('Combined_episodenumber').first.content.to_i
33
+ }
34
+ end
35
+ end
36
+ end
37
+
38
+ end
39
+
40
+ add_datasource :tvdb, TheTVDB
41
+ end
@@ -0,0 +1,49 @@
1
+ module ShowRobot
2
+
3
+ class TVRage < Datasource
4
+ DB_NAME = 'TVRage'
5
+
6
+ def match_query
7
+ "http://services.tvrage.com/feeds/full_search.php?show=#{ShowRobot.url_encode @mediaFile.name_guess}"
8
+ end
9
+
10
+ def episode_query
11
+ "http://services.tvrage.com/feeds/full_show_info.php?sid=#{show_id}"
12
+ end
13
+
14
+ def fetch
15
+ puts " Fetching #{@mediaFile.name_guess} from #{DB_NAME} (#{match_query})" if ShowRobot::VERBOSE
16
+
17
+ doc = XML::Parser.string(open(match_query).read).parse
18
+
19
+ # TODO - make sure this matches
20
+ bestMatch = doc.find('//show').first
21
+ @showName = bestMatch.find('name').first.content
22
+ @showId = bestMatch.find('showid').first.content
23
+
24
+ puts " --> Query: [ #{@mediaFile.name_guess} ] Match: [ #{@showName} ]@#{@showId}" if ShowRobot::VERBOSE
25
+ end
26
+
27
+ def episode
28
+ puts " Fetching episode list for #{show_name}@#{show_id} (#{episode_query})" if ShowRobot::VERBOSE
29
+
30
+ puts fetch(:xml, episode_query).find("//episode[seasonnum/.=#{@mediaFile.season} and epnum/.=#{@mediaFile.episode}]")
31
+ end
32
+
33
+ def show_name
34
+ if @showName.nil?
35
+ fetch
36
+ end
37
+ @showName
38
+ end
39
+
40
+ def show_id
41
+ if @showId.nil?
42
+ fetch
43
+ end
44
+ @showId
45
+ end
46
+ end
47
+
48
+ add_datasource :tvrage, TVRage
49
+ end
@@ -0,0 +1,10 @@
1
+ module ShowRobot
2
+
3
+ def self.log level, message
4
+ puts "[#{level}] #{message}" if ShowRobot.config[:verbose]
5
+ File.open(config[:basepath] + '/log', 'a') do |file|
6
+ file.printf "[%s](%-8s) %s\n", Time.new.strftime("%y/%m/%d %H:%M:%S"), level, message
7
+ end
8
+ end
9
+
10
+ end
@@ -0,0 +1,66 @@
1
+ module ShowRobot
2
+
3
+ class MediaFile
4
+ attr_reader :fileName
5
+
6
+ # class methods
7
+ def self.load fileName
8
+ begin
9
+ @@video_types[File.extname(fileName)].new fileName
10
+ rescue
11
+ raise "No parser exists for files of type '#{File.extname(fileName)}'"
12
+ end
13
+ end
14
+
15
+ def self.isvideo? fileName
16
+ @@video_types.include? File.extname(fileName)
17
+ end
18
+
19
+ def self.addType ext, klass
20
+ @@video_types['.' + ext.to_s] = klass
21
+ end
22
+
23
+ # instance methods
24
+ def isvideo?
25
+ MediaFile.isvideo? @fileName
26
+ end
27
+
28
+ def is_movie?
29
+ parse[:type] == :movie
30
+ end
31
+
32
+ def is_tv?
33
+ parse[:type] == :tv
34
+ end
35
+
36
+ def season
37
+ parse[:season]
38
+ end
39
+
40
+ def episode
41
+ parse[:episode]
42
+ end
43
+
44
+ def name_guess
45
+ @name_guess ||= parse[:name_guess].gsub(/[^a-zA-Z0-9]/, ' ').gsub(/\s+/, ' ').strip
46
+ end
47
+
48
+ def match database
49
+ ShowRobot.datasource_for(database).new(self)
50
+ end
51
+
52
+ protected
53
+ def initialize fileName
54
+ @fileName = fileName
55
+ end
56
+
57
+ # parses a file name for the constituent parts
58
+ def parse
59
+ @parse ||= ShowRobot.parse_filename File.basename(@fileName)
60
+ end
61
+
62
+ private
63
+ @@video_types = {}
64
+ end
65
+
66
+ end
@@ -0,0 +1,52 @@
1
+ module ShowRobot
2
+
3
+ # for processing xml data
4
+ require 'xml'
5
+ # for fetching data
6
+ require 'open-uri'
7
+ # extra file utilities
8
+ require 'fileutils'
9
+
10
+ # Fetches the given site and processes it as the given type
11
+ def fetch type, url
12
+
13
+ if not URI(url).scheme.nil?
14
+ # determine the location of the cache file
15
+ cache_file = File.join(ShowRobot.config[:cache_dir], url.gsub(/([^a-zA-Z0-9_\.-]+)/) { '%' + $1.unpack('H2' * $1.size).join('%').upcase }.tr(' ', '+') + '.cache')
16
+
17
+ # if USE_CACHE is true, attempt to find the file out of the cache
18
+ contents = if ShowRobot.config[:use_cache] && File.exists?(cache_file)
19
+ puts "Found cache entry for [ #{url} ]" if ShowRobot.config[:verbose]
20
+ File.read cache_file
21
+ else # not in cache, fetch from web
22
+ open(url).read
23
+ end
24
+
25
+ # if USE_CACHE and the cache file doesn't exist, write to it
26
+ if ShowRobot.config[:use_cache] and not File.exists? cache_file
27
+ puts "Creating cache entry for [ #{url} ]" if ShowRobot.config[:verbose]
28
+
29
+ # create the cache directory if it doesn't exist
30
+ FileUtils.mkdir_p File.dirname(cache_file) if not File.directory? File.dirname(cache_file)
31
+
32
+ # write to the cache file
33
+ File.open(cache_file, 'w') { |f| f.write contents }
34
+ end
35
+ else
36
+ contents = IO.read(url)
37
+ end
38
+
39
+ # dispatch on requested type
40
+ case type
41
+ when :xml
42
+ XML::Parser.string(contents).parse
43
+ when :yml
44
+ YAML::load(contents)
45
+ else
46
+ raise "Invalid datatype to fetch: [ #{type.to_s} ]"
47
+ end
48
+ end
49
+
50
+ module_function :fetch
51
+
52
+ end
@@ -0,0 +1,20 @@
1
+ module ShowRobot
2
+
3
+ YEAR_PATTERNS = [
4
+ # in parens
5
+ /\((\d{4})\)/,
6
+ # in square brackets
7
+ /\[(\d{4})\]/,
8
+ # ... really, just any 4 numbers in a row.
9
+ /\D(\d{4})\D/
10
+ ]
11
+
12
+ def file_get_year fileName
13
+ if not YEAR_PATTERNS.find { |pattern| pattern.match fileName }.nil?
14
+ $1
15
+ end
16
+ end
17
+
18
+ module_function :file_get_year
19
+
20
+ end
@@ -0,0 +1,35 @@
1
+ module ShowRobot
2
+
3
+ EPISODE_PATTERNS = [
4
+ # S01E02
5
+ /s(?<season>\d{1,4}).?(?<episode>\d{1,4})/i,
6
+ # season01episode02
7
+ /season\s*(?<season>\d{1,4})\sepisode\s*(?<episode>\d{1,4})/i,
8
+ # sxe
9
+ /\D(?<season>\d{1,4})x(?<episode>\d{1,4})\D/
10
+ ]
11
+
12
+ # parse the name of a file into best-guess parts
13
+ def parse_filename fileName
14
+
15
+ # try parsing as a tv show - needs to have season/episode information
16
+ if not EPISODE_PATTERNS.find { |pattern| pattern.match fileName }.nil?
17
+ {
18
+ :name_guess => fileName[0, fileName.index($&)],
19
+ :year => ShowRobot.file_get_year(fileName),
20
+ :season => $~['season'].to_i,
21
+ :episode => $~['episode'].to_i,
22
+ :type => :tv
23
+ }
24
+ else
25
+ # probably a movie
26
+ {
27
+ :name_guess => fileName
28
+ }
29
+ end
30
+
31
+ end
32
+
33
+ module_function :parse_filename
34
+
35
+ end
@@ -0,0 +1,3 @@
1
+ module ShowRobot
2
+ VERSION = '0.0.1' unless defined?(::ShowRobot::VERSION)
3
+ end
@@ -0,0 +1,16 @@
1
+ module ShowRobot
2
+
3
+ class AVIFile < MediaFile
4
+ def initialize fileName
5
+ super(fileName)
6
+ end
7
+
8
+ def duration
9
+ @duration ||= `ffmpeg -i "#{@fileName}" 2>&1`[/Duration: ([\d:\.]*)/, 1].split(':').each_with_index.map { |n, i| n.to_f * (60 ** (2-i)) }.reduce(0, :+) rescue nil
10
+ end
11
+
12
+ end
13
+
14
+ MediaFile.addType :avi, AVIFile
15
+
16
+ end
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: showrobot
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Scott Rabin
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-07-01 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ description: ''
31
+ email:
32
+ - showrobot@scottrabin.com
33
+ executables:
34
+ - showrobot
35
+ extensions: []
36
+ extra_rdoc_files: []
37
+ files:
38
+ - bin/showrobot
39
+ - lib/showrobot/db.rb
40
+ - lib/showrobot/cli/translate.rb
41
+ - lib/showrobot/cli/options.rb
42
+ - lib/showrobot/cli/select.rb
43
+ - lib/showrobot/cli/identify.rb
44
+ - lib/showrobot/log.rb
45
+ - lib/showrobot/config.rb
46
+ - lib/showrobot/version.rb
47
+ - lib/showrobot/video/avi.rb
48
+ - lib/showrobot/media_file.rb
49
+ - lib/showrobot/utility/file_get_year.rb
50
+ - lib/showrobot/utility/parse_filename.rb
51
+ - lib/showrobot/utility/fetch.rb
52
+ - lib/showrobot/db/thetvdb.rb
53
+ - lib/showrobot/db/tvrage.rb
54
+ - lib/showrobot.rb
55
+ - LICENSE
56
+ - README.rdoc
57
+ homepage: ''
58
+ licenses:
59
+ - MIT
60
+ post_install_message:
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ none: false
72
+ requirements:
73
+ - - ! '>='
74
+ - !ruby/object:Gem::Version
75
+ version: 1.3.6
76
+ requirements: []
77
+ rubyforge_project: showrobot
78
+ rubygems_version: 1.8.23
79
+ signing_key:
80
+ specification_version: 3
81
+ summary: An easy-to-use command line tool for sorting, classifying, and organizing
82
+ your media
83
+ test_files: []