movier 0.0.4 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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: []
|