picturehouse_uk 5.0.0 → 6.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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +34 -13
  3. data/.github/workflows/release.yml +61 -14
  4. data/CHANGELOG.md +26 -0
  5. data/README.md +128 -24
  6. data/Rakefile +4 -13
  7. data/lib/picturehouse_uk/cinema.rb +60 -146
  8. data/lib/picturehouse_uk/internal/api_response.rb +35 -0
  9. data/lib/picturehouse_uk/internal/guest_token.rb +28 -0
  10. data/lib/picturehouse_uk/internal/parser/api/showtime.rb +70 -0
  11. data/lib/picturehouse_uk/internal/title_sanitizer.rb +8 -3
  12. data/lib/picturehouse_uk/performance.rb +37 -17
  13. data/lib/picturehouse_uk/version.rb +3 -3
  14. data/lib/picturehouse_uk.rb +19 -5
  15. data/picturehouse_uk.gemspec +7 -5
  16. data/test/fixtures/api/showtimes-019.json +62 -0
  17. data/test/fixtures/api/sites.json +40 -0
  18. data/test/lib/picturehouse_uk/cinema_test.rb +50 -188
  19. data/test/lib/picturehouse_uk/internal/api_response_test.rb +52 -0
  20. data/test/lib/picturehouse_uk/internal/fixture_shape_test.rb +14 -0
  21. data/test/lib/picturehouse_uk/internal/guest_token_test.rb +71 -0
  22. data/test/lib/picturehouse_uk/internal/parser/api/showtime_test.rb +59 -0
  23. data/test/lib/picturehouse_uk/internal/title_sanitizer_test.rb +192 -344
  24. data/test/lib/picturehouse_uk/performance_test.rb +85 -171
  25. data/test/lib/picturehouse_uk/version_test.rb +3 -3
  26. data/test/live/api_shape_test.rb +19 -0
  27. data/test/live/integration_test.rb +17 -41
  28. data/test/live/live_helper.rb +11 -0
  29. data/test/package_test.rb +56 -0
  30. data/test/support/api_fixtures_helper.rb +17 -0
  31. data/test/support/api_shapes.rb +48 -0
  32. data/test/support/fake_api_response.rb +20 -0
  33. data/test/test_helper.rb +7 -6
  34. metadata +71 -55
  35. data/lib/picturehouse_uk/internal/api.rb +0 -42
  36. data/lib/picturehouse_uk/internal/parser/address.rb +0 -66
  37. data/lib/picturehouse_uk/internal/parser/screenings.rb +0 -125
  38. data/lib/picturehouse_uk/internal/website.rb +0 -44
  39. data/rake/fixture_creator.rb +0 -62
  40. data/test/fixtures/cinemas.html +0 -4262
  41. data/test/fixtures/duke-of-york-s-picturehouse/cinema.html +0 -6187
  42. data/test/fixtures/duke-of-york-s-picturehouse/get_movies.json +0 -120552
  43. data/test/fixtures/duke-of-york-s-picturehouse/information.html +0 -3725
  44. data/test/fixtures/duke-s-at-komedia/cinema.html +0 -6138
  45. data/test/fixtures/duke-s-at-komedia/get_movies.json +0 -112
  46. data/test/fixtures/duke-s-at-komedia/information.html +0 -3690
  47. data/test/fixtures/home.html +0 -6254
  48. data/test/fixtures/phoenix-picturehouse/cinema.html +0 -6089
  49. data/test/fixtures/phoenix-picturehouse/get_movies.json +0 -120552
  50. data/test/fixtures/phoenix-picturehouse/information.html +0 -3630
  51. data/test/lib/picturehouse_uk/internal/api_test.rb +0 -92
  52. data/test/lib/picturehouse_uk/internal/parser/address_test.rb +0 -53
  53. data/test/lib/picturehouse_uk/internal/parser/screenings_test.rb +0 -124
  54. data/test/lib/picturehouse_uk/internal/website_test.rb +0 -67
  55. data/test/support/fake_api.rb +0 -16
  56. data/test/support/fake_website.rb +0 -24
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5c4c71062363f2efdabda5696692051f79c7915f250367ff8c16536e058c821b
4
- data.tar.gz: a76bf492b267b7ae89cbbe829b4269dea9ac0207add6b41fe47685d1df76fbb9
3
+ metadata.gz: 00531220ffd28a45ae2bcb4216fffde1e074fb7aaff9e3f7b9d9073c58231c5c
4
+ data.tar.gz: 01e54418fb861d42bd2564ab03a45aec27c0165d2069e22395c62f5d52c7b6e0
5
5
  SHA512:
