picturehouse_uk 6.0.0-aarch64-linux-gnu

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 +7 -0
  2. data/.github/workflows/ci.yml +47 -0
  3. data/.github/workflows/release.yml +73 -0
  4. data/.gitignore +18 -0
  5. data/.ruby-version +1 -0
  6. data/.yardopts +4 -0
  7. data/CHANGELOG.md +213 -0
  8. data/COMM-LICENSE +22 -0
  9. data/Gemfile +4 -0
  10. data/LICENSE +617 -0
  11. data/README.md +175 -0
  12. data/Rakefile +37 -0
  13. data/lib/picturehouse_uk/cinema.rb +127 -0
  14. data/lib/picturehouse_uk/internal/api_response.rb +35 -0
  15. data/lib/picturehouse_uk/internal/guest_token.rb +28 -0
  16. data/lib/picturehouse_uk/internal/parser/api/showtime.rb +70 -0
  17. data/lib/picturehouse_uk/internal/title_sanitizer.rb +78 -0
  18. data/lib/picturehouse_uk/performance.rb +80 -0
  19. data/lib/picturehouse_uk/version.rb +6 -0
  20. data/lib/picturehouse_uk.rb +30 -0
  21. data/picturehouse_uk.gemspec +29 -0
  22. data/test/fixtures/api/showtimes-019.json +62 -0
  23. data/test/fixtures/api/sites.json +40 -0
  24. data/test/lib/picturehouse_uk/cinema_test.rb +85 -0
  25. data/test/lib/picturehouse_uk/internal/api_response_test.rb +52 -0
  26. data/test/lib/picturehouse_uk/internal/fixture_shape_test.rb +14 -0
  27. data/test/lib/picturehouse_uk/internal/guest_token_test.rb +71 -0
  28. data/test/lib/picturehouse_uk/internal/parser/api/showtime_test.rb +59 -0
  29. data/test/lib/picturehouse_uk/internal/title_sanitizer_test.rb +243 -0
  30. data/test/lib/picturehouse_uk/performance_test.rb +111 -0
  31. data/test/lib/picturehouse_uk/version_test.rb +7 -0
  32. data/test/live/api_shape_test.rb +19 -0
  33. data/test/live/integration_test.rb +27 -0
  34. data/test/live/live_helper.rb +11 -0
  35. data/test/package_test.rb +56 -0
  36. data/test/support/api_fixtures_helper.rb +17 -0
  37. data/test/support/api_shapes.rb +48 -0
  38. data/test/support/fake_api_response.rb +20 -0
  39. data/test/test_helper.rb +20 -0
  40. data/vendor/curl-impersonate/SOURCE +5 -0
  41. data/vendor/curl-impersonate/bin/curl-impersonate +0 -0
  42. data/vendor/curl-impersonate/licenses/LICENSE +21 -0
  43. data/vendor/curl-impersonate/licenses/LICENSE_BORINGSSL +238 -0
  44. data/vendor/curl-impersonate/licenses/LICENSE_BROTLI +19 -0
  45. data/vendor/curl-impersonate/licenses/LICENSE_CARES +24 -0
  46. data/vendor/curl-impersonate/licenses/LICENSE_CURL +22 -0
  47. data/vendor/curl-impersonate/licenses/LICENSE_LIBIDN2_GPL2 +339 -0
  48. data/vendor/curl-impersonate/licenses/LICENSE_LIBIDN2_GPL3 +35 -0
  49. data/vendor/curl-impersonate/licenses/LICENSE_LIBIDN2_LGPL3 +165 -0
  50. data/vendor/curl-impersonate/licenses/LICENSE_LIBIDN2_UNICODE +91 -0
  51. data/vendor/curl-impersonate/licenses/LICENSE_NGHTTP2 +23 -0
  52. data/vendor/curl-impersonate/licenses/LICENSE_NGHTTP3 +22 -0
  53. data/vendor/curl-impersonate/licenses/LICENSE_NGTCP2 +22 -0
  54. data/vendor/curl-impersonate/licenses/LICENSE_ZLIB +22 -0
  55. data/vendor/curl-impersonate/licenses/LICENSE_ZSTD +30 -0
  56. metadata +225 -0
