royw-dvdprofiler2xbmc 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +4 -0
- data/Manifest.txt +16 -0
- data/PostInstall.txt +7 -0
- data/README.rdoc +104 -0
- data/Rakefile +33 -0
- data/bin/dvdprofiler2xbmc +10 -0
- data/lib/dvdprofiler2xbmc/app.rb +160 -0
- data/lib/dvdprofiler2xbmc/app_config.rb +95 -0
- data/lib/dvdprofiler2xbmc/cli.rb +118 -0
- data/lib/dvdprofiler2xbmc/collection.rb +161 -0
- data/lib/dvdprofiler2xbmc/extensions.rb +103 -0
- data/lib/dvdprofiler2xbmc/imdb_extensions.rb +95 -0
- data/lib/dvdprofiler2xbmc/media.rb +44 -0
- data/lib/dvdprofiler2xbmc/media_files.rb +38 -0
- data/lib/dvdprofiler2xbmc/nfo.rb +103 -0
- data/lib/dvdprofiler2xbmc.rb +6 -0
- metadata +157 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
History.txt
|
2
|
+
Manifest.txt
|
3
|
+
PostInstall.txt
|
4
|
+
README.rdoc
|
5
|
+
Rakefile
|
6
|
+
bin/dvdprofiler2xbmc
|
7
|
+
lib/dvdprofiler2xbmc.rb
|
8
|
+
lib/dvdprofiler2xbmc/app_config.rb
|
9
|
+
lib/dvdprofiler2xbmc/app.rb
|
10
|
+
lib/dvdprofiler2xbmc/cli.rb
|
11
|
+
lib/dvdprofiler2xbmc/collection.rb
|
12
|
+
lib/dvdprofiler2xbmc/extensions.rb
|
13
|
+
lib/dvdprofiler2xbmc/imdb_extensions.rb
|
14
|
+
lib/dvdprofiler2xbmc/media_files.rb
|
15
|
+
lib/dvdprofiler2xbmc/media.rb
|
16
|
+
lib/dvdprofiler2xbmc/nfo.rb
|
data/PostInstall.txt
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
= dvdprofiler2xbmc
|
2
|
+
|
3
|
+
* http://www.github.com/royw/dvdprofiler2xbmc
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
This script will attempt to match up media files from a set of directories
|
8
|
+
to the collection.xml file exported from DVD Profiler. For matches, the
|
9
|
+
script will then create a {moviename}.nfo from the data in collections.xml
|
10
|
+
and also copy the front cover image to {moviename}.tbn. Both files will
|
11
|
+
be placed in the same directory as the source media file.
|
12
|
+
|
13
|
+
Then on XBMC, set the source content to none to remove the meta data from
|
14
|
+
the library, then set the source content back to Movies to import the
|
15
|
+
media. This time, the data in the .nfo files will be used instead of
|
16
|
+
scraping.
|
17
|
+
|
18
|
+
== NOTES:
|
19
|
+
|
20
|
+
1) currently you lose a few meta data fields such as Rating and Director
|
21
|
+
using this script instead of a scraper.
|
22
|
+
|
23
|
+
2) Currently only supports file based media containers, not directory
|
24
|
+
based.
|
25
|
+
|
26
|
+
3) Media filename convention is to take the media's title from DVD Profiler,
|
27
|
+
replace any punctuation with a space character, then replace any multiple
|
28
|
+
spaces with a single space. Next remove any leading or trailing spaces.
|
29
|
+
Optionally can append " - YYYY" where YYYY is the movie's release year.
|
30
|
+
Naturally the extension is the media's container type. Note, you should
|
31
|
+
not include in the title edition info like "Widescreen" or "Special Edition"
|
32
|
+
eventhough there are some mistakes in the DVD Profiler profiles that do
|
33
|
+
include these in the title.
|
34
|
+
|
35
|
+
== FEATURES/PROBLEMS:
|
36
|
+
|
37
|
+
Features:
|
38
|
+
|
39
|
+
* Creates .nfo files from exported collection.xml from DVD Profiler
|
40
|
+
* If .nfo does not have an <id> tag, then tries to find the IMDB ID by
|
41
|
+
using the title and production/release years to search IMDB.
|
42
|
+
* Sets file permissions for files and directories
|
43
|
+
* Media can be contained in a set of directories (they can be mount points)
|
44
|
+
* Adds sub-directory names as genres to .nfo files
|
45
|
+
|
46
|
+
Problems:
|
47
|
+
|
48
|
+
* Always overwriting .nfo. Needs to only write if there has been a change.
|
49
|
+
* Loses any tags not directly supported. Needs to change from overwrite
|
50
|
+
to merge.
|
51
|
+
* IMDB ID scraping doesn't handle boxed sets or multiple movies per media
|
52
|
+
file. I don't see how to work around this exept to eliminate from media
|
53
|
+
library.
|
54
|
+
* Needs a better method to setup AppConfig defaults.
|
55
|
+
* Needs to support selectable/multiple regex based naming conventions
|
56
|
+
* Needs to support directory containers
|
57
|
+
* Does not support stacking
|
58
|
+
|
59
|
+
== SYNOPSIS:
|
60
|
+
|
61
|
+
Edit the defaults in lib/dvdprofiler2xbmc/app_config.rb to reflect your
|
62
|
+
system. Then run:
|
63
|
+
|
64
|
+
bin/dvdprofiler2xbmc
|
65
|
+
|
66
|
+
For help, run bin/dvdprofiler2xbmc --help
|
67
|
+
|
68
|
+
== REQUIREMENTS:
|
69
|
+
|
70
|
+
* ruby 1.8.x
|
71
|
+
* rubygem
|
72
|
+
|
73
|
+
== INSTALL:
|
74
|
+
|
75
|
+
sudo gem install xml-simple
|
76
|
+
sudo gem install porras-imdb
|
77
|
+
sudo gem install log4r
|
78
|
+
sudo gem install commandline
|
79
|
+
sudo gem install mash
|
80
|
+
|
81
|
+
== LICENSE:
|
82
|
+
|
83
|
+
(The MIT License)
|
84
|
+
|
85
|
+
Copyright (c) 2009 Roy Wright
|
86
|
+
|
87
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
88
|
+
a copy of this software and associated documentation files (the
|
89
|
+
'Software'), to deal in the Software without restriction, including
|
90
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
91
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
92
|
+
permit persons to whom the Software is furnished to do so, subject to
|
93
|
+
the following conditions:
|
94
|
+
|
95
|
+
The above copyright notice and this permission notice shall be
|
96
|
+
included in all copies or substantial portions of the Software.
|
97
|
+
|
98
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
99
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
100
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
101
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
102
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
103
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
104
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
%w[rubygems rake rake/clean fileutils newgem rubigen].each { |f| require f }
|
2
|
+
require File.dirname(__FILE__) + '/lib/dvdprofiler2xbmc'
|
3
|
+
|
4
|
+
# Generate all the Rake tasks
|
5
|
+
# Run 'rake -T' to see list of generated tasks (from gem root directory)
|
6
|
+
$hoe = Hoe.new('dvdprofiler2xbmc', Dvdprofiler2xbmc::VERSION) do |p|
|
7
|
+
p.developer('Roy Wright', 'roy@wright.org')
|
8
|
+
p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
|
9
|
+
p.post_install_message = 'PostInstall.txt' # TODO remove if post-install message not required
|
10
|
+
p.rubyforge_name = p.name # TODO this is default value
|
11
|
+
p.extra_deps = [
|
12
|
+
['activesupport','>= 2.0.2'],
|
13
|
+
['xml-simple','>= 1.0.12'],
|
14
|
+
['porras-imdb','>= 0.0.5'],
|
15
|
+
['log4r','>= 1.0.5'],
|
16
|
+
['commandline','>= 0.7.10'],
|
17
|
+
['mash','>= 0.0.3']
|
18
|
+
]
|
19
|
+
p.extra_dev_deps = [
|
20
|
+
['newgem', ">= #{::Newgem::VERSION}"]
|
21
|
+
]
|
22
|
+
|
23
|
+
p.clean_globs |= %w[**/.DS_Store tmp *.log]
|
24
|
+
path = (p.rubyforge_name == p.name) ? p.rubyforge_name : "\#{p.rubyforge_name}/\#{p.name}"
|
25
|
+
p.remote_rdoc_dir = File.join(path.gsub(/^#{p.rubyforge_name}\/?/,''), 'rdoc')
|
26
|
+
p.rsync_args = '-av --delete --ignore-errors'
|
27
|
+
end
|
28
|
+
|
29
|
+
require 'newgem/tasks' # load /tasks/*.rake
|
30
|
+
Dir['tasks/**/*.rake'].each { |t| load t }
|
31
|
+
|
32
|
+
# TODO - want other tests/tasks run by default? Add them to the list
|
33
|
+
# task :default => [:spec, :features]
|
@@ -0,0 +1,160 @@
|
|
1
|
+
# == Synopsis
|
2
|
+
# Transfer media meta data from DvdProfiler to the format that XBMC needs it (.tbn and .nfo files)
|
3
|
+
#
|
4
|
+
# usage:
|
5
|
+
# app = DvdProfiler2Xbmc.new
|
6
|
+
# app.execute
|
7
|
+
# app.report.each {|line| puts line}
|
8
|
+
class DvdProfiler2Xbmc
|
9
|
+
@interrupted = false
|
10
|
+
|
11
|
+
# A trap("INT") in the Runner calls this to indicate that a ^C has been detected.
|
12
|
+
# Note, once set, it is never cleared
|
13
|
+
def self.interrupt
|
14
|
+
AppConfig[:logger].error { "control-C detected, finishing current task" }
|
15
|
+
@interrupted = true
|
16
|
+
end
|
17
|
+
|
18
|
+
# Long loops should poll this method to see if they should abort
|
19
|
+
# Returns:: true if the application has trapped an "INT", false otherwise
|
20
|
+
def self.interrupted?
|
21
|
+
@interrupted
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize
|
25
|
+
@media_files = nil
|
26
|
+
@collection = nil
|
27
|
+
end
|
28
|
+
|
29
|
+
def execute
|
30
|
+
@media_files = MediaFiles.new(AppConfig[:directories])
|
31
|
+
|
32
|
+
collection_filepath = File.expand_path(AppConfig[:collection_filespec])
|
33
|
+
@collection = Collection.new(collection_filepath)
|
34
|
+
|
35
|
+
@media_files.titles.each do |title, medias|
|
36
|
+
break if DvdProfiler2Xbmc.interrupted?
|
37
|
+
# the following lines are order dependent
|
38
|
+
find_isbns(title, medias)
|
39
|
+
copy_thumbnails(title, medias)
|
40
|
+
create_nfos(title, medias)
|
41
|
+
end
|
42
|
+
|
43
|
+
# set file and directory permissions
|
44
|
+
AppConfig[:directories].each do |dir|
|
45
|
+
Dir.glob(File.join(dir, '**/*')).each do |f|
|
46
|
+
begin
|
47
|
+
if File.directory?(f)
|
48
|
+
File.chmod(AppConfig[:dir_permissions], f) unless AppConfig[:dir_permissions].nil?
|
49
|
+
else
|
50
|
+
File.chmod(AppConfig[:file_permissions], f) unless AppConfig[:file_permissions].nil?
|
51
|
+
end
|
52
|
+
rescue Exception => e
|
53
|
+
AppConfig[:logger].error {e.to_s}
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# generate the report.
|
60
|
+
# Note, must be ran after execute()
|
61
|
+
# returns an array of lines
|
62
|
+
def report
|
63
|
+
buf = []
|
64
|
+
unless DvdProfiler2Xbmc.interrupted?
|
65
|
+
unless @media_files.nil?
|
66
|
+
duplicates = duplicates_report
|
67
|
+
unless duplicates.empty?
|
68
|
+
buf << "Duplicates:\n"
|
69
|
+
buf += duplicates
|
70
|
+
end
|
71
|
+
|
72
|
+
missing_isbns = missing_isbn_report
|
73
|
+
unless missing_isbns.empty?
|
74
|
+
buf += missing_isbns
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
buf
|
79
|
+
end
|
80
|
+
|
81
|
+
protected
|
82
|
+
|
83
|
+
# find ISBN for each title and assign to the media
|
84
|
+
def find_isbns(title, medias)
|
85
|
+
title_pattern = Collection.title_pattern(title)
|
86
|
+
unless @collection.title_isbn_hash[title_pattern].nil?
|
87
|
+
medias.each do |media|
|
88
|
+
media.isbn = @collection.title_isbn_hash[title_pattern]
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# copy images from .../isbn.jpg to .../basename.jpg
|
94
|
+
def copy_thumbnails(title, medias)
|
95
|
+
medias.each do |media|
|
96
|
+
unless media.isbn.nil?
|
97
|
+
media.isbn.each do |isbn|
|
98
|
+
src_image_filespec = File.join(AppConfig[:images_dir], "#{isbn}f.jpg")
|
99
|
+
if File.exist?(src_image_filespec)
|
100
|
+
dest_image_filespec = media.media_path.ext(".#{AppConfig[:thumbnail_extension]}")
|
101
|
+
begin
|
102
|
+
File.copy(src_image_filespec, dest_image_filespec)
|
103
|
+
rescue Exception => e
|
104
|
+
AppConfig[:logger].error {e.to_s}
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# create nfo files from collection.isbn_dvd_hash
|
113
|
+
def create_nfos(title, medias)
|
114
|
+
medias.each do |media|
|
115
|
+
unless media.isbn.nil?
|
116
|
+
media.isbn.each do |isbn|
|
117
|
+
dvd_hash = @collection.isbn_dvd_hash[isbn]
|
118
|
+
unless dvd_hash.nil?
|
119
|
+
nfo = NFO.new(media, dvd_hash)
|
120
|
+
nfo.save
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# duplicate media file report
|
128
|
+
def duplicates_report
|
129
|
+
buf = []
|
130
|
+
duplicates = @media_files.duplicate_titles
|
131
|
+
unless duplicates.empty?
|
132
|
+
duplicates.each do |title, medias|
|
133
|
+
if medias.length > 1
|
134
|
+
buf << title
|
135
|
+
medias.each {|media| buf << " #{media.media_path}"}
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
buf
|
140
|
+
end
|
141
|
+
|
142
|
+
# unable to find ISBN for these titles report
|
143
|
+
def missing_isbn_report
|
144
|
+
buf = []
|
145
|
+
@media_files.titles.each do |title, medias|
|
146
|
+
if medias.nil?
|
147
|
+
buf << "No media for #{title}"
|
148
|
+
else
|
149
|
+
if medias[0].isbn.nil?
|
150
|
+
buf << "ISBN not found for #{title}"
|
151
|
+
medias.each {|media| buf << " #{media.media_path}"}
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
buf
|
156
|
+
end
|
157
|
+
|
158
|
+
end
|
159
|
+
|
160
|
+
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# == Synopsis
|
2
|
+
# This module encapsulates the application's config hash by adding
|
3
|
+
# default, load, and save methods. Also behaves as a global hash,
|
4
|
+
# meaning you can access it from anywhere in your code like:
|
5
|
+
# AppConfig[:images_dir]
|
6
|
+
# or
|
7
|
+
# AppConfig['images_dir']
|
8
|
+
# Note this is because the implementation is a Mash instead of
|
9
|
+
# a Hash and does cause a limitation where the key must be either
|
10
|
+
# a symbol or a string.
|
11
|
+
module AppConfig
|
12
|
+
@config = Mash.new
|
13
|
+
@yaml_filespec = File.join(ENV['HOME'], '.dvdprofiler2xbmcrc')
|
14
|
+
|
15
|
+
def self.[](k)
|
16
|
+
@config[k]
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.[]=(k,v)
|
20
|
+
@config[k] = v
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.save
|
24
|
+
begin
|
25
|
+
File.delete(@yaml_filespec) if File.exist?(yaml_filespec)
|
26
|
+
AppConfig[:logger].info { "saving: #{yaml_filespec}" }
|
27
|
+
File.open(yaml_filespec, "w") do |f|
|
28
|
+
YAML.dump(@config, f)
|
29
|
+
end
|
30
|
+
rescue Exception => e
|
31
|
+
AppConfig[:logger].error { "Error saving config file \"#{@yaml_filespec} - " + e.to_s }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.load
|
36
|
+
begin
|
37
|
+
if File.exist?(@yaml_filespec)
|
38
|
+
@config.merge YAML.load_file(@yaml_filespec)
|
39
|
+
end
|
40
|
+
rescue Exception => e
|
41
|
+
AppConfig[:logger].error { "Error loading config file \"#{@yaml_filespec} - " + e.to_s }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.default
|
46
|
+
# Note, all paths and extensions are case sensitive
|
47
|
+
|
48
|
+
# Array of paths to scan for media
|
49
|
+
# Note, directories underneath these will be added as genres to
|
50
|
+
# each .nfo file. For example:
|
51
|
+
# /media/royw-gentoo/public/data/movies/Action/Bond/Goldeneye.m4v
|
52
|
+
# will add 'Action' and 'Bond' genres to Goldeneye.nfo
|
53
|
+
# Also note, that duplicate genres will be collapsed into single
|
54
|
+
# genres in the .nfo file.
|
55
|
+
@config.directories = [
|
56
|
+
'/media/dad-kubuntu/public/data/videos_iso',
|
57
|
+
'/media/dcerouter/public/data/videos_iso',
|
58
|
+
'/media/royw-gentoo/public/data/videos_iso',
|
59
|
+
'/media/royw-gentoo/public/data/movies'
|
60
|
+
]
|
61
|
+
|
62
|
+
# Typical locations are:
|
63
|
+
# @config.collection_filespec = File.join(ENV['HOME'], 'DVD Profiler/Databases/Exports/Collection.xml')
|
64
|
+
# @config.images_dir = File.join(ENV['HOME'], 'DVD Profiler/Databases/Default/Images')
|
65
|
+
#
|
66
|
+
# My locations are:
|
67
|
+
@config.collection_filespec = '/home/royw/DVD Profiler/Shared/Collection.xml'
|
68
|
+
@config.images_dir = '/home/royw/DVD Profiler/Shared/Images'
|
69
|
+
|
70
|
+
# You will probably need to edit the MEDIA_EXTENSIONS to specify
|
71
|
+
# the containers used in your library
|
72
|
+
@config.media_extensions = [ 'iso', 'm4v' ]
|
73
|
+
|
74
|
+
# You probably will not need to change these
|
75
|
+
# Source file extensions.
|
76
|
+
@config.image_extensions = [ 'jpg', 'jpeg', 'png', 'gif' ]
|
77
|
+
@config.nfo_extensions = [ 'nfo' ]
|
78
|
+
# Destination file extensions
|
79
|
+
@config.thumbnail_extension = 'tbn'
|
80
|
+
@config.nfo_extension = 'nfo'
|
81
|
+
@config.nfo_backup_extension = 'nfo~'
|
82
|
+
|
83
|
+
# map some genre names
|
84
|
+
@config.genre_maps = {
|
85
|
+
'SciFi' => 'Science Fiction',
|
86
|
+
'Science-Fiction' => 'Science Fiction',
|
87
|
+
'Anime' => 'Animation',
|
88
|
+
'Musical' => 'Musicals'
|
89
|
+
}
|
90
|
+
|
91
|
+
@config.file_permissions = 0664
|
92
|
+
@config.dir_permissions = 0777
|
93
|
+
@config.imdb_query = true
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'yaml'
|
3
|
+
require 'xmlsimple'
|
4
|
+
require 'ftools'
|
5
|
+
require 'imdb'
|
6
|
+
require 'pp'
|
7
|
+
require 'mash'
|
8
|
+
require 'log4r'
|
9
|
+
require 'commandline/optionparser'
|
10
|
+
include CommandLine
|
11
|
+
|
12
|
+
require 'dvdprofiler2xbmc/app'
|
13
|
+
require 'dvdprofiler2xbmc/app_config'
|
14
|
+
require 'dvdprofiler2xbmc/collection'
|
15
|
+
require 'dvdprofiler2xbmc/extensions'
|
16
|
+
require 'dvdprofiler2xbmc/imdb_extensions'
|
17
|
+
require 'dvdprofiler2xbmc/media'
|
18
|
+
require 'dvdprofiler2xbmc/media_files'
|
19
|
+
require 'dvdprofiler2xbmc/nfo'
|
20
|
+
|
21
|
+
module Dvdprofiler2xbmc
|
22
|
+
# == Synopsis
|
23
|
+
# Command line exit codes
|
24
|
+
class ExitCode
|
25
|
+
UNKNOWN = 3
|
26
|
+
CRITICAL = 2
|
27
|
+
WARNING = 1
|
28
|
+
OK = 0
|
29
|
+
end
|
30
|
+
|
31
|
+
class CLI
|
32
|
+
include AppConfig
|
33
|
+
|
34
|
+
def self.execute(stdout, arguments=[])
|
35
|
+
exit_code = ExitCode::OK
|
36
|
+
|
37
|
+
# we start a STDOUT logger, but it will be switched after
|
38
|
+
# the config files are read if config[:logger_output] is set
|
39
|
+
logger = Log4r::Logger.new('dvdprofiler2xbmc')
|
40
|
+
logger.outputters = Log4r::StdoutOutputter.new(:console)
|
41
|
+
logger.level = Log4r::DEBUG
|
42
|
+
|
43
|
+
begin
|
44
|
+
# trap ^C interrupts and let the app instance cleanly exit any long loops
|
45
|
+
Signal.trap("INT") {DvdProfiler2Xbmc.interrupt}
|
46
|
+
|
47
|
+
|
48
|
+
# parse the command line
|
49
|
+
options = setupParser()
|
50
|
+
od = options.parse(arguments)
|
51
|
+
|
52
|
+
unless od["--help"]
|
53
|
+
# load config values
|
54
|
+
AppConfig.default
|
55
|
+
AppConfig[:pretend] = od["--pretend"]
|
56
|
+
AppConfig[:imdb_query] = !od["--no_imdb_query"]
|
57
|
+
|
58
|
+
# the first reinitialize_logger adds the command line logging options to the default config
|
59
|
+
# then we load the config files
|
60
|
+
# then we run reinitialize_logger again to modify the logger for any logging options from the config files
|
61
|
+
|
62
|
+
reinitialize_logger(logger, od["--verbose"], od["--debug"])
|
63
|
+
AppConfig.load
|
64
|
+
reinitialize_logger(logger, od["--verbose"], od["--debug"])
|
65
|
+
|
66
|
+
# create and execute class instance here
|
67
|
+
app = DvdProfiler2Xbmc.new
|
68
|
+
app.execute
|
69
|
+
app.report.each {|line| puts line}
|
70
|
+
end
|
71
|
+
rescue Exception => eMsg
|
72
|
+
logger.error {eMsg.to_s}
|
73
|
+
logger.error {options.to_s}
|
74
|
+
logger.error {eMsg.backtrace.join("\n")}
|
75
|
+
exit_code = ExitCode::CRITICAL
|
76
|
+
end
|
77
|
+
exit_code
|
78
|
+
end
|
79
|
+
|
80
|
+
# Setup the command line option parser
|
81
|
+
# Returns:: OptionParser instances
|
82
|
+
def self.setupParser()
|
83
|
+
options = OptionParser.new()
|
84
|
+
options << Option.new(:flag, :names => %w(--help),
|
85
|
+
:opt_found => lambda {Log4r::Logger['dvdprofiler2xbmc'].info{options.to_s}},
|
86
|
+
:opt_description => "This usage information")
|
87
|
+
options << Option.new(:flag, :names => %w(--pretend -p))
|
88
|
+
options << Option.new(:flag, :names => %w(--no_imdb_query -n))
|
89
|
+
options << Option.new(:flag, :names => %w(--verbose -v))
|
90
|
+
options << Option.new(:flag, :names => %w(--debug -d))
|
91
|
+
options
|
92
|
+
end
|
93
|
+
|
94
|
+
# Reinitialize the logger using the loaded config.
|
95
|
+
# logger:: logger for any user messages
|
96
|
+
# config:: is the application's config hash.
|
97
|
+
def self.reinitialize_logger(logger, verbose, debug)
|
98
|
+
# switch the logger to the one specified in the config files
|
99
|
+
unless AppConfig[:logfile].nil?
|
100
|
+
logfile_outputter = Log4r::RollingFileOutputter.new(:logfile, :filename => AppConfig[:logfile], :maxsize => 1000000 )
|
101
|
+
logger.add logfile_outputter
|
102
|
+
logfile_outputter.level = Log4r::INFO
|
103
|
+
Log4r::Outputter[:logfile].formatter = Log4r::PatternFormatter.new(:pattern => "[%l] %d :: %M")
|
104
|
+
unless AppConfig[:logfile_level].nil?
|
105
|
+
level_map = {'DEBUG' => Log4r::DEBUG, 'INFO' => Log4r::INFO, 'WARN' => Log4r::WARN}
|
106
|
+
logfile_outputter.level = level_map[AppConfig[:logfile_level]] || Log4r::INFO
|
107
|
+
end
|
108
|
+
end
|
109
|
+
Log4r::Outputter[:console].level = Log4r::WARN
|
110
|
+
Log4r::Outputter[:console].level = Log4r::INFO if verbose
|
111
|
+
Log4r::Outputter[:console].level = Log4r::DEBUG if debug
|
112
|
+
# logger.trace = true
|
113
|
+
AppConfig[:logger] = logger
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
|
@@ -0,0 +1,161 @@
|
|
1
|
+
# == Synopsis
|
2
|
+
# This model encapsulates the DVDProfiler Collection.xml
|
3
|
+
class Collection
|
4
|
+
# various regexes used to clean up a title for matching purposes.
|
5
|
+
# used in TITLE_REPLACEMENTS hash below
|
6
|
+
PUNCTUATION = /[\?\:\!\"\'\,\.\-\/]/
|
7
|
+
HTML_ESCAPES = /\&[a-zA-Z]+\;/
|
8
|
+
SQUARE_BRACKET_ENCLOSURES = /\[.*?\]/
|
9
|
+
PARENTHESIS_ENCLOSURES = /\(.*?\)/
|
10
|
+
MULTIPLE_WHITESPACES= /\s+/
|
11
|
+
STANDALONE_AMPERSAND = /\s\&\s/
|
12
|
+
WIDESCREEN = /widescreen/i
|
13
|
+
SPECIAL_EDITION = /special edition/i
|
14
|
+
|
15
|
+
# array of hashes is intentional as the order is critical
|
16
|
+
# the enclosures [...] & (...) must be removed first,
|
17
|
+
# then " & " must be replaced by " and ",
|
18
|
+
# then html escapes &...; must be replaced by a space,
|
19
|
+
# then remaining punctuation is replacesed by a space,
|
20
|
+
# finally multiple whitespaces are reduced to single whitespace
|
21
|
+
TITLE_REPLACEMENTS = [
|
22
|
+
{ SQUARE_BRACKET_ENCLOSURES => '' },
|
23
|
+
{ PARENTHESIS_ENCLOSURES => '' },
|
24
|
+
{ STANDALONE_AMPERSAND => ' and ' },
|
25
|
+
{ HTML_ESCAPES => ' ' },
|
26
|
+
{ WIDESCREEN => ' ' },
|
27
|
+
{ SPECIAL_EDITION => ' ' },
|
28
|
+
{ PUNCTUATION => ' ' },
|
29
|
+
{ MULTIPLE_WHITESPACES => ' ' },
|
30
|
+
]
|
31
|
+
|
32
|
+
attr_reader :isbn_dvd_hash, :title_isbn_hash, :isbn_title_hash
|
33
|
+
|
34
|
+
@filespec = nil
|
35
|
+
|
36
|
+
def initialize(filename = 'Collection.xml')
|
37
|
+
@title_isbn_hash = {}
|
38
|
+
@isbn_dvd_hash = {}
|
39
|
+
@isbn_title_hash = {}
|
40
|
+
@filespec = filename
|
41
|
+
reload
|
42
|
+
save
|
43
|
+
end
|
44
|
+
|
45
|
+
# save as a collection.yaml file unless the existing
|
46
|
+
# collection.yaml is newer than the collection.xml
|
47
|
+
def save
|
48
|
+
unless @filespec.nil?
|
49
|
+
yaml_filespec = @filespec.ext('.yaml')
|
50
|
+
if !File.exist?(yaml_filespec) || (File.mtime(@filespec) > File.mtime(yaml_filespec))
|
51
|
+
AppConfig[:logger].info { "saving: #{yaml_filespec}" }
|
52
|
+
File.open(yaml_filespec, "w") do |f|
|
53
|
+
YAML.dump(
|
54
|
+
{
|
55
|
+
:title_isbn_hash => @title_isbn_hash,
|
56
|
+
:isbn_title_hash => @isbn_title_hash,
|
57
|
+
:isbn_dvd_hash => @isbn_dvd_hash,
|
58
|
+
}, f)
|
59
|
+
end
|
60
|
+
else
|
61
|
+
AppConfig[:logger].info { "not saving, yaml file is newer than xml file" }
|
62
|
+
end
|
63
|
+
else
|
64
|
+
AppConfig[:logger].error { "can not save, the filespec is nil" }
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# load the collection from the collection.yaml if it exists,
|
69
|
+
# otherwise from the collection.xml
|
70
|
+
def reload
|
71
|
+
@title_isbn_hash.clear
|
72
|
+
@isbn_dvd_hash.clear
|
73
|
+
@isbn_title_hash.clear
|
74
|
+
collection = {}
|
75
|
+
yaml_filespec = @filespec.ext('.yaml')
|
76
|
+
if File.exist?(yaml_filespec) && (File.mtime(yaml_filespec) > File.mtime(@filespec))
|
77
|
+
AppConfig[:logger].info { "Loading #{yaml_filespec}" }
|
78
|
+
data = YAML.load_file(yaml_filespec)
|
79
|
+
@title_isbn_hash = data[:title_isbn_hash]
|
80
|
+
@isbn_dvd_hash = data[:isbn_dvd_hash]
|
81
|
+
@isbn_title_hash = data[:isbn_title_hash]
|
82
|
+
else
|
83
|
+
elapsed_time = timer do
|
84
|
+
AppConfig[:logger].info { "Loading #{@filespec}" }
|
85
|
+
collection = XmlSimple.xml_in(@filespec, { 'KeyToSymbol' => true})
|
86
|
+
end
|
87
|
+
AppConfig[:logger].info { "XmlSimple.xml_in elapse time: #{elapsed_time.elapsed_time_s}" }
|
88
|
+
collection[:dvd].each do |dvd|
|
89
|
+
isbn = dvd[:id][0]
|
90
|
+
original_title = dvd[:title][0]
|
91
|
+
title = Collection.title_pattern(dvd[:title][0])
|
92
|
+
unless isbn.blank? || title.blank?
|
93
|
+
@title_isbn_hash[title] ||= []
|
94
|
+
@title_isbn_hash[title] << isbn
|
95
|
+
@isbn_title_hash[isbn] = original_title
|
96
|
+
dvd_hash = {}
|
97
|
+
dvd_hash[:isbn] = isbn
|
98
|
+
dvd_hash[:title] = original_title
|
99
|
+
unless dvd[:actors].blank?
|
100
|
+
dvd_hash[:actors] = dvd[:actors].compact.collect {|a| a[:actor]}.flatten.compact.collect do |a|
|
101
|
+
name = []
|
102
|
+
name << a['FirstName'] unless a['FirstName'].blank?
|
103
|
+
name << a['MiddleName'] unless a['MiddleName'].blank?
|
104
|
+
name << a['LastName'] unless a['LastName'].blank?
|
105
|
+
info = {}
|
106
|
+
info['name'] = name.join(' ')
|
107
|
+
info['role'] = a['Role']
|
108
|
+
info
|
109
|
+
end
|
110
|
+
end
|
111
|
+
dvd_hash[:genres] = dvd[:genres].collect{|a| a[:genre]}.flatten unless dvd[:genres].blank?
|
112
|
+
dvd_hash[:studios] = dvd[:studios].collect{|a| a[:studio]}.flatten unless dvd[:studios].blank?
|
113
|
+
dvd_hash[:productionyear] = [dvd[:productionyear].join(',')] unless dvd[:productionyear].blank?
|
114
|
+
dvd_hash[:rating] = [dvd[:rating].join(',')] unless dvd[:rating].blank?
|
115
|
+
dvd_hash[:runningtime] = [dvd[:runningtime].join(',')] unless dvd[:runningtime].blank?
|
116
|
+
dvd_hash[:released] = [dvd[:released].join(',')] unless dvd[:released].blank?
|
117
|
+
dvd_hash[:overview] = [dvd[:overview].join(',')] unless dvd[:overview].blank?
|
118
|
+
dvd_hash[:lastedited] = dvd[:lastedited][0] unless dvd[:lastedited].blank?
|
119
|
+
@isbn_dvd_hash[isbn] = dvd_hash
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# == Synopsis
|
126
|
+
# The titles found between LMCE's Amazon lookup and DVDProfiler sometimes differ in
|
127
|
+
# whether or not a prefix of "The", "A", or "An" is included in the title. Here we
|
128
|
+
# create an Array of possible titles with and without these prefix words.
|
129
|
+
def Collection.title_permutations(base_title)
|
130
|
+
titles = []
|
131
|
+
unless base_title.nil? || base_title.empty?
|
132
|
+
titles << base_title
|
133
|
+
['the', 'a', 'an'].each do |prefix|
|
134
|
+
titles << "#{prefix} " + base_title unless base_title =~ /^#{prefix}\s/
|
135
|
+
titles << $1 if base_title =~ /^#{prefix}\s(.*)$/
|
136
|
+
end
|
137
|
+
end
|
138
|
+
titles
|
139
|
+
end
|
140
|
+
|
141
|
+
# == Synopsis
|
142
|
+
# the titles found between LMCE's Amazon lookup and DVDProfiler quite often differ in the
|
143
|
+
# inclusion of punctuation and capitalization. So we create a pattern of lower case words
|
144
|
+
# without punctuation and with single spaces between words.
|
145
|
+
def Collection.title_pattern(src_title)
|
146
|
+
title = nil
|
147
|
+
unless src_title.nil?
|
148
|
+
title = src_title.dup
|
149
|
+
title.downcase!
|
150
|
+
TITLE_REPLACEMENTS.each do |replacement|
|
151
|
+
replacement.each do |regex, value|
|
152
|
+
title.gsub!(regex, value)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
title.strip!
|
156
|
+
end
|
157
|
+
title
|
158
|
+
end
|
159
|
+
|
160
|
+
end
|
161
|
+
|
@@ -0,0 +1,103 @@
|
|
1
|
+
######################################################################
|
2
|
+
# my extensions to Module. (taken from rake, named changed to not clash
|
3
|
+
# when rake is used for this rails project.
|
4
|
+
#
|
5
|
+
class Module
|
6
|
+
# Check for an existing method in the current class before extending. IF
|
7
|
+
# the method already exists, then a warning is printed and the extension is
|
8
|
+
# not added. Otherwise the block is yielded and any definitions in the
|
9
|
+
# block will take effect.
|
10
|
+
#
|
11
|
+
# Usage:
|
12
|
+
#
|
13
|
+
# class String
|
14
|
+
# rake_extension("xyz") do
|
15
|
+
# def xyz
|
16
|
+
# ...
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
def my_extension(method)
|
22
|
+
unless instance_methods.include?(method.to_s) || instance_methods.include?(method.to_sym)
|
23
|
+
yield
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end # module Module
|
27
|
+
|
28
|
+
######################################################################
|
29
|
+
# User defined methods to be added to String.
|
30
|
+
#
|
31
|
+
class String
|
32
|
+
my_extension("ext") do
|
33
|
+
# Replace the file extension with +newext+. If there is no extenson on
|
34
|
+
# the string, append the new extension to the end. If the new extension
|
35
|
+
# is not given, or is the empty string, remove any existing extension.
|
36
|
+
#
|
37
|
+
# +ext+ is a user added method for the String class.
|
38
|
+
def ext(newext='')
|
39
|
+
return self.dup if ['.', '..'].include? self
|
40
|
+
if newext != ''
|
41
|
+
newext = (newext =~ /^\./) ? newext : ("." + newext)
|
42
|
+
end
|
43
|
+
dup.sub!(%r(([^/\\])\.[^./\\]*$)) { $1 + newext } || self + newext
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end # class String
|
47
|
+
|
48
|
+
# == Synopsis
|
49
|
+
# add a blank? method to all Objects
|
50
|
+
class Object
|
51
|
+
my_extension("blank?") do
|
52
|
+
# return asserted if object is nil or empty
|
53
|
+
# TODO: not the safest coding, probably should dup before stripping. Maybe should also compact
|
54
|
+
def blank?
|
55
|
+
result = nil?
|
56
|
+
unless result
|
57
|
+
if respond_to? 'empty?'
|
58
|
+
if respond_to? 'strip'
|
59
|
+
result = strip.empty?
|
60
|
+
else
|
61
|
+
result = empty?
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
result
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# == Synopsis
|
71
|
+
# add an elapse_time_s method to Numeric
|
72
|
+
class Numeric
|
73
|
+
my_extension("elapsed_time_s") do
|
74
|
+
# return String formated as "HH:MM:SS"
|
75
|
+
def elapsed_time_s
|
76
|
+
seconds = self
|
77
|
+
hours = minutes = 0
|
78
|
+
hours = seconds.div 3600
|
79
|
+
seconds = seconds - (hours * 3600)
|
80
|
+
minutes = seconds.div 60
|
81
|
+
seconds = seconds - (minutes * 60)
|
82
|
+
sprintf("%2.2d:%2.2d:%2.2d", hours, minutes, seconds)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# == Synopsis
|
88
|
+
# add a timer method to the Kernel
|
89
|
+
module Kernel
|
90
|
+
|
91
|
+
my_extension("timer") do
|
92
|
+
# == Synopsis
|
93
|
+
# a simple elapse time for the give block
|
94
|
+
# == Usage
|
95
|
+
# elapse_seconds = timer {...}
|
96
|
+
def timer
|
97
|
+
start_time = Time.now
|
98
|
+
yield
|
99
|
+
Time.now - start_time
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
@@ -0,0 +1,95 @@
|
|
1
|
+
class ImdbMovie
|
2
|
+
def raw_title
|
3
|
+
document.at("h1").innerText
|
4
|
+
end
|
5
|
+
|
6
|
+
def video_game?
|
7
|
+
raw_title =~ /\(VG\)/
|
8
|
+
end
|
9
|
+
|
10
|
+
def release_year
|
11
|
+
document.search("//h5[text()^='Release Date']/..").innerHTML[/\d{4}/]
|
12
|
+
end
|
13
|
+
|
14
|
+
# return an Array of Strings containing AKA titles
|
15
|
+
def also_known_as
|
16
|
+
el = document.search("//h5[text()^='Also Known As:']/..").at('h5')
|
17
|
+
aka = []
|
18
|
+
while(!el.nil?)
|
19
|
+
aka << el.to_s unless el.elem?
|
20
|
+
el = el.next
|
21
|
+
end
|
22
|
+
aka.collect!{|a| a.gsub(/\([^\)]*\)/, '').strip}
|
23
|
+
aka.uniq!
|
24
|
+
aka.collect!{|a| a.blank? ? nil : a}
|
25
|
+
aka.compact!
|
26
|
+
aka
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class ImdbSearch
|
31
|
+
# Find the IMDB ID for the current search title
|
32
|
+
# The find can be helped a lot by including a years option that contains
|
33
|
+
# an Array of integers that are the production year (plus/minus a year)
|
34
|
+
# and the release year.
|
35
|
+
def find_id(options={})
|
36
|
+
id = nil
|
37
|
+
found_movies = self.movies
|
38
|
+
unless found_movies.nil?
|
39
|
+
desired_movies = found_movies.select do |m|
|
40
|
+
aka = m.also_known_as
|
41
|
+
result = imdb_compare_titles(m.title, aka, @query) && !m.video_game? && !m.release_year.blank?
|
42
|
+
if result
|
43
|
+
AppConfig[:logger].debug { m.title }
|
44
|
+
AppConfig[:logger].debug { "m.release_year => #{m.release_year}" }
|
45
|
+
unless options[:years].blank?
|
46
|
+
result = options[:years].include?(m.release_year.to_i)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
result
|
50
|
+
end
|
51
|
+
ids = desired_movies.collect{|m| m.id}.uniq.compact
|
52
|
+
if ids.length == 1
|
53
|
+
id = "tt#{ids[0]}"
|
54
|
+
else
|
55
|
+
AppConfig[:logger].debug { options[:media_path] } unless options[:media_path].nil?
|
56
|
+
AppConfig[:logger].debug { options[:years].pretty_inspect }
|
57
|
+
desired_movies.collect{|m| [m.raw_title, m.id, m.title, m.url, m.release_year.blank? ? 'no release date' : m.release_year]}.uniq.compact.each do |m|
|
58
|
+
AppConfig[:logger].debug { m.pretty_inspect }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
id
|
63
|
+
end
|
64
|
+
|
65
|
+
protected
|
66
|
+
|
67
|
+
# compare the imdb title and the imdb title's AKAs against the media title.
|
68
|
+
# note, on exact match lookups, IMDB will sometimes set the title to
|
69
|
+
# 'trailers and videos' instead of the correct title.
|
70
|
+
def imdb_compare_titles(imdb_title, aka_titles, media_title)
|
71
|
+
result = fuzzy_compare_titles(imdb_title, media_title)
|
72
|
+
unless result
|
73
|
+
result = fuzzy_compare_titles(imdb_title, 'trailers and videos')
|
74
|
+
unless result
|
75
|
+
aka_titles.each do |aka|
|
76
|
+
result = fuzzy_compare_titles(aka, media_title)
|
77
|
+
break if result
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
result
|
82
|
+
end
|
83
|
+
|
84
|
+
# a fuzzy compare that is case insensitive and replaces '&' with 'and'
|
85
|
+
# (because that is what IMDB occasionally does)
|
86
|
+
def fuzzy_compare_titles(title1, title2)
|
87
|
+
t1 = title1.downcase
|
88
|
+
t2 = title2.downcase
|
89
|
+
(t1 == t2) ||
|
90
|
+
(t1.gsub(/&/, 'and') == t2.gsub(/&/, 'and')) ||
|
91
|
+
(t1.gsub(/[-:]/, ' ') == t2.gsub(/[-:]/, ' ')) ||
|
92
|
+
(t1.gsub('more at imdbpro ?', '') == t2)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# == Synopsis
|
2
|
+
# Media encapsulates information about a single media file
|
3
|
+
class Media
|
4
|
+
attr_reader :media_path, :nfo_files, :image_files, :year, :media_subdirs
|
5
|
+
attr_accessor :isbn
|
6
|
+
|
7
|
+
def initialize(directory, media_file)
|
8
|
+
@media_subdirs = File.dirname(media_file)
|
9
|
+
@media_path = File.expand_path(File.join(directory, media_file))
|
10
|
+
Dir.chdir(File.dirname(@media_path))
|
11
|
+
@nfo_files = Dir.glob("*.{#{AppConfig[:nfo_extensions].join(',')}}")
|
12
|
+
@image_files = Dir.glob("*.{#{AppConfig[:media_extensions].join(',')}}")
|
13
|
+
@year = $1 if File.basename(@media_path, ".*") =~ /\s\-\s(\d{4})/
|
14
|
+
end
|
15
|
+
|
16
|
+
# return the media's title extracted from the filename and cleaned up
|
17
|
+
def title
|
18
|
+
if @title.nil?
|
19
|
+
@title = File.basename(@media_path, ".*")
|
20
|
+
@title.gsub!(/\s\-\s\d{4}/, '') # remove year
|
21
|
+
@title.gsub!(/\s\-\s0/, '') # remove "- 0", i.e., bad year
|
22
|
+
@title.gsub!(/\(\d{4}\)/, '') # remove (year)
|
23
|
+
@title.gsub!(/\[.+\]/, '') # remove square brackets
|
24
|
+
@title.gsub!(/\s\s+/, ' ') # remove multiple whitespace
|
25
|
+
@title = @title.strip # remove leading and trailing whitespace
|
26
|
+
end
|
27
|
+
@title
|
28
|
+
end
|
29
|
+
|
30
|
+
# return the media's title but with the (year) appended
|
31
|
+
def title_with_year
|
32
|
+
name = title
|
33
|
+
name = "#{name} (#{@year})" unless @year.nil?
|
34
|
+
name
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_s
|
38
|
+
buf = []
|
39
|
+
buf << @media_path
|
40
|
+
buf << '-'
|
41
|
+
buf << title_with_year
|
42
|
+
buf.join(' ')
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# == Synopsis
|
2
|
+
# encapsulation of all media files
|
3
|
+
class MediaFiles
|
4
|
+
attr_reader :medias, :titles
|
5
|
+
|
6
|
+
# given:
|
7
|
+
# directories Array of String directory pathspecs
|
8
|
+
def initialize(directories)
|
9
|
+
@medias = []
|
10
|
+
directories.each do |dir|
|
11
|
+
Dir.chdir(dir)
|
12
|
+
@medias += Dir.glob("**/*.{#{AppConfig[:media_extensions].join(',')}}").collect do |filename|
|
13
|
+
Media.new(dir, filename)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
@titles = {}
|
17
|
+
@medias.each do |media|
|
18
|
+
title = media.title_with_year
|
19
|
+
@titles[title] ||= []
|
20
|
+
@titles[title] << media
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
# find duplicate titles and return them in a hash
|
26
|
+
# where the key is the title and the value is an
|
27
|
+
# array of Media objects
|
28
|
+
def duplicate_titles
|
29
|
+
duplicates = {}
|
30
|
+
@titles.each do |title, medias|
|
31
|
+
if medias.length > 1
|
32
|
+
duplicates[title] = medias
|
33
|
+
end
|
34
|
+
end
|
35
|
+
duplicates
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# == Synopsis
|
2
|
+
# NFO (info) files
|
3
|
+
class NFO
|
4
|
+
def initialize(media, dvd_hash)
|
5
|
+
@media = media
|
6
|
+
@dvd_hash = dvd_hash
|
7
|
+
load
|
8
|
+
end
|
9
|
+
|
10
|
+
# save as a .nfo file, creating a backup if the .nfo already exists
|
11
|
+
def save
|
12
|
+
begin
|
13
|
+
nfo_filespec = @media.media_path.ext(".#{AppConfig[:nfo_extension]}")
|
14
|
+
nfo_backup_filespec = @media.media_path.ext(".#{AppConfig[:nfo_backup_extension]}")
|
15
|
+
File.delete(nfo_backup_filespec) if File.exist?(nfo_backup_filespec)
|
16
|
+
File.rename(nfo_filespec, nfo_backup_filespec) if File.exist?(nfo_filespec)
|
17
|
+
File.open(nfo_filespec, "w") do |file|
|
18
|
+
file.puts(to_nfo(@dvd_hash))
|
19
|
+
end
|
20
|
+
rescue Exception => e
|
21
|
+
AppConfig[:logger].error { "Error saving nfo file - " + e.to_s }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def load
|
26
|
+
begin
|
27
|
+
nfo_filespec = @media.media_path.ext(".#{AppConfig[:nfo_extension]}")
|
28
|
+
@movie = XmlSimple.xml_in(nfo_filespec) if File.exist? nfo_filespec
|
29
|
+
rescue Exception => e
|
30
|
+
AppConfig[:logger].error { "Error loading \"#{nfo_filespec}\" - " + e.to_s }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# return a nfo xml String from the given dvd_hash (from Collection)
|
35
|
+
def to_nfo(dvd_hash)
|
36
|
+
@movie ||= {}
|
37
|
+
imdb_id = @movie['id']
|
38
|
+
imdb_id = imdb_lookup(dvd_hash) if AppConfig[:imdb_query] && imdb_id.blank?
|
39
|
+
@movie['title'] = dvd_hash[:title]
|
40
|
+
@movie['mpaa'] = dvd_hash[:rating]
|
41
|
+
@movie['year'] = dvd_hash[:productionyear]
|
42
|
+
@movie['outline'] = dvd_hash[:overview]
|
43
|
+
# @movie['plot'] = dvd_hash[:overview]
|
44
|
+
@movie['runtime'] = dvd_hash[:runningtime]
|
45
|
+
@movie['genre'] = map_genres((dvd_hash[:genres] + @media.media_subdirs.split('/')).uniq)
|
46
|
+
@movie['actor'] = dvd_hash[:actors]
|
47
|
+
@movie['id'] = imdb_id unless imdb_id.nil?
|
48
|
+
@movie['isbn'] = dvd_hash[:isbn]
|
49
|
+
|
50
|
+
begin
|
51
|
+
XmlSimple.xml_out(@movie, 'NoAttr' => true, 'RootName' => 'movie')
|
52
|
+
rescue Exception => e
|
53
|
+
AppConfig[:logger].error { "Error creating nfo file - " + e.to_s }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
protected
|
58
|
+
|
59
|
+
def map_genres(genres)
|
60
|
+
new_genres = []
|
61
|
+
genres.each do |genre|
|
62
|
+
new_genres << (AppConfig[:genre_maps][genre].nil? ? genre : AppConfig[:genre_maps][genre])
|
63
|
+
end
|
64
|
+
new_genres.uniq.compact
|
65
|
+
end
|
66
|
+
|
67
|
+
# try to find the imdb id for the movie
|
68
|
+
def imdb_lookup(dvd_hash)
|
69
|
+
id = nil
|
70
|
+
AppConfig[:logger].info { "Searching IMDB for \"#{dvd_hash[:title]}\"" }
|
71
|
+
unless dvd_hash[:title].blank?
|
72
|
+
years = released_years(dvd_hash)
|
73
|
+
begin
|
74
|
+
imdb_search = ImdbSearch.new(dvd_hash[:title])
|
75
|
+
id = imdb_search.find_id(:years => years, :media_path => @media.media_path)
|
76
|
+
rescue Exception => e
|
77
|
+
AppConfig[:logger].error { "Error searching IMDB - " + e.to_s }
|
78
|
+
AppConfig[:logger].error { e.backtrace.join("\n") }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
AppConfig[:logger].info { "IMDB id => #{id}" } unless id.nil?
|
82
|
+
id
|
83
|
+
end
|
84
|
+
|
85
|
+
# Different databases seem to mix up released versus production years.
|
86
|
+
# So we combine both into a Array of integer years.
|
87
|
+
def released_years(dvd_hash)
|
88
|
+
years = []
|
89
|
+
unless dvd_hash[:productionyear].blank?
|
90
|
+
years += dvd_hash[:productionyear].collect{|y| [y.to_i - 1, y.to_i, y.to_i + 1]}.flatten
|
91
|
+
end
|
92
|
+
unless dvd_hash[:released].blank?
|
93
|
+
years += dvd_hash[:released].collect do |date|
|
94
|
+
y = nil
|
95
|
+
y = $1.to_i if date =~ /(\d{4})\-/
|
96
|
+
y
|
97
|
+
end
|
98
|
+
end
|
99
|
+
years.flatten.uniq.compact.sort
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
|
metadata
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: royw-dvdprofiler2xbmc
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Roy Wright
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-03-19 00:00:00 -07:00
|
13
|
+
default_executable: dvdprofiler2xbmc
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: activesupport
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 2.0.2
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: xml-simple
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.0.12
|
34
|
+
version:
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: porras-imdb
|
37
|
+
type: :runtime
|
38
|
+
version_requirement:
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 0.0.5
|
44
|
+
version:
|
45
|
+
- !ruby/object:Gem::Dependency
|
46
|
+
name: log4r
|
47
|
+
type: :runtime
|
48
|
+
version_requirement:
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 1.0.5
|
54
|
+
version:
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: commandline
|
57
|
+
type: :runtime
|
58
|
+
version_requirement:
|
59
|
+
version_requirements: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: 0.7.10
|
64
|
+
version:
|
65
|
+
- !ruby/object:Gem::Dependency
|
66
|
+
name: mash
|
67
|
+
type: :runtime
|
68
|
+
version_requirement:
|
69
|
+
version_requirements: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: 0.0.3
|
74
|
+
version:
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: newgem
|
77
|
+
type: :development
|
78
|
+
version_requirement:
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: 1.2.3
|
84
|
+
version:
|
85
|
+
- !ruby/object:Gem::Dependency
|
86
|
+
name: hoe
|
87
|
+
type: :development
|
88
|
+
version_requirement:
|
89
|
+
version_requirements: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - ">="
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 1.8.0
|
94
|
+
version:
|
95
|
+
description: This script will attempt to match up media files from a set of directories to the collection.xml file exported from DVD Profiler. For matches, the script will then create a {moviename}.nfo from the data in collections.xml and also copy the front cover image to {moviename}.tbn. Both files will be placed in the same directory as the source media file. Then on XBMC, set the source content to none to remove the meta data from the library, then set the source content back to Movies to import the media. This time, the data in the .nfo files will be used instead of scraping.
|
96
|
+
email:
|
97
|
+
- roy@wright.org
|
98
|
+
executables:
|
99
|
+
- dvdprofiler2xbmc
|
100
|
+
extensions: []
|
101
|
+
|
102
|
+
extra_rdoc_files:
|
103
|
+
- History.txt
|
104
|
+
- Manifest.txt
|
105
|
+
- PostInstall.txt
|
106
|
+
- README.rdoc
|
107
|
+
files:
|
108
|
+
- History.txt
|
109
|
+
- Manifest.txt
|
110
|
+
- PostInstall.txt
|
111
|
+
- README.rdoc
|
112
|
+
- Rakefile
|
113
|
+
- bin/dvdprofiler2xbmc
|
114
|
+
- lib/dvdprofiler2xbmc.rb
|
115
|
+
- lib/dvdprofiler2xbmc/app_config.rb
|
116
|
+
- lib/dvdprofiler2xbmc/app.rb
|
117
|
+
- lib/dvdprofiler2xbmc/cli.rb
|
118
|
+
- lib/dvdprofiler2xbmc/collection.rb
|
119
|
+
- lib/dvdprofiler2xbmc/extensions.rb
|
120
|
+
- lib/dvdprofiler2xbmc/imdb_extensions.rb
|
121
|
+
- lib/dvdprofiler2xbmc/media_files.rb
|
122
|
+
- lib/dvdprofiler2xbmc/media.rb
|
123
|
+
- lib/dvdprofiler2xbmc/nfo.rb
|
124
|
+
- test/test_dvdprofiler2xbmc.rb
|
125
|
+
- test/test_helper.rb
|
126
|
+
- test/test_dvdprofiler2xbmc_cli.rb
|
127
|
+
has_rdoc: true
|
128
|
+
homepage: http://www.github.com/royw/dvdprofiler2xbmc
|
129
|
+
post_install_message: PostInstall.txt
|
130
|
+
rdoc_options:
|
131
|
+
- --main
|
132
|
+
- README.rdoc
|
133
|
+
require_paths:
|
134
|
+
- lib
|
135
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
136
|
+
requirements:
|
137
|
+
- - ">="
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: "0"
|
140
|
+
version:
|
141
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: "0"
|
146
|
+
version:
|
147
|
+
requirements: []
|
148
|
+
|
149
|
+
rubyforge_project: dvdprofiler2xbmc
|
150
|
+
rubygems_version: 1.2.0
|
151
|
+
signing_key:
|
152
|
+
specification_version: 2
|
153
|
+
summary: This script will attempt to match up media files from a set of directories to the collection.xml file exported from DVD Profiler
|
154
|
+
test_files:
|
155
|
+
- test/test_dvdprofiler2xbmc.rb
|
156
|
+
- test/test_helper.rb
|
157
|
+
- test/test_dvdprofiler2xbmc_cli.rb
|