princecharles_uk 1.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.
data/README.md ADDED
@@ -0,0 +1,173 @@
1
+ # PrincecharlesUk
2
+
3
+ A simple gem to pull the cinema and its performances from the [Prince Charles Cinema website](https://princecharlescinema.com) and spit out useful formatted info.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'princecharles_uk'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install princecharles_uk
18
+
19
+ ## Usage
20
+
21
+ 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.
22
+
23
+ No token or setup is needed. The website runs on WordPress with Jacro's
24
+ cinema plugin, and tickets are sold through Jacro's TapOS. Its what's on page
25
+ lists every film on sale with every advertised showtime, months ahead, so one
26
+ request reads the whole programme.
27
+
28
+ ### Cinema
29
+
30
+ The Prince Charles Cinema has one venue, keyed by its Jacro site code,
31
+ `PRILON`. The gem holds its details: the address from the website's location
32
+ page and coordinates from OpenStreetMap. Cinema methods make no requests.
33
+
34
+ ``` ruby
35
+ PrincecharlesUk::Cinema.all
36
+ #=> [<PrincecharlesUk::Cinema ...>]
37
+
38
+ cinema = PrincecharlesUk::Cinema.new('PRILON')
39
+ #=> <PrincecharlesUk::Cinema ...>
40
+
41
+ cinema.adr
42
+ #=> {
43
+ street_address: '7 Leicester Place',
44
+ extended_address: nil,
45
+ locality: 'London',
46
+ region: nil,
47
+ postal_code: 'WC2H 7BY',
48
+ country_name: 'United Kingdom'
49
+ }
50
+
51
+ cinema.brand
52
+ #=> 'Independent'
53
+
54
+ cinema.name
55
+ #=> 'Prince Charles Cinema'
56
+
57
+ cinema.id
58
+ #=> 'PRILON'
59
+
60
+ cinema.latitude
61
+ #=> 51.5114907
62
+
63
+ cinema.url
64
+ #=> 'https://princecharlescinema.com'
65
+ ```
66
+
67
+ ### Performances
68
+
69
+ `Performance.at` returns performances for an inclusive range of showing dates,
70
+ defaulting to today in London and the following 13 days. Any range is one
71
+ request.
72
+
73
+ ``` ruby
74
+ PrincecharlesUk::Performance.at('PRILON', from: Date.today, to: Date.today + 365)
75
+ #=> [<PrincecharlesUk::Performance ...>, <PrincecharlesUk::Performance ...>, ...]
76
+
77
+ performance = PrincecharlesUk::Performance.at('PRILON', from: Date.today, to: Date.today + 365).last
78
+ #=> <PrincecharlesUk::Performance ...>
79
+
80
+ performance.film_name
81
+ #=> 'The Rocky Horror Picture Show' # from '... : Sing Along'
82
+
83
+ performance.film_year
84
+ #=> 1975
85
+
86
+ performance.dimension
87
+ #=> '2d'
88
+
89
+ performance.variant
90
+ #=> ['sing_along']
91
+
92
+ performance.subtitled?
93
+ #=> false
94
+
95
+ performance.festival
96
+ #=> nil
97
+
98
+ performance.starting_at
99
+ #=> 2027-06-25 19:30:00 UTC
100
+
101
+ performance.booking_url
102
+ #=> 'https://princecharlescinema.com/prince-charles-cinema/booknow/31628659/'
103
+
104
+ performance.cinema_name
105
+ #=> 'Prince Charles Cinema'
106
+
107
+ performance.cinema_id
108
+ #=> 'PRILON'
109
+ ```
110
+
111
+ Booking links are the website's own booking pages, which hold the TapOS seat
112
+ selection. Sold out screenings are listed with no booking link.
113
+
114
+ Date headings such as 'Friday 25th June' give no year. The gem takes the year
115
+ in which that date falls on that weekday.
116
+
117
+ Variants come from each showtime's tags and from titles: `35mm`, `70mm`,
118
+ `subtitled` (SUB and HOH, hard of hearing, screenings), `q&a`, `sing_along`
119
+ and `arts` (films with a live score). 4K, £1 member tickets, introductions,
120
+ previews and final shows are not variants.
121
+
122
+ Film years come from a year in the title, such as 'Godzilla (1954)' or
123
+ 'The Mummy [1999]', or else from the listing. Placeholder years before 1888,
124
+ which the listing gives to some events such as 'Mystery Movie', are dropped.
125
+
126
+ Titles are cleaned up:
127
+
128
+ - Original titles, cuts and notes in square brackets go: 'Rashomon [Rashômon]'
129
+ becomes 'Rashomon', 'The New World [Extended Cut]' becomes 'The New World'.
130
+ - A final parenthesis goes: 'Daisies (Sedmikrásky)' becomes 'Daisies', and
131
+ 'Ten Years of Monkey Tennis (with Peter Serafinowicz)' loses its guest.
132
+ - Anniversaries, restorations and touring prints go: 'Amelie - 25th
133
+ Anniversary', 'Wake in Fright - 4K Restoration', 'Short Cuts : 35mm Roadshow
134
+ Tour'.
135
+ - Sing-along names go: 'The Rocky Horror Picture Show: Sing Along' and
136
+ 'Sing-A-Long-A Rocky Horror Picture Show'.
137
+ - Q&A add-ons go: 'Dead Souls + Alex Cox Q&A' becomes 'Dead Souls'.
138
+ - Presenters go: 'Funeral Parade presents "The Long Day Closes"' becomes 'The
139
+ Long Day Closes'.
140
+ - Spaced colons close up: '2001 : A Space Odyssey' becomes '2001: A Space
141
+ Odyssey'.
142
+
143
+ Director's and final cuts named after a colon keep their names. Double bills,
144
+ marathons and all-nighters keep the cinema's names, such as 'Studio Ghibli
145
+ All-Nighter' and 'Evangelion: Death (True)² + The End of Evangelion: Double
146
+ Feature'.
147
+
148
+ ## Tests
149
+
150
+ ```sh
151
+ bundle exec rake # unit tests against fixtures
152
+ LIVE=1 bundle exec rake live # live princecharlescinema.com
153
+ ```
154
+
155
+ The fixture in `test/fixtures` is the what's on page, trimmed to a few films
156
+ with shortened descriptions and no posters. `test/support/api_shapes.rb`
157
+ describes the markup the gem reads. The unit suite checks the fixture against
158
+ it and the live suite checks the real page, held only in memory. When the page
159
+ changes, update the shape first, then the fixture.
160
+
161
+ ## Releasing
162
+
163
+ Bump the version, merge, then run the **Release Gem** workflow in GitHub
164
+ Actions. It runs the tests, tags the version and publishes the gem through
165
+ RubyGems trusted publishing.
166
+
167
+ ## Contributing
168
+
169
+ 1. Fork it
170
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
171
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
172
+ 4. Push to the branch (`git push origin my-new-feature`)
173
+ 5. Create new Pull Request
@@ -0,0 +1,100 @@
1
+ module PrincecharlesUk
2
+ # The object representing the Prince Charles Cinema, London
3
+ class Cinema < Cinebase::Cinema
4
+ # @!attribute [r] id
5
+ # @return [String] the cinema's Jacro site code, 'PRILON'
6
+
7
+ # The cinema has one venue. Its Jacro site code names it in ticketing
8
+ # links. The address is from the website's location page, coordinates
9
+ # from OpenStreetMap.
10
+ VENUES = {
11
+ 'PRILON' => {
12
+ name: 'Prince Charles Cinema', street_address: '7 Leicester Place', locality: 'London',
13
+ postal_code: 'WC2H 7BY', latitude: 51.5114907, longitude: -0.1302137
14
+ }
15
+ }.freeze
16
+
17
+ ADDRESS_PARTS = %i(street_address extended_address locality region postal_code).freeze
18
+ URL = 'https://princecharlescinema.com'.freeze
19
+
20
+ # Constructor
21
+ # @param [String] id the cinema's Jacro site code, 'PRILON' or 'prilon'
22
+ # @return [PrincecharlesUk::Cinema]
23
+ def initialize(id)
24
+ @id = id.to_s.upcase
25
+ end
26
+
27
+ # Return basic cinema information for all cinemas
28
+ # @return [Array<PrincecharlesUk::Cinema>] the one cinema
29
+ # @example
30
+ # PrincecharlesUk::Cinema.all
31
+ # #=> [<PrincecharlesUk::Cinema>]
32
+ def self.all
33
+ VENUES.keys.map { |id| new(id) }
34
+ end
35
+
36
+ # @!method address
37
+ # Address of the cinema
38
+ # @return [Hash] of different address parts
39
+ # @see #adr
40
+
41
+ # Address of the cinema
42
+ # @return [Hash] contains :street_address, :extended_address,
43
+ # :locality, :region, :postal_code, :country_name
44
+ # @example
45
+ # cinema = PrincecharlesUk::Cinema.new('PRILON')
46
+ # cinema.adr
47
+ # #=> {
48
+ # street_address: '7 Leicester Place',
49
+ # extended_address: nil,
50
+ # locality: 'London',
51
+ # region: nil,
52
+ # postal_code: 'WC2H 7BY',
53
+ # country_name: 'United Kingdom'
54
+ # }
55
+ # @note Uses method naming as at http://microformats.org/wiki/adr
56
+ def adr
57
+ ADDRESS_PARTS.to_h { |part| [part, venue[part]] }.merge(country_name: 'United Kingdom')
58
+ end
59
+
60
+ # Brand of the cinema
61
+ # @return [String] which will always be 'Independent'
62
+ def brand
63
+ 'Independent'.freeze
64
+ end
65
+
66
+ # @return [Float, nil]
67
+ def latitude
68
+ venue[:latitude]
69
+ end
70
+
71
+ # @return [Float, nil]
72
+ def longitude
73
+ venue[:longitude]
74
+ end
75
+
76
+ # The name of the cinema
77
+ # @return [String]
78
+ # @example
79
+ # PrincecharlesUk::Cinema.new('PRILON').name
80
+ # #=> 'Prince Charles Cinema'
81
+ def name
82
+ venue[:name].to_s
83
+ end
84
+
85
+ # The url of the cinema's website
86
+ # @return [String]
87
+ # @example
88
+ # PrincecharlesUk::Cinema.new('PRILON').url
89
+ # #=> 'https://princecharlescinema.com'
90
+ def url
91
+ venue.empty? ? '' : URL
92
+ end
93
+
94
+ private
95
+
96
+ def venue
97
+ VENUES.fetch(id, {})
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+
5
+ module PrincecharlesUk
6
+ # Internal utility classes: Do not use
7
+ # @api private
8
+ module Internal
9
+ # Requests to princecharlescinema.com
10
+ module Http
11
+ RETRIES = 2
12
+ TRANSIENT_ERRORS = [
13
+ EOFError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout, Net::ReadTimeout, OpenSSL::SSL::SSLError
14
+ ].freeze
15
+
16
+ private
17
+
18
+ def get(url)
19
+ uri = URI(url)
20
+ attempts = 0
21
+ begin
22
+ response = Net::HTTP.start(uri.host, uri.port, use_ssl: true, open_timeout: 10, read_timeout: 30) do |http|
23
+ http.request(Net::HTTP::Get.new(uri))
24
+ end
25
+ rescue *TRANSIENT_ERRORS
26
+ attempts += 1
27
+ raise if attempts > RETRIES
28
+
29
+ sleep(attempts)
30
+ retry
31
+ end
32
+ # Omit response bodies from errors, and never follow redirects.
33
+ raise "Prince Charles Cinema HTTP error #{response.code}" unless response.is_a?(Net::HTTPSuccess)
34
+
35
+ String.new(response.body, encoding: Encoding::UTF_8)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrincecharlesUk
4
+ # @api private
5
+ module Internal
6
+ module Parser
7
+ # One showtime from the what's on page
8
+ Showing = Data.define(:title, :year, :date, :starting_at, :tags, :booking_url)
9
+
10
+ # Parses the what's on page: one block per film, each with its
11
+ # showtimes under date headings such as 'Friday 25th June'
12
+ class WhatsOn
13
+ FILM = /<div class="jacro-event movie-tabs row/
14
+ TITLE = /class="liveeventtitle"[^>]*>([^<]+)</
15
+ # The first detail is the film's year when the listing gives one
16
+ DETAILS = %r{<div class="running-time">\s*<span>([^<]*)</span>}
17
+ YEAR = /\A\d{4}\z/
18
+ SHOWTIMES = %r{<ul class="performance-list-items">(.*?)</ul>}m
19
+ HEADING_OR_SHOWTIME = %r{<div class="heading">([^<]+)</div>|<li class="([^"]*)">(.*?)</li>}m
20
+ HEADING = /\A(\w+) (\d{1,2})(?:st|nd|rd|th) (\w+)\z/
21
+ TIME = %r{<span class="time">\s*(\d{1,2}):(\d{2})\s*([ap]m)\s*</span>}i
22
+ # Sold out showtimes have no link
23
+ BOOKING = %r{href="(https://princecharlescinema\.com/[\w-]+/booknow/\d+)/?"}
24
+ TIME_ZONE = 'Europe/London'
25
+ HOURS_TO_NOON = 12
26
+
27
+ # @param [String] html the what's on page
28
+ # @param [Date] today in London, to place headings, which give no
29
+ # year, in the right year
30
+ def initialize(html, today:)
31
+ @html = html
32
+ @today = today
33
+ end
34
+
35
+ # @return [Array<PrincecharlesUk::Internal::Parser::Showing>]
36
+ def showings
37
+ @html.split(FILM).drop(1).flat_map { |film| film_showings(film) }
38
+ end
39
+
40
+ private
41
+
42
+ def film_showings(film)
43
+ title = CGI.unescapeHTML(film[TITLE, 1].to_s).strip
44
+ year = film[DETAILS, 1].to_s.strip
45
+ year = year.match?(YEAR) ? year.to_i : nil
46
+ date = nil
47
+ film[SHOWTIMES, 1].to_s.scan(HEADING_OR_SHOWTIME).filter_map do |heading, tags, showtime|
48
+ if heading
49
+ date = date(heading.strip)
50
+ nil
51
+ elsif date && showtime.match?(TIME)
52
+ showing(title, year, date, tags, showtime)
53
+ end
54
+ end
55
+ end
56
+
57
+ def showing(title, year, date, tags, showtime)
58
+ Showing.new(
59
+ title: title,
60
+ year: year,
61
+ date: date,
62
+ starting_at: starting_at(date, showtime),
63
+ tags: tags.split,
64
+ booking_url: booking_url(showtime)
65
+ )
66
+ end
67
+
68
+ def booking_url(showtime)
69
+ url = showtime[BOOKING, 1]
70
+ url ? "#{url}/" : nil
71
+ end
72
+
73
+ # The year whose date falls on the heading's weekday, or failing
74
+ # that the first on or after today
75
+ def date(heading)
76
+ weekday, day, name = heading.match(HEADING)&.captures
77
+ month = name && Date::MONTHNAMES.index(name)
78
+ if month
79
+ dates = (@today.year - 1..@today.year + 1).filter_map do |year|
80
+ Date.new(year, month, day.to_i) if Date.valid_date?(year, month, day.to_i)
81
+ end
82
+ dates.find { |date| Date::DAYNAMES[date.wday] == weekday } || dates.find { |date| date >= @today }
83
+ end
84
+ end
85
+
86
+ def starting_at(date, showtime)
87
+ hour, minute, meridiem = showtime.match(TIME).captures
88
+ hour = hour.to_i % HOURS_TO_NOON
89
+ hour += HOURS_TO_NOON if meridiem.casecmp?('pm')
90
+ local = Time.utc(date.year, date.month, date.day, hour, minute.to_i)
91
+ TZInfo::Timezone.get(TIME_ZONE).local_to_utc(local) { |periods| periods.first }.utc
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,41 @@
1
+ module PrincecharlesUk
2
+ # Internal utility classes: Do not use
3
+ # @api private
4
+ module Internal
5
+ # Sanitize and standardize film titles
6
+ class TitleSanitizer < Cinebase::TitleSanitizer
7
+ # @!method initialize(title)
8
+ # Constructor
9
+ # @param [String] title a film title
10
+ # @return [PrincecharlesUk::Internal::TitleSanitizer]
11
+
12
+ # @!method sanitized
13
+ # sanitized and standardized title
14
+ # @return [String] sanitised title
15
+
16
+ private
17
+
18
+ # strings and regex to be removed
19
+ def remove
20
+ [
21
+ /\A.+?\s+presents\s+/i, # presenter, e.g. 'Funeral Parade presents'
22
+ /\ASing-A-Long-A\s+/i, # sing-along prefix
23
+ /:\s*Sing[- ]?A[- ]?Long\z/i, # sing-along suffix
24
+ /\s+-\s+\d+(?:st|nd|rd|th) Anniversary\z/i, # anniversary re-release
25
+ /\s+-\s+4K Restoration\z/i, # restoration
26
+ /\s*:\s*35mm Roadshow Tour\z/i, # touring print
27
+ /\s*\[[^\]]*\]\z/, # original title, cut or year, e.g. '[Rashômon]'
28
+ /\s*\([^)]*\)\z/ # original title, guest or year, e.g. '(Sedmikrásky)'
29
+ ] + COMMON_REMOVE
30
+ end
31
+
32
+ # regexes and their replacements
33
+ def replace
34
+ {
35
+ /\A"(.*)"\z/ => '', # quotes left by a presenter
36
+ /\s+:\s+(.*)/ => ': ' # spaced colon, e.g. '2001 : A Space Odyssey'
37
+ }.merge(COMMON_REPLACE)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrincecharlesUk
4
+ # Internal utility classes: Do not use
5
+ # @api private
6
+ module Internal
7
+ # Reads the Prince Charles Cinema's WordPress website, built on Jacro's
8
+ # cinema plugin. No token is needed.
9
+ class Website
10
+ include Http
11
+
12
+ WHATS_ON = 'https://princecharlescinema.com/whats-on/'
13
+
14
+ # @return [String] every film on sale with every advertised showtime,
15
+ # however far ahead, including sold out ones
16
+ def whats_on
17
+ get(WHATS_ON)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,129 @@
1
+ module PrincecharlesUk
2
+ # A single screening of a film on the Prince Charles Cinema website
3
+ class Performance < Cinebase::Performance
4
+ # @!attribute [r] booking_url
5
+ # @return [String, nil] the booking URL, nil when sold out
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 of the film
24
+ # @option options [Time] :starting_at listed start time of the performance
25
+
26
+ # Showtime tags and the variants they mean. HOH (hard of hearing)
27
+ # screenings are subtitled. Others, such as '4k', '1-mem' (£1 member
28
+ # tickets), 'intro', 'preview' and 'final-show', are not variants.
29
+ TAGS = {
30
+ '35mm' => '35mm',
31
+ '70mm' => '70mm',
32
+ 'hoh' => 'subtitled',
33
+ 'live-score' => 'arts',
34
+ 'q-a' => 'q&a',
35
+ 'sing-along' => 'sing_along',
36
+ 'sub' => 'subtitled'
37
+ }.freeze
38
+
39
+ # Variants some titles name without a tag
40
+ TITLE_VARIANTS = {
41
+ /\b35mm\b/i => '35mm',
42
+ /\b70mm\b/i => '70mm',
43
+ /\bQ&A\b/i => 'q&a',
44
+ /\bSing[- ]?A[- ]?Long\b/i => 'sing_along'
45
+ }.freeze
46
+
47
+ THREE_D = /\b3D\b/i
48
+ # Bracketed years in titles, such as 'The Mummy [1999]'
49
+ TITLE_YEAR = /[(\[](1[89]\d{2}|20\d{2})[)\]]\s*\z/
50
+ # Listings give placeholder years to some events, e.g. 1878
51
+ FIRST_FILM_YEAR = 1888
52
+ TIME_ZONE = 'Europe/London'.freeze
53
+
54
+ # Performances within an inclusive range of showing dates, from one
55
+ # request: the what's on page holds every advertised date.
56
+ # @param [String] cinema_id the cinema's Jacro site code, 'PRILON'
57
+ # @param [Date] from first date (defaults to today in London)
58
+ # @param [Date] to last date (defaults to 13 days after from)
59
+ # @return [Array<PrincecharlesUk::Performance>]
60
+ def self.at(
61
+ cinema_id,
62
+ from: today,
63
+ to: from + 13
64
+ )
65
+ raise ArgumentError, 'to must not precede from' if to < from
66
+
67
+ cinema = Cinema.new(cinema_id)
68
+ if cinema.name.empty?
69
+ []
70
+ else
71
+ performances = showings.select { |showing| (from..to).cover?(showing.date) }.map do |showing|
72
+ new(attributes(showing).merge(cinema_id: cinema.id, cinema_name: cinema.name))
73
+ end
74
+ performances.sort_by(&:starting_at)
75
+ end
76
+ end
77
+
78
+ # @!method showing_on
79
+ # The date of the screening
80
+ # @return [Date]
81
+
82
+ # @!method starting_at
83
+ # UTC time of the screening
84
+ # @return [Time]
85
+
86
+ # @!method variant
87
+ # The kinds of screening (35mm, 70mm, q&a, sing_along, subtitled ...)
88
+ # @return <Array[String]>
89
+
90
+ def self.today
91
+ TZInfo::Timezone.get(TIME_ZONE).now.to_date
92
+ end
93
+ private_class_method :today
94
+
95
+ def self.showings
96
+ html = PrincecharlesUk::Internal::Website.new.whats_on
97
+ PrincecharlesUk::Internal::Parser::WhatsOn.new(html, today: today).showings
98
+ end
99
+ private_class_method :showings
100
+
101
+ def self.attributes(showing)
102
+ title = showing.title
103
+ {
104
+ booking_url: showing.booking_url,
105
+ dimension: [title, *showing.tags].any? { |text| text.match?(THREE_D) } ? '3d' : '2d',
106
+ festival: Cinebase::Festival.from_title(title, year: showing.date.year),
107
+ film_name: PrincecharlesUk::Internal::TitleSanitizer.new(title).sanitized,
108
+ film_year: film_year(showing),
109
+ starting_at: showing.starting_at,
110
+ variant: variant(showing)
111
+ }
112
+ end
113
+ private_class_method :attributes
114
+
115
+ # A year in the title first, as it picks out one version of a film
116
+ def self.film_year(showing)
117
+ year = showing.title[TITLE_YEAR, 1]&.to_i || showing.year
118
+ year if year && year >= FIRST_FILM_YEAR
119
+ end
120
+ private_class_method :film_year
121
+
122
+ def self.variant(showing)
123
+ from_tags = showing.tags.filter_map { |tag| TAGS[tag] }
124
+ from_title = TITLE_VARIANTS.filter_map { |pattern, variant| variant if showing.title.match?(pattern) }
125
+ (from_tags + from_title).uniq
126
+ end
127
+ private_class_method :variant
128
+ end
129
+ end
@@ -0,0 +1,6 @@
1
+ # Ruby interface for https://princecharlescinema.com
2
+ # @version 1.0.0
3
+ module PrincecharlesUk
4
+ # Gem version
5
+ VERSION = '1.0.0'.freeze
6
+ end
@@ -0,0 +1,17 @@
1
+ require 'cgi/escape'
2
+
3
+ require 'cinebase'
4
+
5
+ require_relative './princecharles_uk/version'
6
+
7
+ require_relative './princecharles_uk/internal/http'
8
+ require_relative './princecharles_uk/internal/website'
9
+ require_relative './princecharles_uk/internal/title_sanitizer'
10
+ require_relative './princecharles_uk/internal/parser/whats_on'
11
+
12
+ require_relative './princecharles_uk/cinema'
13
+ require_relative './princecharles_uk/performance'
14
+
15
+ # princecharles_uk gem
16
+ module PrincecharlesUk
17
+ end