6
- metadata.gz: b1722850d1b2f161725a961f420b8122c404161050c794b02d93a8f735c5d7053745c2eb35de8d0200e5cc20cd2525024ed94a4cc63357fe2157fc9e149fdc11
7
- data.tar.gz: d5225d4815b31cd31d072820f55e257e72cc415e619c8cbb3eaa6b5c7609d81b624107554d367036164f07da58d83bc5656d5e0ad42df154d955af02411d06fd
6
+ metadata.gz: 8ee5a705ee67e10f05f6cc12e65ad325bc0b61a7e80d96a79aaf028774cbb45359f100dad3ab5c8b9223dfbe10a3fb0202635f56a4de9495df8bcaef937a140f
7
+ data.tar.gz: b1de3de2012b7263e22d9c7df087ae8737fe748d62077e814ceac33aa998fa7100fcc025fa6a31ba14381f9560fabcbbdb371541ff52e9192c1ad30c0f69760b
@@ -9,18 +9,39 @@ on:
9
9
  jobs:
10
10
  test:
11
11
  runs-on: ubuntu-latest
12
-
12
+ strategy:
13
+ matrix:
14
+ ruby-version: ['3.4', '4.0']
13
15
  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
16
+ - uses: actions/checkout@v4
17
+ - uses: ruby/setup-ruby@v1
18
+ with:
19
+ ruby-version: ${{ matrix.ruby-version }}
20
+ bundler-cache: true
21
+ - run: bundle exec rake test
21
22
 
