picturehouse_uk 4.0.0 → 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 +25 -0
- data/Rakefile +2 -2
- data/lib/picturehouse_uk/cinema.rb +30 -14
- data/lib/picturehouse_uk/internal/api.rb +42 -0
- data/lib/picturehouse_uk/internal/parser/address.rb +21 -6
- data/lib/picturehouse_uk/internal/parser/screenings.rb +85 -133
- data/lib/picturehouse_uk/internal/website.rb +10 -12
- data/lib/picturehouse_uk/version.rb +2 -2
- data/lib/picturehouse_uk.rb +1 -0
- data/picturehouse_uk.gemspec +2 -3
- data/rake/fixture_creator.rb +21 -1
- 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 +6235 -555
- 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 +34 -34
- data/test/lib/picturehouse_uk/internal/api_test.rb +92 -0
- data/test/lib/picturehouse_uk/internal/parser/address_test.rb +8 -8
- data/test/lib/picturehouse_uk/internal/parser/screenings_test.rb +99 -45
- data/test/lib/picturehouse_uk/internal/title_sanitizer_test.rb +48 -48
- data/test/lib/picturehouse_uk/internal/website_test.rb +12 -31
- data/test/lib/picturehouse_uk/performance_test.rb +63 -23
- data/test/lib/picturehouse_uk/version_test.rb +1 -1
- data/test/live/integration_test.rb +8 -8
- data/test/support/fake_api.rb +16 -0
- data/test/support/fake_website.rb +6 -6
- data/test/test_helper.rb +3 -2
- metadata +34 -50
- data/.travis.yml +0 -8
- data/test/fixtures/Duke_Of_Yorks/cinema.html +0 -3408
- data/test/fixtures/Duke_Of_Yorks/info.html +0 -556
- data/test/fixtures/Duke_Of_Yorks/whats_on.html +0 -3159
- data/test/fixtures/Dukes_At_Komedia/cinema.html +0 -4764
- data/test/fixtures/Dukes_At_Komedia/info.html +0 -526
- data/test/fixtures/Dukes_At_Komedia/whats_on.html +0 -4429
- data/test/fixtures/National_Media_Museum/cinema.html +0 -9200
- data/test/fixtures/National_Media_Museum/info.html +0 -606
- data/test/fixtures/National_Media_Museum/whats_on.html +0 -8850
- data/test/fixtures/Phoenix_Picturehouse/cinema.html +0 -8274
- data/test/fixtures/Phoenix_Picturehouse/info.html +0 -542
- data/test/fixtures/Phoenix_Picturehouse/whats_on.html +0 -7986
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 5c4c71062363f2efdabda5696692051f79c7915f250367ff8c16536e058c821b
|
|
4
|
+
data.tar.gz: a76bf492b267b7ae89cbbe829b4269dea9ac0207add6b41fe47685d1df76fbb9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b1722850d1b2f161725a961f420b8122c404161050c794b02d93a8f735c5d7053745c2eb35de8d0200e5cc20cd2525024ed94a4cc63357fe2157fc9e149fdc11
|
|
7
|
+
data.tar.gz: d5225d4815b31cd31d072820f55e257e72cc415e619c8cbb3eaa6b5c7609d81b624107554d367036164f07da58d83bc5656d5e0ad42df154d955af02411d06fd
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ main ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- name: Set up Ruby
|
|
17
|
+
uses: ruby/setup-ruby@v1
|
|
18
|
+
with:
|
|
19
|
+
ruby-version: '3.4'
|
|
20
|
+
bundler-cache: true
|
|
21
|
+
|
|
22
|
+
- name: Run tests
|
|
23
|
+
run: bundle exec rake
|
|
24
|
+
|
|
25
|
+
- name: Run live tests
|
|
26
|
+
run: bundle exec rake live
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: Release Gem
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
|
|
6
|
+
jobs:
|
|
7
|
+
release:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
permissions:
|
|
10
|
+
id-token: write
|
|
11
|
+
contents: write
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- name: Set up Ruby
|
|
17
|
+
uses: ruby/setup-ruby@v1
|
|
18
|
+
with:
|
|
19
|
+
ruby-version: '3.4'
|
|
20
|
+
bundler-cache: true
|
|
21
|
+
|
|
22
|
+
- name: Run tests
|
|
23
|
+
run: bundle exec rake test
|
|
24
|
+
|
|
25
|
+
- name: Release gem
|
|
26
|
+
uses: rubygems/release-gem@v1
|
data/.gitignore
CHANGED
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.4.7
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
All notable changes to this project will be documented in this file.
|
|
3
3
|
This project adheres to [Semantic Versioning](http://semver.org/).
|
|
4
4
|
|
|
5
|
+
## [5.0.0] - 2025-10-31
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- JSON API client for fetching performance data from `/api/get-movies-ajax` endpoint
|
|
9
|
+
- `PicturehouseUk::Internal::Api` for making API requests
|
|
10
|
+
- `PicturehouseUk::Internal::Parser::Screenings` for parsing JSON responses (replaced old HTML parser)
|
|
11
|
+
- Comprehensive tests for JSON API functionality
|
|
12
|
+
- Sample JSON fixtures for testing
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- `Performance.at()` now uses JSON API instead of HTML parsing
|
|
16
|
+
- Improved variant detection (arts, baby, kids, senior, imax)
|
|
17
|
+
- Booking URLs now use Vista format: `https://web.picturehouses.com/order/showtimes/{cinema_id}-{session_id}/seats`
|
|
18
|
+
- Enhanced 3D film detection from both title and attributes
|
|
19
|
+
- Increased test coverage to 98.65%
|
|
20
|
+
|
|
21
|
+
### Removed
|
|
22
|
+
- Old HTML parser `Parser::Screenings` (completely replaced with JSON API version)
|
|
23
|
+
- Test file for old HTML parser
|
|
24
|
+
|
|
25
|
+
### Technical Details
|
|
26
|
+
- The Picturehouse website now dynamically populates screening data via AJAX calls to their JSON API
|
|
27
|
+
- The `#show_all_date_list` div is populated client-side with data from the API endpoint
|
|
28
|
+
- HTML parsing approach replaced with direct API consumption for better reliability
|
|
29
|
+
|
|
5
30
|
## 4.0.0 - 2016-02-10
|
|
6
31
|
|
|
7
32
|
The cinebase standardisation release.
|
data/Rakefile
CHANGED
|
@@ -37,8 +37,8 @@ task :fixtures do
|
|
|
37
37
|
require_relative 'rake/fixture_creator'
|
|
38
38
|
|
|
39
39
|
FixtureCreator.new.home
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
FixtureCreator.new.cinemas
|
|
41
|
+
%w(duke-of-york-s-picturehouse duke-s-at-komedia phoenix-picturehouse).each do |cinema_id|
|
|
42
42
|
FixtureCreator.new.cinema(cinema_id)
|
|
43
43
|
end
|
|
44
44
|
end
|
|
@@ -2,9 +2,9 @@ module PicturehouseUk
|
|
|
2
2
|
# The object representing a cinema on the Picturehouse UK website
|
|
3
3
|
class Cinema < Cinebase::Cinema
|
|
4
4
|
# address css
|
|
5
|
-
ADDRESS_CSS = '.
|
|
6
|
-
# cinema link css
|
|
7
|
-
CINEMA_LINKS_CSS = '
|
|
5
|
+
ADDRESS_CSS = '.cinemaAdrass:not(.openingTime)'.freeze
|
|
6
|
+
# cinema link css on the /cinema page
|
|
7
|
+
CINEMA_LINKS_CSS = 'a[href*="/cinema/"]'.freeze
|
|
8
8
|
|
|
9
9
|
# @!attribute [r] id
|
|
10
10
|
# @return [Integer] the numeric id of the cinema on the Cineworld website
|
|
@@ -147,15 +147,15 @@ module PicturehouseUk
|
|
|
147
147
|
private
|
|
148
148
|
|
|
149
149
|
def self.cinema_links
|
|
150
|
-
|
|
150
|
+
cinemas_doc.css(CINEMA_LINKS_CSS)
|
|
151
151
|
end
|
|
152
152
|
private_class_method :cinema_links
|
|
153
153
|
|
|
154
|
-
def self.
|
|
155
|
-
@
|
|
156
|
-
Nokogiri::HTML(PicturehouseUk::Internal::Website.new.
|
|
154
|
+
def self.cinemas_doc
|
|
155
|
+
@cinemas_doc ||=
|
|
156
|
+
Nokogiri::HTML(PicturehouseUk::Internal::Website.new.cinemas)
|
|
157
157
|
end
|
|
158
|
-
private_class_method :
|
|
158
|
+
private_class_method :cinemas_doc
|
|
159
159
|
|
|
160
160
|
def address_node
|
|
161
161
|
@address_node ||= info_doc.css(ADDRESS_CSS)
|
|
@@ -163,11 +163,11 @@ module PicturehouseUk
|
|
|
163
163
|
|
|
164
164
|
def info_doc
|
|
165
165
|
@info_doc ||=
|
|
166
|
-
Nokogiri::HTML(PicturehouseUk::Internal::Website.new.
|
|
166
|
+
Nokogiri::HTML(PicturehouseUk::Internal::Website.new.information(id))
|
|
167
167
|
end
|
|
168
168
|
|
|
169
169
|
# @api private
|
|
170
|
-
# Utility class to parse the links
|
|
170
|
+
# Utility class to parse the links from the cinemas page
|
|
171
171
|
class ListParser
|
|
172
172
|
def initialize(nodes)
|
|
173
173
|
@nodes = nodes
|
|
@@ -175,22 +175,38 @@ module PicturehouseUk
|
|
|
175
175
|
|
|
176
176
|
def to_hash
|
|
177
177
|
@nodes.each_with_object({}) do |node, result|
|
|
178
|
-
|
|
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) }
|
|
179
186
|
end
|
|
180
187
|
end
|
|
181
188
|
|
|
182
189
|
private
|
|
183
190
|
|
|
184
191
|
def id(node)
|
|
185
|
-
url(node)
|
|
192
|
+
href = url(node)
|
|
193
|
+
return nil unless href
|
|
194
|
+
match = href.match(%r{/cinema/([^/?#]+)})
|
|
195
|
+
match ? match[1] : nil
|
|
186
196
|
end
|
|
187
197
|
|
|
188
198
|
def name(node)
|
|
189
|
-
|
|
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
|
|
190
204
|
end
|
|
191
205
|
|
|
192
206
|
def url(node)
|
|
193
|
-
node.get_attribute('
|
|
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}"
|
|
194
210
|
end
|
|
195
211
|
end
|
|
196
212
|
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,7 +15,7 @@ module PicturehouseUk
|
|
|
15
15
|
# @note Uses the address naming from http://microformats.org/wiki/adr
|
|
16
16
|
def address
|
|
17
17
|
{
|
|
18
|
-
street_address:
|
|
18
|
+
street_address: address_lines[0],
|
|
19
19
|
extended_address: extended_address,
|
|
20
20
|
locality: town,
|
|
21
21
|
region: region,
|
|
@@ -27,23 +27,38 @@ module PicturehouseUk
|
|
|
27
27
|
private
|
|
28
28
|
|
|
29
29
|
def array
|
|
30
|
-
@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
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Skip the first element (cinema name) and return address lines
|
|
40
|
+
def address_lines
|
|
41
|
+
@address_lines ||= array[1..-1] || []
|
|
31
42
|
end
|
|
32
43
|
|
|
33
44
|
def extended_address
|
|
34
|
-
|
|
45
|
+
address_lines.length > 4 ? address_lines[1] : nil
|
|
35
46
|
end
|
|
36
47
|
|
|
37
48
|
def postal_code
|
|
38
|
-
|
|
49
|
+
address_lines[-1]
|
|
39
50
|
end
|
|
40
51
|
|
|
41
52
|
def region
|
|
42
|
-
|
|
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
|
|
43
56
|
end
|
|
44
57
|
|
|
45
58
|
def town
|
|
46
|
-
|
|
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]
|
|
47
62
|
end
|
|
48
63
|
end
|
|
49
64
|
end
|
|
@@ -3,170 +3,122 @@ module PicturehouseUk
|
|
|
3
3
|
module Internal
|
|
4
4
|
# @api private
|
|
5
5
|
module Parser
|
|
6
|
-
# Parses
|
|
6
|
+
# Parses JSON API response into an array of screening hashes
|
|
7
7
|
class Screenings
|
|
8
|
-
# css for a day of films & screenings
|
|
9
|
-
LISTINGS = '.listings li:not(.dark)'.freeze
|
|
10
|
-
DATE = '.nav-collapse.collapse'.freeze
|
|
11
|
-
|
|
12
8
|
def initialize(cinema_id)
|
|
13
9
|
@cinema_id = cinema_id
|
|
14
10
|
end
|
|
15
11
|
|
|
16
|
-
#
|
|
12
|
+
# Parse the JSON response into an array of screening attributes
|
|
17
13
|
# @return [Array<Hash>]
|
|
18
14
|
def to_a
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
23
21
|
end
|
|
24
22
|
|
|
25
23
|
private
|
|
26
24
|
|
|
27
|
-
def
|
|
28
|
-
if
|
|
29
|
-
Date.now
|
|
30
|
-
else
|
|
31
|
-
html.match(/listings-further-ahead-(\d{4})(\d{2})(\d{2})/) do |m|
|
|
32
|
-
Date.new(m[1].to_i, m[2].to_i, m[3].to_i)
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
25
|
+
def parse_movie(movie)
|
|
26
|
+
return [] if movie['show_times'].nil? || movie['show_times'].empty?
|
|
36
27
|
|
|
37
|
-
|
|
38
|
-
|
|
28
|
+
movie['show_times'].map do |timing|
|
|
29
|
+
parse_timing(movie, timing)
|
|
30
|
+
end.compact
|
|
39
31
|
end
|
|
40
32
|
|
|
41
|
-
def
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# @api private
|
|
48
|
-
# collection of timings for a specific film
|
|
49
|
-
class FilmWithShowtimes
|
|
50
|
-
# film name css
|
|
51
|
-
NAME = '.top-mg-sm a'.freeze
|
|
52
|
-
# variants css
|
|
53
|
-
VARIANTS = '.film-times .col-xs-10'.freeze
|
|
54
|
-
|
|
55
|
-
def initialize(node, date)
|
|
56
|
-
@node = node
|
|
57
|
-
@date = date
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# The film name
|
|
61
|
-
# @return [String]
|
|
62
|
-
def name
|
|
63
|
-
TitleSanitizer.new(raw_name).sanitized
|
|
64
|
-
end
|
|
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']
|
|
65
37
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
Array(@node.css(VARIANTS)).flat_map do |variant|
|
|
70
|
-
Variant.new(variant, @date).to_a.map do |hash|
|
|
71
|
-
{
|
|
72
|
-
film_name: name,
|
|
73
|
-
dimension: dimension
|
|
74
|
-
}.merge(hash)
|
|
38
|
+
# Skip advance booking restrictions if not advertised
|
|
39
|
+
if movie['ABgtToday'] == true && movie['AdvertiseAdvanceBookingDate'] == false
|
|
40
|
+
return nil
|
|
75
41
|
end
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
private
|
|
80
42
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
# @api private
|
|
91
|
-
# variants can have multiple screenings
|
|
92
|
-
class Variant
|
|
93
|
-
SHOWTIMES = '.btn'.freeze
|
|
94
|
-
VARIANT = '.film-type-desc'.freeze
|
|
95
|
-
TRANSLATOR = {
|
|
96
|
-
'Big Scream' => 'baby',
|
|
97
|
-
'IMAX' => 'imax',
|
|
98
|
-
"Kids' Club" => 'kids',
|
|
99
|
-
'NT Live' => 'arts',
|
|
100
|
-
'Screen Arts' => 'arts',
|
|
101
|
-
'Silver Screen' => 'senior'
|
|
102
|
-
}.freeze
|
|
103
|
-
|
|
104
|
-
def initialize(node, date)
|
|
105
|
-
@node = node
|
|
106
|
-
@date = date
|
|
107
|
-
end
|
|
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
|
+
}
|
|
50
|
+
end
|
|
108
51
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def to_a
|
|
112
|
-
@node.css(SHOWTIMES).map do |node|
|
|
113
|
-
{ variant: variant }.merge(Showtime.new(@node, @date).to_hash)
|
|
52
|
+
def sanitize_title(title)
|
|
53
|
+
TitleSanitizer.new(title).sanitized
|
|
114
54
|
end
|
|
115
|
-
end
|
|
116
55
|
|
|
117
|
-
|
|
56
|
+
def determine_dimension(title, timing)
|
|
57
|
+
# Check title for 3D indicator
|
|
58
|
+
return '3d' if title =~ /3d/i
|
|
118
59
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
end
|
|
60
|
+
# Check timing attributes for 3D
|
|
61
|
+
if timing['SessionAttributesNames']
|
|
62
|
+
return '3d' if timing['SessionAttributesNames'].any? { |attr| attr =~ /3d/i }
|
|
63
|
+
end
|
|
124
64
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
end
|
|
128
|
-
end
|
|
65
|
+
'2d'
|
|
66
|
+
end
|
|
129
67
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
class Showtime
|
|
133
|
-
def initialize(node, date)
|
|
134
|
-
@node = node
|
|
135
|
-
@date = date
|
|
136
|
-
end
|
|
68
|
+
def extract_variants(timing)
|
|
69
|
+
return [] unless timing['SessionAttributesNames']
|
|
137
70
|
|
|
138
|
-
|
|
139
|
-
{
|
|
140
|
-
booking_url: booking_url,
|
|
141
|
-
starting_at: starting_at
|
|
142
|
-
}
|
|
143
|
-
end
|
|
71
|
+
variants = []
|
|
144
72
|
|
|
145
|
-
|
|
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
|
|
146
81
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
"https://picturehouses.com#{href}"
|
|
150
|
-
end
|
|
82
|
+
variants.uniq.sort
|
|
83
|
+
end
|
|
151
84
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
end
|
|
85
|
+
def booking_url(timing)
|
|
86
|
+
return nil unless timing['SessionId']
|
|
155
87
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
88
|
+
# Vista booking URL format
|
|
89
|
+
"https://web.picturehouses.com/order/showtimes/#{@cinema_id}-#{timing['SessionId']}/seats"
|
|
90
|
+
end
|
|
159
91
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
163
114
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
115
|
+
def json_data
|
|
116
|
+
@json_data ||= api_client.get_movies(@cinema_id)
|
|
117
|
+
end
|
|
167
118
|
|
|
168
|
-
|
|
169
|
-
|
|
119
|
+
def api_client
|
|
120
|
+
@api_client ||= Api.new
|
|
121
|
+
end
|
|
170
122
|
end
|
|
171
123
|
end
|
|
172
124
|
end
|
|
@@ -12,18 +12,10 @@ module PicturehouseUk
|
|
|
12
12
|
get("cinema/#{id}")
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
# get the cinema screenings page for passed id
|
|
16
|
-
# @return [String]
|
|
17
|
-
def whats_on(id)
|
|
18
|
-
get("cinema/#{id}/Whats_On")
|
|
19
|
-
rescue OpenURI::HTTPError
|
|
20
|
-
''
|
|
21
|
-
end
|
|
22
|
-
|
|
23
15
|
# get the cinema contact information page for passed id
|
|
24
16
|
# @return [String]
|
|
25
|
-
def
|
|
26
|
-
get("cinema
|
|
17
|
+
def information(id)
|
|
18
|
+
get("cinema/#{id}/information")
|
|
27
19
|
rescue OpenURI::HTTPError
|
|
28
20
|
''
|
|
29
21
|
end
|
|
@@ -34,12 +26,18 @@ module PicturehouseUk
|
|
|
34
26
|
get(nil)
|
|
35
27
|
end
|
|
36
28
|
|
|
29
|
+
# get the cinemas listing page
|
|
30
|
+
# @return [String]
|
|
31
|
+
def cinemas
|
|
32
|
+
get("cinema")
|
|
33
|
+
end
|
|
34
|
+
|
|
37
35
|
private
|
|
38
36
|
|
|
39
37
|
def get(path)
|
|
40
38
|
# SSL verification doesn't work on picturehouses.com
|
|
41
|
-
open("https://www.picturehouses.com/#{path}",
|
|
42
|
-
|
|
39
|
+
URI.open("https://www.picturehouses.com/#{path}",
|
|
40
|
+
ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE).read
|
|
43
41
|
end
|
|
44
42
|
end
|
|
45
43
|
end
|
data/lib/picturehouse_uk.rb
CHANGED
|
@@ -3,6 +3,7 @@ require 'nokogiri'
|
|
|
3
3
|
|
|
4
4
|
require_relative './picturehouse_uk/version'
|
|
5
5
|
|
|
6
|
+
require_relative './picturehouse_uk/internal/api'
|
|
6
7
|
require_relative './picturehouse_uk/internal/parser/address'
|
|
7
8
|
require_relative './picturehouse_uk/internal/parser/screenings'
|
|
8
9
|
require_relative './picturehouse_uk/internal/title_sanitizer'
|
data/picturehouse_uk.gemspec
CHANGED
|
@@ -12,15 +12,14 @@ Gem::Specification.new do |spec|
|
|
|
12
12
|
spec.homepage = ''
|
|
13
13
|
spec.licenses = %w(AGPL MIT)
|
|
14
14
|
|
|
15
|
-
spec.files = `git ls-files`.split(
|
|
15
|
+
spec.files = `git ls-files`.split("\n")
|
|
16
16
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
17
17
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
|
18
18
|
spec.require_paths = ['lib']
|
|
19
19
|
|
|
20
|
-
spec.add_development_dependency 'bundler', '~> 1.3'
|
|
21
|
-
spec.add_development_dependency 'codeclimate-test-reporter'
|
|
22
20
|
spec.add_development_dependency 'minitest-reporters'
|
|
23
21
|
spec.add_development_dependency 'rake'
|
|
22
|
+
spec.add_development_dependency 'simplecov'
|
|
24
23
|
spec.add_development_dependency 'webmock'
|
|
25
24
|
|
|
26
25
|
spec.add_runtime_dependency 'cinebase', '~> 3.0'
|