tv-data-api-clients 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- 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
|