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.
- data/.gitignore +5 -0
- data/Gemfile +4 -0
- data/Rakefile +11 -0
- data/lib/base.rb +2 -0
- data/lib/base/api/episode.rb +104 -0
- data/lib/base/api/show.rb +132 -0
- data/lib/core_ext/object.rb +15 -0
- data/lib/helpers.rb +22 -0
- data/lib/the_tv_db.rb +23 -0
- data/lib/the_tv_db/api/episode.rb +56 -0
- data/lib/the_tv_db/api/episode_list.rb +28 -0
- data/lib/the_tv_db/api/image.rb +114 -0
- data/lib/the_tv_db/api/image_list.rb +35 -0
- data/lib/the_tv_db/api/image_list_helpers.rb +32 -0
- data/lib/the_tv_db/api/mirror.rb +13 -0
- data/lib/the_tv_db/api/mirrors.rb +11 -0
- data/lib/the_tv_db/api/show.rb +125 -0
- data/lib/the_tv_db/configuration.rb +7 -0
- data/lib/tv_data_api_clients.rb +12 -0
- data/lib/tv_data_api_clients/version.rb +3 -0
- data/lib/tv_rage.rb +7 -0
- data/lib/tv_rage/show.rb +81 -0
- data/test/fixtures/example.gif +0 -0
- data/test/fixtures/the_tv_db/api_get_series_lost.xml +24 -0
- data/test/fixtures/the_tv_db/api_series_12345_all_en.xml +9 -0
- data/test/fixtures/the_tv_db/api_series_73739_banners.xml +157 -0
- data/test/fixtures/the_tv_db/api_series_73739_en.xml +30 -0
- data/test/fixtures/tv_rage/feeds_search_show.xml +11 -0
- data/test/fixtures/tv_rage/feeds_showinfo_sid_7926.xml +28 -0
- data/test/support/shared_episode_naming_tests.rb +90 -0
- data/test/support/shared_show_naming_tests.rb +29 -0
- data/test/test_helper.rb +28 -0
- data/test/unit/base/api/episode_test.rb +11 -0
- data/test/unit/base/api/show_test.rb +11 -0
- data/test/unit/the_tv_db/api/episode_list_test.rb +15 -0
- data/test/unit/the_tv_db/api/episode_test.rb +93 -0
- data/test/unit/the_tv_db/api/image_list_test.rb +42 -0
- data/test/unit/the_tv_db/api/image_test.rb +35 -0
- data/test/unit/the_tv_db/api/mirrors_test.rb +17 -0
- data/test/unit/the_tv_db/api/show_test.rb +57 -0
- data/test/unit/the_tv_db/configuration_test.rb +28 -0
- data/test/unit/tv_rage/show_test.rb +55 -0
- data/tv-data-api-clients.gemspec +29 -0
- metadata +238 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Rakefile
ADDED
data/lib/base.rb
ADDED
@@ -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
|
data/lib/helpers.rb
ADDED
@@ -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
|
data/lib/the_tv_db.rb
ADDED
@@ -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
|