├── .ruby-version ├── spec ├── .gitignore ├── rank_spec.rb ├── download_spec.rb ├── the_rotten_pirate_spec.rb ├── name_cleaner_spec.rb └── upcoming_dvds.txt ├── .ruby-gemset ├── Manifest ├── config ├── prowl.template.yml ├── schedule.rb └── config.template.yml ├── .gitignore ├── Gemfile ├── lib ├── yaml_writer.rb ├── rank.rb ├── fork_logger.rb ├── download.rb ├── the_rotten_pirate.rb └── name_cleaner.rb ├── Gemfile.lock ├── Rakefile └── README.md /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.0.0 2 | -------------------------------------------------------------------------------- /spec/.gitignore: -------------------------------------------------------------------------------- 1 | junk* 2 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | rotten_pirate 2 | -------------------------------------------------------------------------------- /Manifest: -------------------------------------------------------------------------------- 1 | README.md 2 | Rakefile 3 | lib/the_rotten_pirate.rb 4 | Manifest 5 | -------------------------------------------------------------------------------- /config/prowl.template.yml: -------------------------------------------------------------------------------- 1 | --- 2 | active: true 3 | api_key: your_api_key_here -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | db/* 2 | tmp/* 3 | logs/* 4 | config/prowl.yml 5 | config/config.yml 6 | -------------------------------------------------------------------------------- /config/schedule.rb: -------------------------------------------------------------------------------- 1 | set :output, "/dev/null" 2 | 3 | every 2.days, :at => '2:43 pm' do 4 | rake "execute" 5 | end 6 | 7 | every 5.minutes do 8 | rake "download_from_watch_file" 9 | end 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | group :test do 4 | gem 'rspec', '~>2.7' 5 | gem 'awesome_print' 6 | end 7 | 8 | gem 'torrent_api', '0.2.7' 9 | gem 'sequel', '~>3.2' 10 | gem 'whenever' 11 | gem 'sqlite3' 12 | gem 'i18n' 13 | gem 'rake' 14 | gem 'prowl' 15 | gem 'pry' 16 | -------------------------------------------------------------------------------- /lib/yaml_writer.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | 3 | class YAMLWriter 4 | def initialize object 5 | @yaml = object.to_yaml 6 | end 7 | 8 | def write 9 | FileUtils.mkdir_p('logs') 10 | timestamp = Time.now.strftime("%Y%m%d%H%M%S") 11 | filename = "logs/#{timestamp}_results.yaml" 12 | File.open(filename, 'w') { |f| f.puts @yaml } 13 | end 14 | end -------------------------------------------------------------------------------- /config/config.template.yml: -------------------------------------------------------------------------------- 1 | --- 2 | filter_out_less_than_percentage: 85 3 | filter_out_non_certified_fresh: true 4 | filter_out_already_downloaded: true 5 | comments: 6 | analyze: true 7 | quality: high # or low 8 | num_to_analyze: 10 9 | minimum_seeds: 50 10 | download_directory: ~/Torrents 11 | watch_file: /Users/admin/Sites/the_mobile_pirate/movies_to_download.txt -------------------------------------------------------------------------------- /lib/rank.rb: -------------------------------------------------------------------------------- 1 | class Rank 2 | BASE = 6 3 | OFFSET = 2 4 | 5 | def initialize scores 6 | @scores = scores 7 | end 8 | 9 | def score 10 | return 0.0 if @scores.empty? 11 | score = @scores.inject{ |sum, score| sum + score }.to_f / @scores.size 12 | offset = [(@scores.size - Rank::OFFSET), 1 ].max 13 | score += Math.log offset, Rank::BASE 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/rank_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../lib/the_rotten_pirate' 2 | 3 | describe Rank do 4 | describe "initialize" do 5 | it "should take the scores hash as an initializer" do 6 | scores_array = [9.0, 9.0, 9.0, 9.0] 7 | rank = Rank.new scores_array 8 | scores = rank.instance_variable_get(:@scores) 9 | scores.should == scores_array 10 | rank.should_not be_nil 11 | end 12 | end 13 | 14 | describe "score" do 15 | it "should have a higher score if there are more votes (by at least OFFSET) but the average is the same" do 16 | scores_array = [9.0, 9.0, 9.0, 9.0] 17 | rank_higher = Rank.new scores_array 18 | rank_lower = Rank.new [9.0] 19 | rank_higher.score.should > rank_lower.score 20 | end 21 | 22 | it "should have a higher ranking if there are 10 9.125's than if there is 2 10s." do 23 | rank_higher = Rank.new [9.125,9.125,9.125,9.125,9.125,9.125,9.125,9.125] 24 | rank_lower = Rank.new [10.0, 10.0] 25 | rank_higher.score.should > rank_lower.score 26 | end 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /lib/fork_logger.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | 3 | class ForkLogger 4 | def initialize 5 | @loggers = [] 6 | FileUtils.mkdir_p('logs') 7 | timestamp = Time.now.strftime("%Y%m%d%H%M%S") 8 | @loggers << Logger.new("logs/#{timestamp}.log") 9 | @loggers << Logger.new(STDOUT) 10 | @prowl 11 | end 12 | 13 | def prowl_message event, description 14 | require 'prowl' 15 | 16 | prowl_config_file = File.join 'config', 'prowl.yml' 17 | if File.exists? prowl_config_file 18 | prowl_config = YAML.load File.open(prowl_config_file).read 19 | if prowl_config["active"] 20 | Prowl.add( 21 | :apikey => prowl_config["api_key"], 22 | :application => "The Rotten Pirate", 23 | :event => event, 24 | :description => description 25 | ) 26 | end 27 | end 28 | puts "Prowl notification sent '#{event}'" 29 | end 30 | 31 | def puts message 32 | @loggers.each do |logger| 33 | logger.info message 34 | end 35 | end 36 | 37 | def method_missing method, message 38 | @loggers.each do |logger| 39 | logger.send(method, message) 40 | end 41 | end 42 | end -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activesupport (3.1.1) 5 | multi_json (~> 1.0) 6 | awesome_print (0.4.0) 7 | chronic (0.6.5) 8 | coderay (1.1.0) 9 | diff-lcs (1.1.3) 10 | hpricot (0.8.6) 11 | i18n (0.6.0) 12 | method_source (0.8.2) 13 | multi_json (1.0.3) 14 | nokogiri (1.5.2) 15 | prowl (0.1.3) 16 | pry (0.9.12.4) 17 | coderay (~> 1.0) 18 | method_source (~> 0.8) 19 | slop (~> 3.4) 20 | rake (0.9.2.2) 21 | rspec (2.7.0) 22 | rspec-core (~> 2.7.0) 23 | rspec-expectations (~> 2.7.0) 24 | rspec-mocks (~> 2.7.0) 25 | rspec-core (2.7.1) 26 | rspec-expectations (2.7.0) 27 | diff-lcs (~> 1.1.2) 28 | rspec-mocks (2.7.0) 29 | sequel (3.29.0) 30 | slop (3.4.7) 31 | sqlite3 (1.3.4) 32 | torrent_api (0.2.7) 33 | hpricot 34 | nokogiri 35 | whenever (0.7.0) 36 | activesupport (>= 2.3.4) 37 | chronic (~> 0.6.3) 38 | 39 | PLATFORMS 40 | ruby 41 | 42 | DEPENDENCIES 43 | awesome_print 44 | i18n 45 | prowl 46 | pry 47 | rake 48 | rspec (~> 2.7) 49 | sequel (~> 3.2) 50 | sqlite3 51 | torrent_api (= 0.2.7) 52 | whenever 53 | -------------------------------------------------------------------------------- /lib/download.rb: -------------------------------------------------------------------------------- 1 | require 'sequel' 2 | require 'uri' 3 | 4 | class Download 5 | def self.connection 6 | Sequel.sqlite('db/downloads.sqlite') 7 | end 8 | 9 | def self.exists? name 10 | db = connection 11 | !!(db[:downloads].filter(:name => name).first) 12 | end 13 | 14 | def self.insert name 15 | db = connection 16 | db[:downloads].insert(:name => name) 17 | end 18 | 19 | def self.delete name 20 | connection[:downloads].filter(:name => name).delete 21 | end 22 | 23 | def self.clean_title movie_title 24 | movie_title.gsub(/-/, '') 25 | end 26 | 27 | def self.download_directory 28 | config = YAML.load(File.open(File.dirname(__FILE__) + '/../config/config.yml').read) 29 | download_dir = config["download_directory"] || 'tmp/torrent' 30 | dirs = download_dir.strip.split('/') 31 | # This converts any relative home directories to one ruby likes better (osx only probably) 32 | dirs.map { |dir| dir == "~" ? Dir.home : dir } 33 | end 34 | 35 | def self.torrent_from_url url 36 | require 'net/http' 37 | 38 | torrent_filename_match = url.match(/.*\/(.*)/) 39 | torrent_name = torrent_filename_match.nil? ? "tmp.torrent" : torrent_filename_match[1] 40 | torrent_domain, torrent_uri = url.gsub(/https?:\/\//, '').split('.se') 41 | torrent_uri = URI.escape(torrent_uri) 42 | torrent_domain += '.se' 43 | 44 | FileUtils.mkdir_p File.join(self.download_directory) 45 | filename = File.join(self.download_directory, torrent_name) 46 | Net::HTTP.start(torrent_domain) do |http| 47 | resp = http.get(torrent_uri) 48 | if resp.msg == 'OK' 49 | open(filename, "wb") { |file| file.write(resp.body) } 50 | else 51 | puts "HTTP Response not OK; was #{resp.msg}: #{resp.code}" 52 | nil 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/download_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../lib/the_rotten_pirate' 2 | 3 | describe Download do 4 | describe "exists?" do 5 | before do 6 | db = Download.connection 7 | @dataset = db[:downloads] 8 | @dataset.insert(:name => 'Movie Title') 9 | end 10 | 11 | after do 12 | @dataset.filter(:name => 'Movie Title').delete 13 | end 14 | 15 | it "should detect if the download exists in the database" do 16 | Download.exists?("Movie Title").should be_true 17 | end 18 | 19 | it "should return false if the download doesn't exist in the database" do 20 | Download.exists?("Movie Poopy").should be_false 21 | end 22 | end 23 | 24 | describe "insert" do 25 | after do 26 | Download.connection[:downloads].filter(:name => 'Movie Title').delete 27 | end 28 | 29 | it "should insert a download into the database" do 30 | Download.exists?("Movie Title").should be_false 31 | Download.insert("Movie Title").should be_true 32 | Download.exists?("Movie Title").should be_true 33 | end 34 | end 35 | 36 | describe "clean_title" do 37 | it "should remove any minus signs" do 38 | clean_title = Download.clean_title "Harry Potter and the Deathly Hallows - Part 2" 39 | clean_title.should == "Harry Potter and the Deathly Hallows Part 2" 40 | end 41 | end 42 | 43 | describe "torrent_from_url" do 44 | it "should be able to download movies with square brackets in it's name" do 45 | lambda { 46 | Download.torrent_from_url "http://torrents.thepiratebay.se/6581033/Bad_Teacher[2011]R5_Line_XviD-ExtraTorrentRG.6581033.TPB.torrent" 47 | }.should_not raise_error 48 | end 49 | end 50 | 51 | describe "delete" do 52 | it "should delete the download of the same name" do 53 | Download.insert("Movie Title").should be_true 54 | Download.exists?("Movie Title").should be_true 55 | Download.delete("Movie Title").should eq 1 56 | Download.delete("Movie Title").should eq 0 57 | end 58 | end 59 | end 60 | 61 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | task :default => :console 2 | 3 | desc "Loads up a console environment" 4 | task :console do 5 | exec "irb -I lib -r #{File.join(File.dirname(__FILE__), "lib/the_rotten_pirate")}" 6 | end 7 | 8 | desc "Runs download specs" 9 | task :spec do 10 | exec "rspec #{File.join(File.dirname(__FILE__), 'spec/download_spec.rb')}" 11 | end 12 | 13 | desc "Creates the initial database for storing movie downloads and missing config files" 14 | task :initialize do 15 | 16 | require "sequel" 17 | 18 | FileUtils.mkdir_p('db') 19 | DB = Sequel.sqlite('db/downloads.sqlite') 20 | 21 | begin 22 | DB.create_table :downloads do 23 | primary_key :id 24 | String :name 25 | end 26 | puts "Creating sqlite database..." 27 | rescue Exception 28 | puts "Database already exists" 29 | end 30 | 31 | config_templpate = File.join(File.dirname(__FILE__), "config", "config.template.yml") 32 | prowl_template = File.join(File.dirname(__FILE__), "config", "prowl.template.yml") 33 | target_config_file = File.join(File.dirname(__FILE__), "config", "config.yml") 34 | target_prowl_file = File.join(File.dirname(__FILE__), "config", "prowl.yml") 35 | unless File.exists? target_prowl_file 36 | FileUtils.copy(prowl_template, target_prowl_file) 37 | puts "Creating working prowl config file..." 38 | end 39 | 40 | unless File.exists? target_config_file 41 | FileUtils.copy(config_templpate, target_config_file) 42 | puts "Creating working rotten pirate config file..." 43 | end 44 | end 45 | 46 | desc "This task loads the config file, fetches the rotten tomatoes feed, and downloads torrent files." 47 | task :execute do 48 | require_relative 'lib/the_rotten_pirate' 49 | TheRottenPirate.execute 50 | end 51 | 52 | desc "This task loads a file (newline delimited) and downloads each of the movies." 53 | task :download_from_watch_file do 54 | require_relative 'lib/the_rotten_pirate' 55 | trp = TheRottenPirate.new 56 | filename = trp.config['watch_file'] 57 | if filename.nil? 58 | puts "There was no filename specified in the config file" 59 | return 60 | end 61 | 62 | File.open(filename, 'r').each do |movie_title| 63 | trp.initialize_download movie_title 64 | end 65 | 66 | File.open(filename, 'w') {} # truncate file 67 | end 68 | 69 | 70 | desc "This task will download a single movie" 71 | 72 | task :download, :movie do |t, args| 73 | movie = args[:movie] 74 | 75 | abort "You must specify a movie to download: rake download[\"The Lion King\"]" if movie.nil? 76 | 77 | require_relative 'lib/the_rotten_pirate.rb' 78 | trp = TheRottenPirate.new 79 | trp.initialize_download movie 80 | 81 | end 82 | -------------------------------------------------------------------------------- /spec/the_rotten_pirate_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../lib/the_rotten_pirate' 2 | 3 | describe TheRottenPirate do 4 | describe "#extract dvd info" do 5 | it "should extract some dvds given a text file with them" do 6 | text = File.open(File.join('spec', 'upcoming_dvds.txt'),'r').read 7 | dvds = TheRottenPirate.extract_new_dvds text 8 | dvds.first.should == { 9 | "MovieID"=>"770687943", 10 | "Title"=>"Harry Potter and the Deathly Hallows - Part 2", 11 | "URL"=>"http://www.rottentomatoes.com/m/harry_potter_and_the_deathly_hallows_part_2/", 12 | "NumTomatometerPercent"=>"96", 13 | "TomatometerRating"=>"FRESH", 14 | "CertifiedFresh"=>"1", 15 | "ModifiedDate"=>"", 16 | "ShowtimeURL"=>"", 17 | "Description"=>"Thrilling, powerfully acted, and visually dazzling, Deathly Hallows Part II brings the Harry Potter franchise to a satisfying -- and suitably magical -- conclusion."} 18 | end 19 | end 20 | 21 | describe "#fetch_new_dvds" do 22 | it "should grab the upcoming dvds text file and then call extract_new_dvds" do 23 | TheRottenPirate.should_receive(:extract_new_dvds) 24 | TheRottenPirate.new.fetch_new_dvds 25 | end 26 | end 27 | 28 | describe "#filter_out_non_certified_fresh" do 29 | it "should not show any non certified movies" do 30 | dvds = TheRottenPirate.new.filter_out_non_certified_fresh 31 | dvds.each { |dvd| dvd["CertifiedFresh"].should_not eq "0" } 32 | end 33 | end 34 | 35 | describe "#filter_out_already_downloaded" do 36 | before do 37 | Download.insert "The Strange Case Of Angelica" 38 | end 39 | 40 | after do 41 | Download.connection[:downloads].filter(:name => 'Movie Title').delete 42 | end 43 | 44 | it "should filter out already downloaded movies" do 45 | trp = TheRottenPirate.new 46 | trp.filter_out_already_downloaded 47 | dvds = trp.instance_variable_get(:@dvds) 48 | dvds.each { |dvd| dvd["Title"].should_not eq "The Strange Case Of Angelica" } 49 | end 50 | end 51 | 52 | describe "#filter_percentage" do 53 | it "should filter out anything lower than the parameter" do 54 | dvds = TheRottenPirate.new 55 | dvds.fetch_new_dvds 56 | before = dvds.instance_variable_get(:@dvds) 57 | filtered_dvds = dvds.filter_percentage(80) 58 | filtered_dvds.each { |dvd| dvd["NumTomatometerPercent"].to_i.should > 80 } 59 | after = dvds.instance_variable_get(:@dvds) 60 | 61 | before.size.should > after.size 62 | end 63 | 64 | it "should be chainable with non certified" do 65 | filter = TheRottenPirate.new 66 | filter.filter_percentage(80) 67 | eightieth_percentile = filter.instance_variable_get(:@dvds) 68 | 69 | filter.filter_out_non_certified_fresh 70 | certified_and_eighteith_percentile = filter.instance_variable_get(:@dvds) 71 | (eightieth_percentile - certified_and_eighteith_percentile).size.should == 4 72 | end 73 | 74 | end 75 | 76 | end 77 | 78 | -------------------------------------------------------------------------------- /spec/name_cleaner_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../lib/the_rotten_pirate' 2 | 3 | describe NameCleaner do 4 | describe "#inititalize" do 5 | it "should take a string and set it to it's raw_name" do 6 | name = NameCleaner.new "string" 7 | name.raw_name.should == "string" 8 | end 9 | end 10 | 11 | describe "#clean_punctuation" do 12 | it "should get rid of periods and parenthesis" do 13 | nc = NameCleaner.new("Submarine.2011.720p.BDRip.x264.AC3.dxva-HDLiTE") 14 | nc.clean_punctuation 15 | nc.clean_name.should == "Submarine 2011 720p BDRip x264 AC3 dxva HDLiTE" 16 | end 17 | end 18 | 19 | describe "#remove_release_year" do 20 | it "should delete it from the clean name" do 21 | nc = NameCleaner.new("String[2010]") 22 | nc.remove_release_year 23 | nc.clean_name.should == "String[]" 24 | end 25 | end 26 | 27 | describe "#remove_urls" do 28 | it "should delete it from the clean name" do 29 | nc = NameCleaner.new("String www.root.com www.example.com") 30 | nc.remove_urls 31 | nc.clean_name.should == "String " 32 | end 33 | end 34 | 35 | describe "#remove_release_groups" do 36 | it "should delete it from the clean name" do 37 | nc = NameCleaner.new("String MAXSPEED NOVA") 38 | nc.remove_release_groups 39 | nc.clean_name.should == "String " 40 | end 41 | end 42 | 43 | describe "#remove_video_types" do 44 | it "should delete it from the clean name" do 45 | nc = NameCleaner.new("String DVDRip BRRip") 46 | nc.remove_video_types 47 | nc.clean_name.should == "String " 48 | end 49 | end 50 | 51 | describe "the dynamically generated remove methods" do 52 | ["release_year", "video_types", "filetype", "release_groups", "urls"].each do |method| 53 | it "should respond to remove_#{method}" do 54 | nc = NameCleaner.new "doesn't matter" 55 | method = :"remove_#{method}" 56 | nc.should respond_to method 57 | end 58 | end 59 | end 60 | 61 | describe "#release_year" do 62 | it "should extract any four digit number" do 63 | NameCleaner.new("String[2010]").release_year.should == 2010 64 | end 65 | 66 | it "should only extract the year if it's an appropriate year (1950 - Next Year)" do 67 | NameCleaner.new("String[1337]").release_year.should == nil 68 | NameCleaner.new("String[1894]").release_year.should == nil 69 | NameCleaner.new("String[1895]").release_year.should == 1895 70 | NameCleaner.new("String[2050]").release_year.should == nil 71 | end 72 | 73 | it "should only extract the proper four digit number if there are two and one is a year" do 74 | NameCleaner.new("String[1337]2010").release_year.should == 2010 75 | end 76 | end 77 | 78 | describe "#video_types" do 79 | it "should detect if a movie is 720, 1080p, xvid, etc..." do 80 | NameCleaner.new("Submarine 2011 DVDRip Xvid UnKnOwN").video_types.should =~ [index_of("video_types", "dvdrip"), index_of("video_types", "xvid")] 81 | NameCleaner.new("Submarine.2011.720p.BDRip.x264.AC3.dxva-HDLiTE").video_types.should =~ [index_of("video_types", "720p"), index_of("video_types", "bdrip"), index_of("video_types", "x264"), index_of("video_types", "ac3")] 82 | end 83 | 84 | it "should not detect a stupid end of word with 'ts', but should detect a Theatrical Screening" do 85 | NameCleaner.new("Rise.of.the.Planet.of.the.Apes.2011.TS.XviD-NOVA.avi").video_types.should =~ [index_of("video_types", "TS"), index_of("video_types", "xvid")] 86 | NameCleaner.new("Rare.Exports.A.Christmas.Tale.2010.[UsaBit.com].avi").video_types.should =~ [] 87 | end 88 | end 89 | 90 | describe "#remove_whitespace" do 91 | it "should remove spaces on both sides" do 92 | nm = NameCleaner.new " hallllllelujah " 93 | nm.remove_whitespace 94 | nm.clean_name.should == "hallllllelujah" 95 | end 96 | 97 | it "should switch anything greater than one spaces to one spaces" do 98 | nm = NameCleaner.new "there was once many spaces between here" 99 | nm.remove_whitespace 100 | nm.clean_name.should == "there was once many spaces between here" 101 | end 102 | end 103 | 104 | describe "#clean" do 105 | it "should remove all of the junk. ALL OF IT!" do 106 | NameCleaner.new("Rise.of.the.Planet.of.the.Apes.2011.TS.XviD-NOVA.avi").clean.should == "Rise of the Planet of the Apes" 107 | NameCleaner.new("True Grit[2010]BRRip XviD-ExtraTorrentRG").clean.should == "True Grit" 108 | NameCleaner.new("Submarine.2011.720p.BDRip.x264.AC3.dxva-HDLiTE").clean.should == "Submarine" 109 | NameCleaner.new("The.Triplets.of Belleville.2011.TS.XviD-NOVA.avi").clean.should == "The Triplets of Belleville" 110 | end 111 | end 112 | 113 | describe "#filetype" do 114 | it "should detect the type of video that it is if it looks like a filename" do 115 | NameCleaner.new("Rise.of.the.Planet.of.the.Apes.2011.TS.XviD-NOVA.avi").filetype.should == 'avi' 116 | end 117 | end 118 | 119 | describe "#release_groups" do 120 | it "should detect release groups" do 121 | NameCleaner.new("Rise.of.the.Planet.of.the.Apes.2011.TS.XviD-NOVA.avi").release_groups.should == [index_of('release_groups', 'NOVA')] 122 | NameCleaner.new("Cars 2 (2011) DVDRip XviD-MAXSPEED").release_groups.should == [index_of('release_groups', 'MAXSPEED')] 123 | NameCleaner.new("Beginners.2010.LIMITED.BDRip.XviD-TARGET").release_groups.should == [index_of('release_groups', 'TARGET')] 124 | NameCleaner.new("Bridesmaids [2011] UNRATED DVDRip XviD-DUBBY").release_groups.should == [index_of('release_groups', 'DUBBY')] 125 | NameCleaner.new("Bridesmaids.2011.BRRip.XviD.Ac3.Feel-Free.avi.part").release_groups.should == [index_of('release_groups', 'Feel-Free')] 126 | NameCleaner.new("Captain America The First Avenger (2011) DVDRip XviD-MAXSPEED").release_groups.should == [index_of('release_groups', 'MAXSPEED')] 127 | end 128 | end 129 | 130 | describe "#urls" do 131 | it "should extract any and all urls" do 132 | NameCleaner.new("District 9 (2009) DVDRip XviD-MAXSPEED www.torrentzalive.com").urls.should == ['www.torrentzalive.com'] 133 | end 134 | 135 | it "shouldn't die if no URLs were found" do 136 | NameCleaner.new("District 9 (2009) DVDRip XviD-MAXSPEED").urls.should == [] 137 | end 138 | end 139 | end 140 | 141 | def index_of(type, string) 142 | method = :"#{type}_terms" 143 | terms = NameCleaner.send(method) 144 | terms.index { |term| term[:name] == string } 145 | end 146 | -------------------------------------------------------------------------------- /lib/the_rotten_pirate.rb: -------------------------------------------------------------------------------- 1 | require 'awesome_print' 2 | require 'torrent_api' 3 | require 'uri' 4 | require 'yaml' 5 | require_relative 'download' 6 | require_relative 'fork_logger' 7 | require_relative 'name_cleaner' 8 | require_relative 'rank' 9 | require_relative 'yaml_writer' 10 | 11 | class TheRottenPirate 12 | attr_reader :config 13 | 14 | def initialize 15 | @config = YAML.load(File.open(File.dirname(__FILE__) + '/../config/config.yml').read) 16 | @dvds = nil 17 | @l = ForkLogger.new 18 | end 19 | 20 | def initialize_download movie_title 21 | torrent_to_download, full_results = search_for_dvd movie_title 22 | if torrent_to_download.nil? 23 | puts "No results found for #{movie_title}" 24 | return 25 | end 26 | puts "Starting the download for #{movie_title}" 27 | if Download.torrent_from_url URI.escape(torrent_to_download[:link]) 28 | Download.insert torrent_to_download[:title] 29 | puts "Download successfully started." 30 | else 31 | exit("Download failed while starting.") 32 | end 33 | end 34 | 35 | def search_for_dvd(title, full_analysis=[]) 36 | @l.puts "*" * 80 37 | @l.puts "Searching for #{title}" 38 | @l.puts "*" * 80 39 | search = PirateBay::Search.new Download.clean_title(title) 40 | results = search.execute 41 | @l.puts "Found #{results.size} results from the pirate bay." 42 | 43 | return if results.empty? 44 | 45 | if @config["comments"]["analyze"] 46 | num_to_analyze = @config["comments"]["num_to_analyze"] 47 | minimum_seeds = @config["comments"]["minimum_seeds"] 48 | 49 | quality_level = @config["comments"]["quality"] == "low" ? :init : :full 50 | @l.puts "Processing #{[num_to_analyze, results.size].min} torrent pages for comments (as configured)." 51 | 52 | analysis_results = analyze_results results, num_to_analyze, quality_level, minimum_seeds 53 | analysis_results = analysis_results.sort_by { |r| -(r[:video][:rank]) } 54 | 55 | [{ :link => analysis_results.first[:link], :title => title }, analysis_results] 56 | else 57 | { :link => results.first.link, :title => title } 58 | end 59 | end 60 | 61 | 62 | def self.execute 63 | 64 | captain = TheRottenPirate.new 65 | output = captain.instance_variable_get(:@l) 66 | captain.gather_and_filter_dvds 67 | 68 | full_analysis_results = [] 69 | torrents_to_download = [] 70 | 71 | captain.summarize_process_to_output 72 | 73 | captain.dvds.each do |dvd| 74 | torrent_to_download, full_analysis_result = captain.search_for_dvd(dvd["Title"]) 75 | torrents_to_download << torrent_to_download unless torrent_to_download.nil? 76 | full_analysis_results << full_analysis_result unless full_analysis_result.nil? 77 | end 78 | 79 | YAMLWriter.new({ :full_analysis_results => full_analysis_results, :links_to_download => torrents_to_download }).write 80 | 81 | torrents_to_download.each do |download| 82 | output.puts "Starting the download for #{download[:title]}" 83 | if Download.torrent_from_url download[:link] 84 | Download.insert download[:title] 85 | output.puts "Download successfully started." 86 | else 87 | output.error "Download failed while starting." 88 | end 89 | end 90 | 91 | output.puts "Done!" 92 | output.puts "Downloaded a total of #{torrents_to_download.size} torrents" 93 | output.prowl_message "Downloaded #{torrents_to_download.size} movies", torrents_to_download.map{|m| m[:title] }.join(", ") 94 | end 95 | 96 | def analyze_results results, num_to_analyze, quality_level, minimum_seeds 97 | results = results[0,num_to_analyze].map do |result| 98 | url = "http://www.thepiratebay.se/torrent/#{result.id}/" 99 | 100 | if result.seeds < minimum_seeds 101 | @l.puts "Number of seeds are lower than the threshold - not querying for comments." 102 | construct_result_hash result, url 103 | else 104 | html = open(url).read 105 | details = PirateBay::Details.new html, quality_level 106 | @l.puts "Fetching comments results from #{url}" 107 | construct_result_hash result, url, details 108 | end 109 | end 110 | end 111 | 112 | def construct_result_hash result, url, details=nil 113 | if details.nil? 114 | video_average = 0 115 | video_sum = 0 116 | video_votes = 0 117 | video_rank = 0 118 | else 119 | video_average = details.video_quality_average 120 | video_sum = details.video_quality_score_sum 121 | video_votes = details.video_scores.size 122 | video_rank = Rank.new(details.video_scores).score 123 | end 124 | 125 | { 126 | :seeds => result.seeds, 127 | :size => result.size, 128 | :name => result.name, 129 | :video => 130 | { 131 | :average=> video_average, 132 | :sum => video_sum, 133 | :votes => video_votes, 134 | :rank => video_rank 135 | }, 136 | :url => url, 137 | :link => result.link 138 | } 139 | end 140 | 141 | def gather_and_filter_dvds 142 | @l.puts "Searching..." 143 | fetch_new_dvds 144 | 145 | @l.puts "Filtering..." 146 | filter_percentage @config["filter_out_less_than_percentage"] if @config["filter_out_less_than_percentage"] 147 | filter_out_non_certified_fresh if @config["filter_out_non_certified_fresh"] 148 | filter_out_already_downloaded if @config["filter_out_already_downloaded"] 149 | end 150 | 151 | def summarize_process_to_output 152 | @l.puts "*" * 80 153 | @l.puts "Attempting to download the following titles: " 154 | @l.puts dvds.map { |dvd| dvd["Title"] } 155 | @l.puts "*" * 80 156 | end 157 | 158 | def filter_out_already_downloaded 159 | @dvds = dvds.reject { |f| Download.exists? f["Title"] } 160 | end 161 | 162 | def filter_out_non_certified_fresh 163 | @dvds = dvds.select { |f| f["CertifiedFresh"] == "1" } 164 | end 165 | 166 | def filter_percentage threshold 167 | @dvds = dvds.select { |f| f["NumTomatometerPercent"].to_i >= threshold } 168 | end 169 | 170 | def dvds 171 | @dvds || fetch_new_dvds 172 | end 173 | 174 | def fetch_new_dvds 175 | require 'open-uri' 176 | text = open('http://www.rottentomatoes.com/syndication/tab/new_releases.txt').read 177 | @dvds = TheRottenPirate.extract_new_dvds text 178 | end 179 | 180 | def self.extract_new_dvds text 181 | headers = [] 182 | dvds = [] 183 | 184 | text.strip.split("\n").each_with_index do |row, row_index| 185 | dvd = {} unless row_index == 0 186 | row.split("\t").each_with_index do |field, header_index| 187 | field.chomp! 188 | if row_index == 0 189 | headers << field 190 | else 191 | dvd[headers[header_index]] = field 192 | end 193 | end 194 | dvds << dvd unless row_index == 0 195 | end 196 | dvds 197 | end 198 | end 199 | 200 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Rotten Pirate! 2 | (or: "STOP STEALING MY MOVIES YA DAMN KIDS!") 3 | 4 | ### Quick 'n' dirty 5 | 6 | - Fetches the [Rotten Tomatoes New DVDs feed](http://www.rottentomatoes.com/syndication/tab/new_releases.txt) 7 | - Filters the DVDs with *configurable* criterion 8 | - Only "Certified Fresh" movies 9 | - Only movies whose "TomatoMeter Percent" is greater than XX% 10 | - Movies not already downloaded through TRP 11 | - We then pass the movies to the [torrent_api](https://github.com/hjhart/torrent_api) gem. 12 | - The gem looks at comments left by users for ratings of audio and video 13 | - Performs an average of all ratings given 14 | - Selects the highest scoring torrent and 15 | - Then it downloads the appropriate torrents to your configured directory 16 | - If you set this directory up to be "watched" by your torrent program these will start automatically! 17 | - Prowl support to send push notifications to iOS! 18 | - Also, command line interface to download any single movie you want (e.g. rake download["The Big Lebowski"]) 19 | 20 | Pretty neat, yeah? 21 | 22 | ### Setup 23 | 24 | Clone the source and install proper gems 25 | 26 | git clone git@github.com:hjhart/the_rotten_pirate.git 27 | cd the_rotten_pirate 28 | 29 | At this point if RVM is installed you'll get prompted to trust this new .rvmrc file. I *highly* recommend you use RVM (for every project really). Allow it and it should create a new gemset for you. 30 | 31 | Now, after the proper ruby has been picked and you've got a clean gemset, let's get our gemset populated. 32 | 33 | gem install bundler --pre # You may need to install bundler with your fresh gemset – I prefer bundler 1.1 right now 34 | bundle 35 | 36 | Run this command to initialize your sqlite database and make working copies of the config files. 37 | 38 | rake initialize 39 | 40 | ### Configure stuff! 41 | 42 | Important configurations in config/config.yml 43 | 44 | * `["download_directory"]` 45 | 46 | Set your download_directory to a directory you want to drop the torrent files to. 47 | 48 | * `["filter_out_less_than_percentage"]` 49 | 50 | Change this to lowest percent (based on RTs TomatoMeter) that you want to download. 51 | 52 | * `["comments"]["quality"]` 53 | 54 | If this is turned on The Rotten Pirate will query the comments on The Pirate Bay for ratings. It will then calculate a score and choose the highest "quality" torrent for you to download. Takes considerably longer but is well worth the wait. 55 | 56 | * `["comments"]["num_of_results"]` 57 | 58 | The number of search results to query for comments. If there are 30 search results from pirate bay, but you only want to query the top 10 seeded torrents for comments – use this variable. 59 | 60 | * `["comments"]["minimum_seeds"]` 61 | 62 | Don't query for comments unless there are this many seeds on the torrent (there are hardly any comments for lower seeded torrents) 63 | 64 | * `["comments"]["watch_file"]` 65 | 66 | You can point this towards a new-line delimited file – it will download each movie that is listed in the file. (Really nice with the side-project [the_mobile_pirate](https://github.com/hjhart/the_mobile_pirate)) 67 | 68 | If you want to configure prowl notifications, see section below. 69 | 70 | ### Run the process! 71 | 72 | rake execute 73 | 74 | You'll see the search begin (this can take anywhere from 10 seconds to 5 minutes) and output will follow. 75 | 76 | ### Download a Single Movie 77 | 78 | rake download["The Big Lebowski"] 79 | 80 | This will run the search operation as configured – and download a single torrent file. 81 | 82 | 83 | ### Download an Array of Movies 84 | 85 | rake download_from_watch_file 86 | 87 | This will run against any text file that is new-line delimited. Parses through the file and downloads each movie. Really a nice to have with [the_mobile_pirate](https://github.com/hjhart/the_mobile_pirate). 88 | 89 | ### Run tests 90 | 91 | rspec spec 92 | 93 | So far this has been tested on ruby-1.9.2-p290 on OSX 10.6 94 | 95 | ### Automate with a cron job 96 | 97 | Run `crontab -e` and paste the following into there to start a new cronjob. It will run every 2 days at 12:30am. Make sure the change `cd ~/Sites/the_rotten_pirate` to the directory where your code lives. 98 | 99 | 30 0 1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31 * * /bin/bash -l -c 'cd ~/Sites/the_rotten_pirate && bundle exec rake execute --silent >> /dev/null 2>&1' 100 | 101 | ### Play with code 102 | 103 | rake console 104 | TheRottenPirate.execute 105 | 106 | ### The algorithm for scoring torrents 107 | 108 | Inside of The Pirate Bay results you'll see comments looking like: `"Great movie, A - 10, V - 10"` 109 | 110 | This means that the audio track is rated a 10, and the video the same. I'm paginating through every page of comments from the pirate bay and analyzing the comments for those sort of ratings. I then average them up. Since one vote of a 9 should be "scored" less than 5 votes of 9's, I'm adding a booster to scores that have a lot of votes. This boost is calculated on a logarithmic scale (So that 40 votes of 5 doesn't end up beating 5 votes of 9). The boost is then summed to the average rating to produce the total. 111 | 112 | When you turn on the `["comments"]["quality"]` setting in the configuration it will parse through the comments and try to find the highest video quality of all the videos. If you turn it off, it will just grab the search result with the highest seeds (for quick downloading) 113 | 114 | ### Prowl notifications 115 | 116 | - Config goes into config/prowl.yml 117 | - If this file doesn't exist yet run `rake initialize` once more to create it. 118 | - Fill out your api_key – this can be found at the [prowl api keys](https://www.prowlapp.com/api_settings.php) page. 119 | 120 | You'll receive prowl notifications at the end of every run with a rundown of what movies were added. 121 | 122 | ### Potential TODOS 123 | 124 | * Name parser that will take downloaded files and tidy them up! 125 | * Can we search for movies on Amazon/Netflix for instant streaming or purchase? 126 | * Add the ability to organize your files. So, drop the subtitles and the other movie files into another directory. 127 | * Download a single movie rake download "Revenge of the Nerds" 128 | * Generate a little summary of movies that were fetched 129 | * Synapsis 130 | * Link to trailer 131 | * Link to watch the movie 132 | * Show any screenshots of the movie if they have any 133 | * Links to download subtitles (if it's a foreign movie?) 134 | * Allow for import of top movies? 135 | * Add more of http://www.rottentomatoes.com/help_desk/syndication_txt.php these 136 | * Have a method of deleting movies (or at least wiping the database clean) 137 | * I'm getting some Timeout::Errors when I'm downloading too many torrents at once. Should we be catching those? 138 | * Figure out when the rotten tomatoes files update and configure the crontab to run at those times. (DVDS generally come out on Tuesdays, but in some cases they are done other days (e.g. Harry Potter, Twilight, etc...) 139 | * Don't bother to download if the quality isn't up to a configurable rating. 140 | * Database file needs to be not a relative directory. Running specs outside of the root folder fails. 141 | 142 | ### Support 143 | 144 | Tested on ruby 1.9.2-p290 on Mac OSX. 145 | Also tested on ruby 1.8.7-p330 on Mac OSX. 146 | 147 | 148 | ![Counter](http://hjhart.dyndns.org:3003/count.jpg "Counter") 149 | -------------------------------------------------------------------------------- /lib/name_cleaner.rb: -------------------------------------------------------------------------------- 1 | class NameCleaner 2 | attr_accessor :raw_name, :clean_name 3 | 4 | def initialize(raw_name) 5 | @raw_name = raw_name 6 | @clean_name = raw_name 7 | end 8 | 9 | def clean_punctuation 10 | punctuation_to_remove = /[\.\[\]\(\)-]/ 11 | @clean_name = @clean_name.gsub(punctuation_to_remove, " ") 12 | end 13 | 14 | def remove_release_year 15 | @clean_name = @clean_name.gsub(release_year.to_s, "") 16 | end 17 | 18 | ["filetype", "urls"].each do |method| 19 | define_method "remove_#{method}" do 20 | extractions = Array(self.send(method)) 21 | extractions.each do |extraction| 22 | @clean_name = @clean_name.gsub(extraction, "") 23 | end 24 | end 25 | end 26 | 27 | # def remove_filetype 28 | # extractions = Array(filetype) 29 | # extractions.each do |extraction| 30 | # @clean_name = @clean_name.gsub(extraction, "") 31 | # end 32 | # end 33 | # 34 | # def remove_urls 35 | # extractions = Array(urls) 36 | # extractions.each do |extraction| 37 | # @clean_name = @clean_name.gsub(extraction, "") 38 | # end 39 | # end 40 | 41 | ["video_types", "release_groups"].each do |method| 42 | remove_method_name = :"remove_#{method}" 43 | terms_method_name = :"#{method}_terms" 44 | 45 | define_method remove_method_name do 46 | extraction_indices = Array(self.send(method)) 47 | extraction_indices.each do |extraction_index| 48 | term = NameCleaner.send(terms_method_name).at(extraction_index) 49 | @clean_name = @clean_name.gsub(Regexp.new(term[:name], term[:modifier]), "") 50 | end 51 | end 52 | end 53 | 54 | def remove_whitespace 55 | @clean_name = @clean_name.gsub(/\s+/, " ") 56 | @clean_name = @clean_name.strip 57 | end 58 | 59 | def clean 60 | remove_release_year 61 | remove_video_types 62 | remove_filetype 63 | remove_release_groups 64 | remove_urls 65 | clean_punctuation 66 | remove_whitespace 67 | end 68 | 69 | def release_year 70 | year_matches = @raw_name.scan(/(\d{4})/).flatten 71 | two_years_from_now= Time.now.strftime("%Y").to_i + 2 72 | 73 | year_matches = year_matches.map { |year_match| 74 | year = year_match.to_i 75 | }.select { |year_match| 76 | (1895..two_years_from_now).include? year_match 77 | } 78 | 79 | year_matches.first if year_matches.size == 1 80 | end 81 | 82 | def self.video_types_terms 83 | [ 84 | { :name => "dvdrip", :modifier => Regexp::IGNORECASE }, 85 | { :name => "xvid", :modifier => Regexp::IGNORECASE }, 86 | { :name => "720p", :modifier => Regexp::IGNORECASE }, 87 | { :name => "x264", :modifier => Regexp::IGNORECASE }, 88 | { :name => "brrip", :modifier => Regexp::IGNORECASE }, 89 | { :name => "bdrip", :modifier => Regexp::IGNORECASE }, 90 | { :name => "Widescreen", :modifier => Regexp::IGNORECASE }, 91 | { :name => "1080p", :modifier => Regexp::IGNORECASE }, 92 | { :name => "aac", :modifier => Regexp::IGNORECASE }, 93 | { :name => "5.1", :modifier => Regexp::IGNORECASE }, 94 | { :name => "ac3", :modifier => Regexp::IGNORECASE }, 95 | { :name => "TS", :modifier => nil }, 96 | { :name => "scr", :modifier => Regexp::IGNORECASE }, 97 | { :name => "rerip", :modifier => Regexp::IGNORECASE } 98 | ] 99 | end 100 | 101 | def video_types 102 | video_type = [] 103 | 104 | NameCleaner.video_types_terms.each_with_index do |term, index| 105 | regexp = term[:name] 106 | modifier = term[:modifier] 107 | video_type << index if @raw_name.match Regexp.new regexp, modifier # case insensitive 108 | end 109 | 110 | video_type 111 | end 112 | 113 | def self.release_groups_terms 114 | [ 115 | { :name => "NOVA", :modifier => nil }, 116 | { :name => "TWiZTED", :modifier => nil }, 117 | { :name => "Stealthmaster", :modifier => nil }, 118 | { :name => "DUBBY", :modifier => nil }, 119 | { :name => "DoNE", :modifier => nil }, 120 | { :name => "TARGET", :modifier => nil }, 121 | { :name => "Feel-Free", :modifier => nil }, 122 | { :name => "MAXSPEED", :modifier => nil }, 123 | { :name => "1337x", :modifier => nil }, 124 | { :name => "Dita496", :modifier => nil }, 125 | { :name => "AbSurdiTy", :modifier => nil }, 126 | { :name => "dxva", :modifier => nil }, 127 | { :name => "V3nDetta", :modifier => nil }, 128 | { :name => "ExtraTorrentRG", :modifier => nil }, 129 | { :name => "GHZ", :modifier => nil }, 130 | { :name => "AMIABLE", :modifier => nil }, 131 | { :name => "honchorella", :modifier => nil }, 132 | { :name => "MRShanku", :modifier => nil }, 133 | { :name => "Silver RG", :modifier => nil }, 134 | { :name => "ArtSubs", :modifier => nil }, 135 | { :name => "HORiZON", :modifier => nil }, 136 | { :name => "ZJM", :modifier => nil }, 137 | { :name => "PSiG", :modifier => nil }, 138 | { :name => "UsaBit.com", :modifier => nil }, 139 | { :name => "STB", :modifier => nil }, 140 | { :name => "iLG", :modifier => nil }, 141 | { :name => "UnKnOwN", :modifier => nil }, 142 | { :name => "HDLiTE", :modifier => nil }, 143 | { :name => "LPD", :modifier => nil }, 144 | { :name => "Hiest-1337x", :modifier => nil }, 145 | { :name => "aLeX", :modifier => nil }, 146 | { :name => "aXXo", :modifier => nil }, 147 | { :name => "NLT", :modifier => nil }, 148 | { :name => "NeDiVx", :modifier => nil }, 149 | { :name => "PSYCHD", :modifier => nil }, 150 | { :name => "N3WS", :modifier => nil }, 151 | ] 152 | end 153 | 154 | def release_groups 155 | release_groups = [] 156 | 157 | NameCleaner.release_groups_terms.each_with_index do |group, index| 158 | regexp = group[:name] 159 | modifier = group[:modifier] 160 | release_groups << index if @raw_name.match Regexp.new regexp #, modifier 161 | end 162 | 163 | release_groups 164 | end 165 | 166 | def filetype 167 | filetypes = ["avi", "mp4"] 168 | filetypes.find do |filetype| 169 | regexp = Regexp.new "\.#{filetype}" 170 | @raw_name.match regexp 171 | end 172 | end 173 | 174 | def urls 175 | url_regex = /((https?:\/\/)?(www.)\w+\.(\w+\.)?\w{2,3})/ 176 | urls = @raw_name.scan(url_regex) 177 | urls.map { |url| url[0] } 178 | end 179 | end 180 | 181 | # A.Better.Life.LIMITED.DVDRip.XviD-TWiZTED 182 | # Aladdin[1992]DvDrip[Eng]-Stealthmaster.avi 183 | # Attack.The.Block.DVDRip.XviD-DoNE 184 | # Austin Powers International Man of Mystery 1997.720p.x264.BRRip.GokU61 185 | # Beginners.2010.LIMITED.BDRip.XviD-TARGET 186 | # Bridesmaids [2011] UNRATED DVDRip XviD-DUBBY 187 | # Bridesmaids.2011.BRRip.XviD.Ac3.Feel-Free.avi.part 188 | # Captain America The First Avenger (2011) DVDRip XviD-MAXSPEED 189 | # Cars 2 (2011) DVDRip XviD-MAXSPEED 190 | # District 9 (2009) DVDRip XviD-MAXSPEED www.torentz.3xforum.ro.avi 191 | # DodgeBall [2004] [DVDRip XviD] [1337x]-Dita496 192 | # Donnie.Darko.Directors.Cut.DVDRip.Xvid.2001-tots.avi 193 | # Everything.Must.Go.2010.BRRiP.XviD.AbSurdiTy 194 | # Fast Five[2011]BDRip XviD-ExtraTorrentRG.avi 195 | # Finding Nemo (2003) Widescreen DVDrip V3nDetta.avi 196 | # GLADIATOR[2000]DvDrip-GHZ 197 | # Hall Pass 2011 BRRiP XviD AbSurdiTy 198 | # Hanna.2011.DVDRip.XviD-AMIABLE 199 | # Harry Potter And The Deathly Hallows Pt1 2010 BRRip 1080p x264 AAC - honchorella (Kingdom Release) 200 | # Harry Potter and the Deathly Hallows Part 1[2010]DVDRip XviD-ExtraTorrentRG 201 | # Harry Potter and the Deathly Hallows Part 2 720p BRRip AAC 5.1- MRShanku - Silver RG 202 | # Incendies.2010.DVDRip.XviD.AC3.HORiZON-ArtSubs 203 | # Kung Fu Panda 2 (2011) DVDSCR XviD-ZJM.avi 204 | # Melancholia.2011.DVDRiP.XViD-PSiG 205 | # Pirates of the Caribbean On Stranger Tides (2011) DVDRip XviD-MAXSPEED 206 | # Rare.Exports.A.Christmas.Tale.2010.DVDRIP.XVID-STB.[UsaBit.com].avi 207 | # The.Triplets.of Belleville..2011.TS.XviD-NOVA.avi 208 | # Somewhere.2010.DVDRip.XviD-iLG 209 | # Submarine 2011 DVDRip Xvid UnKnOwN 210 | # Submarine.2011.720p.BDRip.x264.AC3.dxva-HDLiTE 211 | # Super 8[2011]DVDScr XviD-ExtraTorrentRG 212 | # Tabloid.2010.LiMiTED.DVDRip.XviD-LPD 213 | # Team America.avi 214 | # The Departed (2006) 215 | # The Guard (2011) DivX [Hiest-1337x].avi.part 216 | # The Tree of Life 2011 (Eng) 217 | # The.Great.Buck.Howard.2008.LiMiTED.DVDRip.XviD-HNR.[www.FilmsBT.com] 218 | # The.Triplets.of Belleville.2003.DVDRip-aLeX 219 | # Transsiberian[2008]DvDrip-aXXo 220 | # True Grit (2011) SCR NL Sub NLT-Release 221 | # True Grit[2010]BRRip XviD-ExtraTorrentRG 222 | # Win.Win.2011.ENG.HDRip.avi 223 | # Winnie.the.Pooh.RERIP.DVDRip.XviD-NeDiVx 224 | # X-Men First Class[2011]BRRip XviD-ExtraTorrentRG 225 | # [ UsaBit.com ] - Rare.Exports.A.Christmas.Tale.2010.LIMITED.BDRip.XviD-PSYCHD 226 | # [ www.Torrenting.com ] - A.Better.Tomorrow.2010.DVDRip.XviD-N3WS 227 | # [ www.Torrenting.com ] - Beats.Rhymes.and.Life.The.Travels.of.a.Tribe.Called.Quest.2011.LIMITED.DOCU.BDRip.XviD-PSYCHD 228 | # brian.regan.i.walked.on.the.moon.2004.xvid 229 | -------------------------------------------------------------------------------- /spec/upcoming_dvds.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | MovieID Title URL NumTomatometerPercent TomatometerRating CertifiedFresh ModifiedDate ShowtimeURL Description 22 | 770687943 Harry Potter and the Deathly Hallows - Part 2 http://www.rottentomatoes.com/m/harry_potter_and_the_deathly_hallows_part_2/ 96 FRESH 1 Thrilling, powerfully acted, and visually dazzling, Deathly Hallows Part II brings the Harry Potter franchise to a satisfying -- and suitably magical -- conclusion. 23 | 16403 A Better Tomorrow http://www.rottentomatoes.com/m/better_tomorrow/ 93 FRESH 0 John Woo established himself as one of Hong Kong's premiere action directors with this ultra-hip, ultra-violent action classic. The film centers around the complex relationship between two brothers: Sung Tse-kit (Leslie Cheung) is a recent graduate... 24 | 771197495 Incendies http://www.rottentomatoes.com/m/incendies/ 92 FRESH 1 It's messy, overlong, and a touch melodramatic, but those flaws pale before Incendies' impressive acting and devastating emotional impact. 25 | 771208471 Tabloid http://www.rottentomatoes.com/m/tabloid_2010/ 92 FRESH 1 It's far from his most thought-provoking work, but Tabloid finds Errol Morris as smart, spirited, and engaging as ever. 26 | 771041148 Winnie the Pooh http://www.rottentomatoes.com/m/winnie_the_pooh_2011/ 91 FRESH 1 Short, nostalgic, and gently whimsical, Winnie the Pooh offers young audiences -- and their parents -- a sweetly traditional family treat. 27 | 771224156 Beats, Rhymes and Life: The Travels of A Tribe Called Quest http://www.rottentomatoes.com/m/beats_rhymes_and_life_the_travels_of_a_tribe_called_quest/ 91 FRESH 1 This documentary focuses less on the music and more on the personality clashes and in-group tensions to great, compelling effect. 28 | 771200719 Rare Exports: A Christmas Tale http://www.rottentomatoes.com/m/rare_exports/ 90 FRESH 1 It's the eve of Christmas in northern Finland, and an 'archeological' dig has just unearthed the real Santa Claus. But this particular Santa isn't the one you want coming to town. When the local children begin mysteriously disappearing, young Pietari... 29 | 771203801 Attack the Block http://www.rottentomatoes.com/m/attack_the_block/ 90 FRESH 1 A thrilling sci-fi yarn whose distinct British flavor supplies energy, wit, and style to burn. 30 | 771039065 Bridesmaids http://www.rottentomatoes.com/m/bridesmaids_2011/ 90 FRESH 1 A marriage of genuine characters, gross out gags, and pathos, Bridesmaids is a female-driven comedy that refuses to be boxed in as Kristen Wiig emerges as a real star. 31 | 702035972 I'm a Cyborg, But That's OK (Saibogujiman kwenchana) http://www.rottentomatoes.com/m/im_a_cyborg_but_thats_ok/ 90 FRESH 0 Young-goon is admitted to a mental institution. Believing herself a cyborg, she charges herself with a transistor radio. Il-soon, a fellow inmate, steals the other inmates' personality traits and believes he is fading and will one day turn into a... 32 | 771041145 X-Men: First Class http://www.rottentomatoes.com/m/x_men_first_class/ 87 FRESH 1 With a strong script, stylish direction, and powerful performances from its well-rounded cast, X-Men: First Class is a welcome return to form for the franchise. 33 | 771224186 Buck http://www.rottentomatoes.com/m/buck/ 87 FRESH 1 "Your horse is a mirror to your soul, and sometimes you may not like what you see. Sometimes, you will." So says Buck Brannaman, a true American cowboy and sage on horseback who travels the country for nine grueling months a year helping horses with... 34 | 770875099 Submarine http://www.rottentomatoes.com/m/submarine-2010/ 86 FRESH 1 Funny, stylish, and ringing with adolescent truth, Submarine marks Richard Ayoade as a talent to watch. 35 | 771225867 Terri http://www.rottentomatoes.com/m/terri_2011/ 86 FRESH 1 A hit at the Sundance 2011 Film Festival, Terri is a moving and often funny film about the relationship between Terri, an oversized teen misfit, and the garrulous but well-meaning vice principal (John C. Reilly) who takes an interest in him. Terri is... 36 | 771182967 A Better Life http://www.rottentomatoes.com/m/a_better_life/ 86 FRESH 1 From the director of About a Boy comes A Better Life - a touching, poignant, multi-generational story about a father's love and the lengths a parent will go to give his child the opportunities he never had. -- (C) Summit 37 | 771205009 The Strange Case Of Angelica http://www.rottentomatoes.com/m/the_strange_case_of_angelica_2010/ 84 FRESH 0 The new film from master filmmaker Manoel de Oliveira, The Strange Case of Angelica is a magical tale about a young photographer who falls madly in love with a woman he can never have, except in his dreams. Late one night, Isaac is summoned by a... 38 | 771205000 The Princess Of Montpensier http://www.rottentomatoes.com/m/the_princess_of_montpensier/ 84 FRESH 1 France, 1562. Against a background of the savage Catholic/Protestant wars, Marie de Mézières (Mélanie Thierry), a beautiful young aristocrat, and the rakish Henri de Guise (Gaspard Ulliel), fall in love, but Marie's father has promised her hand in... 39 | 770793166 The Tree of Life http://www.rottentomatoes.com/m/the_tree_of_life_2011/ 84 FRESH 1 Terrence Malick's singularly deliberate style may prove unrewarding for some, but for patient viewers, Tree of Life is an emotional as well as visual treat. 40 | 771225882 Life in a Day http://www.rottentomatoes.com/m/life_in_a_day_2011/ 81 FRESH 1 The 24th July 2010.... 80,000 Lives.... 4,500 Hours of Footage... 2 Award winning Filmmakers..... Now one incredible motion picture event. What began life as a startling cinematic experiment becomes the must see movie experience of the Summer.... 41 | 771208507 The Last Circus http://www.rottentomatoes.com/m/balada_triste_de_trompeta/ 81 FRESH 0 1937, Spain is in the midst of the brutal Spanish Civil War. A "Happy" circus clown is interrupted mid-performance and forcibly recruited by a militia. Still in his costume, he is handed a machete and led into battle against National soldiers, where... 42 | 771236491 Conan O'Brien Can't Stop http://www.rottentomatoes.com/m/conan_obrien_cant_stop_2011/ 79 FRESH 1 After a much-publicized departure from hosting NBC's Tonight Show - and the severing of a 22-year relationship with the network - O'Brien hit the road with a 32-city music-and-comedy show to exercise his performing chops and exorcise a few demons.... 43 | 770739679 Captain America: The First Avenger http://www.rottentomatoes.com/m/captain-america/ 79 FRESH 1 With plenty of pulpy action, a pleasantly retro vibe, and a handful of fine performances, Captain America is solidly old-fashioned blockbuster entertainment. 44 | 771203531 Crazy, Stupid, Love. http://www.rottentomatoes.com/m/crazy_stupid_love/ 78 FRESH 1 It never lives up to the first part of its title, but Crazy, Stupid, Love's unabashed sweetness -- and its terrifically talented cast -- more than make up for its flaws. 45 | 771201465 Chalet Girl http://www.rottentomatoes.com/m/chalet_girl/ 78 FRESH 0 Chalet Girl is light comedic fun geared for teenage girls, featuring a charming performance from Felicity Jones. 46 | 770873459 Fast Five http://www.rottentomatoes.com/m/fast_five/ 78 FRESH 1 Sleek, loud, and over the top, Fast Five proudly embraces its brainless action thrills. 47 | 770783549 Thor http://www.rottentomatoes.com/m/thor/ 77 FRESH 1 A dazzling blockbuster that tempers its sweeping scope with wit, humor, and human drama, Thor is mighty Marvel entertainment. 48 | 771205671 Everything Must Go http://www.rottentomatoes.com/m/everything_must_go/ 74 FRESH 1 It may not improve on the Raymond Carver short story that inspired it, but Everything Must Go resists cliche and boasts a pair of magnetic performances from the perfectly cast Ferrell and Wallace. 49 | 771091481 Hanna http://www.rottentomatoes.com/m/hanna/ 71 FRESH 1 Fantastic acting and crisply choreographed action sequences propel this unique, cool take on the revenge thriller. 50 | 771224804 Shaolin http://www.rottentomatoes.com/m/shaolin/ 71 FRESH 0 As feuding warlords fight to expand their power, the noble monks of the Shaolin Temple clean up the mess left behind, tending to the injured while trying their best to protect the poor and weak. General Hou (Andy Lau) has caused much of this mess... 51 | 771208626 Beautiful Boy http://www.rottentomatoes.com/m/beautiful_boy/ 70 FRESH 0 Bill (Michael Sheen) and Kate (Maria Bello) hopelessly try to find some hint of an explanation after finding out that their only son committed a mass shooting at his university before taking his own life. They struggle numbly through the funeral, the... 52 | 771041149 Horrible Bosses http://www.rottentomatoes.com/m/horrible_bosses/ 69 FRESH 1 It's nasty, uneven, and far from original, but thanks to a smartly assembled cast that makes the most of a solid premise, Horrible Bosses works. 53 | 771036678 African Cats http://www.rottentomatoes.com/m/african_cats/ 69 FRESH 0 It isn't quite as majestic as its subjects, but African Cats boasts enough astounding footage -- and a big enough heart -- to keep things entertaining. 54 | 771204250 Water for Elephants http://www.rottentomatoes.com/m/water_for_elephants/ 61 FRESH 0 It's a tale tastefully told and beautifully filmed, but Water for Elephants suffers from a pronounced lack of chemistry between its leads. 55 | 771217942 We Are The Night http://www.rottentomatoes.com/m/we_are_the_night/ 60 FRESH 0 Dennis Gansel, responsible for the hit The Wave, explores night time in Berlin to find a sect of seductive lady vampires that hide out in the city's alternative clubs and enjoy the luxury and pleasures their attained immortality provides them. A... 56 | 771034093 Red State http://www.rottentomatoes.com/m/red_state/ 59 ROTTEN 0 Red State is an audacious and brash affair that ultimately fails to provide competent scares or thrills. 57 | 770807411 Scream 4 http://www.rottentomatoes.com/m/scream-4/ 58 ROTTEN 0 The franchise is showing its age, but Scream 4 is undeniably an improvement over its predecessor, with just enough meta humor and clever kills. 58 | 770814817 Hesher http://www.rottentomatoes.com/m/hesher/ 54 ROTTEN 0 It has a dark sense of humor and a refreshing lack of sentimentality, but like its title character, Hesher isn't really interested in going anywhere. 59 | 771030879 Bad Teacher http://www.rottentomatoes.com/m/bad_teacher/ 44 ROTTEN 0 In spite of a promising concept and a charmingly brazen performance from Cameron Diaz, Bad Teacher is never as funny as it should be. 60 | 770816610 A Little Help http://www.rottentomatoes.com/m/a-little-help/ 44 ROTTEN 0 It's a movie for everyone whose life has been thrown off-course, out of whack, or simply not turned out the way they planned it. In other words, it's a movie for everyone, period. Set in suburban Long Island in the summer of 2002, with the psychic... 61 | 770805427 Cars 2 http://www.rottentomatoes.com/m/cars_2/ 38 ROTTEN 0 Cars 2 is as visually appealing as any other Pixar production, but all that dazzle can't disguise the rusty storytelling under the hood. 62 | 771202906 Monte Carlo http://www.rottentomatoes.com/m/monte_carlo_2011/ 38 ROTTEN 0 Although it has its charming moments, Monte Carlo is mostly silly, predictable stuff that never pushes beyond the boundaries of formula. 63 | 771223136 Transformers: Dark of the Moon http://www.rottentomatoes.com/m/transformers_dark_of_the_moon/ 35 ROTTEN 0 Its special effects -- and 3D shots -- are undeniably impressive, but they aren't enough to fill up its loud, bloated running time, or mask its thin, indifferent script. 64 | 770855546 Pirates of the Caribbean: On Stranger Tides http://www.rottentomatoes.com/m/pirates_of_the_caribbean_on_stranger_tides/ 33 ROTTEN 0 It's shorter and leaner than the previous sequel, but this Pirates runs aground on a disjointed plot and a non-stop barrage of noisy action sequences. 65 | 770805051 The Tempest http://www.rottentomatoes.com/m/tempest/ 30 ROTTEN 0 Director Julie Taymor's gender-swapping of roles and some frenzied special effects can't quite disguise an otherwise stagey, uninspired take on Shakespeare's classic. 66 | 770677993 Green Lantern http://www.rottentomatoes.com/m/green_lantern/ 27 ROTTEN 0 Noisy, overproduced, and thinly written, Green Lantern squanders an impressive budget and decades of comics mythology. 67 | 771041150 The Change-Up http://www.rottentomatoes.com/m/the_change_up/ 24 ROTTEN 0 There's a certain amount of fun to be had from watching Bateman and Reynolds play against type, but it isn't enough to carry The Change-Up through its crude humor and formulaic plot. 68 | 771205663 Snow Flower And The Secret Fan http://www.rottentomatoes.com/m/snow_flower_and_the_secret_fan/ 20 ROTTEN 0 In 19th-century China, seven year old girls Snow Flower and Lily are matched as laotong - or "old sames" - bound together for eternity. Isolated by their families, they furtively communicate by taking turns writing in a secret language, nu shu,... 69 | 771229930 Judy Moody and the NOT Bummer Summer http://www.rottentomatoes.com/m/judy_moody_and_the_not_bummer_summer/ 19 ROTTEN 0 Entertaining for some very young viewers, but for those with normal attention spans, Judy Moody is loud hyperactive overload. 70 | 770805419 Zookeeper http://www.rottentomatoes.com/m/zookeeper/ 14 ROTTEN 0 Zookeeper smothers Kevin James's with a sodden script and a surfeit of jokes inappropriate for the young viewers who would be intrigued by its juvenile storyline. 71 | 771033843 Atlas Shrugged Part I http://www.rottentomatoes.com/m/atlas_shrugged_part_i/ 12 ROTTEN 0 Passionate ideologues may find it compelling, but most filmgoers will find this low-budget adaptation of the Ayn Rand bestseller decidedly lacking. 72 | --------------------------------------------------------------------------------