22
- - name: Run tests
23
- run: bundle exec rake
24
-
25
- - name: Run live tests
26
- run: bundle exec rake live
23
+ package:
24
+ strategy:
25
+ matrix:
26
+ include:
27
+ - runner: ubuntu-latest
28
+ platform: x86_64-linux-gnu
29
+ - runner: ubuntu-24.04-arm
30
+ platform: aarch64-linux-gnu
31
+ - runner: macos-15
32
+ platform: arm64-darwin
33
+ - runner: macos-15-intel
34
+ platform: x86_64-darwin
35
+ runs-on: ${{ matrix.runner }}
36
+ steps:
37
+ - uses: actions/checkout@v4
38
+ - uses: ruby/setup-ruby@v1
39
+ with:
40
+ ruby-version: '4.0'
41
+ bundler-cache: true
42
+ - run: bundle exec rake 'package:native[${{ matrix.platform }}]'
43
+ - run: PICTUREHOUSE_PACKAGE="$(echo pkg/*.gem)" bundle exec ruby test/package_test.rb
44
+ - uses: actions/upload-artifact@v4
45
+ with:
46
+ name: gem-${{ matrix.platform }}
47
+ path: pkg/*.gem
@@ -4,23 +4,70 @@ on:
4
4
  workflow_dispatch:
5
5
 
6
6
  jobs:
7
+ package:
8
+ strategy:
9
+ matrix:
10
+ include:
11
+ - runner: ubuntu-latest
12
+ platform: x86_64-linux-gnu
13
+ - runner: ubuntu-24.04-arm
14
+ platform: aarch64-linux-gnu
15
+ - runner: macos-15
16
+ platform: arm64-darwin
17
+ - runner: macos-15-intel
18
+ platform: x86_64-darwin
19
+ runs-on: ${{ matrix.runner }}
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ - uses: ruby/setup-ruby@v1
23
+ with:
24
+ ruby-version: '4.0'
25
+ bundler-cache: true
26
+ - run: bundle exec rake 'package:native[${{ matrix.platform }}]'
27
+ - run: PICTUREHOUSE_PACKAGE="$(echo pkg/*.gem)" bundle exec ruby test/package_test.rb
28
+ - uses: actions/upload-artifact@v4
29
+ with:
30
+ name: gem-${{ matrix.platform }}
31
+ path: pkg/*.gem
32
+
7
33
  release:
34
+ needs: package
8
35
  runs-on: ubuntu-latest
9
36
  permissions:
10
37
  id-token: write
11
38
  contents: write
12
-
13
39
  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
40
+ - uses: actions/checkout@v4
41
+ - uses: ruby/setup-ruby@v1
42
+ with:
43
+ ruby-version: '4.0'
44
+ bundler-cache: true
45
+ - run: bundle exec rake test
46
+ - run: bundle exec rake build
47
+ - uses: actions/download-artifact@v4
48
+ with:
49
+ path: pkg
50
+ pattern: gem-*
51
+ merge-multiple: true
52
+ - uses: rubygems/configure-rubygems-credentials@v1.0.0
53
+ - name: Push the source and platform gems
54
+ run: |
55
+ for gem in pkg/*.gem; do
56
+ if output=$(gem push "$gem" 2>&1); then
57
+ echo "$output"
58
+ elif echo "$output" | grep -q "Repushing of gem versions is not allowed"; then
59
+ echo "Already published: $gem"
60
+ else
61
+ echo "$output"
62
+ exit 1
63
+ fi
64
+ done
65
+ - name: Tag the release
66
+ run: |
67
+ tag="v$(ruby -e 'print Gem::Specification.load(Dir["*.gemspec"].first).version')"
68
+ if git ls-remote --exit-code --tags origin "$tag" >/dev/null; then
69
+ echo "$tag already exists"
70
+ else
71
+ git tag "$tag"
72
+ git push origin "$tag"
73
+ fi
data/CHANGELOG.md CHANGED
@@ -2,6 +2,32 @@
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
+ ## [6.0.0] - 2026-10-22
6
+
7
+ ### Changed
8
+ - Retrieve cinemas and performances from the Vista API behind picturehouses.com, as cineworld_uk and odeon_uk do, instead of scraping HTML pages and the `get-movies-ajax` endpoint.
9
+ - **BREAKING**: Cinema ids are Vista site ids, zero-padded strings such as `'019'` for Duke's at Komedia, instead of page slugs such as `'duke-s-at-komedia'`.
10
+ - **BREAKING**: `Performance.at` accepts inclusive `from:` and `to:` business dates, defaulting to today in London and the following 13 days. One request per day.
11
+ - **BREAKING**: Require cinebase 5.4, and so Ruby 3.3 or newer. Nokogiri is no longer needed.
12
+ - `Cinema#adr` comes from the API's address lines; `region` is no longer given. `Cinema#url` points at the current cinema pages.
13
+ - Film titles also use cinebase's common rules and lose `Dog-Friendly -`, `Relaxed-Screening:`, `LMF Presents:`, `Green Screen:` and `FFQ:` prefixes.
14
+ - Tests use hand-written fixtures in place of recorded pages and API responses.
15
+
16
+ ### Added
17
+ - `PicturehouseUk.token` and `PicturehouseUk.acquire_token!`, with platform gems that bundle curl-impersonate 2.2.3.
18
+ - `Performance#film_year`, `#subtitled?` and `#festival`.
19
+ - Variants from the current Vista attributes: `audio_described`, `baby`, `kids`, `q&a`, `relaxed`, `senior`, `subtitled` and `arts`.
20
+ - Live API shape tests that check real responses without storing them.
21
+
22
+ ### Fixed
23
+ - Performance times were London local times labelled as UTC, an hour out during British Summer Time. They now use Vista's explicit offsets.
24
+ - Booking links used the cinema slug and now use Vista's showtime id, as in `https://web.picturehouses.com/order/showtimes/019-41632/seats`.
25
+ - Performances could include other cinemas' films returned by the old endpoint.
26
+ - Unit tests failed under Minitest 6, which moved `stub` into `minitest-mock`.
27
+
28
+ ### Removed
29
+ - HTML scraping, the `fixtures` rake task, and live tests in CI.
30
+
5
31
  ## [5.0.0] - 2025-10-31
6
32
 
7
33
  ### Added
data/README.md CHANGED
@@ -1,12 +1,8 @@
1
1
  # PicturehouseUk
2
2
 
3
- A simple gem to parse the [Picturehouse Cinemas UK website](http://picturehouses.com) and spit out useful formatted info.
3
+ A simple gem to pull cinemas and performances from the Vista API behind the [Picturehouse website](https://www.picturehouses.com) and spit out useful formatted info.
4
4
 
5
5
  [![Gem Version](https://badge.fury.io/rb/picturehouse_uk.svg)](https://badge.fury.io/rb/picturehouse_uk)
6
- [![Code Climate](https://codeclimate.com/github/andycroll/picturehouse_uk/badges/gpa.svg)](https://codeclimate.com/github/andycroll/picturehouse_uk)
7
- [![Test Coverage](https://codeclimate.com/github/andycroll/picturehouse_uk/badges/coverage.svg)](https://codeclimate.com/github/andycroll/picturehouse_uk/coverage)
8
- [![Build Status](https://travis-ci.org/andycroll/picturehouse_uk.svg?branch=master)](https://travis-ci.org/andycroll/picturehouse_uk)
9
- [![Inline docs](http://inch-ci.org/github/andycroll/picturehouse_uk.svg?branch=master)](http://inch-ci.org/github/andycroll/picturehouse_uk)
10
6
 
11
7
  ## Installation
12
8
 
@@ -24,44 +20,152 @@ Or install it yourself as:
24
20
 
25
21
  ## Usage
26
22
 
27
- ### Cinema
23
+ The gem conforms to the API set down in the `cinebase` gem, [andycroll/cinebase](https://github.com/andycroll/cinebase), which provides a lot of useful base vocabulary and repetitive code for this series of cinema focussed gems.
24
+
25
+ Performance titles are sanitized as much as possible, removing 'screening type' information, years, Q&A add-ons and festival prefixes, and standardising theatre and opera event naming.
26
+
27
+ ### Guest token
28
+
29
+ Every API call needs the public guest token the Picturehouse ticketing site
30
+ gives each visitor. Fetch one before anything else:
31
+
32
+ ``` ruby
33
+ PicturehouseUk.acquire_token!
34
+ PicturehouseUk::Cinema.all
35
+ ```
36
+
37
+ This shells out to **`curl-impersonate`**, not ordinary system `curl`, using its
38
+ Chrome 150 TLS/HTTP profile, and reads `gasToken` from the page data of
39
+ `https://web.picturehouses.com/`. No browser or JavaScript execution is needed.
40
+
41
+ Platform-specific gem packages include the pinned `curl-impersonate` 2.2.3
42
+ executable for glibc Linux (x86-64, ARM64) and macOS (Intel, Apple Silicon).
43
+ On other platforms, or when installing from Git, install a
44
+ [curl-impersonate release](https://github.com/lexiforest/curl-impersonate/releases/tag/v2.2.3)
45
+ yourself and put it on your `PATH` or pass its path:
28
46
 
29
47
  ``` ruby
30
- PicturehouseUK::Cinema.all
31
- #=> [
32
- <PicturehouseUK::Cinema brand="Picturehouse" name="Duke's at Komedia" slug="dukes-at-komedia" chain_id="Dukes_At_Komedia" url="...">,
33
- <PicturehouseUK::Cinema brand="Picturehouse" name="Duke of York's" slug="duke-of-yorks" chain_id="Duke_Of_Yorks" url="...">,
34
- ...
35
- ]
48
+ PicturehouseUk.acquire_token!(curl: '/opt/curl-impersonate/curl-impersonate')
49
+ ```
36
50
 
37
- PicturehouseUK::Cinema.find('Duke_Of_Yorks')
38
- #=> <PicturehouseUK::Cinema brand="Picturehouse" name="Duke_Of_Yorks" slug="duke-of-yorks" address="..." chain_id="Duke_Of_Yorks" url="...">
51
+ Or supply your own current guest JWT, without the `Bearer ` prefix:
52
+
53
+ ``` ruby
54
+ PicturehouseUk.token = ENV.fetch('PICTUREHOUSE_TOKEN')
55
+ ```
56
+
57
+ Tokens currently last 12 hours. The gem checks the declared expiry but does not
58
+ verify the signature. Missing, expired or rejected tokens raise
59
+ `PicturehouseUk::AuthenticationError`; call `PicturehouseUk.acquire_token!`
60
+ again. Normal API calls never acquire or renew a token automatically.
61
+
62
+ ### Cinema
63
+
64
+ Cinema ids are Vista site ids. Duke's at Komedia is `'019'`.
65
+
66
+ ``` ruby
67
+ PicturehouseUk::Cinema.all
68
+ #=> [<PicturehouseUk::Cinema ...>, <PicturehouseUk::Cinema ...>, ...]
69
+
70
+ cinema = PicturehouseUk::Cinema.new(19)
71
+ #=> <PicturehouseUk::Cinema ...>
72
+
73
+ cinema.adr
74
+ #=> {
75
+ street_address: '44-47 Gardner Street',
76
+ extended_address: nil,
77
+ locality: 'Brighton',
78
+ region: nil,
79
+ postal_code: 'BN1 1UN',
80
+ country_name: 'United Kingdom'
81
+ }
39
82
 
40
83
  cinema.brand
41
84
  #=> 'Picturehouse'
42
85
 
43
- cinema.chain_id
44
- #=> 'Duke_Of_Yorks'
86
+ cinema.name
87
+ #=> "Duke's at Komedia"
88
+
89
+ cinema.id
90
+ #=> '019'
45
91
 
46
92
  cinema.url
47
- #=> "http://www.picturehouses.co.uk/cinema/Duke_Of_Yorks/"
93
+ #=> 'https://www.picturehouses.com/cinema/duke-s-at-komedia'
94
+ ```
95
+
96
+ ### Performances
97
+
98
+ `Performance.at` returns performances for an inclusive range of business dates,
99
+ defaulting to today in London and the following 13 days. It makes one request
100
+ per day.
101
+
102
+ ``` ruby
103
+ PicturehouseUk::Performance.at(19, from: Date.today, to: Date.today + 2)
104
+ #=> [<PicturehouseUk::Performance ...>, <PicturehouseUk::Performance ...>, ...]
105
+
106
+ performance = PicturehouseUk::Performance.at(19).first
107
+ #=> <PicturehouseUk::Performance ...>
108
+
109
+ performance.film_name
110
+ #=> 'Sense and Sensibility'
111
+
112
+ performance.film_year
113
+ #=> 2026 # from the title, nil when it has no year
48
114
 
49
- cinema.films
50
- #=> [<PicturehouseUK::Film name="Iron Man 3">, <PicturehouseUK::Film name="Star Trek: Into Darkness">]
115
+ performance.dimension
116
+ #=> '2d'
51
117
 
52
- cinema.screenings
53
- #=> [<PicturehouseUK::Screening film="About Time" when="2013-09-09 11:00 UTC" varient="3d">, <PicturehouseUK::Screening film="Iron Man 3" when="2013-09-09 13:50 UTC" varient="kids">, <PicturehouseUK::Screening ..>, <PicturehouseUK::Screening ...>]
118
+ performance.variant
119
+ #=> ['audio_described', 'subtitled']
54
120
 
121
+ performance.subtitled?
122
+ #=> true
123
+
124
+ performance.festival
125
+ #=> #<data Cinebase::Festival name="CFF", year=2026> # nil outside festivals
126
+
127
+ performance.starting_at
128
+ #=> 2026-10-23 15:00:00 UTC
129
+
130
+ performance.booking_url
131
+ #=> 'https://web.picturehouses.com/order/showtimes/019-41632/seats'
132
+
133
+ performance.cinema_name
134
+ #=> "Duke's at Komedia"
135
+
136
+ performance.cinema_id
137
+ #=> '019'
55
138
  ```
56
139
 
57
- ## Running Tests
140
+ Variants are `arts`, `audio_described`, `baby`, `kids`, `q&a`, `relaxed`,
141
+ `senior` and `subtitled`. Picturehouse doesn't tag audio languages, so
142
+ `language` is always `nil`.
58
143
 
59
- To redownload the test URLs run
144
+ ## Tests
60
145
 
146
+ ```sh
147
+ bundle exec rake # unit tests against fixtures
148
+ bundle exec rake live # live API, needs curl-impersonate or PICTUREHOUSE_TOKEN
61
149
  ```
62
- ruby test/fixture_updater.rb
150
+
151
+ Fixtures in `test/fixtures/api` are hand-written: invented films and
152
+ addresses in the shape of Vista responses, never recorded API output.
153
+ `test/support/api_shapes.rb` describes the fields the gem reads. The unit suite
154
+ checks the fixtures against it and the live suite checks real responses, held
155
+ only in memory. When the API changes, update the shape first, then the fixtures.
156
+
157
+ ## Building release packages
158
+
159
+ ```sh
160
+ bundle exec rake build # Portable source gem; external curl-impersonate required
161
+ bundle exec rake package:all # Four platform gems with bundled executables
63
162
  ```
64
163
 
164
+ To release, bump the version, merge, then run the **Release Gem** workflow in
165
+ GitHub Actions. It builds the source and four platform gems, publishes them
166
+ through RubyGems trusted publishing (no MFA code needed) and tags the release.
167
+ Versions already on RubyGems are skipped, so a failed run can be rerun.
168
+
65
169
  ## Contributing
66
170
 
67
171
  1. Fork it
data/Rakefile CHANGED
@@ -2,13 +2,16 @@
2
2
  require 'bundler/gem_tasks'
3
3
 
4
4
  require 'rake/testtask'
5
+ require 'cinebase/vista/package_task'
6
+
7
+ Cinebase::Vista::PackageTask.new('picturehouse_uk.gemspec', root: __dir__)
5
8
 
6
9
  Rake::TestTask.new do |t|
7
10
  t.libs << 'lib/picturehouse_uk'
8
11
  t.test_files = FileList[
9
12
  'test/lib/picturehouse_uk/*_test.rb',
10
13
  'test/lib/picturehouse_uk/internal/*_test.rb',
11
- 'test/lib/picturehouse_uk/internal/parser/*_test.rb'
14
+ 'test/lib/picturehouse_uk/internal/parser/api/*_test.rb'
12
15
  ]
13
16
  t.verbose = true
14
17
  end
@@ -31,16 +34,4 @@ task :console do
31
34
  IRB.start
32
35
  end
33
36
 
34
- desc 'recreate test fixtures'
35
- task :fixtures do
36
- require 'picturehouse_uk'
37
- require_relative 'rake/fixture_creator'
38
-
39
- FixtureCreator.new.home
40
- FixtureCreator.new.cinemas
41
- %w(duke-of-york-s-picturehouse duke-s-at-komedia phoenix-picturehouse).each do |cinema_id|
42
- FixtureCreator.new.cinema(cinema_id)
43
- end
44
- end
45
-
46
37
  task default: :test
@@ -1,18 +1,24 @@
1
1
  module PicturehouseUk
2
2
  # The object representing a cinema on the Picturehouse UK website
3
3
  class Cinema < Cinebase::Cinema
4
- # address 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
-
9
4
  # @!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]
5
+ # @return [String] the zero-padded Vista id of the cinema, e.g. '019'
6
+
7
+ # Vista site ids whose page slug differs from the cinema name
8
+ URL_SLUGS = {
9
+ '013' => 'picturehouse-at-fact',
10
+ '030' => 'epsom-picturehouse'
11
+ }.freeze
12
+
13
+ # UK postcode, the last line of an address
14
+ POSTCODE = /\A[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}\z/i.freeze
15
+
16
+ # Constructor
17
+ # @param [Integer, String] id the cinema id of the format 19/'019'
18
+ # @return [PicturehouseUk::Cinema]
19
+ def initialize(id)
20
+ @id = id.to_s.rjust(3, '0')
21
+ end
16
22
 
17
23
  # Return basic cinema information for all cinemas
18
24
  # @return [Array<PicturehouseUk::Cinema>]
@@ -20,12 +26,7 @@ module PicturehouseUk
20
26
  # PicturehouseUk::Cinema.all
21
27
  # #=> [<PicturehouseUk::Cinema>, <PicturehouseUk::Cinema>, ...]
22
28
  def self.all
23
- cinema_hash.keys.map { |id| new(id) }
24
- end
25
-
26
- # @api private
27
- def self.cinema_hash
28
- @cinema_hash ||= ListParser.new(cinema_links).to_hash
29
+ sites_hash.keys.map { |id| new(id) }
29
30
  end
30
31
 
31
32
  # @!method address
@@ -35,178 +36,91 @@ module PicturehouseUk
35
36
 
36
37
  # Address of the cinema
37
38
  # @return [Hash] contains :street_address, :extended_address,
38
- # :locality, :postal_code, :country
39
+ # :locality, :region, :postal_code, :country_name
39
40
  # @example
40
- # cinema = PicturehouseUk::Cinema.new('Dukes_At_Komedia')
41
+ # cinema = PicturehouseUk::Cinema.new(19)
41
42
  # cinema.adr
42
43
  # #=> {
43
44
  # street_address: '44-47 Gardner Street',
44
- # extended_address: 'North Laine',
45
+ # extended_address: nil,
45
46
  # locality: 'Brighton',
46
- # region: 'East Sussex',
47
+ # region: nil,
47
48
  # postal_code: 'BN1 1UN',
48
- # country_name: 'United Kingdom'
49
+ # country_name: 'United Kingdom'
49
50
  # }
50
51
  # @note Uses method naming as at http://microformats.org/wiki/adr
51
52
  def adr
52
- PicturehouseUk::Internal::Parser::Address.new(address_node.to_s).address
53
+ lines = address_lines
54
+ postal_code = lines.pop if lines.last&.match?(POSTCODE)
55
+ street_address = lines.shift
56
+ locality = lines.pop
57
+ {
58
+ street_address: street_address,
59
+ extended_address: lines.empty? ? nil : lines.join(', '),
60
+ locality: locality,
61
+ region: nil,
62
+ postal_code: postal_code,
63
+ country_name: 'United Kingdom'.freeze
64
+ }
53
65
  end
54
66
 
55
67
  # Brand of the cinema
56
68
  # @return [String] which will always be 'Picturehouse'
57
- # @example
58
- # cinema = PicturehouseUk::Cinema.new('Dukes_At_Komedia')
59
- # cinema.brand
60
- # #=> 'Picturehouse'
61
69
  def brand
62
70
  'Picturehouse'.freeze
63
71
  end
64
72
 
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'
80
-
81
- # The name of the cinema (might include brand)
73
+ # The name of the cinema, which often includes the brand
82
74
  # @return [String]
83
75
  # @example
84
- # cinema = PicturehouseUk::Cinema.new('Dukes_At_Komedia')
76
+ # cinema = PicturehouseUk::Cinema.new(19)
85
77
  # cinema.full_name
86
- # #=> "Duke's At Komedia"
78
+ # #=> "Duke's at Komedia"
87
79
  def full_name
88
80
  name
89
81
  end
90
82
 
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'
98
-
99
83
  # The name of the cinema
100
84
  # @return [String]
101
85
  # @example
102
- # cinema = PicturehouseUk::Cinema.new('Dukes_At_Komedia')
86
+ # cinema = PicturehouseUk::Cinema.new(19)
103
87
  # cinema.name
104
- # #=> "Duke's At Komedia"
88
+ # #=> "Duke's at Komedia"
105
89
  def name
106
- self.class.cinema_hash.fetch(id, {})[:name]
90
+ site_hash.dig('name', 'text').to_s
107
91
  end
108
92
 
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
93
  # The url of the cinema on the Picturehouse website
142
94
  # @return [String]
143
95
  def url
144
- "http://www.picturehouses.com#/cinema/#{id}"
145
- end
146
-
147
- private
148
-
149
- def self.cinema_links
150
- cinemas_doc.css(CINEMA_LINKS_CSS)
151
- end
152
- private_class_method :cinema_links
153
-
154
- def self.cinemas_doc
155
- @cinemas_doc ||=
156
- Nokogiri::HTML(PicturehouseUk::Internal::Website.new.cinemas)
157
- end
158
- private_class_method :cinemas_doc
159
-
160
- def address_node
161
- @address_node ||= info_doc.css(ADDRESS_CSS)
162
- end
163
-
164
- def info_doc
165
- @info_doc ||=
166
- Nokogiri::HTML(PicturehouseUk::Internal::Website.new.information(id))
96
+ "https://www.picturehouses.com/cinema/#{url_slug}"
167
97
  end
168
98
 
169
99
  # @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
100
+ def self.sites_hash
101
+ @sites_hash ||= PicturehouseUk::Internal::ApiResponse.new.sites.to_h do |site|
102
+ [site.fetch('id'), site]
187
103
  end
104
+ end
105
+ private_class_method :sites_hash
188
106
 
189
- private
107
+ private
190
108
 
191
- def id(node)
192
- href = url(node)
193
- return nil unless href
194
- match = href.match(%r{/cinema/([^/?#]+)})
195
- match ? match[1] : nil
109
+ def address_lines
110
+ address = site_hash.dig('contactDetails', 'addressV2') || {}
111
+ %w(line1 line2 line3 line4).filter_map do |key|
112
+ text = address.dig(key, 'text').to_s.strip
113
+ text unless text.empty?
196
114
  end
115
+ end
197
116
 
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
117
+ def site_hash
118
+ @site_hash ||= self.class.send(:sites_hash).fetch(id, {})
119
+ end
205
120
 
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}"
121
+ def url_slug
122
+ URL_SLUGS.fetch(id) do
123
+ name.downcase.gsub(/[^a-z0-9]+/, '-').gsub(/\A-|-\z/, '')
210
124
  end
211
125
  end
212
126
  end