showrobot 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.rdoc +19 -0
- data/bin/showrobot +114 -0
- data/lib/showrobot.rb +12 -0
- data/lib/showrobot/cli/identify.rb +25 -0
- data/lib/showrobot/cli/options.rb +59 -0
- data/lib/showrobot/cli/select.rb +25 -0
- data/lib/showrobot/cli/translate.rb +3 -0
- data/lib/showrobot/config.rb +53 -0
- data/lib/showrobot/db.rb +52 -0
- data/lib/showrobot/db/thetvdb.rb +41 -0
- data/lib/showrobot/db/tvrage.rb +49 -0
- data/lib/showrobot/log.rb +10 -0
- data/lib/showrobot/media_file.rb +66 -0
- data/lib/showrobot/utility/fetch.rb +52 -0
- data/lib/showrobot/utility/file_get_year.rb +20 -0
- data/lib/showrobot/utility/parse_filename.rb +35 -0
- data/lib/showrobot/version.rb +3 -0
- data/lib/showrobot/video/avi.rb +16 -0
- metadata +83 -0
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.
|
data/README.rdoc
ADDED
@@ -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
|
+
|
data/bin/showrobot
ADDED
@@ -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
|
data/lib/showrobot.rb
ADDED
@@ -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,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
|
data/lib/showrobot/db.rb
ADDED
@@ -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,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: []
|