movier 0.0.4 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/Gemfile +2 -0
- data/Rakefile +44 -0
- data/features/movier.feature +8 -0
- data/features/step_definitions/movier_steps.rb +6 -0
- data/features/support/env.rb +15 -0
- data/lib/movier/api.rb +56 -0
- data/lib/movier/helpers.rb +69 -0
- data/lib/movier/info.rb +32 -0
- data/lib/movier/lmdb.rb +248 -0
- data/lib/movier/movier.rb +17 -0
- data/lib/movier/organize.rb +251 -0
- data/lib/movier/say.rb +49 -0
- data/lib/movier/version.rb +1 -1
- data/lib/movier.rb +1 -1
- data/movier.gemspec +31 -0
- metadata +33 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: af01724f8aee1a5789dc504d4e15af1aef675b84
|
4
|
+
data.tar.gz: 7635c03202e6ea156369c73229e8367bf64494eb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 85b89a53bdd233dd18be3e352708d70629e44823e0b2b3cc0e83e7fbc0e4a5abb864df5b273594b778cff87acb05bb44d81934634e26aa454cda8244d1aecd7f
|
7
|
+
data.tar.gz: bbaf57f751f896609a9365fd2dc12a94eb57eadf33e847332765322a7453b08a69c4384d4735a6da51a01ef971bda6d9d69f5e047f1684d65afe2bf57a8ffda0
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'rake/clean'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'rubygems/package_task'
|
4
|
+
require 'rdoc/task'
|
5
|
+
require 'cucumber'
|
6
|
+
require 'cucumber/rake/task'
|
7
|
+
Rake::RDocTask.new do |rd|
|
8
|
+
rd.main = "README.rdoc"
|
9
|
+
rd.rdoc_files.include("README.rdoc","lib/**/*.rb","bin/**/*")
|
10
|
+
rd.title = 'Your application title'
|
11
|
+
end
|
12
|
+
|
13
|
+
spec = eval(File.read('movier.gemspec'))
|
14
|
+
|
15
|
+
Gem::PackageTask.new(spec) do |pkg|
|
16
|
+
end
|
17
|
+
CUKE_RESULTS = 'results.html'
|
18
|
+
CLEAN << CUKE_RESULTS
|
19
|
+
desc 'Run features'
|
20
|
+
Cucumber::Rake::Task.new(:features) do |t|
|
21
|
+
opts = "features --format html -o #{CUKE_RESULTS} --format progress -x"
|
22
|
+
opts += " --tags #{ENV['TAGS']}" if ENV['TAGS']
|
23
|
+
t.cucumber_opts = opts
|
24
|
+
t.fork = false
|
25
|
+
end
|
26
|
+
|
27
|
+
desc 'Run features tagged as work-in-progress (@wip)'
|
28
|
+
Cucumber::Rake::Task.new('features:wip') do |t|
|
29
|
+
tag_opts = ' --tags ~@pending'
|
30
|
+
tag_opts = ' --tags @wip'
|
31
|
+
t.cucumber_opts = "features --format html -o #{CUKE_RESULTS} --format pretty -x -s#{tag_opts}"
|
32
|
+
t.fork = false
|
33
|
+
end
|
34
|
+
|
35
|
+
task :cucumber => :features
|
36
|
+
task 'cucumber:wip' => 'features:wip'
|
37
|
+
task :wip => 'features:wip'
|
38
|
+
require 'rake/testtask'
|
39
|
+
Rake::TestTask.new do |t|
|
40
|
+
t.libs << "test"
|
41
|
+
t.test_files = FileList['test/*_test.rb']
|
42
|
+
end
|
43
|
+
|
44
|
+
task :default => [:test,:features]
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'aruba/cucumber'
|
2
|
+
|
3
|
+
ENV['PATH'] = "#{File.expand_path(File.dirname(__FILE__) + '/../../bin')}#{File::PATH_SEPARATOR}#{ENV['PATH']}"
|
4
|
+
LIB_DIR = File.join(File.expand_path(File.dirname(__FILE__)),'..','..','lib')
|
5
|
+
|
6
|
+
Before do
|
7
|
+
# Using "announce" causes massive warnings on 1.9.2
|
8
|
+
@puts = true
|
9
|
+
@original_rubylib = ENV['RUBYLIB']
|
10
|
+
ENV['RUBYLIB'] = LIB_DIR + File::PATH_SEPARATOR + ENV['RUBYLIB'].to_s
|
11
|
+
end
|
12
|
+
|
13
|
+
After do
|
14
|
+
ENV['RUBYLIB'] = @original_rubylib
|
15
|
+
end
|
data/lib/movier/api.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
module Movier
|
2
|
+
|
3
|
+
# Fetch information about a movie from IMDB.com
|
4
|
+
#
|
5
|
+
# * *Args* :
|
6
|
+
# - +id+ -> IMDB.com id for the movie
|
7
|
+
# * *Returns* :
|
8
|
+
# - hash of movie information
|
9
|
+
# * *Raises* :
|
10
|
+
# - +RuntimeError+ -> error that occurred when making a request to the API
|
11
|
+
#
|
12
|
+
def self.fetch_details(id)
|
13
|
+
response = omdbapi({ i: id })
|
14
|
+
response["votes"] = response["imdbVotes"].delete(",").to_i
|
15
|
+
response["rating"] = response["imdbRating"].to_f
|
16
|
+
response["weight"] = response["votes"] * response["rating"]
|
17
|
+
response
|
18
|
+
end
|
19
|
+
|
20
|
+
# Search for a title on IMDB.com
|
21
|
+
#
|
22
|
+
# * *Args* :
|
23
|
+
# - +keyword+ -> keywords to search with
|
24
|
+
# - +year+ -> (optional) year in which the title was released
|
25
|
+
# - +type+ -> (default: movie) type of the title to return
|
26
|
+
# * *Returns* :
|
27
|
+
# - hash with the information about movie titles found
|
28
|
+
# * *Raises* :
|
29
|
+
# - +RuntimeError+ -> error that occurred when making a request to the API
|
30
|
+
#
|
31
|
+
def self.search(keyword, year = nil, type = "Movie")
|
32
|
+
response = omdbapi({ s: keyword })["Search"]
|
33
|
+
response = response.select{ |m| m["Type"].downcase == type.downcase } unless type.downcase == "all"
|
34
|
+
response = response.select{ |m| m["Year"] == year } if year
|
35
|
+
response
|
36
|
+
end
|
37
|
+
|
38
|
+
# Make a request to OMDBApi.com
|
39
|
+
#
|
40
|
+
# * *Args* :
|
41
|
+
# - +params+ -> hash of params to pass with this request
|
42
|
+
# * *Returns* :
|
43
|
+
# - hash of response received
|
44
|
+
# * *Raises* :
|
45
|
+
# - +RuntimeError+ -> error that occurred when making a request to the API
|
46
|
+
#
|
47
|
+
def self.omdbapi(params)
|
48
|
+
query = ""; params.each { |k, v| query += "#{k}=#{v}" }
|
49
|
+
response = HTTParty.get("http://omdbapi.com/?#{URI::encode(query)}")
|
50
|
+
failed_with response.response unless response.success?
|
51
|
+
response = JSON.parse(response.parsed_response)
|
52
|
+
failed_with response["Error"] if response["Error"]
|
53
|
+
response
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Movier
|
2
|
+
|
3
|
+
# ask a user for a directory path, until we find one
|
4
|
+
#
|
5
|
+
# * *Args* :
|
6
|
+
# - +message+ -> question to show to the user
|
7
|
+
# * *Returns* :
|
8
|
+
# - path to the provided directory
|
9
|
+
#
|
10
|
+
def self.ask_for_directory(message = nil)
|
11
|
+
message ||= "Please, provide a directory path to use. "
|
12
|
+
dir = nil
|
13
|
+
until dir && File.directory?(dir)
|
14
|
+
warn_with "Found no such directory!" if dir
|
15
|
+
dir = ask(message) { |x| x.default = ENV['HOME'] }
|
16
|
+
end
|
17
|
+
dir
|
18
|
+
end
|
19
|
+
|
20
|
+
# colorize output based on a movie's rating
|
21
|
+
#
|
22
|
+
# * *Args* :
|
23
|
+
# - +status+ -> status for this message
|
24
|
+
# - +message+ -> actual message
|
25
|
+
# - +rating+ -> rating for this movie
|
26
|
+
#
|
27
|
+
def self.say_rated(status, message, rating, do_break = false)
|
28
|
+
rating = rating.to_f
|
29
|
+
scheme = :below6 if rating < 6
|
30
|
+
scheme = :above6 if rating >= 6
|
31
|
+
scheme = :above8 if rating >= 8
|
32
|
+
say_with_status status, message, scheme, do_break
|
33
|
+
end
|
34
|
+
|
35
|
+
# titleize a string
|
36
|
+
#
|
37
|
+
# * *Args* :
|
38
|
+
# - +string+ -> string that will be titleized
|
39
|
+
# * *Returns* :
|
40
|
+
# - titleized string
|
41
|
+
#
|
42
|
+
def self.titleize(string)
|
43
|
+
string.gsub(/\w+/) { |word| word.capitalize }
|
44
|
+
end
|
45
|
+
|
46
|
+
# find the imdb.txt file for a given movie path
|
47
|
+
# this file is created by Movier, once the movie has been parsed
|
48
|
+
#
|
49
|
+
# * *Args* :
|
50
|
+
# - +movie_path+ -> path to the movie being checked
|
51
|
+
# * *Returns* :
|
52
|
+
# - path to the imdb.txt file for the given movie path
|
53
|
+
#
|
54
|
+
def self.imdb_file_for(movie_path)
|
55
|
+
File.join(Filename.dirname(movie_path), "imdb.txt")
|
56
|
+
end
|
57
|
+
|
58
|
+
# check whether the movie with given path has already been organized
|
59
|
+
#
|
60
|
+
# * *Args* :
|
61
|
+
# - +movie_path+ -> path to the movie being checked
|
62
|
+
# * *Returns* :
|
63
|
+
# - true, if the movie has been organized. false, otherwise
|
64
|
+
#
|
65
|
+
def self.organized?(movie_path)
|
66
|
+
File.exists?(imdb_file_for(movie_path))
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
data/lib/movier/info.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
module Movier
|
2
|
+
# Find and return information about a movie with the given keywords
|
3
|
+
#
|
4
|
+
# * *Args* :
|
5
|
+
# - +keyword+ -> keywords to search for in movie title
|
6
|
+
# - +options+ -> hash that alters the behavior of the search
|
7
|
+
# if +:year+ key is specified, restricts search to that year
|
8
|
+
# if +:detailed+ key is specified, shows detailed results
|
9
|
+
def self.info(keyword, options)
|
10
|
+
lmdb = Movier::LMDB.new
|
11
|
+
movies = search(keyword, options[:year], options[:all] ? "all" : "movie")
|
12
|
+
movies.each do |m|
|
13
|
+
m = fetch_details m["imdbID"]
|
14
|
+
nice_name = m["Title"] + " [" + m["Year"] + "]"
|
15
|
+
next if m["votes"] < 1000 && !options[:all]
|
16
|
+
if options[:detailed]
|
17
|
+
tip_now nice_name, titleize(m["Type"])
|
18
|
+
rating = "#{m["Rated"]} at #{m["imdbRating"]} points with #{m["imdbVotes"]} votes"
|
19
|
+
say_rated "Rating", rating, m["rating"]
|
20
|
+
say_rated "Plot", m["Plot"], m["rating"], true
|
21
|
+
say_rated "Actors", m["Actors"], m["rating"], true
|
22
|
+
say_rated "Runtime", m["Runtime"], m["rating"]
|
23
|
+
say_rated "Genre", m["Genre"], m["rating"]
|
24
|
+
puts
|
25
|
+
else
|
26
|
+
message = "#{nice_name} @ #{m["imdbRating"]} ( #{m["imdbVotes"]} votes )"
|
27
|
+
type = lmdb.lookup(m["imdbID"]) ? "Available Movie" : "Movie"
|
28
|
+
say_rated titleize(type), message, m["rating"]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/movier/lmdb.rb
ADDED
@@ -0,0 +1,248 @@
|
|
1
|
+
module Movier
|
2
|
+
class LMDB
|
3
|
+
DataFolder = File.join(ENV['HOME'], ".movier")
|
4
|
+
DataFile = File.join(DataFolder, "data.yaml")
|
5
|
+
|
6
|
+
attr_reader :movies, :boxes
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
FileUtils.mkdir_p DataFolder
|
10
|
+
FileUtils.touch DataFile unless File.exists?(DataFile)
|
11
|
+
read_data
|
12
|
+
end
|
13
|
+
|
14
|
+
def read_data
|
15
|
+
data = Movier.read_yaml(DataFile)
|
16
|
+
@boxes = data ? data[:boxes] : []
|
17
|
+
@movies = data ? data[:movies] : []
|
18
|
+
end
|
19
|
+
|
20
|
+
def write_data(movies = nil, boxes = nil)
|
21
|
+
boxes ||= @boxes; movies ||= @movies;
|
22
|
+
data = { boxes: boxes, movies: movies }
|
23
|
+
Movier.write_yaml DataFile, data
|
24
|
+
read_data
|
25
|
+
end
|
26
|
+
|
27
|
+
def find_persons(kind = :actors)
|
28
|
+
invalid_kind = @movies.first && !@movies.first.has_key?(kind.to_sym)
|
29
|
+
raise "I don't have any info about #{Movier.titleize(kind.to_s)}" if invalid_kind
|
30
|
+
persons = @params[kind.to_sym]
|
31
|
+
if persons
|
32
|
+
persons.split(",").each do |person|
|
33
|
+
@movies.select!{ |movie| movie[kind.to_sym].join(", ").downcase.include? person.downcase.strip}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def genre
|
39
|
+
genre = [ "Action", "Adventure", "Animation", "Biography", "Comedy",
|
40
|
+
"Crime", "Documentary", "Drama", "Family", "Fantasy",
|
41
|
+
"Film-Noir", "Game-Show", "History", "Horror", "Music",
|
42
|
+
"Musical", "Mystery", "News", "Reality-TV", "Romance",
|
43
|
+
"Sci-Fi", "Sport", "Talk-Show", "Thriller", "War", "Western" ]
|
44
|
+
Movier.tip_now "Listing all IMDB.com defined genre"
|
45
|
+
puts " " * 20 + "=" * 20
|
46
|
+
genre.each_slice(6) do |g|
|
47
|
+
puts " "*20 + g.join(", ")
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
def lookup(id)
|
53
|
+
@movies.each do |movie|
|
54
|
+
return movie if movie[:id] == id
|
55
|
+
end
|
56
|
+
false
|
57
|
+
end
|
58
|
+
|
59
|
+
def find(params)
|
60
|
+
read_data; @params = params; @params[:verbose] = true if @params[:shuffle]
|
61
|
+
raise "Please, add some movie boxes, before searching in the local movie database" unless @movies.any?
|
62
|
+
|
63
|
+
@movies.select!{|movie| movie[:title].downcase.include? @params[:keywords].downcase} if @params[:keywords]
|
64
|
+
[:directors, :writers, :actors, :genre, :tags].each { |kind| find_persons(kind) }
|
65
|
+
@movies.select!{|movie| movie[:rated] == @params[:rated] } if @params[:rated]
|
66
|
+
@movies.select!{|movie| movie[:rating] >= @params[:points].to_f } if @params[:points]
|
67
|
+
|
68
|
+
# filter on tag exclusion
|
69
|
+
tags = @params[:exclude_tags]
|
70
|
+
if tags
|
71
|
+
tags.split(",").each do |tag|
|
72
|
+
@movies.reject!{ |movie| movie[:tags].join(", ").downcase.include? tag.downcase.strip}
|
73
|
+
end
|
74
|
+
end
|
75
|
+
@movies.reject!{|movie| movie[:tags].join(", ").downcase.include? "watched"} unless @params[:tags] &&
|
76
|
+
@params[:tags].include?("watched")
|
77
|
+
|
78
|
+
sort_movies
|
79
|
+
@movies = @movies.slice(0, @params[:limit].to_i) if @params[:limit].to_i > 0
|
80
|
+
@movies = [ @movies.shuffle.first ] if @params[:shuffle]
|
81
|
+
|
82
|
+
counter = 1
|
83
|
+
@movies.each do |movie|
|
84
|
+
nice_name = "#{movie[:title]} [#{movie[:year]}]"
|
85
|
+
if @params[:verbose]
|
86
|
+
Movier.tip_now "%03d" % counter + ") " + nice_name, Movier.titleize(movie[:type])
|
87
|
+
Movier.tip_now movie[:path], "Path"
|
88
|
+
rating = "#{movie[:rated]} at #{movie[:rating]} points."
|
89
|
+
Movier.say_rated "Rating", rating, movie[:rating]
|
90
|
+
Movier.say_rated "Votes", "#{movie[:votes]} IMDB.com votes", movie[:rating]
|
91
|
+
Movier.say_rated "Genre", movie[:genre].join(", "), movie[:rating], true
|
92
|
+
Movier.say_rated "Runtime", movie[:runtime], movie[:rating]
|
93
|
+
Movier.say_rated "Directors", movie[:directors].join(", "), movie[:rating], true
|
94
|
+
Movier.say_rated "Actors", movie[:actors].join(", "), movie[:rating], true
|
95
|
+
Movier.say_rated "Writers", movie[:writers].join(", "), movie[:rating], true if @params[:writer]
|
96
|
+
Movier.say_rated "Plot", movie[:plot], movie[:rating], true
|
97
|
+
Movier.say_rated "Tags", movie[:tags].join(", "), movie[:rating] if movie[:tags].any?
|
98
|
+
else
|
99
|
+
message = "#{"%03d" % counter}) #{nice_name}"
|
100
|
+
Movier.say_rated Movier.titleize(movie[:type]), message, movie[:rating]
|
101
|
+
rating = "#{movie[:rated]} at #{movie[:rating]} points with #{movie[:votes]} votes."
|
102
|
+
Movier.say_rated "Rating", rating, movie[:rating]
|
103
|
+
Movier.say_rated "Directors", movie[:directors].join(", "), movie[:rating], true if @params[:directors]
|
104
|
+
Movier.say_rated "Actors", movie[:actors].join(", "), movie[:rating], true
|
105
|
+
Movier.say_rated "Tags", movie[:tags].join(", "), movie[:rating] if movie[:tags].any?
|
106
|
+
end
|
107
|
+
puts
|
108
|
+
counter += 1
|
109
|
+
end
|
110
|
+
return if @movies.empty?
|
111
|
+
filtered = @movies
|
112
|
+
|
113
|
+
# add some tags to this search
|
114
|
+
if @params[:add_tags]
|
115
|
+
tags = @params[:add_tags].split(",").map{|t| Movier.titleize(t.strip)}
|
116
|
+
read_data
|
117
|
+
filtered.each do |f|
|
118
|
+
@movies.each_with_index do |m,i|
|
119
|
+
@movies[i][:tags] |= tags if m[:id] == f[:id]
|
120
|
+
end
|
121
|
+
end
|
122
|
+
write_data
|
123
|
+
Movier.tip_now "Added tags: '#{tags.join(", ")}' to this search!"
|
124
|
+
end
|
125
|
+
|
126
|
+
# TODO: remove tags?
|
127
|
+
|
128
|
+
message = "Do you want me to play #{@params[:shuffle] ? "this" : "some"} movie for you? [enter number from above] "
|
129
|
+
begin; open = ask(message) {|x| x.default = "no" }; rescue Exception; end
|
130
|
+
if open == "no" && @params[:shuffle]
|
131
|
+
find @params
|
132
|
+
elsif open && open.to_i > 0
|
133
|
+
movie = filtered[open.to_i - 1]
|
134
|
+
# TODO: fix this to use a pure ruby implementation
|
135
|
+
nice_name = "#{movie[:title]} [#{movie[:year]}]"
|
136
|
+
movie_dir = "#{movie[:path]}".gsub(" ", "\\ ")
|
137
|
+
movie_file = `find #{movie_dir} -type f`.strip.split("\n")
|
138
|
+
movie_file = movie_file.select{|f| File.size(f) > 100 * 2**20}.first
|
139
|
+
Movier.tip_now "Opening: #{nice_name} with VLC Player"
|
140
|
+
`open "#{movie_file}" -a VLC &`
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def sort_movies
|
145
|
+
@movies = @movies.sort_by {|movie| movie[:weight] }.reverse
|
146
|
+
end
|
147
|
+
|
148
|
+
# Update the local movie database,
|
149
|
+
# by revisiting all tracked directories,
|
150
|
+
# and building the database from there.
|
151
|
+
#
|
152
|
+
def update_itself
|
153
|
+
message = "Found no movie box in the local database.\n"
|
154
|
+
message += "Please, run `movier add` to add some movie boxes, before updating me!"
|
155
|
+
raise message if @boxes.empty?
|
156
|
+
@boxes.each { |box| delete(box); add(box) }
|
157
|
+
end
|
158
|
+
|
159
|
+
def delete(dir = nil)
|
160
|
+
dir = File.expand_path dir
|
161
|
+
@movies.reject!{ |movie| movie[:box] == dir }
|
162
|
+
@boxes.reject!{ |box| box == dir }
|
163
|
+
write_data
|
164
|
+
current_state
|
165
|
+
end
|
166
|
+
|
167
|
+
# Add a given directory to the local movie database.
|
168
|
+
# The directory should be pre-organized using the Organize command.
|
169
|
+
#
|
170
|
+
# * *Args* :
|
171
|
+
# - +dir+ -> directory to add to the local movie database
|
172
|
+
#
|
173
|
+
def add(dir = nil)
|
174
|
+
dir = File.expand_path dir
|
175
|
+
raise "No such directory!" unless File.directory?(dir)
|
176
|
+
|
177
|
+
imdb = Dir.glob("#{dir}/**/imdb.txt")
|
178
|
+
raise 'You should first run `movier organize` on this directory!' unless imdb.count > 0
|
179
|
+
|
180
|
+
count = 0
|
181
|
+
imdb.each do |file|
|
182
|
+
movie = Movier.read_yaml file
|
183
|
+
if in_database?(movie) && !@boxes.include?(dir)
|
184
|
+
Movier.warn_with "#{movie[:imdb]["Title"]} already exists in database!"
|
185
|
+
elsif !in_database?(movie)
|
186
|
+
@movies.push sanitize(movie, dir, file)
|
187
|
+
count += 1
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
@boxes.push dir unless @boxes.include? dir
|
192
|
+
write_data
|
193
|
+
|
194
|
+
Movier.passed_with "Added #{count} new movies in LMDB."
|
195
|
+
current_state
|
196
|
+
end
|
197
|
+
|
198
|
+
def current_state
|
199
|
+
Movier.tip_now "LMDB now contains #{@movies.count} movies, and #{@boxes.count} boxes."
|
200
|
+
end
|
201
|
+
|
202
|
+
def sanitize(movie, dir, imdb_file)
|
203
|
+
imdb = movie[:imdb]
|
204
|
+
nice_name = "#{imdb["Title"]} [#{imdb["Year"]}]"
|
205
|
+
hash = (Digest::MD5.new << nice_name).to_s.slice(0,8)
|
206
|
+
data = {
|
207
|
+
title: imdb["Title"],
|
208
|
+
year: imdb["Year"],
|
209
|
+
rated: imdb["Rated"],
|
210
|
+
released: imdb["Released"],
|
211
|
+
runtime: imdb["Runtime"],
|
212
|
+
genre: make_parts(imdb["Genre"]),
|
213
|
+
directors: make_parts(imdb["Director"]),
|
214
|
+
writers: make_parts(imdb["Writer"]),
|
215
|
+
actors: make_parts(imdb["Actors"]),
|
216
|
+
plot: imdb["Plot"],
|
217
|
+
poster: imdb["Poster"],
|
218
|
+
_poster: File.join(dir, nice_name, "poster#{File.extname(imdb["Poster"])}"),
|
219
|
+
rating: imdb["imdbRating"].to_f,
|
220
|
+
votes: imdb["imdbVotes"].to_s.delete(",").to_i,
|
221
|
+
type: imdb["Type"],
|
222
|
+
id: imdb["imdbID"],
|
223
|
+
hash: hash.force_encoding("UTF-8"),
|
224
|
+
tags: [],
|
225
|
+
box: dir,
|
226
|
+
path: File.dirname(imdb_file)
|
227
|
+
}
|
228
|
+
data[:weight] = (data[:rating] * data[:votes]).to_i
|
229
|
+
data
|
230
|
+
end
|
231
|
+
|
232
|
+
def make_parts(string, delimiter = ",")
|
233
|
+
string.split(delimiter).map{|x| x.strip}
|
234
|
+
end
|
235
|
+
|
236
|
+
def in_database?(movie)
|
237
|
+
@movies.select{|m| m[:id] == movie[:imdb]["imdbID"]}.any?
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def self.read_yaml(file)
|
242
|
+
YAML.load_file(file)
|
243
|
+
end
|
244
|
+
|
245
|
+
def self.write_yaml(file, data)
|
246
|
+
File.open(file, "w") {|f| f.puts data.to_yaml }
|
247
|
+
end
|
248
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# __BEGIN__
|
2
|
+
#
|
3
|
+
#
|
4
|
+
# i am here, because of bundler's love for me :)
|
5
|
+
#
|
6
|
+
#
|
7
|
+
# ============================================
|
8
|
+
# do not hate me for I have no class,
|
9
|
+
# my creator was an idiot, alas!
|
10
|
+
# what started as a simple idea,
|
11
|
+
# soon grew up, like his love for Tia!
|
12
|
+
# maybe, he will give me a brand new skin,
|
13
|
+
# till then, I am just a module, to begin!! :(
|
14
|
+
# ============================================
|
15
|
+
#
|
16
|
+
#
|
17
|
+
# __END__
|
@@ -0,0 +1,251 @@
|
|
1
|
+
module Movier
|
2
|
+
|
3
|
+
# Organize a given folder of movies
|
4
|
+
#
|
5
|
+
# * *Args* :
|
6
|
+
# - +path+ -> path to the folder with movies that need to be organized
|
7
|
+
#
|
8
|
+
def self.organize(path, options = {})
|
9
|
+
# path where files that need to be organized are.
|
10
|
+
path = path.first
|
11
|
+
path = Dir.pwd if not path or path.empty?
|
12
|
+
path = File.expand_path(path)
|
13
|
+
|
14
|
+
# select files that are larger than 100 MB and not yet, organized.
|
15
|
+
movies = Dir.glob("#{path}/**/*")
|
16
|
+
movies = movies.select{ |f| File.size(f) >= 100*2**20 }
|
17
|
+
movies = movies.reject{ |f| organized?(f) }
|
18
|
+
|
19
|
+
# if we can't find such files, let the user know so.
|
20
|
+
# otherwise, display how many files, we have found
|
21
|
+
if movies.count > 0
|
22
|
+
tip_now("Found approx. #{movies.count} movies")
|
23
|
+
|
24
|
+
# ask user where the organized files will be put up?
|
25
|
+
tip_now "Movies will be saved as: <organized>/<Language>/<rating>+/<movie_name> [<year>]"
|
26
|
+
options[:dir] = ask_for_directory("Where should I put the organized files? ")
|
27
|
+
|
28
|
+
# organize movies one by one
|
29
|
+
movies.each { |movie_path| organize_movie movie_path, options }
|
30
|
+
|
31
|
+
# show that we are done
|
32
|
+
passed_with("All movies were organized!")
|
33
|
+
else
|
34
|
+
passed_with("All movies have already been organized.")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
# Organize a single movie, given a path
|
41
|
+
#
|
42
|
+
# * *Args* :
|
43
|
+
# - +movie_path+ -> path of the movie that will be organized
|
44
|
+
#
|
45
|
+
def self.organize_movie(movie_path, options)
|
46
|
+
movie_path = File.expand_path(movie_path)
|
47
|
+
# let the user know which movie we are parsing..
|
48
|
+
tip_now movie_path, "Checking.."
|
49
|
+
|
50
|
+
# pick a target movie from IMDB.com based on the file path
|
51
|
+
movie = pick_movie movie_path, options[:guess]
|
52
|
+
|
53
|
+
# if we were unable to find the movie, or it was ignored, move ahead
|
54
|
+
return unless movie
|
55
|
+
|
56
|
+
# let the user know, which target was selected
|
57
|
+
imdb = movie[:imdb]
|
58
|
+
selected = "#{imdb["Title"]} [#{imdb["Year"]}"
|
59
|
+
selected += " at #{imdb["imdbRating"]} points with #{imdb["imdbVotes"]} votes"
|
60
|
+
say_rated "Selected", selected, imdb["imdbRating"]
|
61
|
+
|
62
|
+
# since, we can't find movie's language, ask user for it.
|
63
|
+
lang = options[:lang] || ask("What language this movie is in? ") {|x| x.default = "en" }
|
64
|
+
|
65
|
+
# rearrange/reorganize the movie
|
66
|
+
set_language_and_rearrange(movie, options[:dir], lang)
|
67
|
+
end
|
68
|
+
|
69
|
+
# pick a target movie from IMDB.com based on the movie's file path
|
70
|
+
#
|
71
|
+
# * *Args* :
|
72
|
+
# - +movie+ -> path to the movie
|
73
|
+
# * *Returns* :
|
74
|
+
# - hash of information about the selected movie target
|
75
|
+
#
|
76
|
+
def self.pick_movie(movie_path, guess = true)
|
77
|
+
# pick -> our selected target from IMDB.com
|
78
|
+
pick = false
|
79
|
+
|
80
|
+
# get whatever information we can get from movie's path
|
81
|
+
movie = sanitize_and_get_information_from movie_path
|
82
|
+
|
83
|
+
# search for movies with this information
|
84
|
+
search = search_movie(movie[:name], movie[:year])
|
85
|
+
|
86
|
+
# get information from IMDB.com for each movie found
|
87
|
+
search.map!{ |mov| fetch_details mov["imdbID"] }
|
88
|
+
# sort our movies
|
89
|
+
search.sort_by {|m| m["weight"] }
|
90
|
+
|
91
|
+
# can we make an intelligent guess ?
|
92
|
+
guessable = guess &&
|
93
|
+
search.count > 1 && (
|
94
|
+
search[1]["votes"] < 5000 &&
|
95
|
+
search[0]["votes"] > 100000 ) || (
|
96
|
+
search[0]['votes'] > 10000 &&
|
97
|
+
search[0]["weight"] > 40 * search[1]["weight"] )
|
98
|
+
|
99
|
+
# pick if only one movie was found, or if search is guessable
|
100
|
+
pick = search[0] if search.count == 1 || guessable
|
101
|
+
|
102
|
+
# let the user pick a movie otherwise from given options
|
103
|
+
unless pick
|
104
|
+
tip_now "Please, choose the correct movie title below:"
|
105
|
+
choose do |menu|
|
106
|
+
# let the user pick from found movie titles
|
107
|
+
search.each do |m|
|
108
|
+
m["details"] = "#{m["Title"]} [#{m["Year"]}] at "
|
109
|
+
m["details"] += "#{m["imdbRating"]} points "
|
110
|
+
m["details"] += "with #{m["imdbVotes"]} votes.\n\t"
|
111
|
+
m["details"] += "Plot: #{m["Plot"]}\n\t"
|
112
|
+
m["details"] += "Actors: #{m["Actors"]}\n"
|
113
|
+
menu.choice(m["details"]) { pick = m }
|
114
|
+
end
|
115
|
+
# let the user provide an IMDB ID
|
116
|
+
menu.choice("[ Use IMDB ID ]") do
|
117
|
+
id = ask("IMDB.com ID for the given title? ")
|
118
|
+
pick = fetch_details id
|
119
|
+
end
|
120
|
+
menu.choice("[ Use another keyword ]") do
|
121
|
+
keyword = ask("Keyword to search with? ")
|
122
|
+
pick_movie keyword
|
123
|
+
end
|
124
|
+
# let the user ignore this movie for organizing purposes
|
125
|
+
menu.choice("[ Ignore ]") { return false }
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# return our pick or false for skipping this movie
|
130
|
+
movie[:imdb] = pick
|
131
|
+
pick ? movie : false
|
132
|
+
end
|
133
|
+
|
134
|
+
# Search a movie given some keywords, and optionally, an year.
|
135
|
+
# Loop until we have a target movie from IMDB.com.
|
136
|
+
# If need arises, ask user to provide us some keywords or an IMDB.com ID.
|
137
|
+
#
|
138
|
+
# * *Args* :
|
139
|
+
# - +keyword+ -> keywords for the search
|
140
|
+
# - +year+ -> (optional) year of this movie
|
141
|
+
# - +ask_user+ -> if true, asks a user for movie name/id
|
142
|
+
# * *Returns* :
|
143
|
+
# - hash of search results
|
144
|
+
#
|
145
|
+
def self.search_movie(keyword, year = nil, ask_user = false)
|
146
|
+
search = []; counter = 1
|
147
|
+
until search.any?
|
148
|
+
# on first search, it will search with keyword being passed
|
149
|
+
# on second search, will search with initial two words in the keyword
|
150
|
+
# on third search and onwards, will ask user for keywords/id
|
151
|
+
tip_now "Using keywords: #{keyword}", "Searching.." if counter > 1
|
152
|
+
begin; search = search(keyword, year, "movie"); rescue; end
|
153
|
+
begin; search = search(keyword, nil, "Movie") if year and not search.any?; rescue; end
|
154
|
+
|
155
|
+
if counter == 1
|
156
|
+
keyword = keyword.split
|
157
|
+
keyword = keyword.count == 1 ? keyword[0] : "#{keyword[0]} #{keyword[1]}"
|
158
|
+
elsif not search.any?
|
159
|
+
keyword = ask("Please, provide keywords/IMDB ID to search with: ")
|
160
|
+
return [{ "imdbID" => keyword }] if keyword =~ /tt\d{7}/
|
161
|
+
end
|
162
|
+
|
163
|
+
counter += 1;
|
164
|
+
end
|
165
|
+
search
|
166
|
+
end
|
167
|
+
|
168
|
+
# Set language for a movie, and then rearrange/reorganize it on the disk
|
169
|
+
#
|
170
|
+
# * *Args* :
|
171
|
+
# - +movie+ -> hash of information about this movie
|
172
|
+
# - +path+ -> directory where the organized movies will be kept
|
173
|
+
# - +lang+ -> language of this movie
|
174
|
+
#
|
175
|
+
def self.set_language_and_rearrange(movie, org_path, lang = "en")
|
176
|
+
# get the language for this movie
|
177
|
+
lang = case lang.downcase
|
178
|
+
when "en", "eng", "english" then "English"
|
179
|
+
when "hi", "hindi" then "Hindi"
|
180
|
+
when "fr", "french" then "French"
|
181
|
+
else lang.titlecase
|
182
|
+
end
|
183
|
+
|
184
|
+
# set the directory path for this movie
|
185
|
+
# directory structure is:
|
186
|
+
# <org_path>/<language>/<rating>+/<movie name> [<year>]/
|
187
|
+
# e.g. for the Top Gun movie:
|
188
|
+
# /Volumes/JukeBox/Movies/English/6+/Top Gun [1986]/
|
189
|
+
imdb = movie[:imdb]
|
190
|
+
nice_name = "#{imdb["Title"]} [#{imdb["Year"]}]".gsub(/(\\|\/|\:)/, ' - ')
|
191
|
+
movie_path = File.join(org_path, lang, "#{imdb["imdbRating"]}+", nice_name)
|
192
|
+
FileUtils.mkdir_p movie_path
|
193
|
+
|
194
|
+
Dir.chdir(movie_path) do
|
195
|
+
# move the movie to this folder
|
196
|
+
# TODO: skip copying the file if this movie already exists
|
197
|
+
FileUtils.mv movie[:path], nice_name + File.extname(movie[:path])
|
198
|
+
# create imdb.txt file inside this folder
|
199
|
+
File.open("imdb.txt", "w") {|f| f.puts movie.to_yaml}
|
200
|
+
# download the movie posted to this folder
|
201
|
+
`wget #{imdb["Poster"]} -qO poster#{File.extname(imdb["Poster"])}` unless
|
202
|
+
imdb["Poster"] == "N/A"
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
# Sanitize whatever information we got from movie's path
|
207
|
+
#
|
208
|
+
# * *Args* :
|
209
|
+
# - +path+ -> path to the movie
|
210
|
+
# * *Returns* :
|
211
|
+
# - hash of sanitized information about the movie
|
212
|
+
#
|
213
|
+
def self.sanitize_and_get_information_from(path)
|
214
|
+
movie = capture_movie(path)
|
215
|
+
movie[:name] = movie[:name].gsub(/(BR|DVD|[a-z]*)Rip/i, "")
|
216
|
+
movie[:name] = movie[:name].gsub(/divx/i, "")
|
217
|
+
movie[:name] = movie[:name].gsub(/[._-]/, " ").strip
|
218
|
+
movie[:parent] = File.basename(File.dirname(path))
|
219
|
+
movie[:path] = path
|
220
|
+
movie
|
221
|
+
end
|
222
|
+
|
223
|
+
# Capture information about the movie from its path
|
224
|
+
#
|
225
|
+
# * *Args* :
|
226
|
+
# - +path+ -> path of this movie
|
227
|
+
# * *Returns* :
|
228
|
+
# - hash of information about the movie
|
229
|
+
#
|
230
|
+
def self.capture_movie(path)
|
231
|
+
movie = File.basename(path, File.extname(path))
|
232
|
+
|
233
|
+
# try for: movie name [year] something.something
|
234
|
+
regex = /(.*)\[(\d{4})\].*/
|
235
|
+
match = movie.match regex
|
236
|
+
return {type: 1, name: match[1], year: match[2]} if match
|
237
|
+
|
238
|
+
# try for: movie name (year) something.something
|
239
|
+
regex = /(.*)\((\d{4})\).*/
|
240
|
+
match = movie.match regex
|
241
|
+
return {type: 2, name: match[1], year: match[2]} if match
|
242
|
+
|
243
|
+
# try for: movie name year something.something
|
244
|
+
regex = /(.*)(\d{4}).*/
|
245
|
+
match = movie.match regex
|
246
|
+
return {type: 3, name: match[1], year: match[2]} if match
|
247
|
+
|
248
|
+
# go generic
|
249
|
+
return {type: 0, name: movie }
|
250
|
+
end
|
251
|
+
end
|
data/lib/movier/say.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
module Movier
|
2
|
+
# say something in a nicely formatted way
|
3
|
+
#
|
4
|
+
# * *Args* :
|
5
|
+
# - +status+ -> status for this message
|
6
|
+
# - +message+ -> actual message
|
7
|
+
# - +colorscheme+ -> color scheme to be used for this message
|
8
|
+
def self.say_with_status(status, message, colorscheme = nil, do_break = false)
|
9
|
+
# Create a color scheme, naming color patterns with symbol names.
|
10
|
+
ft = HighLine::ColorScheme.new do |cs|
|
11
|
+
cs[:regular] = [ ]
|
12
|
+
cs[:above8] = [:bold, :green]
|
13
|
+
cs[:above6] = [:bold, :yellow]
|
14
|
+
cs[:below6] = [:red]
|
15
|
+
cs[:information] = [ :bold, :cyan ]
|
16
|
+
cs[:success] = [ :green ]
|
17
|
+
cs[:error] = [ :bold, :red]
|
18
|
+
cs[:warning] = [ :bold, :yellow ]
|
19
|
+
end
|
20
|
+
# Assign that color scheme to HighLine...
|
21
|
+
HighLine.color_scheme = ft
|
22
|
+
# default color scheme
|
23
|
+
colorscheme ||= :regular
|
24
|
+
status += " " * (20 - status.length)
|
25
|
+
message = message.gsub("'", %q(\\\'))
|
26
|
+
if do_break
|
27
|
+
message = message.split.each_slice(10).map{|x| x.join(" ") }
|
28
|
+
message = message.join("\n" + " " * 25)
|
29
|
+
end
|
30
|
+
say("<%= color(' #{status} #{message}', '#{colorscheme}') %>")
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.tip_now(message, status="Information", do_break = false)
|
34
|
+
say_with_status status, message, :information, do_break
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.passed_with(message, do_break = false)
|
38
|
+
say_with_status "Success", message, :success, do_break
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.warn_with(message, do_break = false)
|
42
|
+
say_with_status "Warning", message, :warning, do_break
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.failed_with(message, do_break = false)
|
46
|
+
say_with_status "ERROR", message, :error, do_break
|
47
|
+
raise message
|
48
|
+
end
|
49
|
+
end
|
data/lib/movier/version.rb
CHANGED
data/lib/movier.rb
CHANGED
@@ -3,11 +3,11 @@ require 'movier/version.rb'
|
|
3
3
|
# Add requires for other files you add to your project here, so
|
4
4
|
# you just need to require this one file in your bin file
|
5
5
|
|
6
|
+
# require 'pry'
|
6
7
|
require 'open-uri'
|
7
8
|
require 'json'
|
8
9
|
require 'httparty'
|
9
10
|
require 'highline/import'
|
10
|
-
# require 'pry'
|
11
11
|
require 'digest/md5'
|
12
12
|
|
13
13
|
require 'movier/say.rb'
|
data/movier.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# Ensure we require the local version and not one we might have installed already
|
2
|
+
require File.join([File.dirname(__FILE__),'lib','movier','version.rb'])
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = 'movier'
|
5
|
+
s.version = Movier::VERSION
|
6
|
+
s.author = 'Nikhil Gupta'
|
7
|
+
s.email = 'me@nikhgupta.com'
|
8
|
+
s.homepage = 'http://nikhgupta.com'
|
9
|
+
s.platform = Gem::Platform::RUBY
|
10
|
+
s.summary = 'Movier is a gem that allows you to quickly organize your movies'
|
11
|
+
s.description = 'Movier is a gem that allows you to quickly organize your movies'
|
12
|
+
# Add your other files here if you make them
|
13
|
+
s.files = `git ls-files`.split
|
14
|
+
s.require_paths << 'lib'
|
15
|
+
s.has_rdoc = true
|
16
|
+
s.extra_rdoc_files = ['README.rdoc','movier.rdoc']
|
17
|
+
s.rdoc_options << '--title' << 'movier' << '--main' << 'README.rdoc' << '-ri'
|
18
|
+
s.bindir = 'bin'
|
19
|
+
s.executables << 'movier'
|
20
|
+
|
21
|
+
s.add_dependency 'json'
|
22
|
+
s.add_dependency 'highline'
|
23
|
+
s.add_dependency 'httparty'
|
24
|
+
|
25
|
+
s.add_development_dependency 'pry'
|
26
|
+
s.add_development_dependency 'rake'
|
27
|
+
s.add_development_dependency 'rdoc'
|
28
|
+
s.add_development_dependency 'aruba'
|
29
|
+
|
30
|
+
s.add_runtime_dependency 'gli','2.5.3'
|
31
|
+
end
|
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: movier
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nikhil Gupta
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-03-
|
11
|
+
date: 2013-03-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: json
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: highline
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -108,7 +122,7 @@ dependencies:
|
|
108
122
|
- - '='
|
109
123
|
- !ruby/object:Gem::Version
|
110
124
|
version: 2.5.3
|
111
|
-
description:
|
125
|
+
description: Movier is a gem that allows you to quickly organize your movies
|
112
126
|
email: me@nikhgupta.com
|
113
127
|
executables:
|
114
128
|
- movier
|
@@ -117,10 +131,24 @@ extra_rdoc_files:
|
|
117
131
|
- README.rdoc
|
118
132
|
- movier.rdoc
|
119
133
|
files:
|
134
|
+
- .gitignore
|
135
|
+
- Gemfile
|
136
|
+
- README.rdoc
|
137
|
+
- Rakefile
|
120
138
|
- bin/movier
|
121
|
-
-
|
139
|
+
- features/movier.feature
|
140
|
+
- features/step_definitions/movier_steps.rb
|
141
|
+
- features/support/env.rb
|
122
142
|
- lib/movier.rb
|
123
|
-
-
|
143
|
+
- lib/movier/api.rb
|
144
|
+
- lib/movier/helpers.rb
|
145
|
+
- lib/movier/info.rb
|
146
|
+
- lib/movier/lmdb.rb
|
147
|
+
- lib/movier/movier.rb
|
148
|
+
- lib/movier/organize.rb
|
149
|
+
- lib/movier/say.rb
|
150
|
+
- lib/movier/version.rb
|
151
|
+
- movier.gemspec
|
124
152
|
- movier.rdoc
|
125
153
|
homepage: http://nikhgupta.com
|
126
154
|
licenses: []
|