diff --git a/.gitignore b/.gitignore index bbc4510..d0b3af6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,20 @@ .DS_Store .ruby-gemset .ruby-version +/.bundle/ +/.yardoc +/Gemfile.lock +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ Gemfile.lock -posters \ No newline at end of file +posters +test.rb +encode.log +temp/ +log/ + + diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..8c18f1a --- /dev/null +++ b/.rspec @@ -0,0 +1,2 @@ +--format documentation +--color diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..a618741 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,4 @@ +language: ruby +rvm: + - 2.2.0 +before_install: gem install bundler -v 1.10.6 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..ce9bee7 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,13 @@ +# Contributor Code of Conduct + +As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. + +We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. + +Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. + +This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) diff --git a/Gemfile b/Gemfile index 3506246..fa75df1 100644 --- a/Gemfile +++ b/Gemfile @@ -1,17 +1,3 @@ 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' +gemspec diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..55cd580 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 matthew-nichols + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.markdown b/README.markdown deleted file mode 100644 index 801f70b..0000000 --- a/README.markdown +++ /dev/null @@ -1,19 +0,0 @@ -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/README.md b/README.md new file mode 100644 index 0000000..d46775d --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# dvd_ripper + +A command line tool that will make it easy to rip and tag your dvd collection. It ties together many great tools to get the job done. -- HandBrake, AtomicParsley, exiftool, imdb and tmdb. + +It takes a bit on configuration, but once it is setup, ripping your DVD collection will be a breeze. + +Enjoy! + +## Installation + +Install Handbrake: [downloads](https://handbrake.fr/downloads.php) +Install HandBrakeCLI: [downloads](https://handbrake.fr/downloads2.php) + + $ brew install AtomicParsley + + $ brew install exiftool + + $ brew install tag + + $ brew install libdvdcss + + $ gem install dvd_ripper + +## Usage + + $ dvd_ripper + +## Troubleshooting + +### nokogiri fails to install + $ xcode-select --install + + $ gem install nokogiri -- --use-system-libraries + + $ bundle config build.nokogiri --use-system-libraries + +### HandBrake Errors + +* Make sure that you have the same version of HandBrake and HandBrakeCLI +* Make sure that you have installed libdvdcss + + +## 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 + + +## To Do + +* Add ability to set HandBrake encoding options +* Add better support for TV show ripping diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..b7e9ed5 --- /dev/null +++ b/Rakefile @@ -0,0 +1,6 @@ +require "bundler/gem_tasks" +require "rspec/core/rake_task" + +RSpec::Core::RakeTask.new(:spec) + +task :default => :spec diff --git a/bin/console b/bin/console new file mode 100755 index 0000000..1d263ca --- /dev/null +++ b/bin/console @@ -0,0 +1,14 @@ +#!/usr/bin/env ruby + +require "bundler/setup" +require "dvd_ripper" + +# You can add fixtures and/or initialization code here to make experimenting +# with your gem easier. You can also use a different console, if you like. + +# (If you use this, don't forget to add pry to your Gemfile!) +# require "pry" +# Pry.start + +require "irb" +IRB.start diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..b65ed50 --- /dev/null +++ b/bin/setup @@ -0,0 +1,7 @@ +#!/bin/bash +set -euo pipefail +IFS=$'\n\t' + +bundle install + +# Do any other automated setup that you need to do here diff --git a/config.rb b/config.rb deleted file mode 100644 index ab0ac9b..0000000 --- a/config.rb +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 2247f69..0000000 --- a/dvd.rb +++ /dev/null @@ -1,45 +0,0 @@ -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/dvd_ripper.gemspec b/dvd_ripper.gemspec new file mode 100644 index 0000000..6546a5a --- /dev/null +++ b/dvd_ripper.gemspec @@ -0,0 +1,33 @@ +# coding: utf-8 +lib = File.expand_path('../lib', __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'dvd_ripper/version' + +Gem::Specification.new do |spec| + spec.name = "dvd_ripper" + spec.version = DvdRipper::VERSION + spec.authors = ["matthew-nichols"] + spec.email = ["matt@nichols.link"] + + spec.summary = %q{Quickly rip and tag DVDs} + spec.description = %q{A command-line utility that makes it easy to rip and tag DVDs. Just install and run the utility. Then insert a DVD and follow prompts. Repeat.} + spec.homepage = "TODO: Put your gem's website or public repo URL here." + spec.license = "MIT" + + spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } + spec.bindir = "exe" + spec.executables = ["dvd_ripper"] + spec.require_paths = ["lib"] + + spec.add_runtime_dependency 'thor' + spec.add_runtime_dependency 'nokogiri', '~> 1.6.0' + spec.add_runtime_dependency 'themoviedb', '~> 0.0.20' + spec.add_runtime_dependency 'imdb', '~> 0.8.1' + spec.add_runtime_dependency 'rb-fsevent', '~> 0.9.4' + spec.add_runtime_dependency 'levenshtein', '~> 0.2.2' + spec.add_runtime_dependency 'atomic-parsley-ruby' #, :git => 'https://github.com/cparratto/atomic-parsley-ruby.git' + + spec.add_development_dependency "bundler", "~> 1.10" + spec.add_development_dependency "rake", "~> 10.0" + spec.add_development_dependency "rspec", '~> 3.2' +end diff --git a/exe/dvd_ripper b/exe/dvd_ripper new file mode 100755 index 0000000..7c82d87 --- /dev/null +++ b/exe/dvd_ripper @@ -0,0 +1,54 @@ +#!/usr/bin/env ruby + +require 'dvd_ripper' +require "thor" + +module DvdRipper + class Cli < ::Thor + desc "start", "start tool" + def start + check_config! + dvd_ripper = ::DvdRipper::Client.new + dvd_ripper.start + end + map %w[--version -v] => :__print_version + + desc "--version, -v", "print the version" + def __print_version + puts "DvdRipper v#{::DvdRipper::VERSION}" + end + + desc "config", "configure dvd_ripper" + def config + Config.instance.prompt! + end + + desc "help", "display this help" + def help_banner + puts <<-BANNER + Dvd Ripper + + This commandline utility cleanly and simply captures your dvd contents and + tags the movies. + + Movie information is pulled from www.imdb.com and www.themoviedb.org + + Dvd content captured using Handbrake (https://handbrake.fr/) + BANNER + help + end + + no_commands do + def check_config! + unless Config.instance.exists? + config + end + end + end + + # "www.themoviedb.org" + + end +end + +::DvdRipper::Cli.start diff --git a/lib/dvd_ripper.rb b/lib/dvd_ripper.rb new file mode 100644 index 0000000..eb20b42 --- /dev/null +++ b/lib/dvd_ripper.rb @@ -0,0 +1,21 @@ +require "dvd_ripper/version" + +require 'thread' +require 'rb-fsevent' +require 'atomic-parsley-ruby' +require 'fileutils' +require 'io/console' +require 'imdb' +require 'themoviedb' +require 'levenshtein' + +require_relative 'dvd_ripper/movie_stub.rb' +require_relative 'dvd_ripper/movie_extensions.rb' +require_relative 'dvd_ripper/search_factory.rb' +require_relative 'dvd_ripper/search_imdb.rb' +require_relative 'dvd_ripper/search_tmdb.rb' +require_relative 'dvd_ripper/search.rb' +require_relative 'dvd_ripper/dvd.rb' +require_relative 'dvd_ripper/movie.rb' +require_relative 'dvd_ripper/config.rb' +require_relative 'dvd_ripper/client.rb' diff --git a/lib/dvd_ripper/client.rb b/lib/dvd_ripper/client.rb new file mode 100644 index 0000000..a4f977e --- /dev/null +++ b/lib/dvd_ripper/client.rb @@ -0,0 +1,129 @@ +module DvdRipper + class Client + def initialize + @command = nil + @title = nil + @last_volume = "" + @dvd = Dvd.new + end + + def process_dvd + puts + puts "--------------------------------------------------------------" + puts "-- Volume: #{@dvd.volume}" + puts "-- Title: #{@dvd.title}" + puts "--------------------------------------------------------------" + puts + + searcher = Search.new + movie = searcher.closest(@title || @dvd.title) + if movie.nil? + puts "- Provide details for movie -" + puts "Enter Movie Title:" + movie = ::Movie.new + movie.title = $stdin.gets + puts "Enter Movie Year:" + movie.release_date = "#{$stdin.gets.chomp}-01-01" + end + title = "#{movie.title} (#{movie.year})" + output = File.join(::DvdRipper::Config.instance.working_dir, "#{title.gsub(": ", " - ").gsub(":", " - ")}.m4v") + puts + puts "--------------------------------------------------------------" + puts "-- Title: #{movie.title}" + puts "-- Year: #{movie.year}" + puts "--------------------------------------------------------------" + puts + puts "Continue? (y/n) |y|" + continue = $stdin.gets.strip + continue = 'y' if continue.blank? + if continue != 'y' + return + end + + @dvd.rip(output) + + movie.tag(output) unless movie.nil? + + puts "\"#{output}\" complete" + @dvd.eject + FileUtils.mv(output, ::DvdRipper::Config.instance.dest_dir) + end + + def force_user_input(input) + @command = input + @user_command.raise("wakeup") + end + + def start + Tmdb::Api.key(::DvdRipper::Config.instance.tmdb_api_key) + + @user_command = Thread.new do + while true + begin + if @command.nil? + puts "Choose command [(r)ip, (s)pecify title, (q)uit, (e)ject] or insert a DVD" + @command = $stdin.gets.chomp + end + sleep 1 + rescue Exception => e + puts e.message + end + end + end + + fsevent = FSEvent.new + fsevent.watch %W(/Volumes) do |directories| + if @command.nil? and @dvd.present? and (@last_volume != @dvd.volume) + force_user_input("r") + @last_volume = @dvd.volume + end + end + + Signal.trap("INT") do + fsevent.stop + Kernel.exit(0) + end + + message_thread = Thread.new do + while true + begin + while @command.nil? + sleep 1 + end + if @command == "r" + process_dvd + end + if @command == "s" + puts "Enter title:" + @title = $stdin.gets + process_dvd + end + if @command == "q" + puts "Quitting..." + fsevent.stop + break + end + if @command == "e" + @dvd.eject + end + rescue Exception => e + puts e.message + puts e.class + puts e.backtrace + ensure + @command = nil + @title = nil + end + end + end + + fsevent.run + + message_thread.kill + message_thread.join + + @user_command.kill + @user_command.join + end + end +end diff --git a/lib/dvd_ripper/config.rb b/lib/dvd_ripper/config.rb new file mode 100644 index 0000000..0d2a424 --- /dev/null +++ b/lib/dvd_ripper/config.rb @@ -0,0 +1,84 @@ +require 'singleton' +require 'yaml' + +module DvdRipper + class Config + include Singleton + + attr_accessor :config + + DEFAULTS = { + :working_dir => "~/tmp/movies", + :dest_dir => "~/movies", + :poster_dir => "~/tmp/movies/posters", + :distance_threshold => 0.2, + :tmdb_api_key => nil + }.freeze + + def load + self.config = DEFAULTS.merge(read_config) + end + + def read_config + yml = "" + yml = File.read(config_path) if exists? + + return YAML.load(yml) unless yml.blank? + + {} + end + + def exists? + File.exist?(config_path) + end + + def working_dir + config[:working_dir] + end + + def dest_dir + config[:dest_dir] + end + + def tmdb_api_key + config[:tmdb_api_key] + end + + def poster_dir + config[:poster_dir] + end + + def distance_threshold + config[:distance_threshold] + end + + def config_path + File.expand_path("~/.dvd_ripper") + end + + def save! + File.open(config_path, "w+") do |config_file| + config_file.write(config.to_yaml) + end + end + + def prompt! + config.each do |k, v| + puts "#{k} (ENTER: #{v}):" + new_value = $stdin.gets + config[k] = new_value.strip unless (new_value.strip.blank?) + end + + save! + end + end +end + +::DvdRipper::Config.instance.load + + # WORKING_DIR = "/Users/mnichols/Movies/Ripping" + # DEST_DIR = "/Users/mnichols/Movies/Ripped" + # POSTER_PATH = "./Posters" + # DISTANCE_THRESHOLD = 0.2 + + # @api_key = "867c178725a7e646a0f108ee78781a49" diff --git a/lib/dvd_ripper/dvd.rb b/lib/dvd_ripper/dvd.rb new file mode 100644 index 0000000..eca4ea0 --- /dev/null +++ b/lib/dvd_ripper/dvd.rb @@ -0,0 +1,119 @@ +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) + + state = :scanning + percent = "0.0" + eta = "unknown" + dc = 0 + spinner = ["-", "\\", "|", "/"] + + encoding_reg = /Encoding: task \d+ of \d+, (?\d+\.\d+) %.*ETA (?\d{2}h\d{2}m\d{2}s)/i + status_update = Thread.new do + while true do + sleep(1) + dc = dc + 1 + + if state == :encoding + print "\r#{spinner[dc%4]} Encoding #{percent} % (time left #{eta})" + end + + if state == :scanning + print("\r" + (" " * 50)) if ((dc % 20) == 0) + print "\r#{spinner[dc%4]} Scanning Titles.#{"." * (dc % 20)}" + end + end + end + + command_output = [] + last_line = nil + File.open("./encode.log", "w+") do |log| + IO.popen("HandBrakeCLI -Z \"AppleTV 3\" --main-feature -i \"#{volume}\" -o \"#{output}\" 2>&1", "w+") do |f| + begin + while buf = f.read(255) + lines = buf.split(/\r|\n/) + lines.compact! + + if (lines.size > 1) and (not last_line.nil?) + command_output << last_line + lines.shift + end + + if buf[-1] == "\r" or buf[-1] == "\n" + last_line = nil + else + last_line = lines.pop + end + + command_output << lines + command_output.flatten! + + while ln = command_output.shift + ln = ln.strip unless ln.nil? + + # Change to encoding as soon as a matching line is found + if encoding_reg =~ ln + state = :encoding + end + + if state == :encoding + if m = encoding_reg.match(ln) + percent = m[:percent] + eta = m[:eta] + end + end + + log.write "#{state}:#{percent}:#{eta} - #{ln}\n" + end + command_output = [] + end + rescue Exception => e + puts e.message + puts e.class + puts e.backtrace + end + end + end + exit_code = $? + + if exit_code.exitstatus != 0 + `killall HandBrakeCLI` + puts "Encoding Failed. See encode.log" + end + ensure + status_update.kill + status_update.join + end + + def eject + system("drutil tray eject") + end +end diff --git a/movie.rb b/lib/dvd_ripper/movie.rb similarity index 52% rename from movie.rb rename to lib/dvd_ripper/movie.rb index c1a5fa5..7a937de 100644 --- a/movie.rb +++ b/lib/dvd_ripper/movie.rb @@ -1,55 +1,58 @@ class Movie extend Forwardable - - def initialize(tmdb_info, imdb_info) - @tmdb_info = tmdb_info - @imdb_info = imdb_info + + def initialize(tmdb_info=nil, imdb_info=nil) + @tmdb_info = tmdb_info.nil? ? ::MovieStub.new : tmdb_info + @imdb_info = imdb_info.nil? ? ::MovieStub.new : 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() + poster_file_path = nil + unless poster_path.nil? + Dir.mkdir(::DvdRipper::Config.instance.poster_dir) if not Dir.exists?(::DvdRipper::Config.instance.poster_dir) + poster_file_path = File.expand_path(File.join(::DvdRipper::Config.instance.poster_dir, 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 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.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.artwork poster_file_path unless poster_file_path.nil? 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["studio//name"] = "Studio C" # m.save end -end \ No newline at end of file +end diff --git a/movie_extensions.rb b/lib/dvd_ripper/movie_extensions.rb similarity index 100% rename from movie_extensions.rb rename to lib/dvd_ripper/movie_extensions.rb diff --git a/lib/dvd_ripper/movie_stub.rb b/lib/dvd_ripper/movie_stub.rb new file mode 100644 index 0000000..64d6965 --- /dev/null +++ b/lib/dvd_ripper/movie_stub.rb @@ -0,0 +1,3 @@ +class MovieStub + attr_accessor :title, :year, :poster_path, :plot, :genres, :plot_summary +end \ No newline at end of file diff --git a/search.rb b/lib/dvd_ripper/search.rb similarity index 69% rename from search.rb rename to lib/dvd_ripper/search.rb index b152d47..2a4aef6 100644 --- a/search.rb +++ b/lib/dvd_ripper/search.rb @@ -1,33 +1,33 @@ -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 + puts " - Title Not Found - " + puts "Enter movie title (or enter to skip search):" + title = $stdin.gets.chomp + return nil if title.empty? 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 + puts "s. Skip" + title_sel = $stdin.gets.chomp + return nil if title_sel == 's' 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) @@ -35,21 +35,21 @@ def leven_sort(search_title, movies) 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) + return movies[0] if (movies.size == 1) or ((not movies.empty?) and (movies[0].title_distance < ::DvdRipper::Config.instance.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 lowest_distance > ::DvdRipper::Config.instance.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 +end diff --git a/search_factory.rb b/lib/dvd_ripper/search_factory.rb similarity index 76% rename from search_factory.rb rename to lib/dvd_ripper/search_factory.rb index d0985bb..994b0b0 100644 --- a/search_factory.rb +++ b/lib/dvd_ripper/search_factory.rb @@ -1,10 +1,7 @@ -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 +end diff --git a/search_imdb.rb b/lib/dvd_ripper/search_imdb.rb similarity index 86% rename from search_imdb.rb rename to lib/dvd_ripper/search_imdb.rb index f73415b..daf865c 100644 --- a/search_imdb.rb +++ b/lib/dvd_ripper/search_imdb.rb @@ -1,5 +1,3 @@ -require 'imdb' - class SearchImdb def search(title) Imdb::Search.new(title).movies @@ -8,4 +6,4 @@ def search(title) class Imdb::Movie include MovieExtensions -end \ No newline at end of file +end diff --git a/search_tmdb.rb b/lib/dvd_ripper/search_tmdb.rb similarity index 82% rename from search_tmdb.rb rename to lib/dvd_ripper/search_tmdb.rb index 6b247af..836b39d 100644 --- a/search_tmdb.rb +++ b/lib/dvd_ripper/search_tmdb.rb @@ -1,5 +1,3 @@ -require 'themoviedb' - class SearchTmdb def search(title) Tmdb::Movie.find(title) @@ -8,4 +6,4 @@ def search(title) class Tmdb::Movie include MovieExtensions -end \ No newline at end of file +end diff --git a/lib/dvd_ripper/version.rb b/lib/dvd_ripper/version.rb new file mode 100644 index 0000000..f5ab914 --- /dev/null +++ b/lib/dvd_ripper/version.rb @@ -0,0 +1,3 @@ +module DvdRipper + VERSION = "0.1.0" +end diff --git a/run.rb b/run.rb deleted file mode 100644 index 310d686..0000000 --- a/run.rb +++ /dev/null @@ -1,109 +0,0 @@ -#!/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/spec/dvd_ripper_spec.rb b/spec/dvd_ripper_spec.rb new file mode 100644 index 0000000..0169bfc --- /dev/null +++ b/spec/dvd_ripper_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +describe DvdRipper do + it 'has a version number' do + expect(DvdRipper::VERSION).not_to be nil + end + + it 'does something useful' do + expect(false).to eq(true) + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..7719db9 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,2 @@ +$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) +require 'dvd_ripper' diff --git a/test.rb b/test.rb deleted file mode 100644 index a7e2bae..0000000 --- a/test.rb +++ /dev/null @@ -1,40 +0,0 @@ -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