AtomicTV 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,85 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{AtomicTV}
8
+ s.version = "1.0.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Olly Legg"]
12
+ s.date = %q{2011-06-01}
13
+ s.description = %q{A command line tool to tag MP4 TV shows with metadata pulled from TheTVDB.com. It uses AtomicParsley to process the file.}
14
+ s.email = %q{olly@51degrees.net}
15
+ s.executables = ["AtomicTV"]
16
+ s.extra_rdoc_files = [
17
+ "LICENSE.txt",
18
+ "README.rdoc"
19
+ ]
20
+ s.files = [
21
+ ".document",
22
+ ".rspec",
23
+ "AtomicTV.gemspec",
24
+ "Gemfile",
25
+ "Gemfile.lock",
26
+ "LICENSE.txt",
27
+ "README.rdoc",
28
+ "Rakefile",
29
+ "VERSION",
30
+ "bin/AtomicTV",
31
+ "lib/AtomicTV.rb",
32
+ "lib/AtomicTV/atomic_parsley_tagger.rb",
33
+ "lib/AtomicTV/episode_metadata.rb",
34
+ "lib/AtomicTV/filename_parser.rb",
35
+ "lib/AtomicTV/tvdb_episode.rb",
36
+ "spec/AtomicTV/atomic_parsley_tagger_spec.rb",
37
+ "spec/AtomicTV/episode_metadata_spec.rb",
38
+ "spec/AtomicTV/filename_parser_spec.rb",
39
+ "spec/AtomicTV/tvdb_episode_spec.rb",
40
+ "spec/spec_helper.rb"
41
+ ]
42
+ s.homepage = %q{http://github.com/olly/AtomicTV}
43
+ s.licenses = ["MIT"]
44
+ s.require_paths = ["lib"]
45
+ s.rubygems_version = %q{1.7.2}
46
+ s.summary = %q{A command line tool to tag MP4 TV shows with metadata pulled from TheTVDB.com.}
47
+ s.test_files = [
48
+ "spec/AtomicTV/atomic_parsley_tagger_spec.rb",
49
+ "spec/AtomicTV/episode_metadata_spec.rb",
50
+ "spec/AtomicTV/filename_parser_spec.rb",
51
+ "spec/AtomicTV/tvdb_episode_spec.rb",
52
+ "spec/spec_helper.rb"
53
+ ]
54
+
55
+ if s.respond_to? :specification_version then
56
+ s.specification_version = 3
57
+
58
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
59
+ s.add_runtime_dependency(%q<plist>, ["= 3.1.0"])
60
+ s.add_runtime_dependency(%q<tvdb_party>, ["= 0.6.0"])
61
+ s.add_development_dependency(%q<rspec>, ["~> 2.3.0"])
62
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
63
+ s.add_development_dependency(%q<jeweler>, ["~> 1.5.2"])
64
+ s.add_development_dependency(%q<rcov>, [">= 0"])
65
+ s.add_development_dependency(%q<webmock>, ["~> 1.6.2"])
66
+ else
67
+ s.add_dependency(%q<plist>, ["= 3.1.0"])
68
+ s.add_dependency(%q<tvdb_party>, ["= 0.6.0"])
69
+ s.add_dependency(%q<rspec>, ["~> 2.3.0"])
70
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
71
+ s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
72
+ s.add_dependency(%q<rcov>, [">= 0"])
73
+ s.add_dependency(%q<webmock>, ["~> 1.6.2"])
74
+ end
75
+ else
76
+ s.add_dependency(%q<plist>, ["= 3.1.0"])
77
+ s.add_dependency(%q<tvdb_party>, ["= 0.6.0"])
78
+ s.add_dependency(%q<rspec>, ["~> 2.3.0"])
79
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
80
+ s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
81
+ s.add_dependency(%q<rcov>, [">= 0"])
82
+ s.add_dependency(%q<webmock>, ["~> 1.6.2"])
83
+ end
84
+ end
85
+
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "plist", "3.1.0"
4
+ gem "tvdb_party", "0.6.0"
5
+
6
+ group :development do
7
+ gem "rspec", "~> 2.3.0"
8
+ gem "bundler", "~> 1.0.0"
9
+ gem "jeweler", "~> 1.5.2"
10
+ gem "rcov", ">= 0"
11
+ gem "webmock", "~> 1.6.2"
12
+ end
@@ -0,0 +1,41 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ addressable (2.2.5)
5
+ crack (0.1.8)
6
+ diff-lcs (1.1.2)
7
+ git (1.2.5)
8
+ httparty (0.7.7)
9
+ crack (= 0.1.8)
10
+ jeweler (1.5.2)
11
+ bundler (~> 1.0.0)
12
+ git (>= 1.2.5)
13
+ rake
14
+ plist (3.1.0)
15
+ rake (0.8.7)
16
+ rcov (0.9.9)
17
+ rspec (2.3.0)
18
+ rspec-core (~> 2.3.0)
19
+ rspec-expectations (~> 2.3.0)
20
+ rspec-mocks (~> 2.3.0)
21
+ rspec-core (2.3.1)
22
+ rspec-expectations (2.3.0)
23
+ diff-lcs (~> 1.1.2)
24
+ rspec-mocks (2.3.0)
25
+ tvdb_party (0.6.0)
26
+ httparty (>= 0.6.1)
27
+ webmock (1.6.2)
28
+ addressable (>= 2.2.2)
29
+ crack (>= 0.1.7)
30
+
31
+ PLATFORMS
32
+ ruby
33
+
34
+ DEPENDENCIES
35
+ bundler (~> 1.0.0)
36
+ jeweler (~> 1.5.2)
37
+ plist (= 3.1.0)
38
+ rcov
39
+ rspec (~> 2.3.0)
40
+ tvdb_party (= 0.6.0)
41
+ webmock (~> 1.6.2)
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Oliver Legg
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,19 @@
1
+ = AtomicTV
2
+
3
+ Description goes here.
4
+
5
+ == Contributing to AtomicTV
6
+
7
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
8
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
9
+ * Fork the project
10
+ * Start a feature/bugfix branch
11
+ * Commit and push until you are happy with your contribution
12
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
13
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
14
+
15
+ == Copyright
16
+
17
+ Copyright (c) 2011 Oliver Legg. See LICENSE.txt for
18
+ further details.
19
+
@@ -0,0 +1,50 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'rake'
11
+
12
+ require 'jeweler'
13
+ Jeweler::Tasks.new do |gem|
14
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
15
+ gem.name = "AtomicTV"
16
+ gem.homepage = "http://github.com/olly/AtomicTV"
17
+ gem.license = "MIT"
18
+ gem.summary = %Q{A command line tool to tag MP4 TV shows with metadata pulled from TheTVDB.com.}
19
+ gem.description = %Q{A command line tool to tag MP4 TV shows with metadata pulled from TheTVDB.com. It uses AtomicParsley to process the file.}
20
+ gem.email = "olly@51degrees.net"
21
+ gem.authors = ["Olly Legg"]
22
+ # Include your dependencies below. Runtime dependencies are required when using your gem,
23
+ # and development dependencies are only needed for development (ie running rake tasks, tests, etc)
24
+ # gem.add_runtime_dependency 'jabber4r', '> 0.1'
25
+ # gem.add_development_dependency 'rspec', '> 1.2.3'
26
+ end
27
+ Jeweler::RubygemsDotOrgTasks.new
28
+
29
+ require 'rspec/core'
30
+ require 'rspec/core/rake_task'
31
+ RSpec::Core::RakeTask.new(:spec) do |spec|
32
+ spec.pattern = FileList['spec/**/*_spec.rb']
33
+ end
34
+
35
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
36
+ spec.pattern = 'spec/**/*_spec.rb'
37
+ spec.rcov = true
38
+ end
39
+
40
+ task :default => :spec
41
+
42
+ require 'rake/rdoctask'
43
+ Rake::RDocTask.new do |rdoc|
44
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
45
+
46
+ rdoc.rdoc_dir = 'rdoc'
47
+ rdoc.title = "AtomicTV #{version}"
48
+ rdoc.rdoc_files.include('README*')
49
+ rdoc.rdoc_files.include('lib/**/*.rb')
50
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.1
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env ruby -rubygems
2
+
3
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'AtomicTV'))
4
+
5
+ include AtomicTV
6
+
7
+ def display_error(error)
8
+ $stderr.puts "! ERROR: #{error.human_message}"
9
+ end
10
+
11
+ ARGV.each do |path|
12
+ begin
13
+ file_path = Pathname.new(path)
14
+ metadata = TVDBEpisode.metadata_for_filename(file_path.basename)
15
+ tagger = AtomicParsleyTagger.new(file_path, metadata)
16
+ tagger.run
17
+ puts "* Tagged: #{file_path.basename}"
18
+ rescue AtomicParsleyTagger::AtomicParsleyUnavailable => error
19
+ display_error(error)
20
+ exit 1
21
+ rescue AtomicTVError => error
22
+ display_error(error)
23
+ end
24
+ end
@@ -0,0 +1,15 @@
1
+ require 'fileutils'
2
+ require 'open-uri'
3
+ require 'pathname'
4
+ require 'tmpdir'
5
+
6
+ require 'plist'
7
+ require 'tvdb_party'
8
+
9
+ module AtomicTV
10
+ class AtomicTVError < StandardError; end
11
+ end
12
+
13
+ ['atomic_parsley_tagger', 'episode_metadata', 'filename_parser', 'tvdb_episode'].each do |file|
14
+ require File.expand_path(File.join(File.dirname(__FILE__), 'AtomicTV', file))
15
+ end
@@ -0,0 +1,99 @@
1
+ module AtomicTV
2
+ class AtomicParsleyTagger
3
+
4
+ class AtomicParsleyUnavailable < ::AtomicTV::AtomicTVError
5
+ def human_message
6
+ 'AtomicParsley is not installed or could not be found. Try checking your PATH.'
7
+ end
8
+ end
9
+
10
+ class FileNotFound < ::AtomicTV::AtomicTVError
11
+ def initialize(file_path)
12
+ @file_path = file_path
13
+ end
14
+
15
+ attr_reader :file_path
16
+
17
+ def human_message
18
+ "File not found: #{file_path}"
19
+ end
20
+ end
21
+
22
+ class TaggingError < ::AtomicTV::AtomicTVError
23
+ def initialize(command)
24
+ @command = command
25
+ end
26
+
27
+ attr_reader :command
28
+
29
+ def human_message
30
+ "A tagging error occured: #{command}."
31
+ end
32
+
33
+ end
34
+
35
+ def self.executable
36
+ path = Pathname.new(`which AtomicParsley`.chomp)
37
+ raise AtomicParsleyUnavailable unless path.executable?
38
+ path
39
+ end
40
+
41
+ def initialize(file_path, metadata)
42
+ @file_path = file_path
43
+ raise FileNotFound.new(file_path) unless file_path.exist?
44
+
45
+ @metadata = metadata
46
+ end
47
+
48
+ def cast_metadata
49
+ format_names = lambda {|name| {'name' => name}}
50
+ {
51
+ 'cast' => metadata.actors.map(&format_names),
52
+ 'directors' => metadata.directors.map(&format_names),
53
+ 'screenwriters' => metadata.writers.map(&format_names)
54
+ }.to_plist
55
+ end
56
+
57
+ def run
58
+ options = {
59
+ 'stik' => metadata.media_type,
60
+ 'artist' => metadata.artist,
61
+ 'title' => metadata.title,
62
+ 'album' => metadata.album,
63
+ 'genre' => metadata.genre,
64
+ 'description' => metadata.description,
65
+ 'longdesc' => metadata.long_description,
66
+ 'TVNetwork' => metadata.tv_network,
67
+ 'TVShowName' => metadata.tv_show_name,
68
+ 'TVEpisode' => metadata.tv_episode,
69
+ 'TVSeasonNum' => metadata.tv_season_number,
70
+ 'TVEpisodeNum' => metadata.tv_episode_number,
71
+ 'tracknum' => metadata.track_number,
72
+ 'year' => metadata.air_date
73
+ }
74
+
75
+ metadata.with_loaded_posters do
76
+ command = %Q{#{self.class.executable} }
77
+ command << %Q{"#{file_path}" }
78
+ command << %Q{--overWrite }
79
+ command << %Q{--rDNSatom '#{cast_metadata}' name=iTunMOVI domain=com.apple.iTunes }
80
+ metadata.posters.each do |poster|
81
+ command << %Q{--artwork #{poster.path} }
82
+ end
83
+ command << options.map {|option, value| %Q{--#{option} "#{escape_double_quotes(value)}"}}.join(' ')
84
+
85
+ `#{command}`
86
+ raise TaggingError.new(command) unless $?.success?
87
+ end
88
+ end
89
+
90
+ private
91
+
92
+ attr_reader :file_path, :metadata
93
+
94
+ def escape_double_quotes(str)
95
+ str.gsub('"', '\"')
96
+ end
97
+
98
+ end
99
+ end
@@ -0,0 +1,111 @@
1
+ module AtomicTV
2
+ class EpisodeMetadata
3
+
4
+ ArtworkBaseURL = "http://thetvdb.com/banners/"
5
+
6
+ def initialize(series, episode)
7
+ @series, @episode = series, episode
8
+ end
9
+
10
+ attr_reader :series, :episode
11
+
12
+ def media_type
13
+ 'TV Show'
14
+ end
15
+
16
+ def artist
17
+ series.name
18
+ end
19
+
20
+ def title
21
+ episode.name
22
+ end
23
+
24
+ def album
25
+ "#{series.name}, Season #{episode.season_number}"
26
+ end
27
+
28
+ def genre
29
+ series.genres.first
30
+ end
31
+
32
+ def description
33
+ episode.overview[0,255].gsub(/\.(.*)\Z/, '.')
34
+ end
35
+
36
+ def long_description
37
+ episode.overview
38
+ end
39
+
40
+ def tv_network
41
+ series.network
42
+ end
43
+
44
+ def tv_show_name
45
+ series.name
46
+ end
47
+
48
+ def tv_episode
49
+ "#{episode.season_number}#{episode.number.to_s.rjust(2, '0')}"
50
+ end
51
+
52
+ def tv_season_number
53
+ episode.season_number
54
+ end
55
+
56
+ def tv_episode_number
57
+ episode.number
58
+ end
59
+
60
+ def track_number
61
+ episode.number
62
+ end
63
+
64
+ def air_date
65
+ episode.air_date && episode.air_date.to_s + 'T00:00:00Z'
66
+ end
67
+
68
+ def actors
69
+ (series.actors.map {|a| a.name} + episode.guest_stars).uniq
70
+ end
71
+
72
+ def directors
73
+ parse_names(episode.director)
74
+ end
75
+
76
+ def writers
77
+ parse_names(episode.writer)
78
+ end
79
+
80
+ attr_reader :posters
81
+
82
+ def with_loaded_posters
83
+ temporary_directory = Dir.mktmpdir
84
+
85
+ @posters = series.season_posters(episode.season_number, 'en').map do |poster|
86
+ url = ArtworkBaseURL + poster.path
87
+ file = File.new(File.join(temporary_directory, File.basename(url)), 'w')
88
+ file.write(open(url).read)
89
+ file.close
90
+ file
91
+ end
92
+
93
+ yield
94
+
95
+ ensure
96
+ FileUtils.rm_rf(temporary_directory)
97
+ @posters = nil
98
+ end
99
+
100
+ private
101
+
102
+ def parse_names(str)
103
+ return [] if str.nil?
104
+
105
+ str.gsub!(/\A\|/, '')
106
+ str.gsub!(/\|\Z/, '')
107
+ str.split(/\|+/)
108
+ end
109
+
110
+ end
111
+ end
@@ -0,0 +1,37 @@
1
+ module AtomicTV
2
+ class FilenameParser
3
+
4
+ class InvalidFilename < ::AtomicTV::AtomicTVError
5
+ def initialize(filename)
6
+ @filename = filename
7
+ end
8
+
9
+ attr_reader :filename
10
+
11
+ def human_message
12
+ "Invalid filename: #{filename} (filenames must be in the format: 'Series Name - S01E01')."
13
+ end
14
+ end
15
+
16
+ FilenameFormat = /\A(.*) - S(\d{2})E(\d{2})\.\w{3}\Z/
17
+
18
+ def self.parse(filename)
19
+ if filename.to_s =~ FilenameFormat
20
+ new($1, $2.to_i(10), $3.to_i(10))
21
+ else
22
+ raise InvalidFilename.new(filename)
23
+ end
24
+ end
25
+
26
+ attr_reader :series_name, :season_number, :episode_number
27
+
28
+ private
29
+
30
+ def initialize(series_name, season_number, episode_number)
31
+ @series_name = series_name
32
+ @season_number = season_number
33
+ @episode_number = episode_number
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,46 @@
1
+ module AtomicTV
2
+ class TVDBEpisode
3
+
4
+ class UnknownSeries < ::AtomicTV::AtomicTVError
5
+ def initialize(series_name)
6
+ @series_name = series_name
7
+ end
8
+
9
+ attr_reader :series_name
10
+
11
+ def human_message
12
+ "Unknown TV series: '#{series_name}'"
13
+ end
14
+ end
15
+
16
+ class UnknownEpisode < ::AtomicTV::AtomicTVError
17
+ def initialize(series_name, season_number, episode_number)
18
+ @episode_id = "#{series_name} - S#{season_number.to_s.rjust(2, '0')}E#{episode_number.to_s.rjust(2, '0')}"
19
+ end
20
+
21
+ attr_reader :episode_id
22
+
23
+ def human_message
24
+ "Unknown episode: #{episode_id}"
25
+ end
26
+ end
27
+
28
+ def self.metadata_for_filename(filename)
29
+ parser = FilenameParser.parse(filename)
30
+ search_results = client.search(parser.series_name)
31
+ raise UnknownSeries.new(parser.series_name) if search_results.empty?
32
+
33
+ series = client.get_series_by_id(search_results.first['seriesid'])
34
+ episode = client.get_episode(series, parser.season_number, parser.episode_number)
35
+ raise UnknownEpisode.new(series.name, parser.season_number, parser.episode_number) if episode.nil?
36
+ return EpisodeMetadata.new(series, episode)
37
+ end
38
+
39
+ private
40
+
41
+ def self.client
42
+ @client ||= TvdbParty::Search.new('BD90B148E7D9E897', 'en')
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,60 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
2
+
3
+ describe AtomicTV::AtomicParsleyTagger do
4
+
5
+ describe ".executable" do
6
+
7
+ before(:each) do
8
+ stub!(:`).and_return('')
9
+ end
10
+
11
+ context "with AtomicParsley installed" do
12
+
13
+ let(:executable_path) { stub(:executable? => true) }
14
+
15
+ before(:each) do
16
+ Pathname.stub!(:new).and_return(executable_path)
17
+ end
18
+
19
+ it "should return a pathname with executable's location" do
20
+ path = AtomicTV::AtomicParsleyTagger.executable
21
+ path.should == executable_path
22
+ end
23
+
24
+ end
25
+
26
+ context "without AtomicParsley installed" do
27
+
28
+ before(:each) do
29
+ Pathname.stub!(:new).and_return(stub(:executable? => false))
30
+ end
31
+
32
+ it "should raise a AtomicParsleyUnavailable error" do
33
+ expect {
34
+ AtomicTV::AtomicParsleyTagger.executable
35
+ }.to raise_error(AtomicTV::AtomicParsleyTagger::AtomicParsleyUnavailable)
36
+ end
37
+
38
+ end
39
+
40
+ end
41
+
42
+ describe "#cast_metadata" do
43
+
44
+ let(:file_path) { stub(:exist? => true) }
45
+
46
+ let(:metadata) do
47
+ stub(
48
+ :actors => ['Actor 1', 'Actor 2'],
49
+ :directors => ['Director 1', 'Director 2'],
50
+ :writers => ['Writer 1', 'Writer 2']
51
+ )
52
+ end
53
+
54
+ it "should generate a plist string from the actors, directors and writers" do
55
+ tagger = AtomicTV::AtomicParsleyTagger.new(file_path, metadata)
56
+ tagger.cast_metadata.should == "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>cast</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>name</key>\n\t\t\t<string>Actor 1</string>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>name</key>\n\t\t\t<string>Actor 2</string>\n\t\t</dict>\n\t</array>\n\t<key>directors</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>name</key>\n\t\t\t<string>Director 1</string>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>name</key>\n\t\t\t<string>Director 2</string>\n\t\t</dict>\n\t</array>\n\t<key>screenwriters</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>name</key>\n\t\t\t<string>Writer 1</string>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>name</key>\n\t\t\t<string>Writer 2</string>\n\t\t</dict>\n\t</array>\n</dict>\n</plist>\n"
57
+ end
58
+ end
59
+
60
+ end
@@ -0,0 +1,147 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
2
+
3
+ describe AtomicTV::EpisodeMetadata do
4
+
5
+ let(:series) do
6
+ stub('Mock Series',
7
+ :name => 'House', :network => 'FOX',
8
+ :genres => ['Drama'],
9
+ :artwork => [
10
+ stub('Artwork 1', :path => 'seasons/12345-7-1.jpg'),
11
+ stub('Artwork 2', :path => 'seasons/12345-7-2.jpg')],
12
+ :actors => [
13
+ stub('Actor 1', :name => 'Hugh Laurie'),
14
+ stub('Actor 2', :name => 'Olivia Wilde'),
15
+ stub('Actor 3', :name => 'Amber Tamblyn')]
16
+ )
17
+ end
18
+
19
+ let(:episode) do
20
+ stub('Mock Episode',
21
+ :name => 'Last Temptation',
22
+ :season_number => '7',
23
+ :number => '19',
24
+ :overview => %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.},
25
+ :air_date => Date.new(2011, 4, 18),
26
+ :guest_stars => ['Amber Tamblyn', 'Ron Perkins', 'Jennifer Landon'],
27
+ :director => 'Tim Southam',
28
+ :writer => '|David Foster|Liz Friedman|'
29
+ )
30
+ end
31
+
32
+ let(:metadata) { AtomicTV::EpisodeMetadata.new(series, episode) }
33
+
34
+ context "with complete show information" do
35
+
36
+ it "should return 'TV Show' for media_type" do
37
+ metadata.media_type.should == 'TV Show'
38
+ end
39
+
40
+ it "should return the series name for artist" do
41
+ metadata.artist.should == 'House'
42
+ end
43
+
44
+ it "should return the episode name for title" do
45
+ metadata.title.should == 'Last Temptation'
46
+ end
47
+
48
+ it "should return the series name and season for album" do
49
+ metadata.album.should == 'House, Season 7'
50
+ end
51
+
52
+ it "should return the first genre for genre" do
53
+ metadata.genre.should == 'Drama'
54
+ end
55
+
56
+ it "should return the first 255 characters, trucated to a complete sentence, of the overview for description" do
57
+ 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.'
58
+ end
59
+
60
+ it "should return the full overview for long description" do
61
+ 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.}
62
+ end
63
+
64
+ it "should return the series network for tv network" do
65
+ metadata.tv_network.should == 'FOX'
66
+ end
67
+
68
+ it "should return the series name for tv show name" do
69
+ metadata.tv_show_name.should == 'House'
70
+ end
71
+
72
+ it "should return the series number and episode number for tv episode" do
73
+ metadata.tv_episode.should == '719'
74
+ end
75
+
76
+ it "should return the season number for tv season number" do
77
+ metadata.tv_season_number.should == '7'
78
+ end
79
+
80
+ it "should return the episode number for tv episode number" do
81
+ metadata.tv_episode_number.should == '19'
82
+ end
83
+
84
+ it "should return the episode number for track number" do
85
+ metadata.track_number.should == '19'
86
+ end
87
+
88
+ it "should return a date with timezone for air date" do
89
+ metadata.air_date.should == '2011-04-18T00:00:00Z'
90
+ end
91
+
92
+ it "should return series cast and guest cast for actors" do
93
+ metadata.actors.should =~ ['Hugh Laurie', 'Olivia Wilde', 'Amber Tamblyn', 'Ron Perkins', 'Jennifer Landon']
94
+ end
95
+
96
+ it "should return the episode's directors for directors" do
97
+ metadata.directors.should =~ ['Tim Southam']
98
+ end
99
+
100
+ it "should return the episode's writers for writers" do
101
+ metadata.writers.should =~ ['David Foster', 'Liz Friedman']
102
+ end
103
+
104
+ end
105
+
106
+ context "with a missing air date" do
107
+
108
+ before(:each) do
109
+ episode.stub!(:air_date).and_return(nil)
110
+ end
111
+
112
+ it "should return nil for air_date" do
113
+ metadata.air_date.should be_nil
114
+ end
115
+
116
+ end
117
+
118
+ context "with missing artwork" do
119
+
120
+ end
121
+
122
+ describe "#description" do
123
+ pending "more logic"
124
+ end
125
+
126
+ describe "#tv_episode" do
127
+
128
+ it "should format the episode number to two digits" do
129
+ episode.stub!(:number).and_return('2')
130
+ metadata.tv_episode.should == '702'
131
+ end
132
+
133
+ end
134
+
135
+ describe "#directors" do
136
+ pending "more logic"
137
+ end
138
+
139
+ describe "#writers" do
140
+ pending "more logic"
141
+ end
142
+
143
+ describe "#with_loaded_posters" do
144
+ pending
145
+ end
146
+
147
+ end
@@ -0,0 +1,46 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
2
+
3
+ describe AtomicTV::FilenameParser do
4
+
5
+ describe ".parse" do
6
+
7
+ context "with valid filenames" do
8
+
9
+ let(:parser) { AtomicTV::FilenameParser.parse('Battlestar Galactica (2003) - S04E20.m4v') }
10
+
11
+ it "should extract the series name" do
12
+ parser.series_name.should == 'Battlestar Galactica (2003)'
13
+ end
14
+
15
+ it "should extract the season number" do
16
+ parser.season_number.should == 4
17
+ end
18
+
19
+ it "should extract the episode number" do
20
+ parser.episode_number.should == 20
21
+ end
22
+ end
23
+
24
+ context "with invalid filenames" do
25
+
26
+ it "should raise an AtomicTV::FilenameParser::InvalidFilename error" do
27
+ expect {
28
+ AtomicTV::FilenameParser.parse('V for Vendetta (2006).m4v')
29
+ }.to raise_error(AtomicTV::FilenameParser::InvalidFilename)
30
+ end
31
+
32
+ it "should expose the invalid filename in the error" do
33
+ invalid_filename = 'V for Vendetta (2006).m4v'
34
+
35
+ begin
36
+ AtomicTV::FilenameParser.parse(invalid_filename)
37
+ rescue AtomicTV::FilenameParser::InvalidFilename => error
38
+ error.filename.should == invalid_filename
39
+ end
40
+ end
41
+
42
+ end
43
+
44
+ end
45
+
46
+ end
@@ -0,0 +1,98 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
2
+
3
+ WebMock.disable_net_connect!
4
+
5
+ describe AtomicTV::TVDBEpisode do
6
+
7
+ describe ".metadata_for_filename" do
8
+
9
+ context "with a correct series, season and episode" do
10
+
11
+ before(:each) do
12
+ @mock_parser = stub('Mock Parser', :series_name => 'The Wire', :season_number => 3, :episode_number => 6)
13
+ AtomicTV::FilenameParser.stub!(:parse).and_return(@mock_parser)
14
+
15
+ @result1 = stub('Mock Search Result - 1', :[] => '123456789')
16
+ @result2 = stub('Mock Search Result - 2', :[] => '987654321')
17
+ @episode = stub('Mock Episode')
18
+ @mock_client = stub('Mock Client', :search => [@result1, @result2], :get_series_by_id => @result1, :get_episode => @episode)
19
+ AtomicTV::TVDBEpisode.stub!(:client).and_return(@mock_client)
20
+ end
21
+
22
+ it "should search for the series using the name" do
23
+ @mock_client.should_receive(:search).with('The Wire')
24
+
25
+ AtomicTV::TVDBEpisode.metadata_for_filename('The Wire - S03E06.m4v')
26
+ end
27
+
28
+ it "should use the first search result as the series" do
29
+ @mock_client.should_receive(:get_series_by_id).with('123456789')
30
+
31
+ AtomicTV::TVDBEpisode.metadata_for_filename('The Wire - S03E06.m4v')
32
+ end
33
+
34
+ it "should return the correct episode for the series" do
35
+ @mock_client.should_receive(:get_episode).with(@result1, 3, 6)
36
+
37
+ AtomicTV::TVDBEpisode.metadata_for_filename('The Wire - S03E06.m4v')
38
+ end
39
+
40
+ it "should return an EpisodeMetadata instance for the episode" do
41
+ metadata = AtomicTV::TVDBEpisode.metadata_for_filename('The Wire - S03E06.m4v')
42
+ metadata.should be_kind_of(AtomicTV::EpisodeMetadata)
43
+ metadata.series.should == @result1
44
+ metadata.episode.should == @episode
45
+ end
46
+
47
+ end
48
+
49
+ context "with an unknown series" do
50
+
51
+ before(:each) do
52
+ @mock_client = stub('Mock Client', :search => [])
53
+ AtomicTV::TVDBEpisode.stub!(:client).and_return(@mock_client)
54
+ end
55
+
56
+ it "should raise an AtomicTV::TVDBEpisode::UnknownSeries error" do
57
+ expect {
58
+ AtomicTV::TVDBEpisode.metadata_for_filename('Teh Wires - S01E01.m4v')
59
+ }.to raise_error(AtomicTV::TVDBEpisode::UnknownSeries)
60
+ end
61
+
62
+ it "should expose the invalid series name in the error" do
63
+ begin
64
+ AtomicTV::TVDBEpisode.metadata_for_filename('Teh Wires - S01E01.m4v')
65
+ rescue AtomicTV::TVDBEpisode::UnknownSeries => error
66
+ error.series_name.should == 'Teh Wires'
67
+ end
68
+ end
69
+
70
+ end
71
+
72
+ context "with an unknown episode" do
73
+
74
+ before(:each) do
75
+ @series = stub('Mock Series', :[] => '1234789', :name => 'The Wire')
76
+ @mock_client = stub('Mock Client', :search => [@series], :get_series_by_id => @series, :get_episode => nil)
77
+ AtomicTV::TVDBEpisode.stub!(:client).and_return(@mock_client)
78
+ end
79
+
80
+ it "should raise an AtomicTV::TVDBEpisode::UnknownEpisode error" do
81
+ expect {
82
+ AtomicTV::TVDBEpisode.metadata_for_filename('The Wire - S01E99.m4v')
83
+ }.to raise_error(AtomicTV::TVDBEpisode::UnknownEpisode)
84
+ end
85
+
86
+ it "should expose the invalid episode ID in the error" do
87
+ begin
88
+ AtomicTV::TVDBEpisode.metadata_for_filename('The Wire - S01E99.m4v')
89
+ rescue AtomicTV::TVDBEpisode::UnknownEpisode => error
90
+ error.episode_id.should == 'The Wire - S01E99'
91
+ end
92
+ end
93
+
94
+ end
95
+
96
+ end
97
+
98
+ end
@@ -0,0 +1,13 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rspec'
4
+ require 'webmock/rspec'
5
+ require 'AtomicTV'
6
+
7
+ # Requires supporting files with custom matchers and macros, etc,
8
+ # in ./support/ and its subdirectories.
9
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
10
+
11
+ RSpec.configure do |config|
12
+
13
+ end
metadata ADDED
@@ -0,0 +1,198 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: AtomicTV
3
+ version: !ruby/object:Gem::Version
4
+ hash: 21
5
+ prerelease:
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - 1
10
+ version: 1.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Olly Legg
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-06-01 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ version_requirements: &id001 !ruby/object:Gem::Requirement
22
+ none: false
23
+ requirements:
24
+ - - "="
25
+ - !ruby/object:Gem::Version
26
+ hash: 3
27
+ segments:
28
+ - 3
29
+ - 1
30
+ - 0
31
+ version: 3.1.0
32
+ name: plist
33
+ prerelease: false
34
+ type: :runtime
35
+ requirement: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ version_requirements: &id002 !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - "="
41
+ - !ruby/object:Gem::Version
42
+ hash: 7
43
+ segments:
44
+ - 0
45
+ - 6
46
+ - 0
47
+ version: 0.6.0
48
+ name: tvdb_party
49
+ prerelease: false
50
+ type: :runtime
51
+ requirement: *id002
52
+ - !ruby/object:Gem::Dependency
53
+ version_requirements: &id003 !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ~>
57
+ - !ruby/object:Gem::Version
58
+ hash: 3
59
+ segments:
60
+ - 2
61
+ - 3
62
+ - 0
63
+ version: 2.3.0
64
+ name: rspec
65
+ prerelease: false
66
+ type: :development
67
+ requirement: *id003
68
+ - !ruby/object:Gem::Dependency
69
+ version_requirements: &id004 !ruby/object:Gem::Requirement
70
+ none: false
71
+ requirements:
72
+ - - ~>
73
+ - !ruby/object:Gem::Version
74
+ hash: 23
75
+ segments:
76
+ - 1
77
+ - 0
78
+ - 0
79
+ version: 1.0.0
80
+ name: bundler
81
+ prerelease: false
82
+ type: :development
83
+ requirement: *id004
84
+ - !ruby/object:Gem::Dependency
85
+ version_requirements: &id005 !ruby/object:Gem::Requirement
86
+ none: false
87
+ requirements:
88
+ - - ~>
89
+ - !ruby/object:Gem::Version
90
+ hash: 7
91
+ segments:
92
+ - 1
93
+ - 5
94
+ - 2
95
+ version: 1.5.2
96
+ name: jeweler
97
+ prerelease: false
98
+ type: :development
99
+ requirement: *id005
100
+ - !ruby/object:Gem::Dependency
101
+ version_requirements: &id006 !ruby/object:Gem::Requirement
102
+ none: false
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ hash: 3
107
+ segments:
108
+ - 0
109
+ version: "0"
110
+ name: rcov
111
+ prerelease: false
112
+ type: :development
113
+ requirement: *id006
114
+ - !ruby/object:Gem::Dependency
115
+ version_requirements: &id007 !ruby/object:Gem::Requirement
116
+ none: false
117
+ requirements:
118
+ - - ~>
119
+ - !ruby/object:Gem::Version
120
+ hash: 11
121
+ segments:
122
+ - 1
123
+ - 6
124
+ - 2
125
+ version: 1.6.2
126
+ name: webmock
127
+ prerelease: false
128
+ type: :development
129
+ requirement: *id007
130
+ description: A command line tool to tag MP4 TV shows with metadata pulled from TheTVDB.com. It uses AtomicParsley to process the file.
131
+ email: olly@51degrees.net
132
+ executables:
133
+ - AtomicTV
134
+ extensions: []
135
+
136
+ extra_rdoc_files:
137
+ - LICENSE.txt
138
+ - README.rdoc
139
+ files:
140
+ - .document
141
+ - .rspec
142
+ - AtomicTV.gemspec
143
+ - Gemfile
144
+ - Gemfile.lock
145
+ - LICENSE.txt
146
+ - README.rdoc
147
+ - Rakefile
148
+ - VERSION
149
+ - bin/AtomicTV
150
+ - lib/AtomicTV.rb
151
+ - lib/AtomicTV/atomic_parsley_tagger.rb
152
+ - lib/AtomicTV/episode_metadata.rb
153
+ - lib/AtomicTV/filename_parser.rb
154
+ - lib/AtomicTV/tvdb_episode.rb
155
+ - spec/AtomicTV/atomic_parsley_tagger_spec.rb
156
+ - spec/AtomicTV/episode_metadata_spec.rb
157
+ - spec/AtomicTV/filename_parser_spec.rb
158
+ - spec/AtomicTV/tvdb_episode_spec.rb
159
+ - spec/spec_helper.rb
160
+ homepage: http://github.com/olly/AtomicTV
161
+ licenses:
162
+ - MIT
163
+ post_install_message:
164
+ rdoc_options: []
165
+
166
+ require_paths:
167
+ - lib
168
+ required_ruby_version: !ruby/object:Gem::Requirement
169
+ none: false
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ hash: 3
174
+ segments:
175
+ - 0
176
+ version: "0"
177
+ required_rubygems_version: !ruby/object:Gem::Requirement
178
+ none: false
179
+ requirements:
180
+ - - ">="
181
+ - !ruby/object:Gem::Version
182
+ hash: 3
183
+ segments:
184
+ - 0
185
+ version: "0"
186
+ requirements: []
187
+
188
+ rubyforge_project:
189
+ rubygems_version: 1.7.2
190
+ signing_key:
191
+ specification_version: 3
192
+ summary: A command line tool to tag MP4 TV shows with metadata pulled from TheTVDB.com.
193
+ test_files:
194
+ - spec/AtomicTV/atomic_parsley_tagger_spec.rb
195
+ - spec/AtomicTV/episode_metadata_spec.rb
196
+ - spec/AtomicTV/filename_parser_spec.rb
197
+ - spec/AtomicTV/tvdb_episode_spec.rb
198
+ - spec/spec_helper.rb