picturehouse_uk 3.0.14 → 5.0.0
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.
- checksums.yaml +5 -5
- data/.github/workflows/ci.yml +26 -0
- data/.github/workflows/release.yml +26 -0
- data/.gitignore +1 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +44 -0
- data/LICENSE +617 -0
- data/README.md +5 -4
- data/Rakefile +12 -0
- data/lib/picturehouse_uk/cinema.rb +152 -127
- data/lib/picturehouse_uk/internal/api.rb +42 -0
- data/lib/picturehouse_uk/internal/parser/address.rb +36 -9
- data/lib/picturehouse_uk/internal/parser/screenings.rb +89 -95
- data/lib/picturehouse_uk/internal/title_sanitizer.rb +61 -65
- data/lib/picturehouse_uk/internal/website.rb +10 -12
- data/lib/picturehouse_uk/performance.rb +60 -0
- data/lib/picturehouse_uk/version.rb +2 -2
- data/lib/picturehouse_uk.rb +3 -4
- data/picturehouse_uk.gemspec +5 -7
- data/rake/fixture_creator.rb +62 -0
- data/test/fixtures/cinemas.html +4262 -0
- data/test/fixtures/duke-of-york-s-picturehouse/cinema.html +6187 -0
- data/test/fixtures/duke-of-york-s-picturehouse/get_movies.json +120552 -0
- data/test/fixtures/duke-of-york-s-picturehouse/information.html +3725 -0
- data/test/fixtures/duke-s-at-komedia/cinema.html +6138 -0
- data/test/fixtures/duke-s-at-komedia/get_movies.json +112 -0
- data/test/fixtures/duke-s-at-komedia/information.html +3690 -0
- data/test/fixtures/home.html +6236 -565
- data/test/fixtures/phoenix-picturehouse/cinema.html +6089 -0
- data/test/fixtures/phoenix-picturehouse/get_movies.json +120552 -0
- data/test/fixtures/phoenix-picturehouse/information.html +3630 -0
- data/test/lib/picturehouse_uk/cinema_test.rb +113 -142
- data/test/lib/picturehouse_uk/internal/api_test.rb +92 -0
- data/test/lib/picturehouse_uk/internal/parser/{address_parser_test.rb → address_test.rb} +11 -11
- data/test/lib/picturehouse_uk/internal/parser/screenings_test.rb +100 -48
- data/test/lib/picturehouse_uk/internal/title_sanitizer_test.rb +48 -48
- data/test/lib/picturehouse_uk/internal/website_test.rb +15 -31
- data/test/lib/picturehouse_uk/performance_test.rb +197 -0
- data/test/lib/picturehouse_uk/version_test.rb +1 -1
- data/test/live/integration_test.rb +15 -32
- data/test/support/fake_api.rb +16 -0
- data/test/support/fake_website.rb +24 -0
- data/test/test_helper.rb +13 -2
- metadata +52 -67
- data/.rdoc_options +0 -16
- data/.travis.yml +0 -5
- data/lib/picturehouse_uk/film.rb +0 -59
- data/lib/picturehouse_uk/screening.rb +0 -70
- data/test/fixture_updater.rb +0 -73
- data/test/fixtures/cinema/Duke_Of_Yorks.html +0 -2984
- data/test/fixtures/cinema/Dukes_At_Komedia.html +0 -5518
- data/test/fixtures/cinema/National_Media_Museum.html +0 -10266
- data/test/fixtures/cinema/Phoenix_Picturehouse.html +0 -5202
- data/test/fixtures/info/Duke_Of_Yorks.html +0 -549
- data/test/fixtures/info/Dukes_At_Komedia.html +0 -537
- data/test/fixtures/info/Phoenix_Picturehouse.html +0 -553
- data/test/fixtures/whats_on/Duke_Of_Yorks.html +0 -2737
- data/test/fixtures/whats_on/Dukes_At_Komedia.html +0 -5132
- data/test/fixtures/whats_on/National_Media_Museum.html +0 -9690
- data/test/fixtures/whats_on/Phoenix_Picturehouse.html +0 -4916
- data/test/lib/picturehouse_uk/film_test.rb +0 -141
- data/test/lib/picturehouse_uk/screening_test.rb +0 -181
- /data/{LICENSE.txt → COMM-LICENSE} +0 -0
|
@@ -1,59 +1,43 @@
|
|
|
1
1
|
module PicturehouseUk
|
|
2
2
|
# The object representing a cinema on the Picturehouse UK website
|
|
3
|
-
class Cinema
|
|
3
|
+
class Cinema < Cinebase::Cinema
|
|
4
4
|
# address css
|
|
5
|
-
ADDRESS_CSS = '.
|
|
6
|
-
# cinema link css
|
|
7
|
-
CINEMA_LINKS_CSS = '.
|
|
8
|
-
|
|
9
|
-
#
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
#
|
|
14
|
-
|
|
15
|
-
#
|
|
16
|
-
attr_reader :slug
|
|
17
|
-
# @return [String] the url of the cinema on the Picturehouse website
|
|
18
|
-
attr_reader :url
|
|
19
|
-
|
|
20
|
-
# @param [Hash] options id, name and url of the cinemas
|
|
21
|
-
# @return [PicturehouseUk::Cinema]
|
|
22
|
-
def initialize(options)
|
|
23
|
-
@brand = 'Picturehouse'
|
|
24
|
-
@id = options[:id]
|
|
25
|
-
@name = options[:name]
|
|
26
|
-
@slug = @name.downcase.gsub(/[^0-9a-z ]/, '').gsub(/\s+/, '-')
|
|
27
|
-
@url = if options[:url][0] == '/'
|
|
28
|
-
"http://www.picturehouses.com#{options[:url]}"
|
|
29
|
-
else
|
|
30
|
-
options[:url]
|
|
31
|
-
end
|
|
32
|
-
end
|
|
5
|
+
ADDRESS_CSS = '.cinemaAdrass:not(.openingTime)'.freeze
|
|
6
|
+
# cinema link css on the /cinema page
|
|
7
|
+
CINEMA_LINKS_CSS = 'a[href*="/cinema/"]'.freeze
|
|
8
|
+
|
|
9
|
+
# @!attribute [r] id
|
|
10
|
+
# @return [Integer] the numeric id of the cinema on the Cineworld website
|
|
11
|
+
|
|
12
|
+
# @!method initialize(options)
|
|
13
|
+
# Constructor
|
|
14
|
+
# @param [String] id the cinema id of the cinema in capitalized snake case
|
|
15
|
+
# @return [PicturehouseUk::Cinema]
|
|
33
16
|
|
|
34
17
|
# Return basic cinema information for all cinemas
|
|
35
18
|
# @return [Array<PicturehouseUk::Cinema>]
|
|
36
19
|
# @example
|
|
37
20
|
# PicturehouseUk::Cinema.all
|
|
38
|
-
#
|
|
21
|
+
# #=> [<PicturehouseUk::Cinema>, <PicturehouseUk::Cinema>, ...]
|
|
39
22
|
def self.all
|
|
40
|
-
|
|
23
|
+
cinema_hash.keys.map { |id| new(id) }
|
|
41
24
|
end
|
|
42
25
|
|
|
43
|
-
#
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
# @example
|
|
47
|
-
# PicturehouseUk::Cinema.find('Dukes_At_Komedia')
|
|
48
|
-
# # => <PicturehouseUK::Cinema ...>
|
|
49
|
-
def self.find(id)
|
|
50
|
-
all.find { |cinema| cinema.id == id }
|
|
26
|
+
# @api private
|
|
27
|
+
def self.cinema_hash
|
|
28
|
+
@cinema_hash ||= ListParser.new(cinema_links).to_hash
|
|
51
29
|
end
|
|
52
30
|
|
|
31
|
+
# @!method address
|
|
32
|
+
# Address of the cinema
|
|
33
|
+
# @return [Hash] of different address parts
|
|
34
|
+
# @see #adr
|
|
35
|
+
|
|
53
36
|
# Address of the cinema
|
|
54
|
-
# @return [Hash]
|
|
37
|
+
# @return [Hash] contains :street_address, :extended_address,
|
|
38
|
+
# :locality, :postal_code, :country
|
|
55
39
|
# @example
|
|
56
|
-
# cinema = PicturehouseUk::Cinema.
|
|
40
|
+
# cinema = PicturehouseUk::Cinema.new('Dukes_At_Komedia')
|
|
57
41
|
# cinema.adr
|
|
58
42
|
# #=> {
|
|
59
43
|
# street_address: '44-47 Gardner Street',
|
|
@@ -67,122 +51,163 @@ module PicturehouseUk
|
|
|
67
51
|
def adr
|
|
68
52
|
PicturehouseUk::Internal::Parser::Address.new(address_node.to_s).address
|
|
69
53
|
end
|
|
70
|
-
alias_method :address, :adr
|
|
71
|
-
|
|
72
|
-
# The second address line of of the cinema
|
|
73
|
-
# @return [String, nil]
|
|
74
|
-
# @example
|
|
75
|
-
# cinema = PicturehouseUk::Cinema.find('Dukes_At_Komedia')
|
|
76
|
-
# cinema.extended_address
|
|
77
|
-
# #=> 'North Laine'
|
|
78
|
-
# @note Uses method naming as at http://microformats.org/wiki/adr
|
|
79
|
-
def extended_address
|
|
80
|
-
address[:extended_address]
|
|
81
|
-
end
|
|
82
54
|
|
|
83
|
-
#
|
|
84
|
-
# @return [
|
|
55
|
+
# Brand of the cinema
|
|
56
|
+
# @return [String] which will always be 'Picturehouse'
|
|
85
57
|
# @example
|
|
86
|
-
# cinema = PicturehouseUk::Cinema.
|
|
87
|
-
# cinema.
|
|
88
|
-
#
|
|
89
|
-
def
|
|
90
|
-
|
|
91
|
-
end
|
|
58
|
+
# cinema = PicturehouseUk::Cinema.new('Dukes_At_Komedia')
|
|
59
|
+
# cinema.brand
|
|
60
|
+
# #=> 'Picturehouse'
|
|
61
|
+
def brand
|
|
62
|
+
'Picturehouse'.freeze
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @!method country_name
|
|
66
|
+
# Country of the cinema
|
|
67
|
+
# @return [String] which will always be 'United Kingdom'
|
|
68
|
+
# @example
|
|
69
|
+
# cinema = PicturehouseUk::Cinema.new('Dukes_At_Komedia')
|
|
70
|
+
# cinema.country_name
|
|
71
|
+
# #=> 'United Kingdom'
|
|
72
|
+
|
|
73
|
+
# @!method extended_address
|
|
74
|
+
# The second address line of the cinema
|
|
75
|
+
# @return [String]
|
|
76
|
+
# @example
|
|
77
|
+
# cinema = PicturehouseUk::Cinema.new('Dukes_At_Komedia')
|
|
78
|
+
# cinema.extended_address
|
|
79
|
+
# #=> 'North Laine'
|
|
92
80
|
|
|
93
81
|
# The name of the cinema (might include brand)
|
|
94
82
|
# @return [String]
|
|
95
83
|
# @example
|
|
96
|
-
# cinema = PicturehouseUk::Cinema.
|
|
84
|
+
# cinema = PicturehouseUk::Cinema.new('Dukes_At_Komedia')
|
|
97
85
|
# cinema.full_name
|
|
98
86
|
# #=> "Duke's At Komedia"
|
|
99
87
|
def full_name
|
|
100
88
|
name
|
|
101
89
|
end
|
|
102
90
|
|
|
103
|
-
#
|
|
104
|
-
#
|
|
105
|
-
#
|
|
106
|
-
#
|
|
107
|
-
#
|
|
108
|
-
#
|
|
109
|
-
#
|
|
110
|
-
def locality
|
|
111
|
-
address[:locality]
|
|
112
|
-
end
|
|
91
|
+
# @!method locality
|
|
92
|
+
# The locality (town) of the cinema
|
|
93
|
+
# @return [String]
|
|
94
|
+
# @example
|
|
95
|
+
# cinema = PicturehouseUk::Cinema.new('Dukes_At_Komedia')
|
|
96
|
+
# cinema.locality
|
|
97
|
+
# #=> 'Brighton'
|
|
113
98
|
|
|
114
|
-
#
|
|
99
|
+
# The name of the cinema
|
|
115
100
|
# @return [String]
|
|
116
101
|
# @example
|
|
117
|
-
# cinema = PicturehouseUk::Cinema.
|
|
118
|
-
# cinema.
|
|
119
|
-
# #=> '
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
#
|
|
102
|
+
# cinema = PicturehouseUk::Cinema.new('Dukes_At_Komedia')
|
|
103
|
+
# cinema.name
|
|
104
|
+
# #=> "Duke's At Komedia"
|
|
105
|
+
def name
|
|
106
|
+
self.class.cinema_hash.fetch(id, {})[:name]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# @!method postal_code
|
|
110
|
+
# Post code of the cinema
|
|
111
|
+
# @return [String]
|
|
112
|
+
# @example
|
|
113
|
+
# cinema = PicturehouseUk::Cinema.new('Dukes_At_Komedia')
|
|
114
|
+
# cinema.postal_code
|
|
115
|
+
# #=> 'BN1 1UN'
|
|
116
|
+
|
|
117
|
+
# @!method region
|
|
118
|
+
# The region (county) of the cinema if provided
|
|
119
|
+
# @return [String]
|
|
120
|
+
# @example
|
|
121
|
+
# cinema = PicturehouseUk::Cinema.new('Dukes_At_Komedia')
|
|
122
|
+
# cinema.region
|
|
123
|
+
# #=> 'East Sussex'
|
|
124
|
+
|
|
125
|
+
# @!method slug
|
|
126
|
+
# The URL-able slug of the cinema
|
|
127
|
+
# @return [String]
|
|
128
|
+
# @example
|
|
129
|
+
# cinema = PicturehouseUk::Cinema.new('Dukes_At_Komedia')
|
|
130
|
+
# cinema.slug
|
|
131
|
+
# #=> 'dukes-at-komedia'
|
|
132
|
+
|
|
133
|
+
# @!method street_address
|
|
134
|
+
# The street address of the cinema
|
|
135
|
+
# @return [String]
|
|
136
|
+
# @example
|
|
137
|
+
# cinema = PicturehouseUk::Cinema.new('Dukes_At_Komedia')
|
|
138
|
+
# cinema.street_address
|
|
139
|
+
# #=> '44-47 Gardner Street'
|
|
140
|
+
|
|
141
|
+
# The url of the cinema on the Picturehouse website
|
|
126
142
|
# @return [String]
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
# cinema.region
|
|
130
|
-
# #=> 'East Sussex'
|
|
131
|
-
# @note Uses the standard method naming as at http://microformats.org/wiki/adr
|
|
132
|
-
def region
|
|
133
|
-
address[:region]
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
# All planned screenings
|
|
137
|
-
# @return [Array<PicturehouseUk::Screening>]
|
|
138
|
-
# @example
|
|
139
|
-
# cinema = PicturehouseUk::Cinema.find('Dukes_At_Komedia')
|
|
140
|
-
# cinema.screenings
|
|
141
|
-
# # => [<PicturehouseUk::Screening ...>, <PicturehouseUk::Screening ...>]
|
|
142
|
-
def screenings
|
|
143
|
-
PicturehouseUk::Screening.at(@id)
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
# The street adress of the cinema
|
|
147
|
-
# @return a String
|
|
148
|
-
# @example
|
|
149
|
-
# cinema = PicturehouseUk::Cinema.find('Dukes_At_Komedia')
|
|
150
|
-
# cinema.street_address
|
|
151
|
-
# #=> '44-47 Gardner Street'
|
|
152
|
-
# @note Uses the standard method naming as at http://microformats.org/wiki/adr
|
|
153
|
-
def street_address
|
|
154
|
-
address[:street_address]
|
|
143
|
+
def url
|
|
144
|
+
"http://www.picturehouses.com#/cinema/#{id}"
|
|
155
145
|
end
|
|
156
146
|
|
|
157
147
|
private
|
|
158
148
|
|
|
159
149
|
def self.cinema_links
|
|
160
|
-
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
def self.home_doc
|
|
164
|
-
@home_doc ||= Nokogiri::HTML(website.home)
|
|
150
|
+
cinemas_doc.css(CINEMA_LINKS_CSS)
|
|
165
151
|
end
|
|
152
|
+
private_class_method :cinema_links
|
|
166
153
|
|
|
167
|
-
def self.
|
|
168
|
-
@
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
def self.new_from_link(link)
|
|
172
|
-
url = link.get_attribute('data-href')
|
|
173
|
-
name = link.children.first.to_s.split(' — ')[1]
|
|
174
|
-
|
|
175
|
-
new(id: url.match(%r{/cinema/(.+)$})[1],
|
|
176
|
-
name: name,
|
|
177
|
-
url: url)
|
|
154
|
+
def self.cinemas_doc
|
|
155
|
+
@cinemas_doc ||=
|
|
156
|
+
Nokogiri::HTML(PicturehouseUk::Internal::Website.new.cinemas)
|
|
178
157
|
end
|
|
158
|
+
private_class_method :cinemas_doc
|
|
179
159
|
|
|
180
160
|
def address_node
|
|
181
161
|
@address_node ||= info_doc.css(ADDRESS_CSS)
|
|
182
162
|
end
|
|
183
163
|
|
|
184
164
|
def info_doc
|
|
185
|
-
@info_doc ||=
|
|
165
|
+
@info_doc ||=
|
|
166
|
+
Nokogiri::HTML(PicturehouseUk::Internal::Website.new.information(id))
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# @api private
|
|
170
|
+
# Utility class to parse the links from the cinemas page
|
|
171
|
+
class ListParser
|
|
172
|
+
def initialize(nodes)
|
|
173
|
+
@nodes = nodes
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def to_hash
|
|
177
|
+
@nodes.each_with_object({}) do |node, result|
|
|
178
|
+
cinema_id = id(node)
|
|
179
|
+
cinema_name = name(node)
|
|
180
|
+
# Skip links that don't have valid cinema data
|
|
181
|
+
next unless cinema_id && cinema_name
|
|
182
|
+
# Skip duplicate entries
|
|
183
|
+
next if result.key?(cinema_id)
|
|
184
|
+
|
|
185
|
+
result[cinema_id] = { name: cinema_name, url: url(node) }
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
private
|
|
190
|
+
|
|
191
|
+
def id(node)
|
|
192
|
+
href = url(node)
|
|
193
|
+
return nil unless href
|
|
194
|
+
match = href.match(%r{/cinema/([^/?#]+)})
|
|
195
|
+
match ? match[1] : nil
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def name(node)
|
|
199
|
+
# Cinema name is in a <p> tag within the link
|
|
200
|
+
p_tag = node.css('p').first
|
|
201
|
+
return nil unless p_tag
|
|
202
|
+
# Get text and remove the location span
|
|
203
|
+
p_tag.children.find { |child| child.text? }&.text&.strip
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def url(node)
|
|
207
|
+
href = node.get_attribute('href')
|
|
208
|
+
# Convert relative URLs to absolute if needed
|
|
209
|
+
href&.start_with?('http') ? href : "https://www.picturehouses.com#{href}"
|
|
210
|
+
end
|
|
186
211
|
end
|
|
187
212
|
end
|
|
188
213
|
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require 'net/http'
|
|
2
|
+
require 'json'
|
|
3
|
+
require 'openssl'
|
|
4
|
+
|
|
5
|
+
module PicturehouseUk
|
|
6
|
+
# @api private
|
|
7
|
+
module Internal
|
|
8
|
+
# Fetches JSON data from the Picturehouse API
|
|
9
|
+
class Api
|
|
10
|
+
API_BASE = 'https://www.picturehouses.com'.freeze
|
|
11
|
+
API_ENDPOINT = '/api/get-movies-ajax'.freeze
|
|
12
|
+
|
|
13
|
+
# Fetch movie data for a cinema
|
|
14
|
+
# @param cinema_id [String] the cinema ID
|
|
15
|
+
# @param date [String] the date in YYYY-MM-DD format, or 'show_all_dates'
|
|
16
|
+
# @return [Hash] parsed JSON response
|
|
17
|
+
def get_movies(cinema_id, date = 'show_all_dates')
|
|
18
|
+
uri = URI("#{API_BASE}#{API_ENDPOINT}")
|
|
19
|
+
|
|
20
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
21
|
+
http.use_ssl = true
|
|
22
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
23
|
+
|
|
24
|
+
request = Net::HTTP::Post.new(uri.path)
|
|
25
|
+
request.set_form_data(
|
|
26
|
+
'start_date' => date,
|
|
27
|
+
'cinema_id' => cinema_id,
|
|
28
|
+
'filters' => []
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
response = http.request(request)
|
|
32
|
+
|
|
33
|
+
return {} unless response.is_a?(Net::HTTPSuccess)
|
|
34
|
+
|
|
35
|
+
JSON.parse(response.body)
|
|
36
|
+
rescue StandardError => e
|
|
37
|
+
warn "Failed to fetch movies for cinema #{cinema_id}: #{e.message}"
|
|
38
|
+
{}
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -15,23 +15,50 @@ module PicturehouseUk
|
|
|
15
15
|
# @note Uses the address naming from http://microformats.org/wiki/adr
|
|
16
16
|
def address
|
|
17
17
|
{
|
|
18
|
-
street_address:
|
|
19
|
-
extended_address:
|
|
18
|
+
street_address: address_lines[0],
|
|
19
|
+
extended_address: extended_address,
|
|
20
20
|
locality: town,
|
|
21
|
-
region:
|
|
22
|
-
postal_code:
|
|
23
|
-
|
|
21
|
+
region: region,
|
|
22
|
+
postal_code: postal_code,
|
|
23
|
+
country_name: 'United Kingdom'.freeze
|
|
24
24
|
}
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
private
|
|
28
28
|
|
|
29
|
-
def
|
|
30
|
-
@
|
|
29
|
+
def array
|
|
30
|
+
@array ||= begin
|
|
31
|
+
# Split on <br> tags first, then strip all HTML tags from each part
|
|
32
|
+
@html.split(/<br\s*\/?>/).map do |part|
|
|
33
|
+
# Strip all HTML tags and clean up whitespace
|
|
34
|
+
Nokogiri::HTML::DocumentFragment.parse(part).text.strip
|
|
35
|
+
end.reject(&:empty?)
|
|
36
|
+
end
|
|
31
37
|
end
|
|
32
38
|
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
# Skip the first element (cinema name) and return address lines
|
|
40
|
+
def address_lines
|
|
41
|
+
@address_lines ||= array[1..-1] || []
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def extended_address
|
|
45
|
+
address_lines.length > 4 ? address_lines[1] : nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def postal_code
|
|
49
|
+
address_lines[-1]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def region
|
|
53
|
+
# If there are 4+ address lines (street, town, region, postcode),
|
|
54
|
+
# the region is at -2, otherwise nil
|
|
55
|
+
address_lines.length >= 4 ? address_lines[-2] : nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def town
|
|
59
|
+
# Town is always the second-to-last item (before postcode)
|
|
60
|
+
# unless there's a region, then it's third-to-last
|
|
61
|
+
@town ||= address_lines.length >= 4 ? address_lines[-3] : address_lines[-2]
|
|
35
62
|
end
|
|
36
63
|
end
|
|
37
64
|
end
|
|
@@ -3,127 +3,121 @@ module PicturehouseUk
|
|
|
3
3
|
module Internal
|
|
4
4
|
# @api private
|
|
5
5
|
module Parser
|
|
6
|
-
# Parses
|
|
7
|
-
Screenings
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
# Parses JSON API response into an array of screening hashes
|
|
7
|
+
class Screenings
|
|
8
|
+
def initialize(cinema_id)
|
|
9
|
+
@cinema_id = cinema_id
|
|
10
|
+
end
|
|
11
11
|
|
|
12
|
-
#
|
|
12
|
+
# Parse the JSON response into an array of screening attributes
|
|
13
13
|
# @return [Array<Hash>]
|
|
14
14
|
def to_a
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
return [] unless json_data['response'] == 'success'
|
|
16
|
+
return [] unless json_data['movies']
|
|
17
|
+
|
|
18
|
+
json_data['movies'].flat_map do |movie|
|
|
19
|
+
parse_movie(movie)
|
|
20
|
+
end.compact
|
|
18
21
|
end
|
|
19
22
|
|
|
20
23
|
private
|
|
21
24
|
|
|
22
|
-
def
|
|
23
|
-
if
|
|
24
|
-
Date.now
|
|
25
|
-
else
|
|
26
|
-
html.match(/listings-further-ahead-(\d{4})(\d{2})(\d{2})/) do |m|
|
|
27
|
-
Date.new(m[1].to_i, m[2].to_i, m[3].to_i)
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
25
|
+
def parse_movie(movie)
|
|
26
|
+
return [] if movie['show_times'].nil? || movie['show_times'].empty?
|
|
31
27
|
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
movie['show_times'].map do |timing|
|
|
29
|
+
parse_timing(movie, timing)
|
|
30
|
+
end.compact
|
|
34
31
|
end
|
|
35
32
|
|
|
36
|
-
def
|
|
37
|
-
|
|
33
|
+
def parse_timing(movie, timing)
|
|
34
|
+
# Skip sold out or unavailable screenings
|
|
35
|
+
return nil if timing['SoldoutStatus'] == 2
|
|
36
|
+
return nil unless timing['date_f'] && timing['time']
|
|
37
|
+
|
|
38
|
+
# Skip advance booking restrictions if not advertised
|
|
39
|
+
if movie['ABgtToday'] == true && movie['AdvertiseAdvanceBookingDate'] == false
|
|
40
|
+
return nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
{
|
|
44
|
+
film_name: sanitize_title(movie['Title']),
|
|
45
|
+
dimension: determine_dimension(movie['Title'], timing),
|
|
46
|
+
variant: extract_variants(timing),
|
|
47
|
+
booking_url: booking_url(timing),
|
|
48
|
+
starting_at: parse_datetime(timing['date_f'], timing['time'])
|
|
49
|
+
}
|
|
38
50
|
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
51
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
# variants css
|
|
46
|
-
VARIANTS = '.film-times .col-xs-10'
|
|
52
|
+
def sanitize_title(title)
|
|
53
|
+
TitleSanitizer.new(title).sanitized
|
|
54
|
+
end
|
|
47
55
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
TitleSanitizer.new(raw_name).sanitized
|
|
52
|
-
end
|
|
56
|
+
def determine_dimension(title, timing)
|
|
57
|
+
# Check title for 3D indicator
|
|
58
|
+
return '3d' if title =~ /3d/i
|
|
53
59
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
Array(node.css(VARIANTS)).flat_map do |variant|
|
|
58
|
-
Variant.new(variant, date).to_a.map do |hash|
|
|
59
|
-
{
|
|
60
|
-
film_name: name,
|
|
61
|
-
dimension: dimension
|
|
62
|
-
}.merge(hash)
|
|
60
|
+
# Check timing attributes for 3D
|
|
61
|
+
if timing['SessionAttributesNames']
|
|
62
|
+
return '3d' if timing['SessionAttributesNames'].any? { |attr| attr =~ /3d/i }
|
|
63
63
|
end
|
|
64
|
+
|
|
65
|
+
'2d'
|
|
64
66
|
end
|
|
65
|
-
end
|
|
66
67
|
|
|
67
|
-
|
|
68
|
+
def extract_variants(timing)
|
|
69
|
+
return [] unless timing['SessionAttributesNames']
|
|
68
70
|
|
|
69
|
-
|
|
70
|
-
raw_name.match(/3d/i) ? '3d' : '2d'
|
|
71
|
-
end
|
|
71
|
+
variants = []
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
timing['SessionAttributesNames'].each do |attribute|
|
|
74
|
+
# Map known attributes to variant types
|
|
75
|
+
variants << 'baby' if attribute =~ /big scream/i
|
|
76
|
+
variants << 'imax' if attribute =~ /imax/i
|
|
77
|
+
variants << 'kids' if attribute =~ /kids|toddler/i
|
|
78
|
+
variants << 'arts' if attribute =~ /nt live|screen arts|rbo|roh|met opera/i
|
|
79
|
+
variants << 'senior' if attribute =~ /silver screen/i
|
|
80
|
+
end
|
|
77
81
|
|
|
78
|
-
|
|
79
|
-
Variant = Struct.new(:node, :date) do
|
|
80
|
-
SHOWTIMES = '.btn'
|
|
81
|
-
VARIANT = '.film-type-desc'
|
|
82
|
-
TRANSLATOR = {
|
|
83
|
-
'Big Scream' => 'baby',
|
|
84
|
-
'IMAX' => 'imax',
|
|
85
|
-
"Kids' Club" => 'kids',
|
|
86
|
-
'NT Live' => 'arts',
|
|
87
|
-
'Screen Arts' => 'arts',
|
|
88
|
-
'Silver Screen' => 'senior'
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
# Variant arrays
|
|
92
|
-
# @return [Array<Hash>]
|
|
93
|
-
def to_a
|
|
94
|
-
node.css(SHOWTIMES).map do |node|
|
|
95
|
-
{ variant: variant }.merge(Showtime.new(node, date).to_hash)
|
|
82
|
+
variants.uniq.sort
|
|
96
83
|
end
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
private
|
|
100
84
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
variant_text.include?(k)
|
|
104
|
-
end.values.uniq
|
|
105
|
-
end
|
|
85
|
+
def booking_url(timing)
|
|
86
|
+
return nil unless timing['SessionId']
|
|
106
87
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
end
|
|
88
|
+
# Vista booking URL format
|
|
89
|
+
"https://web.picturehouses.com/order/showtimes/#{@cinema_id}-#{timing['SessionId']}/seats"
|
|
90
|
+
end
|
|
111
91
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
92
|
+
def parse_datetime(date_str, time_str)
|
|
93
|
+
# date_str format: "2024-10-30"
|
|
94
|
+
# time_str format: "19:30"
|
|
95
|
+
return nil unless date_str && time_str
|
|
96
|
+
|
|
97
|
+
date_parts = date_str.split('-').map(&:to_i)
|
|
98
|
+
time_parts = time_str.split(':').map(&:to_i)
|
|
99
|
+
|
|
100
|
+
# Create Time object in local timezone, then convert to UTC
|
|
101
|
+
Time.new(
|
|
102
|
+
date_parts[0], # year
|
|
103
|
+
date_parts[1], # month
|
|
104
|
+
date_parts[2], # day
|
|
105
|
+
time_parts[0], # hour
|
|
106
|
+
time_parts[1], # minute
|
|
107
|
+
0, # second
|
|
108
|
+
'+00:00' # UTC timezone
|
|
109
|
+
)
|
|
110
|
+
rescue StandardError => e
|
|
111
|
+
warn "Failed to parse datetime from #{date_str} #{time_str}: #{e.message}"
|
|
112
|
+
nil
|
|
113
|
+
end
|
|
120
114
|
|
|
121
|
-
|
|
115
|
+
def json_data
|
|
116
|
+
@json_data ||= api_client.get_movies(@cinema_id)
|
|
117
|
+
end
|
|
122
118
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
hour, min = node.text.split('.').map(&:to_i)
|
|
126
|
-
date.to_time + (hour * 60 + min) * 60
|
|
119
|
+
def api_client
|
|
120
|
+
@api_client ||= Api.new
|
|
127
121
|
end
|
|
128
122
|
end
|
|
129
123
|
end
|