diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bbc4510 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +.ruby-gemset +.ruby-version +Gemfile.lock +posters \ No newline at end of file diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..3506246 --- /dev/null +++ b/Gemfile @@ -0,0 +1,17 @@ +source 'https://rubygems.org' + +# Movie DBs +gem 'themoviedb' +gem 'imdb' + +# mp4 Meta Data Writing +gem 'atomic-parsley-ruby' + +# Help with searches +gem 'levenshtein' + +# Watch for a new DVD insertion +gem 'rb-fsevent' + +# Console IO +gem 'highline' diff --git a/README.markdown b/README.markdown new file mode 100644 index 0000000..801f70b --- /dev/null +++ b/README.markdown @@ -0,0 +1,19 @@ +Install: + +brew install AtomicParsley +brew install exiftool +brew install tag + + +Notes: + +http://manpages.ubuntu.com/manpages/hardy/man1/AtomicParsley.1.html +https://github.com/cparratto/atomic-parsley-ruby + +http://www.sno.phy.queensu.ca/~phil/exiftool/ +http://miniexiftool.rubyforge.org + +https://github.com/jdberry/tag + +https://github.com/ahmetabdi/themoviedb +https://github.com/ariejan/imdb diff --git a/config.rb b/config.rb new file mode 100644 index 0000000..ab0ac9b --- /dev/null +++ b/config.rb @@ -0,0 +1,18 @@ +require 'thread' +# require 'fsevent' +require 'rb-fsevent' +require 'highline/import' +require 'atomic-parsley-ruby' + +require './movie_extensions' +require './search' +require './dvd' +require './movie' + +@api_key = "867c178725a7e646a0f108ee78781a49" +WORKING_DIR = "/Users/mnichols/Movies/Ripping" +DEST_DIR = "/Users/mnichols/Movies/Ripped" +POSTER_PATH = "./Posters" +DISTANCE_THRESHOLD = 0.2 + +Tmdb::Api.key(@api_key) diff --git a/dvd.rb b/dvd.rb new file mode 100644 index 0000000..2247f69 --- /dev/null +++ b/dvd.rb @@ -0,0 +1,45 @@ +class Dvd + attr_accessor :volumes + + def initialize + @volumes = "/Volumes" + end + + def volume + vol = nil + Dir.entries(@volumes).each do |video_dir| + if not [".", ".."].member?(video_dir) + full_path = File.join(@volumes, video_dir) + if Dir.exists?(File.join(full_path, "VIDEO_TS")) + vol = full_path + break + end + end + end + vol + end + + def title + raise "Invalid DVD Root or DVD not mounted at #{@volumes}" if not present? + File.basename(volume).gsub("_", " ").capitalize + end + + def present? + !volume.nil? + end + + def rip(output) + File.delete(output) if File.exists?(output) + + system("HandBrakeCLI -Z \"AppleTV 3\" --main-feature -i \"#{volume}\" -o \"#{output}\"") + exit_code = $? + if exit_code.exitstatus != 0 + `killall HandBrakeCLI` + Kernel.exit(exit_code.exitstatus) + end + end + + def eject + `drutil tray eject` + end +end \ No newline at end of file diff --git a/movie.rb b/movie.rb new file mode 100644 index 0000000..c1a5fa5 --- /dev/null +++ b/movie.rb @@ -0,0 +1,55 @@ +class Movie + extend Forwardable + + def initialize(tmdb_info, imdb_info) + @tmdb_info = tmdb_info + @imdb_info = imdb_info + end + + def_delegators :@tmdb_info, :title, :year, :poster_path + def_delegators :@imdb_info, :plot, :genres, :plot_summary + + require 'open-uri' + + def tag(output) + Dir.mkdir(POSTER_PATH) if not Dir.exists?(POSTER_PATH) + poster_file_path = File.expand_path(File.join(POSTER_PATH, poster_path)) + poster = File.open(poster_file_path, "wb") + begin + open("http://image.tmdb.org/t/p/w500#{poster_path}", "rb") do |download| + poster.write(download.read) + end + ensure + poster.close() + end + + # Remove stik and other artwork, just in case + v = AtomicParsleyRuby::Media.new(output) + v.artwork "REMOVE_ALL" + v.stik "remove" + v.overwrite true + v.process + + v = AtomicParsleyRuby::Media.new(output) + v.overwrite true + v.title title + v.comment plot unless plot.nil? + v.description plot unless plot.nil? + v.year year.to_s + v.stik "0" + v.genre genres[0] + v.longdesc plot_summary unless plot_summary.nil? + v.artwork poster_file_path + v.process + + # m = MiniExiftool.new output + # m["director"] = info.director + + # Need to use mp4v2 lib extensions to write this stuff + # m["directors//name"] = "Bob" + # m["producers//name"] = "George" + # m["screenwriters//name"] = "Writers" + # m["studio//name"] = "Studio C" + # m.save + end +end \ No newline at end of file diff --git a/movie_extensions.rb b/movie_extensions.rb new file mode 100644 index 0000000..90ab6e4 --- /dev/null +++ b/movie_extensions.rb @@ -0,0 +1,13 @@ +module MovieExtensions + def title_distance + @title_distance + end + + def title_distance=(distance) + @title_distance = distance + end + + def year + Date.parse(release_date).year + end +end \ No newline at end of file diff --git a/run.rb b/run.rb new file mode 100644 index 0000000..310d686 --- /dev/null +++ b/run.rb @@ -0,0 +1,109 @@ +#!/usr/bin/ruby + +require './config.rb' +require 'fileutils' +require 'io/console' + +@command = nil +@last_volume = "" +@dvd = Dvd.new + +def process_dvd + puts + puts "--------------------------------------------------------------" + puts "-- Volume: #{@dvd.volume}" + puts "-- Title: #{@dvd.title}" + puts "--------------------------------------------------------------" + puts + + searcher = Search.new + movie = searcher.closest(@dvd.title) + title = "#{movie.title} (#{movie.year})" + puts WORKING_DIR + puts DEST_DIR + + output = File.join(WORKING_DIR, "#{title.gsub(": ", " - ").gsub(":", " - ")}.m4v") + puts + puts "--------------------------------------------------------------" + puts "-- Title: #{movie.title}" + puts "-- File: #{output}" + puts "--------------------------------------------------------------" + puts + continue = ask("Continue? (y/n)") { |q| q.default = "y" } + if continue != 'y' + puts "Aborting..." + return + end + + puts + puts "Ripping..." + puts + puts + + @dvd.rip(output) + movie.tag(output) + + puts + say "\"#{output}\" complete" + puts + puts + system("drutil tray eject") + FileUtils.mv(output, DEST_DIR) + + # puts `exiftool "#{output}"` +end + +fsevent = FSEvent.new +fsevent.watch %W(/Volumes) do |directories| + if @command.nil? and @dvd.present? and (@last_volume != @dvd.volume) + @last_volume = @dvd.volume + @command = "r" + end +end + +user_command = Thread.new do + while true + if @command.nil? + puts "Enter command (r|q) or insert DVD:" + @command = STDIN.getch.chomp + end + sleep 1 + end +end + +Signal.trap("INT") do + @command = "q" +end + +message_thread = Thread.new do + while true + begin + while @command.nil? + sleep 1 + end + if @command == "r" + process_dvd + end + if @command == "q" + puts "Quitting..." + fsevent.stop + break + end + rescue Exception => e + puts e.message + puts e.backtrace + ensure + @command = nil + end + end +end + +fsevent.run + +message_thread.kill +message_thread.join + +user_command.kill +user_command.join + +exit 0 \ No newline at end of file diff --git a/search.rb b/search.rb new file mode 100644 index 0000000..b152d47 --- /dev/null +++ b/search.rb @@ -0,0 +1,55 @@ +require './search_factory' +require 'levenshtein' + +class Search + def closest(title) + searcher = SearchFactory.get("tmdb") + + movies = leven_sort(title, searcher.search(title)) + while movies.empty? + puts "Unable to locate title" + puts "Enter movie title:" + title = gets.chomp + movies = leven_sort(title, searcher.search(title)) + end + + movie = auto_select(movies) + if movie.nil? or ambiguous?(movies) + puts "Please choose:" + movies.each_with_index do |movie, i| + puts "#{i}. #{movie.title} (#{movie.release_date}) (#{movie.title_distance.round(3)})" + end + title_sel = gets + movie = movies[title_sel.to_i] + end + info = SearchFactory.get("imdb").search(title)[0] + + movie = Movie.new(movie, info) + movie + end + + def leven_sort(search_title, movies) + movies.each do |movie| + distance = Levenshtein.normalized_distance(search_title, movie.title) + movie.title_distance = distance + end + movies.reverse.sort_by { |m| m.title_distance } + end + + def auto_select(movies) + return movies[0] if (not movies.empty?) and (movies[0].title_distance < DISTANCE_THRESHOLD) + nil + end + + def ambiguous?(movies) + return true if movies.empty? + return false if movies.size == 1 + + lowest_distance = movies[0].title_distance + + return true if lowest_distance > DISTANCE_THRESHOLD + return true if movies.find_all { |m| m.title_distance == lowest_distance }.size > 1 + + false + end +end \ No newline at end of file diff --git a/search_factory.rb b/search_factory.rb new file mode 100644 index 0000000..d0985bb --- /dev/null +++ b/search_factory.rb @@ -0,0 +1,10 @@ +require './search_imdb' +require './search_tmdb' + +class SearchFactory + def self.get(name) + return SearchImdb.new if name == 'imdb' + return SearchTmdb.new if name == 'tmdb' + raise "Invalid Search Engine Name" + end +end \ No newline at end of file diff --git a/search_imdb.rb b/search_imdb.rb new file mode 100644 index 0000000..f73415b --- /dev/null +++ b/search_imdb.rb @@ -0,0 +1,11 @@ +require 'imdb' + +class SearchImdb + def search(title) + Imdb::Search.new(title).movies + end +end + +class Imdb::Movie + include MovieExtensions +end \ No newline at end of file diff --git a/search_tmdb.rb b/search_tmdb.rb new file mode 100644 index 0000000..6b247af --- /dev/null +++ b/search_tmdb.rb @@ -0,0 +1,11 @@ +require 'themoviedb' + +class SearchTmdb + def search(title) + Tmdb::Movie.find(title) + end +end + +class Tmdb::Movie + include MovieExtensions +end \ No newline at end of file diff --git a/test.rb b/test.rb new file mode 100644 index 0000000..a7e2bae --- /dev/null +++ b/test.rb @@ -0,0 +1,40 @@ +require 'thread' + +c = Thread.current + +command = nil +a = Thread.new do + while true + command = gets + end +end + +t = Thread.new do + while true + sleep 5 + command = "h" + end +end + +Signal.trap("INT") do + puts "Terminating..." + t.wakeup + a.wakeup + + t.kill + a.kill + + t.join + a.join + + c.wakeup + exit 0 +end + +while true + while command.nil? + sleep 1 + end + puts "Got #{command}" + command = nil +end \ No newline at end of file