tv-data-api-clients 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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