tv-data-api-clients 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/.gitignore +5 -0
  2. data/Gemfile +4 -0
  3. data/Rakefile +11 -0
  4. data/lib/base.rb +2 -0
  5. data/lib/base/api/episode.rb +104 -0
  6. data/lib/base/api/show.rb +132 -0
  7. data/lib/core_ext/object.rb +15 -0
  8. data/lib/helpers.rb +22 -0
  9. data/lib/the_tv_db.rb +23 -0
  10. data/lib/the_tv_db/api/episode.rb +56 -0
  11. data/lib/the_tv_db/api/episode_list.rb +28 -0
  12. data/lib/the_tv_db/api/image.rb +114 -0
  13. data/lib/the_tv_db/api/image_list.rb +35 -0
  14. data/lib/the_tv_db/api/image_list_helpers.rb +32 -0
  15. data/lib/the_tv_db/api/mirror.rb +13 -0
  16. data/lib/the_tv_db/api/mirrors.rb +11 -0
  17. data/lib/the_tv_db/api/show.rb +125 -0
  18. data/lib/the_tv_db/configuration.rb +7 -0
  19. data/lib/tv_data_api_clients.rb +12 -0
  20. data/lib/tv_data_api_clients/version.rb +3 -0
  21. data/lib/tv_rage.rb +7 -0
  22. data/lib/tv_rage/show.rb +81 -0
  23. data/test/fixtures/example.gif +0 -0
  24. data/test/fixtures/the_tv_db/api_get_series_lost.xml +24 -0
  25. data/test/fixtures/the_tv_db/api_series_12345_all_en.xml +9 -0
  26. data/test/fixtures/the_tv_db/api_series_73739_banners.xml +157 -0
  27. data/test/fixtures/the_tv_db/api_series_73739_en.xml +30 -0
  28. data/test/fixtures/tv_rage/feeds_search_show.xml +11 -0
  29. data/test/fixtures/tv_rage/feeds_showinfo_sid_7926.xml +28 -0
  30. data/test/support/shared_episode_naming_tests.rb +90 -0
  31. data/test/support/shared_show_naming_tests.rb +29 -0
  32. data/test/test_helper.rb +28 -0
  33. data/test/unit/base/api/episode_test.rb +11 -0
  34. data/test/unit/base/api/show_test.rb +11 -0
  35. data/test/unit/the_tv_db/api/episode_list_test.rb +15 -0
  36. data/test/unit/the_tv_db/api/episode_test.rb +93 -0
  37. data/test/unit/the_tv_db/api/image_list_test.rb +42 -0
  38. data/test/unit/the_tv_db/api/image_test.rb +35 -0
  39. data/test/unit/the_tv_db/api/mirrors_test.rb +17 -0
  40. data/test/unit/the_tv_db/api/show_test.rb +57 -0
  41. data/test/unit/the_tv_db/configuration_test.rb +28 -0
  42. data/test/unit/tv_rage/show_test.rb +55 -0
  43. data/tv-data-api-clients.gemspec +29 -0
  44. metadata +238 -0
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .DS_Store
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in TheTvDb.gemspec
4
+ gemspec
@@ -0,0 +1,11 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ require 'rake/testtask'
4
+
5
+ task :default => ['test:units']
6
+
7
+ Rake::TestTask.new('test:units') do |t|
8
+ t.libs << "test"
9
+ t.test_files = FileList['test/unit/**/*_test.rb']
10
+ t.verbose = true
11
+ end
@@ -0,0 +1,2 @@
1
+ require 'base/api/episode'
2
+ require 'base/api/show'
@@ -0,0 +1,104 @@
1
+ module Base
2
+ module Api
3
+ class Episode
4
+
5
+ include Helpers
6
+
7
+ attr_accessor :xml_doc
8
+
9
+ def initialize(xml_document)
10
+ @xml_doc = xml_document
11
+ self
12
+ end
13
+
14
+ def to_s
15
+ "#{self.class} #{name || ('Untitled')}"
16
+ end
17
+
18
+ def number; end
19
+ def overview; end
20
+ def network_id; end
21
+ def original_name; end
22
+ def absolute_number; end
23
+
24
+ def usable?
25
+ number.present?
26
+ end
27
+
28
+ def name
29
+ if original_name_looks_like_season_or_episode_number?
30
+ return nil
31
+ elsif original_name_container_bracketed_digits?
32
+ return_name = original_name.gsub(/\(\d{2}\)/, '')
33
+ elsif original_name_looks_like_date?
34
+ return nil
35
+ elsif original_name_is_quoted_string?
36
+ return_name = original_name[1..-2]
37
+ elsif original_name_has_more_symbols_and_whitespace_than_letters?
38
+ return nil
39
+ elsif original_name_is_nonsense_edgecase?
40
+ return nil
41
+ end
42
+ (return_name.present? ? return_name : original_name).strip
43
+ end
44
+
45
+ def air_dates; []; end
46
+ def network_ids; []; end
47
+
48
+ def attributes
49
+ {
50
+ :name => name,
51
+ :number => number,
52
+ :season_number => season_number,
53
+ :absolute_number => absolute_number,
54
+ }
55
+ end
56
+
57
+ private
58
+
59
+ def xpath(path)
60
+ xml_doc.xpath(path).try(:first).try(:content).try(:strip)
61
+ end
62
+
63
+ def original_name_container_bracketed_digits?
64
+ original_name =~ /\(\d{2}\)/
65
+ end
66
+
67
+ def original_name_looks_like_season_or_episode_number?
68
+ on = original_name
69
+ # Includes both season/series and episode
70
+ if on =~ /(?:season|series)/i && on =~ /episode/i
71
+ return true
72
+ elsif on =~ /\A[eE]\d+\z/
73
+ return true
74
+ elsif on =~ /(?:season|series)/i || on =~ /episode/i
75
+ original_string_length = on.length
76
+ stripped_episode_name = on.gsub(/(?:series|season|episode|\s+)/i, '')
77
+ return (original_string_length >= (stripped_episode_name.length * 2.5))
78
+ end
79
+ false
80
+ end
81
+
82
+ def original_name_looks_like_date?
83
+ parsable_date?(original_name)
84
+ end
85
+
86
+ def original_name_has_more_symbols_and_whitespace_than_letters?
87
+ symbols = original_name.match(/(\W+)/).to_a.join
88
+ letters = original_name.match(/(\w+)/).to_a.join
89
+ symbols.length > letters.length
90
+ end
91
+
92
+ def original_name_is_quoted_string?
93
+ ona = original_name.chars.to_a
94
+ ona.first == '"' && ona.last == '"'
95
+ end
96
+
97
+ def original_name_is_nonsense_edgecase?
98
+ original_name == 'no info'
99
+ end
100
+
101
+ end
102
+ end
103
+ end
104
+
@@ -0,0 +1,132 @@
1
+ require 'nokogiri'
2
+
3
+ module Base
4
+ module Api
5
+
6
+ UnprocessableXMLError = Class.new(RuntimeError)
7
+ RemoteDocumentRetrievalError = Class.new(RuntimeError)
8
+
9
+ class Show
10
+
11
+ extend Helpers
12
+ include Helpers
13
+
14
+ attr_accessor :xml_doc, :network
15
+
16
+ def initialize(xml_document)
17
+ @xml_doc = xml_document
18
+ self
19
+ end
20
+
21
+ def to_s
22
+ "#{self.class} #{name}"
23
+ end
24
+
25
+ #
26
+ # Returns a list of attribute hashes, from the Show
27
+ # calls internal lookup methods, always a hash
28
+ #
29
+ def attributes
30
+ {
31
+ name: name,
32
+ country: country,
33
+ started: started,
34
+ overview: overview,
35
+ status: status
36
+ }
37
+ end
38
+
39
+ def ==(other)
40
+ attributes == other.attributes
41
+ end
42
+
43
+ def name
44
+ if original_name_contains_bracketed_year?
45
+ original_name.gsub(/\(\d{4}\)/, '').strip
46
+ elsif original_name_contains_bracketed_country?
47
+ original_name.gsub(/\([A-Z]{2,3}\)/, '').strip
48
+ else
49
+ original_name
50
+ end
51
+ end
52
+
53
+ def actors; []; end
54
+ def air_dates; []; end
55
+ def aliases; []; end
56
+ def genres; []; end
57
+ def remote_ids; []; end
58
+ def status; []; end
59
+
60
+ def classification; nil; end
61
+ def country; nil; end
62
+ def language; nil; end
63
+ def network; nil; end
64
+ def original_name; nil; end
65
+ def overview; nil; end
66
+ def runtime; nil; end
67
+ def started; nil; end
68
+
69
+ def network_id; raise NotImplementedErorr; end
70
+
71
+ private
72
+
73
+ def xpath(path)
74
+ xml_doc.xpath(path).try(:first).try(:content).try(:strip)
75
+ end
76
+
77
+ def original_name_contains_bracketed_year?
78
+ original_name =~ /\((\d{4})\)/
79
+ end
80
+
81
+ def original_name_contains_bracketed_country?
82
+ original_name =~ /\([A-Z]{2,3}\)/
83
+ end
84
+
85
+ class << self
86
+
87
+ def find(name_or_id)
88
+ if name_or_id.is_a?(String)
89
+ find_by_name(name_or_id)
90
+ elsif name_or_id.is_a?(Integer)
91
+ find_by_id(name_or_id)
92
+ end
93
+ end
94
+
95
+ def xml_string(series_id)
96
+ get_remote_file_as_string(url_for(series_id))
97
+ end
98
+
99
+ def search_xml_string(series_name)
100
+ get_remote_file_as_string(search_url_for(series_name))
101
+ end
102
+
103
+ def xml_document(series_id)
104
+ if string = xml_string(series_id)
105
+ document = Nokogiri::XML(string, 'UTF-8')
106
+ if not document.root.nil?
107
+ document
108
+ else
109
+ raise UnprocessableXMLError
110
+ end
111
+ else
112
+ raise RemoteDocumentRetrievalError
113
+ end
114
+ end
115
+
116
+ def search_xml_document(series_name)
117
+ if string = search_xml_string(series_name)
118
+ document = Nokogiri::XML(string, 'UTF-8')
119
+ if not document.root.nil?
120
+ document
121
+ else
122
+ raise UnprocessableXMLError
123
+ end
124
+ else
125
+ raise RemoteDocumentRetrivalError
126
+ end
127
+ end
128
+
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,15 @@
1
+ class Object
2
+
3
+ def try(method, *args)
4
+ send(method, *args) if respond_to? method
5
+ end
6
+
7
+ def blank?
8
+ respond_to?(:empty?) ? empty? : !self
9
+ end
10
+
11
+ def present?
12
+ !blank?
13
+ end
14
+
15
+ end
@@ -0,0 +1,22 @@
1
+ require 'open-uri'
2
+
3
+ module Helpers
4
+
5
+ def parsable_date?(date = nil)
6
+ return false unless date
7
+ !Date._parse(date).empty?
8
+ end
9
+
10
+ def safely_parse_date(date)
11
+ begin
12
+ return Date.parse(date)
13
+ rescue TypeError, ArgumentError
14
+ return nil
15
+ end
16
+ end
17
+
18
+ def get_remote_file_as_string(url)
19
+ Kernel.open(url)
20
+ end
21
+
22
+ end
@@ -0,0 +1,23 @@
1
+ require 'the_tv_db/configuration'
2
+
3
+ require 'the_tv_db/api/episode'
4
+ require 'the_tv_db/api/episode_list'
5
+ require 'the_tv_db/api/image'
6
+ require 'the_tv_db/api/image_list'
7
+ require 'the_tv_db/api/image_list_helpers'
8
+ require 'the_tv_db/api/mirror'
9
+ require 'the_tv_db/api/mirrors'
10
+ require 'the_tv_db/api/show'
11
+
12
+ module TheTvDb
13
+
14
+ class << self
15
+ attr_accessor :configuration
16
+ end
17
+
18
+ def self.configure
19
+ self.configuration ||= Configuration.new
20
+ yield(configuration)
21
+ end
22
+
23
+ end
@@ -0,0 +1,56 @@
1
+ module TheTvDb
2
+ module Api
3
+ class Episode < Base::Api::Episode
4
+
5
+ def network_id
6
+ xpath('.//id').try(:to_i)
7
+ end
8
+
9
+ def season_number
10
+ sn = xpath('.//SeasonNumber').try(:to_i)
11
+ (sn.nil? || sn == 0) ? nil : sn
12
+ end
13
+
14
+ def number
15
+ xpath('.//EpisodeNumber').try(:to_i)
16
+ end
17
+
18
+ def overview
19
+ xpath('.//Overview')
20
+ end
21
+
22
+ def original_name
23
+ xpath('.//EpisodeName')
24
+ end
25
+
26
+ def rating
27
+ r = xpath('.//Rating')
28
+ if r.to_f > 0
29
+ return (r.to_f * 10).to_i
30
+ end
31
+ end
32
+
33
+ def number_of_ratings
34
+ xpath('.//RatingCount').try(:to_i)
35
+ end
36
+
37
+ def air_dates
38
+ [ Date.parse(xpath('.//FirstAired'))] rescue []
39
+ end
40
+
41
+ def banner_path
42
+ # See footnotes
43
+ # http://thetvdb.com/wiki/index.php?title=Programmers_API#Episode_Image_Notes
44
+ if (e = xpath('.//EpImgFlag').try(:to_i)) && e <= 2 && i = xpath('.//filename')
45
+ return "#{TheTvDb::Api::Mirrors.all.first}/banners/#{i}" unless i.blank?
46
+ end
47
+ end
48
+
49
+ def absolute_number
50
+ an = xpath('.//absolute_number')
51
+ an.blank? ? nil : an
52
+ end
53
+
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,28 @@
1
+ require 'nokogiri'
2
+
3
+ module TheTvDb
4
+ module Api
5
+ class EpisodeList
6
+ extend Helpers
7
+ class << self
8
+ def get_for(series_id)
9
+ return [] if series_id.nil?
10
+ episode_list_xml_document(series_id).xpath('//Episode').collect do |node|
11
+ TheTvDb::Api::Episode.new(node.unlink)
12
+ end
13
+ end
14
+ private
15
+ def episode_list_xml_document(series_id)
16
+ @cached_document_objects ||= Hash.new
17
+ @cached_document_objects[series_id] = Nokogiri::XML(episode_list_xml_string(series_id), 'UTF-8')
18
+ end
19
+ def episode_list_xml_string(series_id)
20
+ get_remote_file_as_string(episode_list_url(series_id))
21
+ end
22
+ def episode_list_url(series_id)
23
+ "#{Mirrors.all.first}/api/#{TheTvDb.configuration.api_key}/series/#{series_id}/all/en.xml"
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,114 @@
1
+ require 'open-uri'
2
+ require 'fastimage'
3
+
4
+ module TheTvDb
5
+ module Api
6
+ class Image
7
+
8
+ attr_accessor :xml_node
9
+
10
+ def initialize(input_xml_node)
11
+ @xml_node = input_xml_node
12
+ self
13
+ end
14
+
15
+ def rating
16
+ xpath('.//Rating').try(:to_f).try(:round, 1)
17
+ end
18
+
19
+ def type
20
+ xpath('.//BannerType').try(:to_sym)
21
+ end
22
+
23
+ def language
24
+ xpath('.//Language').try(:to_sym)
25
+ end
26
+
27
+ def sub_type
28
+ xpath('.//BannerType2').try(:to_sym) unless subtype_looks_like_dimensions?
29
+ end
30
+
31
+ def path
32
+ "#{TheTvDb::Api::Mirrors.all.first}/banners/#{xpath('.//BannerPath')}"
33
+ end
34
+
35
+ def network_id
36
+ xpath('.//id').to_i
37
+ end
38
+
39
+ def widescreen?
40
+ aspect && (aspect[0].to_f / aspect[1] >= (16.to_f / 9 ))
41
+ end
42
+
43
+ def pixels
44
+ (width * height).to_i rescue 0
45
+ end
46
+
47
+ def width
48
+ if subtype_looks_like_dimensions?
49
+ width_from_subtype
50
+ elsif can_read_image_file?
51
+ image_geometry[:width]
52
+ end
53
+ end
54
+
55
+ def height
56
+ if subtype_looks_like_dimensions?
57
+ height_from_subtype
58
+ elsif can_read_image_file?
59
+ image_geometry[:height]
60
+ end
61
+ end
62
+
63
+ def aspect
64
+ if height and width
65
+ gcd = height.gcd(width)
66
+ [(width / gcd), (height / gcd)]
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def image_geometry
73
+ unless @image_geometry
74
+ img = FastImage.size(path)
75
+ return @image_geometry = {:width => img[0], :height => img[1]}
76
+ end
77
+ @image_geometry
78
+ end
79
+
80
+ def can_read_image_file?
81
+ #
82
+ # File.readable doesn't exist?
83
+ #
84
+ File.readable?(path)
85
+ end
86
+
87
+ def subtype_looks_like_dimensions?
88
+ xpath('.//BannerType2').match /\A(\d+)x(\d+)\z/
89
+ end
90
+
91
+ def width_from_subtype
92
+ if xpath('.//BannerType2').match /\A(\d+)x\d+\z/
93
+ $1.to_i
94
+ end
95
+ end
96
+
97
+ def height_from_subtype
98
+ if xpath('.//BannerType2').match /\A\d+x(\d+)\z/
99
+ $1.to_i
100
+ end
101
+ end
102
+
103
+ def xpath(xpath_query)
104
+ pah = xml_node.xpath(xpath_query)
105
+ if pah.length == 1
106
+ pah.try(:first).try(:content).try(:strip)
107
+ else
108
+ raise RuntimeError, "Unexpected Number of XML Entities (got #{pah.length}, expected 1)"
109
+ end
110
+ end
111
+
112
+ end
113
+ end
114
+ end