data/README.md ADDED
@@ -0,0 +1,175 @@
1
+ # PicturehouseUk
2
+
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
+
5
+ [![Gem Version](https://badge.fury.io/rb/picturehouse_uk.svg)](https://badge.fury.io/rb/picturehouse_uk)
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ gem 'picturehouse_uk'
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install picturehouse_uk
20
+
21
+ ## Usage
22
+
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:
46
+
47
+ ``` ruby
48
+ PicturehouseUk.acquire_token!(curl: '/opt/curl-impersonate/curl-impersonate')
49
+ ```
50
+
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
+ }
82
+
83
+ cinema.brand
84
+ #=> 'Picturehouse'
85
+
86
+ cinema.name
87
+ #=> "Duke's at Komedia"
88
+
89
+ cinema.id
90
+ #=> '019'
91
+
92
+ cinema.url
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
114
+
115
+ performance.dimension
116
+ #=> '2d'
117
+
118
+ performance.variant
119
+ #=> ['audio_described', 'subtitled']
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'
138
+ ```
139
+
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`.
143
+
144
+ ## Tests
145
+
146
+ ```sh
147
+ bundle exec rake # unit tests against fixtures
148
+ bundle exec rake live # live API, needs curl-impersonate or PICTUREHOUSE_TOKEN
149
+ ```
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
162
+ ```
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
+
169
+ ## Contributing
170
+
171
+ 1. Fork it
172
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
173
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
174
+ 4. Push to the branch (`git push origin my-new-feature`)
175
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env rake
2
+ require 'bundler/gem_tasks'
3
+
4
+ require 'rake/testtask'
5
+ require 'cinebase/vista/package_task'
6
+
7
+ Cinebase::Vista::PackageTask.new('picturehouse_uk.gemspec', root: __dir__)
8
+
9
+ Rake::TestTask.new do |t|
10
+ t.libs << 'lib/picturehouse_uk'
11
+ t.test_files = FileList[
12
+ 'test/lib/picturehouse_uk/*_test.rb',
13
+ 'test/lib/picturehouse_uk/internal/*_test.rb',
14
+ 'test/lib/picturehouse_uk/internal/parser/api/*_test.rb'
15
+ ]
16
+ t.verbose = true
17
+ end
18
+
19
+ Rake::TestTask.new do |t|
20
+ t.libs << 'lib/picturehouse_uk'
21
+ t.name = :live
22
+ t.test_files = FileList[
23
+ 'test/live/*_test.rb'
24
+ ]
25
+ t.verbose = true
26
+ end
27
+
28
+ # http://erniemiller.org/2014/02/05/7-lines-every-gems-rakefile-should-have/
29
+ task :console do
30
+ require 'irb'
31
+ require 'irb/completion'
32
+ require 'picturehouse_uk'
33
+ ARGV.clear
34
+ IRB.start
35
+ end
36
+
37
+ task default: :test
@@ -0,0 +1,127 @@
1
+ module PicturehouseUk
2
+ # The object representing a cinema on the Picturehouse UK website
3
+ class Cinema < Cinebase::Cinema
4
+ # @!attribute [r] id
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
22
+
23
+ # Return basic cinema information for all cinemas
24
+ # @return [Array<PicturehouseUk::Cinema>]
25
+ # @example
26
+ # PicturehouseUk::Cinema.all
27
+ # #=> [<PicturehouseUk::Cinema>, <PicturehouseUk::Cinema>, ...]
28
+ def self.all
29
+ sites_hash.keys.map { |id| new(id) }
30
+ end
31
+
32
+ # @!method address
33
+ # Address of the cinema
34
+ # @return [Hash] of different address parts
35
+ # @see #adr
36
+
37
+ # Address of the cinema
38
+ # @return [Hash] contains :street_address, :extended_address,
39
+ # :locality, :region, :postal_code, :country_name
40
+ # @example
41
+ # cinema = PicturehouseUk::Cinema.new(19)
42
+ # cinema.adr
43
+ # #=> {
44
+ # street_address: '44-47 Gardner Street',
45
+ # extended_address: nil,
46
+ # locality: 'Brighton',
47
+ # region: nil,
48
+ # postal_code: 'BN1 1UN',
49
+ # country_name: 'United Kingdom'
50
+ # }
51
+ # @note Uses method naming as at http://microformats.org/wiki/adr
52
+ def adr
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
+ }
65
+ end
66
+
67
+ # Brand of the cinema
68
+ # @return [String] which will always be 'Picturehouse'
69
+ def brand
70
+ 'Picturehouse'.freeze
71
+ end
72
+
73
+ # The name of the cinema, which often includes the brand
74
+ # @return [String]
75
+ # @example
76
+ # cinema = PicturehouseUk::Cinema.new(19)
77
+ # cinema.full_name
78
+ # #=> "Duke's at Komedia"
79
+ def full_name
80
+ name
81
+ end
82
+
83
+ # The name of the cinema
84
+ # @return [String]
85
+ # @example
86
+ # cinema = PicturehouseUk::Cinema.new(19)
87
+ # cinema.name
88
+ # #=> "Duke's at Komedia"
89
+ def name
90
+ site_hash.dig('name', 'text').to_s
91
+ end
92
+
93
+ # The url of the cinema on the Picturehouse website
94
+ # @return [String]
95
+ def url
96
+ "https://www.picturehouses.com/cinema/#{url_slug}"
97
+ end
98
+
99
+ # @api private
100
+ def self.sites_hash
101
+ @sites_hash ||= PicturehouseUk::Internal::ApiResponse.new.sites.to_h do |site|
102
+ [site.fetch('id'), site]
103
+ end
104
+ end
105
+ private_class_method :sites_hash
106
+
107
+ private
108
+
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?
114
+ end
115
+ end
116
+
117
+ def site_hash
118
+ @site_hash ||= self.class.send(:sites_hash).fetch(id, {})
119
+ end
120
+
121
+ def url_slug
122
+ URL_SLUGS.fetch(id) do
123
+ name.downcase.gsub(/[^a-z0-9]+/, '-').gsub(/\A-|-\z/, '')
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module PicturehouseUk
6
+ # Internal utility classes: Do not use
7
+ # @api private
8
+ module Internal
9
+ # Fetches cinema and showtime data from the Vista API behind picturehouses.com
10
+ class ApiResponse
11
+ BASE_URL = "https://digital-api.picturehouses.com/ocapi/v1/"
12
+
13
+ # @return [Array<Hash>] cinemas
14
+ def sites
15
+ client.get("sites").fetch("sites")
16
+ end
17
+
18
+ # @param [String] cinema_id zero-padded site id, e.g. '019'
19
+ # @param [Date] date business date
20
+ # @return [Hash] showtimes plus their related films and attributes
21
+ def showtimes(cinema_id, date)
22
+ client.get("showtimes/by-business-date/#{date.iso8601}",
23
+ "siteIds" => cinema_id)
24
+ end
25
+
26
+ private
27
+
28
+ # Picturehouse's API rejects every Connect-Region-Code, so send none.
29
+ def client
30
+ Cinebase::Vista::Client.new(base_url: BASE_URL, region_code: nil,
31
+ token: PicturehouseUk.token)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module PicturehouseUk
6
+ module Internal
7
+ # Reads the public guest token from the ticketing site's page data
8
+ class GuestToken < Cinebase::Vista::GuestToken
9
+ PAGE_URL = "https://web.picturehouses.com/"
10
+
11
+ private
12
+
13
+ # The ticketing site serves the same page data for unknown paths.
14
+ def accepted_statuses
15
+ %w[200 404]
16
+ end
17
+
18
+ def bundled_curl
19
+ File.expand_path("../../../vendor/curl-impersonate/bin/curl-impersonate", __dir__)
20
+ end
21
+
22
+ def extract_token(html)
23
+ script = html[%r{<script id="__NEXT_DATA__"[^>]*>(.*?)</script>}m, 1].to_s
24
+ JSON.parse(script).dig("props", "pageProps", "environment", "gasToken")
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module PicturehouseUk
6
+ # @api private
7
+ module Internal
8
+ module Parser
9
+ module Api
10
+ # Parses a single Vista showtime into performance attributes
11
+ class Showtime
12
+ BOOKING_URL = "https://web.picturehouses.com/order/showtimes/%s/seats"
13
+
14
+ # Vista attribute names, stripped
15
+ VARIANTS = {
16
+ "AD" => "audio_described",
17
+ "Kids' Club" => "kids",
18
+ "Q&A" => "q&a",
19
+ "Relaxed" => "relaxed",
20
+ "Screen Arts" => "arts",
21
+ "Silver Screen" => "senior",
22
+ "Subtitled" => "subtitled",
23
+ "Subtitled (Captioned)" => "subtitled",
24
+ "Toddler Time" => "kids",
25
+ "Watch with Baby" => "baby"
26
+ }.freeze
27
+
28
+ # @param [Hash] data one showtime
29
+ # @param [Hash{String => Hash}] films related films keyed by id
30
+ # @param [Hash{String => Hash}] attributes related attributes keyed by id
31
+ def initialize(data, films:, attributes:)
32
+ @data = data
33
+ @films = films
34
+ @attributes = attributes
35
+ end
36
+
37
+ # @return [Hash] options for PicturehouseUk::Performance.new
38
+ def to_hash
39
+ {
40
+ booking_url: format(BOOKING_URL, @data.fetch("id")),
41
+ dimension: @data["requires3dGlasses"] ? "3d" : "2d",
42
+ festival: Cinebase::Festival.from_title(title, year: starting_at.year),
43
+ film_name: TitleSanitizer.new(title).sanitized,
44
+ film_year: Cinebase::TitleSanitizer.film_year(title),
45
+ starting_at: starting_at,
46
+ variant: variant
47
+ }
48
+ end
49
+
50
+ private
51
+
52
+ def starting_at
53
+ Time.iso8601(@data.dig("schedule", "startsAt")).utc
54
+ end
55
+
56
+ def title
57
+ @films.fetch(@data.fetch("filmId")).dig("title", "text")
58
+ end
59
+
60
+ def variant
61
+ @data.fetch("attributeIds", []).filter_map do |id|
62
+ name = @attributes.dig(id, "name", "text").to_s.strip
63
+ VARIANTS[name.delete_prefix("+ ")]
64
+ end.uniq
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,78 @@
1
+ module PicturehouseUk
2
+ # @api private
3
+ module Internal
4
+ # Sanitize and standardize film titles
5
+ class TitleSanitizer < Cinebase::TitleSanitizer
6
+ # @!method initialize(title)
7
+ # Constructor
8
+ # @param [String] title a film title
9
+ # @return [CineworldUk::Internal::TitleSanitizer]
10
+
11
+ # @!method sanitized
12
+ # sanitized and standardized title
13
+ # @return [String] sanitised title
14
+
15
+ private
16
+
17
+ # strings and regex to be removed
18
+ def remove
19
+ [
20
+ /\s\[(AS LIVE:\s*)?[ACPGU1258]+\]/, # regular certificate
21
+ /\s\[R18\]/, # R18
22
+ /\s+\(?[23][dD]\)?/, # 2d or 3d from title
23
+ /\bIMAX\b/i, # imax from title
24
+ /\s\[NO CERT\]/, # no certificate
25
+ /\s\[TBC\]/, # tbc
26
+ /\s\[N\/A\]/i, # n/a
27
+ /\s\(Theatre\)/i, # (theatre)
28
+ /\s\[\]/, # blank certificate
29
+ /ourscreen\: /, # ourscreen
30
+ /\s\(Re(\: \d{0,4})?\)/i, # Re-release
31
+ /\s\[CERT TBC\]/, # certificate TBC
32
+ /\s?\-\s?autism.*ing\s?/i, # austim screening
33
+ /\s?\+\s?Q\&(amp;)*A\.?/i, # +Q&A
34
+ /KIDS CLUB\s*/i, # kids club
35
+ /DISCOVER TUE(sday\:)*\s*/i, # discover tue
36
+ /FREE Screening\s*-\s*/i, # free screening
37
+ /\s*-?\s*Big Scream\s*-?\s*/i, # big scream
38
+ /\*?HOH Subtitled\*?/i, # subtitled
39
+ /\s*\-?\s*Reminiscence/i, # reminiscence
40
+ /\s*\-?\s*\(?Re\-issue\)?/i, # reissue
41
+ /\s*-?\s*Kids\'? Club\s*-?\s*/, # kids club
42
+ /\s*plus Q\&A.*/i, # extended Q&A
43
+ /Cinemania\s*[\:\-]/i, # cinemania
44
+ /\@\s*komedia/i, # @ komedia
45
+ /\s*\+ panel.*/i, # panel
46
+ /toddler time\s*\:*\-*/i, # toddler time
47
+ /\(.*\d{4}\)/, # year or captured year
48
+ /\bsingalong\b/i, # singalong
49
+ /\s+\-?\s*Parents \& Babies/i, # parents and babies
50
+ /Subtitled\:*\s*/i, # subtitled
51
+ /\(?live\)?\z/i, # live
52
+ /amp\;/i,
53
+ /\ADog-Friendly - /, # dog friendly
54
+ /\ARelaxed-Screening: /, # relaxed screening
55
+ /\ALMF Presents: /, # LMF strand
56
+ /\AGreen Screen: /, # Green Screen strand
57
+ /\AFFQ: / # FFQ strand
58
+ ] + COMMON_REMOVE
59
+ end
60
+
61
+ # regexes and their replacements
62
+ def replace
63
+ {
64
+ /Met\.? Encore:\s*(.*)/ => 'Met Opera: ',
65
+ /Met\.? Opera:\s*(.*)/ => 'Met Opera: ',
66
+ /National Theatre:\s*(.*)/ => 'NT Live: ',
67
+ /NT Encore:\s*(.*)/ => 'NT Live: ',
68
+ /NT Live:\s*(.*)/ => 'NT Live: ',
69
+ /ROH\.? Live:\s*(.*)/ => 'Royal Opera House: ',
70
+ /ROH\.? Encore:\s*(.*)/ => 'Royal Opera House: ',
71
+ /RSC\.? Live:\s*(.*)/ => 'Royal Shakespeare Company: ',
72
+ /RSC\.? Encore:\s*(.*)/ => 'Royal Shakespeare Company: ',
73
+ /(.*) \(Bolshoi Ballet\)/ => 'Bolshoi Ballet: '
74
+ }.merge(COMMON_REPLACE)
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,80 @@
1
+ module PicturehouseUk
2
+ # A single screening of a film on the Picturehouse UK website
3
+ class Performance < Cinebase::Performance
4
+ # @!attribute [r] booking_url
5
+ # @return [String] the booking URL on the cinema website
6
+ # @!attribute [r] cinema_name
7
+ # @return [String] the cinema name
8
+ # @!attribute [r] cinema_id
9
+ # @return [String] the cinema id
10
+ # @!attribute [r] dimension
11
+ # @return [String] 2d or 3d
12
+ # @!attribute [r] film_name
13
+ # @return [String] the film name
14
+
15
+ # @!method initialize(options)
16
+ # @param [Hash] options options hash
17
+ # @option options [String] :booking_url (nil) buying url for the screening
18
+ # @option options [String] :cinema_name name of the cinema
19
+ # @option options [String] :cinema_id website id of the cinema
20
+ # @option options [String] :dimension ('2d') dimension of the screening
21
+ # @option options [Cinebase::Festival] :festival (nil) festival edition
22
+ # @option options [String] :film_name name of the film
23
+ # @option options [Integer] :film_year (nil) year from the film's title
24
+ # @option options [Time] :starting_at listed start time of the performance
25
+
26
+ # Performances within an inclusive range of cinema business dates.
27
+ # @param [Integer, String] cinema_id id of the cinema
28
+ # @param [Date] from first date (defaults to today in London)
29
+ # @param [Date] to last date (defaults to 13 days after from)
30
+ # @return [Array<PicturehouseUk::Performance>]
31
+ def self.at(
32
+ cinema_id,
33
+ from: TZInfo::Timezone.get('Europe/London').now.to_date,
34
+ to: from + 13
35
+ )
36
+ raise ArgumentError, 'to must not precede from' if to < from
37
+
38
+ api = PicturehouseUk::Internal::ApiResponse.new
39
+ cinema = Cinema.new(cinema_id)
40
+ (from..to).flat_map do |date|
41
+ performances_on(api, cinema, date)
42
+ end
43
+ end
44
+
45
+ # @!method showing_on
46
+ # The date of the screening
47
+ # @return [Date]
48
+
49
+ # @!method starting_at
50
+ # UTC time of the screening
51
+ # @return [Time]
52
+
53
+ # @!method variant
54
+ # The kinds of screening (arts, baby, kids, senior, subtitled ...)
55
+ # @return <Array[String]>
56
+
57
+ # private
58
+
59
+ def self.performances_on(api, cinema, date)
60
+ response = api.showtimes(cinema.id, date)
61
+ related = response.fetch('relatedData')
62
+ films = by_id(related.fetch('films'))
63
+ attributes = by_id(related.fetch('attributes'))
64
+
65
+ response.fetch('showtimes').map do |data|
66
+ showtime = PicturehouseUk::Internal::Parser::Api::Showtime.new(
67
+ data, films: films, attributes: attributes
68
+ )
69
+ new(showtime.to_hash.merge(cinema_id: cinema.id,
70
+ cinema_name: cinema.name))
71
+ end
72
+ end
73
+ private_class_method :performances_on
74
+
75
+ def self.by_id(items)
76
+ items.to_h { |item| [item.fetch('id'), item] }
77
+ end
78
+ private_class_method :by_id
79
+ end
80
+ end
@@ -0,0 +1,6 @@
1
+ # Ruby interface for https://www.picturehouses.com
2
+ # @version 6.0.0
3
+ module PicturehouseUk
4
+ # Gem version
5
+ VERSION = '6.0.0'.freeze
6
+ end
@@ -0,0 +1,30 @@
1
+ require 'json'
2
+
3
+ require 'cinebase'
4
+ require 'cinebase/vista'
5
+
6
+ require_relative './picturehouse_uk/version'
7
+
8
+ require_relative './picturehouse_uk/internal/api_response'
9
+ require_relative './picturehouse_uk/internal/guest_token'
10
+ require_relative './picturehouse_uk/internal/title_sanitizer'
11
+
12
+ require_relative './picturehouse_uk/internal/parser/api/showtime'
13
+
14
+ require_relative './picturehouse_uk/cinema'
15
+ require_relative './picturehouse_uk/performance'
16
+
17
+ # picturehouse_uk gem
18
+ module PicturehouseUk
19
+ AuthenticationError = Cinebase::Vista::AuthenticationError
20
+
21
+ # A raw guest JWT, without the "Bearer " prefix. Never persisted by the gem.
22
+ class << self
23
+ attr_accessor :token
24
+
25
+ # Explicit acquisition/renewal; ordinary API calls never launch a subprocess.
26
+ def acquire_token!(curl: nil)
27
+ self.token = Internal::GuestToken.new(curl: curl).fetch
28
+ end
29
+ end
30
+ end