From b05ca78e967ddfce31422fd29ec7c3b60fa4e0b3 Mon Sep 17 00:00:00 2001 From: Tim Galeckas Date: Tue, 18 Mar 2014 06:25:46 -0500 Subject: [PATCH 1/7] whitespace deletions --- lib/AtomicTV/atomic_parsley_tagger.rb | 38 +++++++++--------- lib/AtomicTV/episode_metadata.rb | 58 +++++++++++++-------------- lib/AtomicTV/filename_parser.rb | 18 ++++----- lib/AtomicTV/tvdb_episode.rb | 22 +++++----- 4 files changed, 68 insertions(+), 68 deletions(-) diff --git a/lib/AtomicTV/atomic_parsley_tagger.rb b/lib/AtomicTV/atomic_parsley_tagger.rb index b9a7237..ad47b70 100644 --- a/lib/AtomicTV/atomic_parsley_tagger.rb +++ b/lib/AtomicTV/atomic_parsley_tagger.rb @@ -1,50 +1,50 @@ module AtomicTV class AtomicParsleyTagger - + class AtomicParsleyUnavailable < ::AtomicTV::AtomicTVError def human_message 'AtomicParsley is not installed or could not be found. Try checking your PATH.' end end - + class FileNotFound < ::AtomicTV::AtomicTVError def initialize(file_path) @file_path = file_path end - + attr_reader :file_path - + def human_message "File not found: #{file_path}" end end - + class TaggingError < ::AtomicTV::AtomicTVError def initialize(command) @command = command end - + attr_reader :command - + def human_message "A tagging error occured: #{command}." end - + end - + def self.executable path = Pathname.new(`which AtomicParsley`.chomp) raise AtomicParsleyUnavailable unless path.executable? path end - + def initialize(file_path, metadata) @file_path = file_path raise FileNotFound.new(file_path) unless file_path.exist? - + @metadata = metadata end - + def cast_metadata format_names = lambda {|name| {'name' => name}} { @@ -53,7 +53,7 @@ def cast_metadata 'screenwriters' => metadata.writers.map(&format_names) }.to_plist end - + def run options = { 'stik' => metadata.media_type, @@ -71,7 +71,7 @@ def run 'tracknum' => metadata.track_number, 'year' => metadata.air_date } - + metadata.with_loaded_posters do command = %Q{#{self.class.executable} } command << %Q{"#{file_path}" } @@ -81,19 +81,19 @@ def run command << %Q{--artwork #{poster.path} } end command << options.map {|option, value| %Q{--#{option} "#{escape_double_quotes(value)}"}}.join(' ') - + `#{command}` raise TaggingError.new(command) unless $?.success? end end - + private - + attr_reader :file_path, :metadata - + def escape_double_quotes(str) str.to_s.gsub('"', '\"') end - + end end diff --git a/lib/AtomicTV/episode_metadata.rb b/lib/AtomicTV/episode_metadata.rb index 3d910d2..59d93ca 100644 --- a/lib/AtomicTV/episode_metadata.rb +++ b/lib/AtomicTV/episode_metadata.rb @@ -1,87 +1,87 @@ module AtomicTV class EpisodeMetadata - + ArtworkBaseURL = "http://thetvdb.com/banners/" - + def initialize(series, episode) @series, @episode = series, episode end - + attr_reader :series, :episode - + def media_type 'TV Show' end - + def artist series.name end - + def title episode.name end - + def album "#{series.name}, Season #{episode.season_number}" end - + def genre series.genres.first end - + def description episode.overview.to_s[0,255].gsub(/\.(.*)\Z/, '.') end - + def long_description episode.overview.to_s end - + def tv_network series.network end - + def tv_show_name series.name end - + def tv_episode "#{episode.season_number}#{episode.number.to_s.rjust(2, '0')}" end - + def tv_season_number episode.season_number end - + def tv_episode_number episode.number end - + def track_number episode.number end - + def air_date episode.air_date && episode.air_date.to_s + 'T00:00:00Z' end - + def actors (Array(series.actors).map {|a| a.name} + episode.guest_stars).uniq end - + def directors parse_names(episode.director) end - + def writers parse_names(episode.writer) end - + attr_reader :posters - + def with_loaded_posters temporary_directory = Dir.mktmpdir - + season_posters = series.season_posters(episode.season_number, 'en') season_posters = series.posters('en') if season_posters.empty? @posters = season_posters.map do |poster| @@ -93,21 +93,21 @@ def with_loaded_posters end yield - + ensure FileUtils.rm_rf(temporary_directory) @posters = nil end - + private - + def parse_names(str) return [] if str.nil? - + str.gsub!(/\A\|/, '') - str.gsub!(/\|\Z/, '') + str.gsub!(/\|\Z/, '') str.split(/\|+/) end - + end end diff --git a/lib/AtomicTV/filename_parser.rb b/lib/AtomicTV/filename_parser.rb index d9a94e6..8802fac 100644 --- a/lib/AtomicTV/filename_parser.rb +++ b/lib/AtomicTV/filename_parser.rb @@ -1,20 +1,20 @@ module AtomicTV class FilenameParser - + class InvalidFilename < ::AtomicTV::AtomicTVError def initialize(filename) @filename = filename end - + attr_reader :filename - + def human_message "Invalid filename: #{filename} (filenames must be in the format: 'Series Name - S01E01')." end end - + FilenameFormat = /\A(.*) - S(\d{2})E(\d{2})\.\w{3}\Z/ - + def self.parse(filename) if filename.to_s =~ FilenameFormat new($1, $2.to_i(10), $3.to_i(10)) @@ -22,16 +22,16 @@ def self.parse(filename) raise InvalidFilename.new(filename) end end - + attr_reader :series_name, :season_number, :episode_number - + private - + def initialize(series_name, season_number, episode_number) @series_name = series_name @season_number = season_number @episode_number = episode_number end - + end end diff --git a/lib/AtomicTV/tvdb_episode.rb b/lib/AtomicTV/tvdb_episode.rb index 94acd46..792d305 100644 --- a/lib/AtomicTV/tvdb_episode.rb +++ b/lib/AtomicTV/tvdb_episode.rb @@ -1,46 +1,46 @@ module AtomicTV class TVDBEpisode - + class UnknownSeries < ::AtomicTV::AtomicTVError def initialize(series_name) @series_name = series_name end - + attr_reader :series_name - + def human_message "Unknown TV series: '#{series_name}'" end end - + class UnknownEpisode < ::AtomicTV::AtomicTVError def initialize(series_name, season_number, episode_number) @episode_id = "#{series_name} - S#{season_number.to_s.rjust(2, '0')}E#{episode_number.to_s.rjust(2, '0')}" end - + attr_reader :episode_id - + def human_message "Unknown episode: #{episode_id}" end end - + def self.metadata_for_filename(filename) parser = FilenameParser.parse(filename) search_results = client.search(parser.series_name) raise UnknownSeries.new(parser.series_name) if search_results.empty? - + series = client.get_series_by_id(search_results.first['seriesid']) episode = client.get_episode(series, parser.season_number, parser.episode_number) raise UnknownEpisode.new(series.name, parser.season_number, parser.episode_number) if episode.nil? return EpisodeMetadata.new(series, episode) end - + private - + def self.client @client ||= TvdbParty::Search.new('BD90B148E7D9E897', 'en') end - + end end From 59d20713e676143fb7c1dbe0001841a856ad04ab Mon Sep 17 00:00:00 2001 From: Tim Galeckas Date: Tue, 18 Mar 2014 10:45:14 -0500 Subject: [PATCH 2/7] more whitespace --- spec/AtomicTV/atomic_parsley_tagger_spec.rb | 38 +++++------ spec/AtomicTV/episode_metadata_spec.rb | 76 ++++++++++----------- spec/AtomicTV/filename_parser_spec.rb | 26 +++---- spec/AtomicTV/tvdb_episode_spec.rb | 48 ++++++------- 4 files changed, 94 insertions(+), 94 deletions(-) diff --git a/spec/AtomicTV/atomic_parsley_tagger_spec.rb b/spec/AtomicTV/atomic_parsley_tagger_spec.rb index 750628f..408958c 100644 --- a/spec/AtomicTV/atomic_parsley_tagger_spec.rb +++ b/spec/AtomicTV/atomic_parsley_tagger_spec.rb @@ -1,60 +1,60 @@ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper')) describe AtomicTV::AtomicParsleyTagger do - + describe ".executable" do - + before(:each) do Kernel.stub(:`).and_return('') end - + context "with AtomicParsley installed" do - + let(:executable_path) { double(:executable? => true) } - + before(:each) do Pathname.stub(:new).and_return(executable_path) end - + it "should return a pathname with executable's location" do path = AtomicTV::AtomicParsleyTagger.executable path.should == executable_path end - + end - + context "without AtomicParsley installed" do - + before(:each) do Pathname.stub(:new).and_return(double(:executable? => false)) end - + it "should raise a AtomicParsleyUnavailable error" do expect { AtomicTV::AtomicParsleyTagger.executable }.to raise_error(AtomicTV::AtomicParsleyTagger::AtomicParsleyUnavailable) end - + end - + end - + describe "#cast_metadata" do - + let(:file_path) { double(:exist? => true) } - + let(:metadata) do double( - :actors => ['Actor 1', 'Actor 2'], - :directors => ['Director 1', 'Director 2'], + :actors => ['Actor 1', 'Actor 2'], + :directors => ['Director 1', 'Director 2'], :writers => ['Writer 1', 'Writer 2'] ) end - + it "should generate a plist string from the actors, directors and writers" do tagger = AtomicTV::AtomicParsleyTagger.new(file_path, metadata) tagger.cast_metadata.should == "\n\n\n\n\tcast\n\t\n\t\t\n\t\t\tname\n\t\t\tActor 1\n\t\t\n\t\t\n\t\t\tname\n\t\t\tActor 2\n\t\t\n\t\n\tdirectors\n\t\n\t\t\n\t\t\tname\n\t\t\tDirector 1\n\t\t\n\t\t\n\t\t\tname\n\t\t\tDirector 2\n\t\t\n\t\n\tscreenwriters\n\t\n\t\t\n\t\t\tname\n\t\t\tWriter 1\n\t\t\n\t\t\n\t\t\tname\n\t\t\tWriter 2\n\t\t\n\t\n\n\n" end end - + end diff --git a/spec/AtomicTV/episode_metadata_spec.rb b/spec/AtomicTV/episode_metadata_spec.rb index 509e38e..8d2c472 100644 --- a/spec/AtomicTV/episode_metadata_spec.rb +++ b/spec/AtomicTV/episode_metadata_spec.rb @@ -1,13 +1,13 @@ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper')) describe AtomicTV::EpisodeMetadata do - + let(:series) do - double('Mock Series', + double('Mock Series', :name => 'House', :network => 'FOX', :genres => ['Drama'], :artwork => [ - double('Artwork 1', :path => 'seasons/12345-7-1.jpg'), + double('Artwork 1', :path => 'seasons/12345-7-1.jpg'), double('Artwork 2', :path => 'seasons/12345-7-2.jpg')], :actors => [ double('Actor 1', :name => 'Hugh Laurie'), @@ -15,7 +15,7 @@ double('Actor 3', :name => 'Amber Tamblyn')] ) end - + let(:episode) do double('Mock Episode', :name => 'Last Temptation', @@ -28,120 +28,120 @@ :writer => '|David Foster|Liz Friedman|' ) end - + let(:metadata) { AtomicTV::EpisodeMetadata.new(series, episode) } - + context "with complete show information" do - + it "should return 'TV Show' for media_type" do metadata.media_type.should == 'TV Show' end - + it "should return the series name for artist" do metadata.artist.should == 'House' end - + it "should return the episode name for title" do metadata.title.should == 'Last Temptation' end - + it "should return the series name and season for album" do metadata.album.should == 'House, Season 7' end - + it "should return the first genre for genre" do metadata.genre.should == 'Drama' end - + it "should return the first 255 characters, trucated to a complete sentence, of the overview for description" do metadata.description.should == 'Masters faces a career crossroads on her last day as a medical student and struggles with the choice to continue on the path to become a surgeon or to accept the rare opportunity to join House’s team officially.' end - + it "should return the full overview for long description" do metadata.long_description.should == %Q{Masters faces a career crossroads on her last day as a medical student and struggles with the choice to continue on the path to become a surgeon or to accept the rare opportunity to join House’s team officially. Meanwhile, the team treats a 16-year-old girl who inexplicably collapsed days before embarking on an ambitious sailing tour around the globe. Despite the patient's life-changing diagnosis, the patient's family insists on getting her back on the seas in time for her potentially record-breaking launch. But to the team's surprise, including House, Masters makes a bold decision regarding the patient’s treatment.} end - + it "should return the series network for tv network" do metadata.tv_network.should == 'FOX' end - + it "should return the series name for tv show name" do metadata.tv_show_name.should == 'House' end - + it "should return the series number and episode number for tv episode" do metadata.tv_episode.should == '719' end - + it "should return the season number for tv season number" do metadata.tv_season_number.should == '7' end - + it "should return the episode number for tv episode number" do metadata.tv_episode_number.should == '19' end - + it "should return the episode number for track number" do metadata.track_number.should == '19' end - + it "should return a date with timezone for air date" do metadata.air_date.should == '2011-04-18T00:00:00Z' end - + it "should return series cast and guest cast for actors" do metadata.actors.should =~ ['Hugh Laurie', 'Olivia Wilde', 'Amber Tamblyn', 'Ron Perkins', 'Jennifer Landon'] end - + it "should return the episode's directors for directors" do metadata.directors.should =~ ['Tim Southam'] end - + it "should return the episode's writers for writers" do metadata.writers.should =~ ['David Foster', 'Liz Friedman'] end - + end - + context "with a missing air date" do - + before(:each) do episode.stub(:air_date).and_return(nil) end - + it "should return nil for air_date" do metadata.air_date.should be_nil end - + end - + context "with missing artwork" do - + end - + describe "#description" do pending "more logic" end - + describe "#tv_episode" do - + it "should format the episode number to two digits" do episode.stub(:number).and_return('2') metadata.tv_episode.should == '702' end - + end - + describe "#directors" do pending "more logic" end - + describe "#writers" do pending "more logic" end - + describe "#with_loaded_posters" do pending end - + end diff --git a/spec/AtomicTV/filename_parser_spec.rb b/spec/AtomicTV/filename_parser_spec.rb index dc3915f..0bc1dd0 100644 --- a/spec/AtomicTV/filename_parser_spec.rb +++ b/spec/AtomicTV/filename_parser_spec.rb @@ -1,46 +1,46 @@ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper')) describe AtomicTV::FilenameParser do - + describe ".parse" do - + context "with valid filenames" do - + let(:parser) { AtomicTV::FilenameParser.parse('Battlestar Galactica (2003) - S04E20.m4v') } - + it "should extract the series name" do parser.series_name.should == 'Battlestar Galactica (2003)' end - + it "should extract the season number" do parser.season_number.should == 4 end - + it "should extract the episode number" do parser.episode_number.should == 20 end end - + context "with invalid filenames" do - + it "should raise an AtomicTV::FilenameParser::InvalidFilename error" do expect { AtomicTV::FilenameParser.parse('V for Vendetta (2006).m4v') }.to raise_error(AtomicTV::FilenameParser::InvalidFilename) end - + it "should expose the invalid filename in the error" do invalid_filename = 'V for Vendetta (2006).m4v' - + begin AtomicTV::FilenameParser.parse(invalid_filename) rescue AtomicTV::FilenameParser::InvalidFilename => error error.filename.should == invalid_filename end end - + end - + end - + end diff --git a/spec/AtomicTV/tvdb_episode_spec.rb b/spec/AtomicTV/tvdb_episode_spec.rb index f6090fb..c131ce9 100644 --- a/spec/AtomicTV/tvdb_episode_spec.rb +++ b/spec/AtomicTV/tvdb_episode_spec.rb @@ -3,62 +3,62 @@ WebMock.disable_net_connect! describe AtomicTV::TVDBEpisode do - + describe ".metadata_for_filename" do - + context "with a correct series, season and episode" do - + before(:each) do @mock_parser = double('Mock Parser', :series_name => 'The Wire', :season_number => 3, :episode_number => 6) AtomicTV::FilenameParser.stub(:parse).and_return(@mock_parser) - + @result1 = double('Mock Search Result - 1', :[] => '123456789') @result2 = double('Mock Search Result - 2', :[] => '987654321') @episode = double('Mock Episode') @mock_client = double('Mock Client', :search => [@result1, @result2], :get_series_by_id => @result1, :get_episode => @episode) AtomicTV::TVDBEpisode.stub(:client).and_return(@mock_client) end - + it "should search for the series using the name" do @mock_client.should_receive(:search).with('The Wire') - + AtomicTV::TVDBEpisode.metadata_for_filename('The Wire - S03E06.m4v') end - + it "should use the first search result as the series" do @mock_client.should_receive(:get_series_by_id).with('123456789') - + AtomicTV::TVDBEpisode.metadata_for_filename('The Wire - S03E06.m4v') end - + it "should return the correct episode for the series" do @mock_client.should_receive(:get_episode).with(@result1, 3, 6) - + AtomicTV::TVDBEpisode.metadata_for_filename('The Wire - S03E06.m4v') end - + it "should return an EpisodeMetadata instance for the episode" do metadata = AtomicTV::TVDBEpisode.metadata_for_filename('The Wire - S03E06.m4v') metadata.should be_kind_of(AtomicTV::EpisodeMetadata) metadata.series.should == @result1 metadata.episode.should == @episode end - + end - + context "with an unknown series" do - + before(:each) do @mock_client = double('Mock Client', :search => []) AtomicTV::TVDBEpisode.stub(:client).and_return(@mock_client) end - + it "should raise an AtomicTV::TVDBEpisode::UnknownSeries error" do expect { AtomicTV::TVDBEpisode.metadata_for_filename('Teh Wires - S01E01.m4v') }.to raise_error(AtomicTV::TVDBEpisode::UnknownSeries) end - + it "should expose the invalid series name in the error" do begin AtomicTV::TVDBEpisode.metadata_for_filename('Teh Wires - S01E01.m4v') @@ -66,23 +66,23 @@ error.series_name.should == 'Teh Wires' end end - + end - + context "with an unknown episode" do - + before(:each) do @series = double('Mock Series', :[] => '1234789', :name => 'The Wire') @mock_client = double('Mock Client', :search => [@series], :get_series_by_id => @series, :get_episode => nil) AtomicTV::TVDBEpisode.stub(:client).and_return(@mock_client) end - + it "should raise an AtomicTV::TVDBEpisode::UnknownEpisode error" do expect { AtomicTV::TVDBEpisode.metadata_for_filename('The Wire - S01E99.m4v') }.to raise_error(AtomicTV::TVDBEpisode::UnknownEpisode) end - + it "should expose the invalid episode ID in the error" do begin AtomicTV::TVDBEpisode.metadata_for_filename('The Wire - S01E99.m4v') @@ -90,9 +90,9 @@ error.episode_id.should == 'The Wire - S01E99' end end - + end - + end - + end From a4982de6c950b18b32c387f7234918c6bcbbbd5a Mon Sep 17 00:00:00 2001 From: Tim Galeckas Date: Tue, 18 Mar 2014 14:57:25 -0500 Subject: [PATCH 3/7] add dry-run, itunes searching, some command-line niceties, and upgrade tvdb_party to fix httparty_icebox errors --- Gemfile | 6 +- Gemfile.lock | 17 +++- bin/AtomicTV | 22 +++++- lib/AtomicTV.rb | 3 +- lib/AtomicTV/itunes_episode.rb | 51 ++++++++++++ lib/AtomicTV/itunes_episode_metadata.rb | 101 ++++++++++++++++++++++++ 6 files changed, 193 insertions(+), 7 deletions(-) create mode 100644 lib/AtomicTV/itunes_episode.rb create mode 100644 lib/AtomicTV/itunes_episode_metadata.rb diff --git a/Gemfile b/Gemfile index 6ab928f..a867369 100644 --- a/Gemfile +++ b/Gemfile @@ -1,9 +1,13 @@ source "http://rubygems.org" gem "plist", "3.1.0" -gem "tvdb_party", "0.6.0" +gem "tvdb_party", "0.7.0" +gem "itunes", "0.6.0" +gem "rash" +gem "trollop" group :development do + gem "pry" gem "rspec" gem "bundler" gem "jeweler" diff --git a/Gemfile.lock b/Gemfile.lock index 72614c6..4b6af6f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,6 +3,7 @@ GEM specs: addressable (2.3.5) builder (3.2.2) + coderay (1.1.0) crack (0.4.2) safe_yaml (~> 1.0.0) descendants_tracker (0.0.3) @@ -23,6 +24,8 @@ GEM httparty (0.13.0) json (~> 1.8) multi_xml (>= 0.5.2) + itunes (0.6.0) + httparty jeweler (2.0.1) builder bundler (>= 1.0) @@ -35,6 +38,7 @@ GEM json (1.8.1) jwt (0.1.11) multi_json (>= 1.5) + method_source (0.8.2) mini_portile (0.5.2) multi_json (1.9.0) multi_xml (0.5.5) @@ -48,6 +52,10 @@ GEM multi_xml (~> 0.5) rack (~> 1.2) plist (3.1.0) + pry (0.9.12.6) + coderay (~> 1.0) + method_source (~> 0.8) + slop (~> 3.4) rack (1.5.2) rake (10.1.1) rdoc (4.1.1) @@ -61,7 +69,9 @@ GEM diff-lcs (>= 1.1.3, < 2.0) rspec-mocks (2.14.6) safe_yaml (1.0.1) - tvdb_party (0.6.0) + slop (3.5.0) + trollop (2.0) + tvdb_party (0.7.0) httparty (>= 0.6.1) webmock (1.17.4) addressable (>= 2.2.7) @@ -72,8 +82,11 @@ PLATFORMS DEPENDENCIES bundler + itunes jeweler plist (= 3.1.0) + pry rspec - tvdb_party (= 0.6.0) + trollop + tvdb_party (= 0.7.0) webmock diff --git a/bin/AtomicTV b/bin/AtomicTV index 8ce6e34..ecb0f70 100755 --- a/bin/AtomicTV +++ b/bin/AtomicTV @@ -1,6 +1,7 @@ #!/usr/bin/env ruby -rubygems require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'AtomicTV')) +require 'trollop' include AtomicTV @@ -8,12 +9,27 @@ def display_error(error) $stderr.puts "! ERROR: #{error.human_message}" end +opts = Trollop.options do + banner("Usage: AtomicTV [options]") + opt :dry_run, "Only output basic episode information for debug", type: :flag + opt :use_itunes, "Use the itunes store api instead of tvdb", type: :flag +end + ARGV.each do |path| begin file_path = Pathname.new(path) - metadata = TVDBEpisode.metadata_for_filename(file_path.basename) - tagger = AtomicParsleyTagger.new(file_path, metadata) - tagger.run + metadata = if opts[:use_itunes] + ItunesEpisode.metadata_for_filename(file_path.basename) + else + TVDBEpisode.metadata_for_filename(file_path.basename) + end + if opts[:dry_run] + require 'pp' + pp metadata + else + tagger = AtomicParsleyTagger.new(file_path, metadata) + tagger.run + end puts "* Tagged: #{file_path.basename}" rescue AtomicParsleyTagger::AtomicParsleyUnavailable => error display_error(error) diff --git a/lib/AtomicTV.rb b/lib/AtomicTV.rb index 1b1e510..e17f924 100644 --- a/lib/AtomicTV.rb +++ b/lib/AtomicTV.rb @@ -5,11 +5,12 @@ require 'plist' require 'tvdb_party' +require 'itunes' module AtomicTV class AtomicTVError < StandardError; end end -['atomic_parsley_tagger', 'episode_metadata', 'filename_parser', 'tvdb_episode'].each do |file| +['atomic_parsley_tagger', 'episode_metadata', 'filename_parser', 'tvdb_episode', 'itunes_episode', 'itunes_episode_metadata'].each do |file| require File.expand_path(File.join(File.dirname(__FILE__), 'AtomicTV', file)) end diff --git a/lib/AtomicTV/itunes_episode.rb b/lib/AtomicTV/itunes_episode.rb new file mode 100644 index 0000000..85ae527 --- /dev/null +++ b/lib/AtomicTV/itunes_episode.rb @@ -0,0 +1,51 @@ +require 'itunes' + +module AtomicTV + class ItunesEpisode + + class UnknownSeries < ::AtomicTV::AtomicTVError + def initialize(series_name) + @series_name = series_name + end + + attr_reader :series_name + + def human_message + "Unknown TV series: '#{series_name}'" + end + end + + class UnknownEpisode < ::AtomicTV::AtomicTVError + def initialize(series_name, season_number, episode_number) + @episode_id = "#{series_name} - S#{season_number.to_s.rjust(2, '0')}E#{episode_number.to_s.rjust(2, '0')}" + end + + attr_reader :episode_id + + def human_message + "Unknown episode: #{episode_id}" + end + end + + def self.metadata_for_filename(filename) + parser = FilenameParser.parse(filename) + season_name = "#{parser.series_name}, Season #{parser.season_number}" + search_results = client.tv_show(season_name).results. + select{|s|s.artist_name == parser.series_name && s.collection_name == season_name} + + raise UnknownSeries.new(parser.series_name) if search_results.empty? + + possible_episodes = search_results.select{|s|s.track_number == parser.episode_number} + raise UnknownEpisode.new(parser.series_name, parser.season_number, parser.episode_number) if possible_episodes.size != 1 + return ItunesEpisodeMetadata.new(possible_episodes.first) + end + + private + + def self.client + @client ||= ITunes::Client.new + end + + end +end + diff --git a/lib/AtomicTV/itunes_episode_metadata.rb b/lib/AtomicTV/itunes_episode_metadata.rb new file mode 100644 index 0000000..c545a56 --- /dev/null +++ b/lib/AtomicTV/itunes_episode_metadata.rb @@ -0,0 +1,101 @@ +module AtomicTV + class ItunesEpisodeMetadata + + ArtworkBaseURL = "http://thetvdb.com/banners/" + + def initialize(episode) + @episode = episode + end + + attr_reader :episode + + def media_type + 'TV Show' + end + + def artist + episode.artist_name + end + + def title + episode.name + end + + def album + episode.collection_name + end + + def genre + episode.primary_genre_name + end + + def description + episode.short_description + end + + def long_description + episode.long_description + end + + def tv_network + '' + end + + def tv_show_name + artist + end + + def tv_episode + "#{tv_season_number}#{tv_episode_number.to_s.rjust(2, '0')}" + end + + def tv_season_number + /Season ([0-9]*)/.match(episode.collection_name)[1].to_i + end + + def tv_episode_number + track_number + end + + def track_number + episode.track_number + end + + def air_date + episode.release_date + end + + def actors + [] + end + + def directors + [] + end + + def writers + [] + end + + attr_reader :posters + + def with_loaded_posters + temporary_directory = Dir.mktmpdir + + url = episode.artwork_url100.gsub('100x100','300x300') + file = File.new(File.join(temporary_directory, File.basename(url)), 'w') + file.write(open(url).read) + file.close + + @posters = [file] + + yield + + ensure + FileUtils.rm_rf(temporary_directory) + @posters = nil + end + + end +end + From 2e810bc395886379b25ec9eda2be62abb6e9cb83 Mon Sep 17 00:00:00 2001 From: Tim Galeckas Date: Thu, 20 Mar 2014 21:23:14 -0500 Subject: [PATCH 4/7] be a little bit looser with filenames to accept --- lib/AtomicTV/filename_parser.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/AtomicTV/filename_parser.rb b/lib/AtomicTV/filename_parser.rb index 8802fac..e533282 100644 --- a/lib/AtomicTV/filename_parser.rb +++ b/lib/AtomicTV/filename_parser.rb @@ -13,11 +13,15 @@ def human_message end end - FilenameFormat = /\A(.*) - S(\d{2})E(\d{2})\.\w{3}\Z/ + FilenameFormat = /\A(.*)(?: - |\.)S(\d{2})E(\d{2}).*\.\w{3}\Z/ def self.parse(filename) if filename.to_s =~ FilenameFormat - new($1, $2.to_i(10), $3.to_i(10)) + series_name, season_num, episode_num = $1, $2, $3 + series_name.gsub!('.', ' ') + season_num = season_num.to_i(10) + episode_num = episode_num.to_i(10) + new(series_name, season_num, episode_num) else raise InvalidFilename.new(filename) end From 45d02e0aa7333ecee3a0076b84648df429a33820 Mon Sep 17 00:00:00 2001 From: Tim Galeckas Date: Fri, 21 Mar 2014 07:05:57 -0500 Subject: [PATCH 5/7] clean up code, make itunes additive, add content advisory, explicitness, and content_id --- Gemfile | 2 +- Gemfile.lock | 15 ++- bin/AtomicTV | 9 +- lib/AtomicTV.rb | 3 +- lib/AtomicTV/atomic_parsley_tagger.rb | 6 +- lib/AtomicTV/episode_metadata.rb | 121 ++++++++---------------- lib/AtomicTV/itunes_episode.rb | 4 +- lib/AtomicTV/itunes_episode_metadata.rb | 101 -------------------- lib/AtomicTV/tvdb_episode.rb | 4 +- 9 files changed, 66 insertions(+), 199 deletions(-) delete mode 100644 lib/AtomicTV/itunes_episode_metadata.rb diff --git a/Gemfile b/Gemfile index a867369..674cbec 100644 --- a/Gemfile +++ b/Gemfile @@ -2,7 +2,7 @@ source "http://rubygems.org" gem "plist", "3.1.0" gem "tvdb_party", "0.7.0" -gem "itunes", "0.6.0" +gem "itunes" gem "rash" gem "trollop" diff --git a/Gemfile.lock b/Gemfile.lock index 4b6af6f..0eadca0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,8 +8,10 @@ GEM safe_yaml (~> 1.0.0) descendants_tracker (0.0.3) diff-lcs (1.2.5) - faraday (0.9.0) - multipart-post (>= 1.2, < 3) + faraday (0.8.9) + multipart-post (~> 1.2.0) + faraday_middleware (0.9.0) + faraday (>= 0.7.4, < 0.9) git (1.2.6) github_api (0.11.3) addressable (~> 2.3) @@ -25,7 +27,9 @@ GEM json (~> 1.8) multi_xml (>= 0.5.2) itunes (0.6.0) - httparty + faraday_middleware (~> 0.7) + multi_json (~> 1.0) + rash (~> 0.3) jeweler (2.0.1) builder bundler (>= 1.0) @@ -42,7 +46,7 @@ GEM mini_portile (0.5.2) multi_json (1.9.0) multi_xml (0.5.5) - multipart-post (2.0.0) + multipart-post (1.2.0) nokogiri (1.6.1) mini_portile (~> 0.5.0) oauth2 (0.9.3) @@ -58,6 +62,8 @@ GEM slop (~> 3.4) rack (1.5.2) rake (10.1.1) + rash (0.4.0) + hashie (~> 2.0.0) rdoc (4.1.1) json (~> 1.4) rspec (2.14.1) @@ -86,6 +92,7 @@ DEPENDENCIES jeweler plist (= 3.1.0) pry + rash rspec trollop tvdb_party (= 0.7.0) diff --git a/bin/AtomicTV b/bin/AtomicTV index ecb0f70..f34d245 100755 --- a/bin/AtomicTV +++ b/bin/AtomicTV @@ -18,11 +18,10 @@ end ARGV.each do |path| begin file_path = Pathname.new(path) - metadata = if opts[:use_itunes] - ItunesEpisode.metadata_for_filename(file_path.basename) - else - TVDBEpisode.metadata_for_filename(file_path.basename) - end + series, episode = TVDBEpisode.series_and_episode_for(file_path.basename) + itunes_episode = ItunesEpisode.episode_for(file_path.basename) if opts[:use_itunes] + metadata = EpisodeMetadata.new(series, episode, itunes_episode) + if opts[:dry_run] require 'pp' pp metadata diff --git a/lib/AtomicTV.rb b/lib/AtomicTV.rb index e17f924..de12fc7 100644 --- a/lib/AtomicTV.rb +++ b/lib/AtomicTV.rb @@ -1,4 +1,5 @@ require 'fileutils' +require 'hashie' require 'open-uri' require 'pathname' require 'tmpdir' @@ -11,6 +12,6 @@ module AtomicTV class AtomicTVError < StandardError; end end -['atomic_parsley_tagger', 'episode_metadata', 'filename_parser', 'tvdb_episode', 'itunes_episode', 'itunes_episode_metadata'].each do |file| +['atomic_parsley_tagger', 'episode_metadata', 'filename_parser', 'tvdb_episode', 'itunes_episode'].each do |file| require File.expand_path(File.join(File.dirname(__FILE__), 'AtomicTV', file)) end diff --git a/lib/AtomicTV/atomic_parsley_tagger.rb b/lib/AtomicTV/atomic_parsley_tagger.rb index ad47b70..be78503 100644 --- a/lib/AtomicTV/atomic_parsley_tagger.rb +++ b/lib/AtomicTV/atomic_parsley_tagger.rb @@ -69,7 +69,10 @@ def run 'TVSeasonNum' => metadata.tv_season_number, 'TVEpisodeNum' => metadata.tv_episode_number, 'tracknum' => metadata.track_number, - 'year' => metadata.air_date + 'year' => metadata.air_date, + 'contentRating' => metadata.content_advisory_rating, + 'advisory' => metadata.track_is_clean ? 'clean' : 'explicit', + 'cnID' => metadata.track_id, } metadata.with_loaded_posters do @@ -77,6 +80,7 @@ def run command << %Q{"#{file_path}" } command << %Q{--overWrite } command << %Q{--rDNSatom "#{escape_double_quotes(cast_metadata)}" name=iTunMOVI domain=com.apple.iTunes } + command << %Q{--artwork REMOVE_ALL } metadata.posters.each do |poster| command << %Q{--artwork #{poster.path} } end diff --git a/lib/AtomicTV/episode_metadata.rb b/lib/AtomicTV/episode_metadata.rb index 59d93ca..2a724a1 100644 --- a/lib/AtomicTV/episode_metadata.rb +++ b/lib/AtomicTV/episode_metadata.rb @@ -1,91 +1,48 @@ module AtomicTV - class EpisodeMetadata - - ArtworkBaseURL = "http://thetvdb.com/banners/" - - def initialize(series, episode) - @series, @episode = series, episode - end - - attr_reader :series, :episode - - def media_type - 'TV Show' - end - - def artist - series.name - end - - def title - episode.name - end - - def album - "#{series.name}, Season #{episode.season_number}" - end - - def genre - series.genres.first - end - - def description - episode.overview.to_s[0,255].gsub(/\.(.*)\Z/, '.') - end - - def long_description - episode.overview.to_s - end - - def tv_network - series.network + class EpisodeMetadata < Hash + include Hashie::Extensions::MethodAccess + include Hashie::Extensions::MergeInitializer + + TVDBArtworkBaseURL = "http://thetvdb.com/banners/" + + def initialize(series, episode, itunes_episode) + super({ + :series => series, + :episode => episode, + :itunes_episode => itunes_episode, + :media_type => 'TV Show', + :artist => series.name, + :title => episode.name, + :album => "#{series.name}, Season #{episode.season_number}", + :genre => series.genres.first, + :description => itunes_episode ? itunes_episode.short_description : episode.overview.to_s[0,255].gsub(/\.(.*)\Z/, '.'), + :long_description => itunes_episode ? itunes_episode.long_description : episode.overview.to_s, + :tv_network => series.network, + :tv_show_name => series.name, + :tv_episode => "#{episode.season_number}#{episode.number.to_s.rjust(2, '0')}", + :tv_season_number => episode.season_number, + :tv_episode_number => episode.number, + :track_number => episode.number, + :air_date => episode.air_date && episode.air_date.to_s + 'T00:00:00Z', + :actors => (Array(series.actors).map {|a| a.name} + episode.guest_stars).uniq, + :directors => parse_names(episode.director), + :writers => parse_names(episode.writer), + :artist_id => itunes_episode && itunes_episode.artist_id, + :collection_id => itunes_episode && itunes_episode.collection_id, + :track_id => itunes_episode && itunes_episode.track_id, + :content_advisory_rating => itunes_episode && itunes_episode.content_advisory_rating, + :track_is_clean => !itunes_episode || itunes_episode.track_explicitness == 'notExplicit' + }) end - def tv_show_name - series.name - end - - def tv_episode - "#{episode.season_number}#{episode.number.to_s.rjust(2, '0')}" - end - - def tv_season_number - episode.season_number - end - - def tv_episode_number - episode.number - end - - def track_number - episode.number - end - - def air_date - episode.air_date && episode.air_date.to_s + 'T00:00:00Z' - end - - def actors - (Array(series.actors).map {|a| a.name} + episode.guest_stars).uniq - end - - def directors - parse_names(episode.director) - end - - def writers - parse_names(episode.writer) - end - - attr_reader :posters - def with_loaded_posters temporary_directory = Dir.mktmpdir - season_posters = series.season_posters(episode.season_number, 'en') - season_posters = series.posters('en') if season_posters.empty? - @posters = season_posters.map do |poster| - url = ArtworkBaseURL + poster.path + season_posters = [] + season_posters << itunes_episode.artwork_url100.gsub('.100x100-75','') if itunes_episode + season_posters += series.season_posters(episode.season_number, 'en').map{|poster| TVDBArtworkBaseURL + poster.path } + season_posters += series.posters('en').map{|poster| TVDBArtworkBaseURL + poster.path } if season_posters.empty? + self.posters = season_posters.map do |url| file = File.new(File.join(temporary_directory, File.basename(url)), 'w') file.write(open(url).read) file.close diff --git a/lib/AtomicTV/itunes_episode.rb b/lib/AtomicTV/itunes_episode.rb index 85ae527..05219a3 100644 --- a/lib/AtomicTV/itunes_episode.rb +++ b/lib/AtomicTV/itunes_episode.rb @@ -27,7 +27,7 @@ def human_message end end - def self.metadata_for_filename(filename) + def self.episode_for(filename) parser = FilenameParser.parse(filename) season_name = "#{parser.series_name}, Season #{parser.season_number}" search_results = client.tv_show(season_name).results. @@ -37,7 +37,7 @@ def self.metadata_for_filename(filename) possible_episodes = search_results.select{|s|s.track_number == parser.episode_number} raise UnknownEpisode.new(parser.series_name, parser.season_number, parser.episode_number) if possible_episodes.size != 1 - return ItunesEpisodeMetadata.new(possible_episodes.first) + possible_episodes.first end private diff --git a/lib/AtomicTV/itunes_episode_metadata.rb b/lib/AtomicTV/itunes_episode_metadata.rb deleted file mode 100644 index c545a56..0000000 --- a/lib/AtomicTV/itunes_episode_metadata.rb +++ /dev/null @@ -1,101 +0,0 @@ -module AtomicTV - class ItunesEpisodeMetadata - - ArtworkBaseURL = "http://thetvdb.com/banners/" - - def initialize(episode) - @episode = episode - end - - attr_reader :episode - - def media_type - 'TV Show' - end - - def artist - episode.artist_name - end - - def title - episode.name - end - - def album - episode.collection_name - end - - def genre - episode.primary_genre_name - end - - def description - episode.short_description - end - - def long_description - episode.long_description - end - - def tv_network - '' - end - - def tv_show_name - artist - end - - def tv_episode - "#{tv_season_number}#{tv_episode_number.to_s.rjust(2, '0')}" - end - - def tv_season_number - /Season ([0-9]*)/.match(episode.collection_name)[1].to_i - end - - def tv_episode_number - track_number - end - - def track_number - episode.track_number - end - - def air_date - episode.release_date - end - - def actors - [] - end - - def directors - [] - end - - def writers - [] - end - - attr_reader :posters - - def with_loaded_posters - temporary_directory = Dir.mktmpdir - - url = episode.artwork_url100.gsub('100x100','300x300') - file = File.new(File.join(temporary_directory, File.basename(url)), 'w') - file.write(open(url).read) - file.close - - @posters = [file] - - yield - - ensure - FileUtils.rm_rf(temporary_directory) - @posters = nil - end - - end -end - diff --git a/lib/AtomicTV/tvdb_episode.rb b/lib/AtomicTV/tvdb_episode.rb index 792d305..a5c6a86 100644 --- a/lib/AtomicTV/tvdb_episode.rb +++ b/lib/AtomicTV/tvdb_episode.rb @@ -25,7 +25,7 @@ def human_message end end - def self.metadata_for_filename(filename) + def self.series_and_episode_for(filename) parser = FilenameParser.parse(filename) search_results = client.search(parser.series_name) raise UnknownSeries.new(parser.series_name) if search_results.empty? @@ -33,7 +33,7 @@ def self.metadata_for_filename(filename) series = client.get_series_by_id(search_results.first['seriesid']) episode = client.get_episode(series, parser.season_number, parser.episode_number) raise UnknownEpisode.new(series.name, parser.season_number, parser.episode_number) if episode.nil? - return EpisodeMetadata.new(series, episode) + return series, episode end private From f823d64177303903e9a0050b86e18528f47e5fe4 Mon Sep 17 00:00:00 2001 From: Tim Galeckas Date: Fri, 21 Mar 2014 07:11:45 -0500 Subject: [PATCH 6/7] differentiate between itunes and tvdb in error messages --- lib/AtomicTV/itunes_episode.rb | 4 ++-- lib/AtomicTV/tvdb_episode.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/AtomicTV/itunes_episode.rb b/lib/AtomicTV/itunes_episode.rb index 05219a3..6f11a46 100644 --- a/lib/AtomicTV/itunes_episode.rb +++ b/lib/AtomicTV/itunes_episode.rb @@ -11,7 +11,7 @@ def initialize(series_name) attr_reader :series_name def human_message - "Unknown TV series: '#{series_name}'" + "ITunes: Unknown TV series: '#{series_name}'" end end @@ -23,7 +23,7 @@ def initialize(series_name, season_number, episode_number) attr_reader :episode_id def human_message - "Unknown episode: #{episode_id}" + "ITunes: Unknown episode: #{episode_id}" end end diff --git a/lib/AtomicTV/tvdb_episode.rb b/lib/AtomicTV/tvdb_episode.rb index a5c6a86..1988bdd 100644 --- a/lib/AtomicTV/tvdb_episode.rb +++ b/lib/AtomicTV/tvdb_episode.rb @@ -9,7 +9,7 @@ def initialize(series_name) attr_reader :series_name def human_message - "Unknown TV series: '#{series_name}'" + "TVDB: Unknown TV series: '#{series_name}'" end end @@ -21,7 +21,7 @@ def initialize(series_name, season_number, episode_number) attr_reader :episode_id def human_message - "Unknown episode: #{episode_id}" + "TVDB: Unknown episode: #{episode_id}" end end From 089c6d14435dbfe270b0517d8565429dd86b864e Mon Sep 17 00:00:00 2001 From: Tim Galeckas Date: Sat, 12 Sep 2015 15:39:09 -0500 Subject: [PATCH 7/7] add the ability fallback a couple different ways if we cannot find the metadata --- bin/AtomicTV | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/bin/AtomicTV b/bin/AtomicTV index f34d245..bf21334 100755 --- a/bin/AtomicTV +++ b/bin/AtomicTV @@ -2,6 +2,7 @@ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'AtomicTV')) require 'trollop' +require 'readline' include AtomicTV @@ -11,14 +12,36 @@ end opts = Trollop.options do banner("Usage: AtomicTV [options]") - opt :dry_run, "Only output basic episode information for debug", type: :flag - opt :use_itunes, "Use the itunes store api instead of tvdb", type: :flag + opt :dry_run, "Only output basic episode information for debug", type: :flag + opt :use_itunes, "Use the itunes store api instead of tvdb", type: :flag + opt :try_gsub, "Use gsub to try a filname mutation", type: :string + opt :interactive, "If we can't find a match ask for human edit", type: :flag end ARGV.each do |path| begin file_path = Pathname.new(path) - series, episode = TVDBEpisode.series_and_episode_for(file_path.basename) + series, episode = begin + TVDBEpisode.series_and_episode_for(file_path.basename) + rescue + if opts[:interactive] + Readline.pre_input_hook = -> do + Readline.insert_text file_path.basename.to_s + Readline.redisplay + + # Remove the hook right away. + Readline.pre_input_hook = nil + end + + filename = Readline.readline("Couldn't find the series/episode try something different?> ", false) + TVDBEpisode.series_and_episode_for(filename) + elsif opts[:try_gsub] + filename = file_path.basename.to_s.gsub(*opts[:try_gsub].split('|')) + TVDBEpisode.series_and_episode_for(filename) + else + raise + end + end itunes_episode = ItunesEpisode.episode_for(file_path.basename) if opts[:use_itunes] metadata = EpisodeMetadata.new(series, episode, itunes_episode)