watershed_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,169 @@
1
+ # WatershedUk
2
+
3
+ A simple gem to pull cinema and performance information for [Watershed](https://www.watershed.co.uk), the independent cinema in Bristol, and spit out useful formatted info.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'watershed_uk'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install watershed_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
+ Performance titles are sanitized as much as possible, removing BBFC
24
+ certificates and the `S` and `PS` (partly subtitled) markers after them,
25
+ presenter, partner, preview and strand prefixes (`Bristol Bad Film Club
26
+ presents:`, `Spike Island x Watershed:`, `Preview:`, `Bristol Surf Cinema:`,
27
+ `Sonic Cinema:`), 35mm, director's cut and anniversary markers, years, and
28
+ Q&A and panel discussion add-ons, and naming National Theatre Live and Royal
29
+ Ballet and Opera screenings in full.
30
+
31
+ No token or setup is needed. Watershed sells tickets through Ticketsolve,
32
+ whose public XML feed lists every show on sale.
33
+
34
+ ### Cinema
35
+
36
+ Watershed is one venue. The feed names it but gives no address, so the gem
37
+ holds it: the address from the website's footer and coordinates from
38
+ OpenStreetMap. Cinema methods make no requests.
39
+
40
+ ``` ruby
41
+ WatershedUk::Cinema.all
42
+ #=> [<WatershedUk::Cinema ...>]
43
+
44
+ cinema = WatershedUk::Cinema.new('watershed')
45
+ #=> <WatershedUk::Cinema ...>
46
+
47
+ cinema.adr
48
+ #=> {
49
+ street_address: '1 Canons Road',
50
+ extended_address: 'Harbourside',
51
+ locality: 'Bristol',
52
+ region: nil,
53
+ postal_code: 'BS1 5TX',
54
+ country_name: 'United Kingdom'
55
+ }
56
+
57
+ cinema.brand
58
+ #=> 'Independent'
59
+
60
+ cinema.name
61
+ #=> 'Watershed'
62
+
63
+ cinema.full_name
64
+ #=> 'Independent Watershed'
65
+
66
+ cinema.id
67
+ #=> 'watershed'
68
+
69
+ cinema.latitude
70
+ #=> 51.4517753
71
+
72
+ cinema.longitude
73
+ #=> -2.598143
74
+
75
+ cinema.url
76
+ #=> 'https://www.watershed.co.uk'
77
+ ```
78
+
79
+ ### Performances
80
+
81
+ `Performance.at` returns performances for an inclusive range of showing dates,
82
+ defaulting to today in London and the following 13 days. The Ticketsolve feed
83
+ holds every advertised date, about six months ahead, so any range is one
84
+ request of about 320 KB.
85
+
86
+ ``` ruby
87
+ WatershedUk::Performance.at('watershed', from: Date.today, to: Date.today + 2)
88
+ #=> [<WatershedUk::Performance ...>, <WatershedUk::Performance ...>, ...]
89
+
90
+ performance = WatershedUk::Performance.at('watershed').first
91
+ #=> <WatershedUk::Performance ...>
92
+
93
+ performance.film_name
94
+ #=> 'Daïchi Saïto' # from 'Sonic Cinema: Daïchi Saïto (35mm) (18 CTBA)'
95
+
96
+ performance.film_year
97
+ #=> nil # from the title, e.g. 1983 for 'The Store (1983) (18 CTBA)'
98
+
99
+ performance.dimension
100
+ #=> '2d'
101
+
102
+ performance.variant
103
+ #=> ['35mm', 'q&a']
104
+
105
+ performance.subtitled?
106
+ #=> false
107
+
108
+ performance.festival
109
+ #=> nil # e.g. #<data Cinebase::Festival name="Afrika Eye", year=2026>
110
+
111
+ performance.starting_at
112
+ #=> 2026-11-26 18:30:00 UTC
113
+
114
+ performance.booking_url
115
+ #=> 'https://watershed.ticketsolve.com/shows/1173695141/events/428916513'
116
+
117
+ performance.cinema_name
118
+ #=> 'Watershed'
119
+
120
+ performance.cinema_id
121
+ #=> 'watershed'
122
+ ```
123
+
124
+ Booking links are each event's Ticketsolve page, which leads to ticket and
125
+ seat selection. Sold out performances have no booking link.
126
+
127
+ Only films at Watershed itself are returned. The feed also holds Watershed's
128
+ partner venues, its event spaces and the Pervasive Media Studio, which the gem
129
+ skips. Within the cinema it keeps the `Film` and `Event Cinema` categories,
130
+ and shows with no category that have a certificate in their name. Talks,
131
+ workshops, other events and seat sponsorships are dropped. Screenings with
132
+ an introduction, a Q&A or a panel are films.
133
+
134
+ Variants come from show tags, each event's comment and titles: `subtitled`
135
+ (descriptive subtitles or captions), `relaxed`, `baby` (parents and carers
136
+ of babies), `audio_described`, `35mm`, `arts` (National Theatre Live and
137
+ other event cinema) and `q&a`. Films subtitled from another language are
138
+ not a variant.
139
+
140
+ Festival tags name Afrika Eye and the Bristol Palestine Film Festival
141
+ editions.
142
+
143
+ ## Tests
144
+
145
+ ```sh
146
+ bundle exec rake # unit tests against fixtures
147
+ bundle exec rake live # live Ticketsolve feed
148
+ ```
149
+
150
+ The fixture in `test/fixtures` is the Ticketsolve feed, trimmed to a few
151
+ shows at three of its venues, a few events each and no descriptions or
152
+ images. `test/support/api_shapes.rb` describes the parts the gem reads. The
153
+ unit suite checks the fixture against it and the live suite checks the real
154
+ feed, held only in memory. When the feed changes, update the shape first,
155
+ then the fixture.
156
+
157
+ ## Releasing
158
+
159
+ Bump the version, merge, then run the **Release Gem** workflow in GitHub
160
+ Actions. It runs the tests, tags the version and publishes the gem through
161
+ RubyGems trusted publishing.
162
+
163
+ ## Contributing
164
+
165
+ 1. Fork it
166
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
167
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
168
+ 4. Push to the branch (`git push origin my-new-feature`)
169
+ 5. Create new Pull Request
@@ -0,0 +1,107 @@
1
+ module WatershedUk
2
+ # The object representing the Watershed cinema in Bristol
3
+ class Cinema < Cinebase::Cinema
4
+ # @!attribute [r] id
5
+ # @return [String] the cinema's id, 'watershed'
6
+
7
+ # Watershed is one venue, sold through Ticketsolve alongside its partner
8
+ # venues and event spaces. The address is from the website's footer, the
9
+ # coordinates from the cinema's OpenStreetMap node 12973154703.
10
+ VENUES = {
11
+ 'watershed' => {
12
+ name: 'Watershed', ticketsolve_venue_id: '426565625',
13
+ street_address: '1 Canons Road', extended_address: 'Harbourside', locality: 'Bristol',
14
+ postal_code: 'BS1 5TX', latitude: 51.4517753, longitude: -2.598143
15
+ }
16
+ }.freeze
17
+
18
+ ADDRESS_PARTS = %i(street_address extended_address locality region postal_code).freeze
19
+ URL = 'https://www.watershed.co.uk'.freeze
20
+
21
+ # Constructor
22
+ # @param [String] id the cinema's id, 'watershed'
23
+ # @return [WatershedUk::Cinema]
24
+ def initialize(id)
25
+ @id = id.to_s.downcase
26
+ end
27
+
28
+ # Return basic cinema information for all cinemas
29
+ # @return [Array<WatershedUk::Cinema>]
30
+ # @example
31
+ # WatershedUk::Cinema.all
32
+ # #=> [<WatershedUk::Cinema>]
33
+ def self.all
34
+ VENUES.keys.map { |id| new(id) }
35
+ end
36
+
37
+ # @!method address
38
+ # Address of the cinema
39
+ # @return [Hash] of different address parts
40
+ # @see #adr
41
+
42
+ # Address of the cinema
43
+ # @return [Hash] contains :street_address, :extended_address,
44
+ # :locality, :region, :postal_code, :country_name
45
+ # @example
46
+ # cinema = WatershedUk::Cinema.new('watershed')
47
+ # cinema.adr
48
+ # #=> {
49
+ # street_address: '1 Canons Road',
50
+ # extended_address: 'Harbourside',
51
+ # locality: 'Bristol',
52
+ # region: nil,
53
+ # postal_code: 'BS1 5TX',
54
+ # country_name: 'United Kingdom'
55
+ # }
56
+ # @note Uses method naming as at http://microformats.org/wiki/adr
57
+ def adr
58
+ ADDRESS_PARTS.to_h { |part| [part, venue[part]] }.merge(country_name: 'United Kingdom')
59
+ end
60
+
61
+ # Brand of the cinema
62
+ # @return [String] which will always be 'Independent'
63
+ def brand
64
+ 'Independent'.freeze
65
+ end
66
+
67
+ # @return [Float, nil]
68
+ def latitude
69
+ venue[:latitude]
70
+ end
71
+
72
+ # @return [Float, nil]
73
+ def longitude
74
+ venue[:longitude]
75
+ end
76
+
77
+ # The name of the cinema
78
+ # @return [String]
79
+ # @example
80
+ # WatershedUk::Cinema.new('watershed').name
81
+ # #=> 'Watershed'
82
+ def name
83
+ venue[:name].to_s
84
+ end
85
+
86
+ # The url of the cinema's website
87
+ # @return [String]
88
+ # @example
89
+ # WatershedUk::Cinema.new('watershed').url
90
+ # #=> 'https://www.watershed.co.uk'
91
+ def url
92
+ venue.empty? ? '' : URL
93
+ end
94
+
95
+ # @api private
96
+ # @return [String, nil] the venue's id in the Ticketsolve feed
97
+ def ticketsolve_venue_id
98
+ venue[:ticketsolve_venue_id]
99
+ end
100
+
101
+ private
102
+
103
+ def venue
104
+ VENUES.fetch(id, {})
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+
5
+ module WatershedUk
6
+ # Internal utility classes: Do not use
7
+ # @api private
8
+ module Internal
9
+ # Requests to Ticketsolve
10
+ module Http
11
+ TIMEOUT = 30
12
+ RETRIES = 2
13
+ USER_AGENT = 'Mozilla/5.0 (compatible; watershed_uk; +https://github.com/andycroll/watershed_uk)'
14
+ TRANSIENT_ERRORS = [
15
+ EOFError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout, Net::ReadTimeout, OpenSSL::SSL::SSLError
16
+ ].freeze
17
+
18
+ private
19
+
20
+ def get(url)
21
+ uri = URI(url)
22
+ attempts = 0
23
+ begin
24
+ response = Net::HTTP.start(uri.host, uri.port, use_ssl: true, open_timeout: TIMEOUT, read_timeout: TIMEOUT) do |http|
25
+ http.request(Net::HTTP::Get.new(uri, 'User-Agent' => USER_AGENT))
26
+ end
27
+ rescue *TRANSIENT_ERRORS
28
+ attempts += 1
29
+ raise if attempts > RETRIES
30
+
31
+ sleep(attempts)
32
+ retry
33
+ end
34
+ # Omit response bodies from errors, and never follow redirects.
35
+ raise "Ticketsolve HTTP error #{response.code}" unless response.is_a?(Net::HTTPSuccess)
36
+
37
+ String.new(response.body, encoding: Encoding::UTF_8)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WatershedUk
4
+ # @api private
5
+ module Internal
6
+ module Parser
7
+ # Parses a Ticketsolve shows.xml feed: venues, each with its shows, each
8
+ # with its events. Nothing here is particular to Watershed.
9
+ class Feed
10
+ # A Ticketsolve venue, e.g. id '426565625', name 'Watershed Cinemas'
11
+ Venue = Data.define(:id, :name, :shows)
12
+
13
+ # A production on sale, e.g. a film, with the tags and category the
14
+ # box office gives it
15
+ Show = Data.define(:id, :name, :category, :tags, :url, :events)
16
+
17
+ # A single performance of a show, with its UTC start, its status such
18
+ # as 'available' or 'sold out', the box office's comment and its
19
+ # booking page
20
+ Event = Data.define(:id, :starting_at, :status, :comment, :url)
21
+
22
+ # @param [String] xml a shows.xml document
23
+ def initialize(xml)
24
+ @document = REXML::Document.new(xml)
25
+ end
26
+
27
+ # @return [Array<Venue>]
28
+ def venues
29
+ @document.get_elements('venues/venue').map do |venue|
30
+ Venue.new(id: venue.attributes['id'], name: text(venue, 'name'),
31
+ shows: venue.get_elements('shows/show').map { |show| show(show) })
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def show(show)
38
+ Show.new(
39
+ id: show.attributes['id'],
40
+ name: text(show, 'name'),
41
+ category: text(show, 'event_category'),
42
+ tags: show.get_elements('tags/tag').map { |tag| tag.text.to_s.strip },
43
+ url: text(show, 'url'),
44
+ events: show.get_elements('events/event').map { |event| event(event) }
45
+ )
46
+ end
47
+
48
+ def event(event)
49
+ Event.new(
50
+ id: event.attributes['id'],
51
+ starting_at: Time.iso8601(text(event, 'date_time_iso')).utc,
52
+ status: text(event, 'status'),
53
+ comment: text(event, 'comment'),
54
+ url: text(event, 'url')
55
+ )
56
+ end
57
+
58
+ # Names and comments are CDATA sections between whitespace
59
+ def text(element, path)
60
+ node = element.elements[path]
61
+ node ? node.texts.map(&:value).join.strip : ""
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WatershedUk
4
+ # @api private
5
+ module Internal
6
+ module Parser
7
+ # Reads one Ticketsolve event of a Watershed show as a film screening
8
+ class Screening
9
+ # Watershed's categories for screenings, including broadcasts such as
10
+ # National Theatre Live. Talks, workshops and other events have their
11
+ # own categories.
12
+ FILM_CATEGORIES = ['Film', 'Event Cinema'].freeze
13
+
14
+ SOLD_OUT = 'sold out'
15
+
16
+ CATEGORY_VARIANTS = { 'Event Cinema' => 'arts' }.freeze
17
+ TAG_VARIANTS = { '35mm' => '35mm', 'audio_description' => 'audio_described', 'ntl' => 'arts' }.freeze
18
+
19
+ # The box office notes each event's access and extras in its comment,
20
+ # e.g. 'This screening has descriptive subtitles'
21
+ COMMENT_VARIANTS = {
22
+ /descriptive subtitles|captioned/i => 'subtitled',
23
+ /\bbabies\b/i => 'baby',
24
+ /\brelaxed screening\b/i => 'relaxed',
25
+ /\bQ&A\b/ => 'q&a'
26
+ }.freeze
27
+ TITLE_VARIANTS = { /\(35mm\b/i => '35mm', /\bQ&A\b/ => 'q&a' }.freeze
28
+
29
+ # Festival tags carry a two digit year, e.g. 'afrika_eye_ae26'
30
+ FESTIVAL_TAGS = {
31
+ /\Aafrika_eye_ae(\d{2})\z/ => 'Afrika Eye',
32
+ /\Abpff(\d{2})_/ => 'Bristol Palestine Film Festival'
33
+ }.freeze
34
+
35
+ # National Theatre Live shows are tagged, and often untitled as such
36
+ NT_LIVE_TAG = 'ntl'
37
+ NT_LIVE = /\A(?:NT|National Theatre) Live:/
38
+ THREE_D = /\b3D\b/i
39
+
40
+ # @param [Feed::Show] show
41
+ # @return [Boolean] whether the show is a film, rather than a talk,
42
+ # workshop or other event. Some films have no category, but every
43
+ # film has a certificate in its name.
44
+ def self.film?(show)
45
+ if show.category.empty?
46
+ show.name.match?(TitleSanitizer::CERTIFICATE)
47
+ else
48
+ FILM_CATEGORIES.include?(show.category)
49
+ end
50
+ end
51
+
52
+ # @param [Feed::Show] show the film
53
+ # @param [Feed::Event] event one screening of it
54
+ def initialize(show, event)
55
+ @show = show
56
+ @event = event
57
+ end
58
+
59
+ # @return [Hash] options for WatershedUk::Performance.new
60
+ def to_hash
61
+ {
62
+ booking_url: @event.status == SOLD_OUT ? nil : @event.url,
63
+ dimension: @show.name.match?(THREE_D) ? '3d' : '2d',
64
+ festival: festival,
65
+ film_name: TitleSanitizer.new(title).sanitized,
66
+ film_year: Cinebase::TitleSanitizer.film_year(title),
67
+ starting_at: @event.starting_at,
68
+ variant: variant
69
+ }
70
+ end
71
+
72
+ private
73
+
74
+ def festival
75
+ FESTIVAL_TAGS.each do |pattern, name|
76
+ tag = @show.tags.grep(pattern).first
77
+ return Cinebase::Festival.new(name: name, year: 2000 + tag[pattern, 1].to_i) if tag
78
+ end
79
+ Cinebase::Festival.from_title(title, year: @event.starting_at.year)
80
+ end
81
+
82
+ def title
83
+ if @show.tags.include?(NT_LIVE_TAG) && !@show.name.match?(NT_LIVE)
84
+ "NT Live: #{@show.name}"
85
+ else
86
+ @show.name
87
+ end
88
+ end
89
+
90
+ def variant
91
+ variants = @show.tags.filter_map { |tag| TAG_VARIANTS[tag] }
92
+ variants << CATEGORY_VARIANTS[@show.category]
93
+ variants += COMMENT_VARIANTS.filter_map { |pattern, variant| variant if @event.comment.match?(pattern) }
94
+ variants += TITLE_VARIANTS.filter_map { |pattern, variant| variant if @show.name.match?(pattern) }
95
+ variants.compact.uniq
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WatershedUk
4
+ # Internal utility classes: Do not use
5
+ # @api private
6
+ module Internal
7
+ # Reads Watershed's Ticketsolve box office
8
+ class Ticketsolve
9
+ include Http
10
+
11
+ # Every show on sale at Watershed and its partner venues, with every
12
+ # advertised event, in one public document
13
+ FEED_URL = 'https://watershed.ticketsolve.com/shows.xml'
14
+
15
+ # @return [String] the shows feed, parsed by {Parser::Feed}
16
+ def feed
17
+ get(FEED_URL)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,43 @@
1
+ module WatershedUk
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 [WatershedUk::Internal::TitleSanitizer]
11
+
12
+ # @!method sanitized
13
+ # sanitized and standardized title
14
+ # @return [String] sanitised title
15
+
16
+ # BBFC certificate, or CTBA when it is yet to be announced, e.g. '(15)',
17
+ # '(18 CTBA)' or '(18) CTBA'
18
+ CERTIFICATE = /\((?:U|PG|12A?|15|18|CTBA)(?: CTBA)?\)(?:\s*CTBA)?/
19
+
20
+ private
21
+
22
+ # strings and regex to be removed
23
+ def remove
24
+ [
25
+ /\s*#{CERTIFICATE}(?:\s+P?S)?\s*\z/, # certificate, then (partly) subtitled
26
+ /\A[^:]+ presents:\s*/i, # presenter
27
+ /\A[^:]+ x Watershed:\s*/i, # partner
28
+ /\A(?:Preview|Bristol Surf Cinema|Sonic Cinema):\s*/i, # strand
29
+ /\s*\(35mm [^)]*\)/i, # film print tour
30
+ /\s*\(Director[’']s Cut\)/i, # cut
31
+ /\s*\+\s*panel discussion\z/i # panel add-on
32
+ ] + COMMON_REMOVE
33
+ end
34
+
35
+ # regexes and their replacements
36
+ def replace
37
+ {
38
+ /\A(?:NT|National Theatre) Live:\s*(.*)/ => 'National Theatre Live: '
39
+ }.merge(COMMON_REPLACE)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,77 @@
1
+ module WatershedUk
2
+ # A single screening of a film at the Watershed cinema
3
+ class Performance < Cinebase::Performance
4
+ # @!attribute [r] booking_url
5
+ # @return [String, nil] the Ticketsolve 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 from the film's title
24
+ # @option options [Time] :starting_at listed start time of the performance
25
+
26
+ TIME_ZONE = 'Europe/London'.freeze
27
+
28
+ # Performances within an inclusive range of showing dates. The Ticketsolve
29
+ # feed holds every advertised date, so any range is one request.
30
+ # @param [String] cinema_id the cinema's id, 'watershed'
31
+ # @param [Date] from first date (defaults to today in London)
32
+ # @param [Date] to last date (defaults to 13 days after from)
33
+ # @return [Array<WatershedUk::Performance>]
34
+ def self.at(
35
+ cinema_id,
36
+ from: TZInfo::Timezone.get(TIME_ZONE).now.to_date,
37
+ to: from + 13
38
+ )
39
+ raise ArgumentError, 'to must not precede from' if to < from
40
+
41
+ cinema = Cinema.new(cinema_id)
42
+ if cinema.name.empty?
43
+ []
44
+ else
45
+ venues = Internal::Parser::Feed.new(Internal::Ticketsolve.new.feed).venues
46
+ shows = venues.select { |venue| venue.id == cinema.ticketsolve_venue_id }.flat_map(&:shows)
47
+ performances = screenings(shows, from..to).map do |screening|
48
+ new(screening.to_hash.merge(cinema_id: cinema.id, cinema_name: cinema.name))
49
+ end
50
+ performances.sort_by(&:starting_at)
51
+ end
52
+ end
53
+
54
+ # @!method showing_on
55
+ # The date of the screening
56
+ # @return [Date]
57
+
58
+ # @!method starting_at
59
+ # UTC time of the screening
60
+ # @return [Time]
61
+
62
+ # @!method variant
63
+ # The kinds of screening (35mm, arts, audio_described, baby, q&a,
64
+ # relaxed, subtitled)
65
+ # @return <Array[String]>
66
+
67
+ def self.screenings(shows, dates)
68
+ london = TZInfo::Timezone.get(TIME_ZONE)
69
+ shows.select { |show| Internal::Parser::Screening.film?(show) }.flat_map do |show|
70
+ show.events
71
+ .select { |event| dates.cover?(london.to_local(event.starting_at).to_date) }
72
+ .map { |event| Internal::Parser::Screening.new(show, event) }
73
+ end
74
+ end
75
+ private_class_method :screenings
76
+ end
77
+ end
@@ -0,0 +1,6 @@
1
+ # Ruby interface for https://www.watershed.co.uk
2
+ # @version 1.0.0
3
+ module WatershedUk
4
+ # Gem version
5
+ VERSION = '1.0.0'.freeze
6
+ end
@@ -0,0 +1,20 @@
1
+ require 'rexml/document'
2
+ require 'time'
3
+
4
+ require 'cinebase'
5
+
6
+ require_relative './watershed_uk/version'
7
+
8
+ require_relative './watershed_uk/internal/http'
9
+ require_relative './watershed_uk/internal/ticketsolve'
10
+ require_relative './watershed_uk/internal/title_sanitizer'
11
+
12
+ require_relative './watershed_uk/internal/parser/feed'
13
+ require_relative './watershed_uk/internal/parser/screening'
14
+
15
+ require_relative './watershed_uk/cinema'
16
+ require_relative './watershed_uk/performance'
17
+
18
+ # watershed_uk gem
19
+ module WatershedUk
20
+ end