cineworld_uk 1.0.5 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -1
  3. data/CHANGELOG.md +64 -0
  4. data/README.md +2 -1
  5. data/cineworld_uk.gemspec +3 -3
  6. data/lib/cineworld_uk/cinema.rb +60 -69
  7. data/lib/cineworld_uk/film.rb +25 -7
  8. data/lib/cineworld_uk/internal/film_with_screenings_parser.rb +29 -71
  9. data/lib/cineworld_uk/internal/name_parser.rb +20 -18
  10. data/lib/cineworld_uk/internal/screening_parser.rb +132 -0
  11. data/lib/cineworld_uk/internal/titleize.rb +19 -19
  12. data/lib/cineworld_uk/internal/website.rb +33 -0
  13. data/lib/cineworld_uk/internal/whatson_parser.rb +44 -0
  14. data/lib/cineworld_uk/screening.rb +65 -20
  15. data/lib/cineworld_uk/version.rb +2 -2
  16. data/lib/cineworld_uk.rb +4 -0
  17. data/test/fixture_updater.rb +64 -0
  18. data/test/fixtures/cinemas.html +134 -41
  19. data/test/fixtures/{cinemas/bury-st-edmunds.html → information/brighton.html} +488 -437
  20. data/test/fixtures/{cinemas → information}/bristol.html +463 -410
  21. data/test/fixtures/whatson/brighton/film_first.html +775 -0
  22. data/test/fixtures/whatson/brighton/film_last.html +52 -0
  23. data/test/fixtures/whatson/brighton/film_second.html +645 -0
  24. data/test/fixtures/whatson/brighton.html +3564 -2690
  25. data/test/fixtures/whatson/glasgow-imax-at-gsc/film_first.html +140 -0
  26. data/test/fixtures/whatson/the-o2-greenwich/film_first.html +1242 -0
  27. data/test/lib/cineworld_uk/cinema_test.rb +104 -301
  28. data/test/lib/cineworld_uk/film_test.rb +49 -2
  29. data/test/lib/cineworld_uk/internal/film_with_screenings_parser_test.rb +83 -81
  30. data/test/lib/cineworld_uk/internal/titleize_test.rb +12 -5
  31. data/test/lib/cineworld_uk/internal/website_test.rb +57 -0
  32. data/test/lib/cineworld_uk/internal/whatson_parser_test.rb +58 -0
  33. data/test/lib/cineworld_uk/screening_test.rb +155 -23
  34. data/test/lib/cineworld_uk/version_test.rb +1 -1
  35. data/test/test_helper.rb +3 -1
  36. metadata +30 -33
  37. data/test/fixtures/cinemas/brighton.html +0 -8
  38. data/test/fixtures/cinemas/chelsea.html +0 -1030
  39. data/test/fixtures/cinemas/glasgow-imax-at-gsc.html +0 -916
  40. data/test/fixtures/cinemas/the-o2-grenwich.html +0 -1191
  41. data/test/fixtures/whatson/brighton/gravity.html +0 -2129
  42. data/test/fixtures/whatson/glasgow-imax-at-gsc/the-hunger-games-catching-fire.html +0 -498
  43. data/test/fixtures/whatson/glasgow-imax-at-gsc-cinema.html +0 -3160
  44. data/test/fixtures/whatson/the-o2-greenwich/gravity.html +0 -784
  45. data/test/fixtures/whatson/the-o2-greenwich/the-hobbit-desolation-of-smaug.html +0 -764
  46. data/test/fixtures/whatson/the-o2-greenwich.html +0 -6854
  47. data/test/fixtures/whatson/wandsworth.html +0 -13729
@@ -0,0 +1,132 @@
1
+ module CineworldUk
2
+ # Internal utility classes: Do not use
3
+ # @api private
4
+ module Internal
5
+ # parse a single performance
6
+ class ScreeningParser
7
+ # regex for date in url
8
+ DATE_REGEX = /date\=(\d{4})(\d{2})(\d{2})/
9
+ # regex for time in url
10
+ TIME_REGEX = /time\=(\d{2})\:(\d{2})/
11
+
12
+ # css selector for dimension
13
+ DIMENSION_CSS = '.icon-service-2d, .icon-service-3d'
14
+ # css selector for baby screening
15
+ BABY_CSS = '.icon-service-cb'
16
+ # css selector for d-box screening
17
+ DBOX_CSS = '.icon-service-dx'
18
+ # css selector for hfr screening
19
+ HFR_CSS = '.icon-service-hfr'
20
+ # css selector for imax screening
21
+ IMAX_CSS = '.icon-service-ix'
22
+ # css selector for kids screening
23
+ KIDS_CSS = '.icon-service-m4j'
24
+
25
+ # @param [String] html a chunk of html representing one screening
26
+ def initialize(html)
27
+ @html = html.to_s
28
+ end
29
+
30
+ # url to book this screening
31
+ # @return [String]
32
+ def booking_url
33
+ return unless bookable?
34
+ 'http://www.cineworld.co.uk' + doc.css('a').attribute('href')
35
+ end
36
+
37
+ # the dimension of the screening
38
+ # @return [String]
39
+ def dimension
40
+ return unless bookable?
41
+ doc.css(DIMENSION_CSS).text.downcase
42
+ end
43
+
44
+ # utc time of the screening
45
+ # @return [Time]
46
+ def time
47
+ return unless bookable?
48
+ timezone.local_to_utc(Time.utc(*date_array + time_array))
49
+ end
50
+
51
+ # anything special about the screening
52
+ # @return [Array<String>]
53
+ # @example
54
+ # Cineworld::Internal::ScreeningParser.new(html).tags
55
+ # => ["imax"]
56
+ def variant
57
+ return unless bookable?
58
+ [baby, dbox, hfr, imax, kids].compact
59
+ end
60
+
61
+ # a attributes of a single screening
62
+ # @return [Hash]
63
+ # @example
64
+ # Cineworld::Internal::ScreeningParser.new(html).to_hash
65
+ # => {
66
+ # booking_url: 'http://...',
67
+ # dimension: '2d',
68
+ # time: <Time>,
69
+ # variant: ['imax']
70
+ # }
71
+ def to_hash
72
+ return unless bookable?
73
+ {
74
+ booking_url: booking_url,
75
+ dimension: dimension,
76
+ time: time,
77
+ variant: variant
78
+ }
79
+ end
80
+
81
+ private
82
+
83
+ def baby
84
+ 'baby' if doc.css(BABY_CSS).length > 0
85
+ end
86
+
87
+ def bookable?
88
+ @bookable ||= doc.css('a.performance').size > 0
89
+ end
90
+
91
+ def date_array
92
+ @date_array ||= performance_link_html.match(DATE_REGEX) do |match|
93
+ [match[1], match[2], match[3]]
94
+ end
95
+ end
96
+
97
+ def dbox
98
+ 'dbox' if doc.css(DBOX_CSS).length > 0
99
+ end
100
+
101
+ def doc
102
+ @doc ||= Nokogiri::HTML(@html)
103
+ end
104
+
105
+ def hfr
106
+ 'hfr' if doc.css(HFR_CSS).length > 0
107
+ end
108
+
109
+ def imax
110
+ 'imax' if doc.css(IMAX_CSS).length > 0
111
+ end
112
+
113
+ def kids
114
+ 'kids' if doc.css(KIDS_CSS).length > 0
115
+ end
116
+
117
+ def performance_link_html
118
+ @performance_link_html ||= doc.css('a.performance').to_s
119
+ end
120
+
121
+ def time_array
122
+ @time_array ||= performance_link_html.match(TIME_REGEX) do |match|
123
+ [match[1], match[2]]
124
+ end
125
+ end
126
+
127
+ def timezone
128
+ @timezone ||= TZInfo::Timezone.get('Europe/London')
129
+ end
130
+ end
131
+ end
132
+ end
@@ -1,15 +1,13 @@
1
+ # encoding: UTF-8
1
2
  module CineworldUk
2
-
3
3
  # Internal utility classes: Do not use
4
4
  # @api private
5
5
  module Internal
6
-
7
6
  # @note Modified from titleize gem
8
7
  # https://github.com/granth/titleize
9
8
  module Titleize
10
-
11
9
  # List of words not to capitalize unless they lead a phrase
12
- SMALL_WORDS = %w{a an and as at but by en for if in of on or the to via vs vs.}
10
+ WORDS = %w(a an and as at but by en for if in of on or the to via vs vs.)
13
11
 
14
12
  extend self
15
13
 
@@ -27,24 +25,24 @@ module CineworldUk
27
25
  # @return [String]
28
26
  def titleize(title)
29
27
  title = title.dup
30
- title.downcase! unless title[/[[:lower:]]/] # assume all-caps need fixing
28
+ title.downcase! unless title[/[[:lower:]]/] # assume all-caps fixing
31
29
 
32
30
  phrases(title).map do |phrase|
33
31
  words = phrase.split
34
32
  words.map do |word|
35
33
  def word.capitalize
36
34
  # like String#capitalize, but it starts with the first letter
37
- self.sub(/[[:alpha:]].*/) {|subword| subword.capitalize}
35
+ sub(/[[:alpha:]].*/) { |subword| subword.capitalize }
38
36
  end
39
37
 
40
38
  case word
41
- when /[[:alpha:]]\.[[:alpha:]]/ # words with dots in, like "example.com"
39
+ when /[[:alpha:]]\.[[:alpha:]]/ # words with dots in
42
40
  word
43
41
  when /[-‑]/ # hyphenated word (regular and non-breaking)
44
42
  word.split(/([-‑])/).map do |part|
45
- SMALL_WORDS.include?(part) ? part : part.capitalize
43
+ WORDS.include?(part) ? part : part.capitalize
46
44
  end.join
47
- when /^[[:alpha:]].*[[:upper:]]/ # non-first letter capitalized already
45
+ when /^[[:alpha:]].*[[:upper:]]/ # non-first letter capitalized
48
46
  word
49
47
  when /^[[:digit:]]/ # first character is a number
50
48
  word
@@ -52,31 +50,33 @@ module CineworldUk
52
50
  word.upcase
53
51
  when words.first, words.last
54
52
  word.capitalize
55
- when *(SMALL_WORDS + SMALL_WORDS.map {|small| small.capitalize })
53
+ when *(WORDS + WORDS.map { |small| small.capitalize })
56
54
  word.downcase
57
55
  else
58
56
  word.capitalize
59
57
  end
60
- end.join(" ")
61
- end.join(" ")
58
+ end.join(' ')
59
+ end.join(' ')
62
60
  end
63
61
 
64
62
  # Splits a title into an array based on punctuation.
65
63
  # @param [String] title Film title
66
64
  # @return [Array<String>]
67
65
  #
68
- # "simple title" # => ["simple title"]
69
- # "more complicated: titling" # => ["more complicated:", "titling"]
70
- # "even more: complicated - titling" # => ["even more:", "complicated -", "titling"]
66
+ # Titleize.phrases("simple title")
67
+ # #=> ["simple title"]
68
+ # Titleize.phrases("more complicated: titling")
69
+ # #=> ["more complicated:", "titling"]
70
+ # Titleize.phrases("even more: complicated - titling")
71
+ # #=> ["even more:", "complicated -", "titling"]
71
72
  def phrases(title)
72
- phrases = title.scan(/.+?(?:[-:.;?!] |$)/).map {|phrase| phrase.strip }
73
+ phrases = title.scan(/.+?(?:[-:.;?!] |$)/).map { |phrase| phrase.strip }
73
74
 
74
75
  # rejoin phrases that were split on the '.' from a small word
75
76
  if phrases.size > 1
76
77
  phrases[0..-2].each_with_index do |phrase, index|
77
- if SMALL_WORDS.include?(phrase.split.last.downcase)
78
- phrases[index] << " " + phrases.slice!(index + 1)
79
- end
78
+ next unless WORDS.include?(phrase.split.last.downcase)
79
+ phrases[index] << ' ' + phrases.slice!(index + 1)
80
80
  end
81
81
  end
82
82
 
@@ -0,0 +1,33 @@
1
+ # encoding: UTF-8
2
+ module CineworldUk
3
+ # Internal utility classes: Do not use
4
+ # @api private
5
+ module Internal
6
+ # fetches pages from the cineworld.co.uk website
7
+ class Website
8
+ # get the cinema information page for passed id
9
+ # @return [String]
10
+ def cinema_information(id)
11
+ get("cinemas/#{id}/information")
12
+ end
13
+
14
+ # get the cinemas page
15
+ # @return [String]
16
+ def cinemas
17
+ get('cinemas')
18
+ end
19
+
20
+ # get the cinema page containing all upcoming films and screenings
21
+ # @return [String]
22
+ def whatson(id)
23
+ get("whatson?cinema=#{id}")
24
+ end
25
+
26
+ private
27
+
28
+ def get(path)
29
+ Net::HTTP.get(URI("http://www.cineworld.co.uk/#{path}"))
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,44 @@
1
+ module CineworldUk
2
+ # Internal utility classes: Do not use
3
+ # @api private
4
+ module Internal
5
+ # Parses a chunk of HTML to derive movie showing data
6
+ class WhatsonParser
7
+ # css selector for film html chunks
8
+ FILM_CSS = '#filter-reload > .span9 > .row:not(.schedule)'
9
+ # css selector for screenings html chunks
10
+ SCREENINGS_CSS = '#filter-reload > .span9 > .schedule'
11
+
12
+ # @param [Integer] cinema_id cineworld cinema id
13
+ def initialize(cinema_id)
14
+ @cinema_id = cinema_id
15
+ end
16
+
17
+ # break up the whats on page into individual chunks for each film
18
+ # @return [Array<String>] html chunks for a film and it's screenings
19
+ def films_with_screenings
20
+ films_html.each_with_index.map do |html, i|
21
+ html + screenings_html[i]
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def films_html
28
+ whatson_doc.css(FILM_CSS).map { |n| n.to_s.gsub(/^\s+/, '') }
29
+ end
30
+
31
+ def screenings_html
32
+ whatson_doc.css(SCREENINGS_CSS).map { |n| n.to_s.gsub(/^\s+/, '') }
33
+ end
34
+
35
+ def whatson
36
+ @whatson ||= CineworldUk::Internal::Website.new.whatson(@cinema_id)
37
+ end
38
+
39
+ def whatson_doc
40
+ @whatson_doc ||= Nokogiri::HTML(whatson)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -1,39 +1,84 @@
1
1
  module CineworldUk
2
-
3
2
  # The object representing a single screening on the Cineworld UK website
4
3
  class Screening
5
-
6
4
  # @return [String] the booking URL on the cinema website
7
5
  attr_reader :booking_url
6
+ # @return [String] 2d or 3d
7
+ attr_reader :dimension
8
8
  # @return [String] the cinema name
9
9
  attr_reader :cinema_name
10
10
  # @return [String] the film name
11
11
  attr_reader :film_name
12
- # @return [Time] the UTC time of the screening
13
- attr_reader :when
14
- # @return [String] the type of screening (2D, 3D, IMAX...)
15
- attr_reader :variant
16
12
 
17
- # @param [String] film_name the film name
18
- # @param [String] cinema_name the cinema name
19
- # @param [Time] time datetime of the screening (UTC preferred)
20
- # @param [String] booking_url direct link to the booking page for this screening
21
- # @param [String] variant the type of showing (e.g. 3d/baby/live)
22
- def initialize(film_name, cinema_name, time, booking_url=nil, variant=nil)
23
- @booking_url, @cinema_name, @film_name, @variant = booking_url, cinema_name, film_name, variant
24
- @when = time.utc? ? time : TZInfo::Timezone.get('Europe/London').local_to_utc(time)
13
+ # @param [Hash] options options hash
14
+ # @option options [String] :booking_url (nil) booking url for the screening
15
+ # @option options [String] :cinema_name name of the cinema
16
+ # @option options [String] :cinema_id website id of the cinema
17
+ # @option options [String] :dimension ('2d') dimension of the screening
18
+ # @option options [String] :film_name name of the film
19
+ # @option options [Time] :time utc time of the screening
20
+ # @option options [Array<String>] :variant ([]) type of screening
21
+ def initialize(options)
22
+ @booking_url = options.fetch(:booking_url, nil)
23
+ @cinema_name = options.fetch(:cinema_name)
24
+ @cinema_id = options.fetch(:cinema_id)
25
+ @dimension = options.fetch(:dimension, '2d')
26
+ @film_name = options.fetch(:film_name)
27
+ @time = options.fetch(:time)
28
+ @variant = options.fetch(:variant, [])
29
+ end
30
+
31
+ # All currently listed films showing at a cinema
32
+ # @param [Integer] cinema_id id of the cinema on the website
33
+ # @return [Array<CineworldUk::Screening>]
34
+ def self.at(cinema_id)
35
+ whatson_parser(cinema_id).films_with_screenings.map do |html|
36
+ create_for_single_film(html, cinema_id)
37
+ end.flatten
25
38
  end
26
39
 
27
40
  # The date of the screening
28
41
  # @return [Date]
29
- def date
30
- @when.to_date
42
+ def showing_on
43
+ showing_at.to_date
44
+ end
45
+
46
+ # The UTC time of the screening
47
+ # @return [Time]
48
+ def showing_at
49
+ @when ||= begin
50
+ if @time.utc?
51
+ @time
52
+ else
53
+ TZInfo::Timezone.get('Europe/London').local_to_utc(@time)
54
+ end
55
+ end
56
+ end
57
+
58
+ # The kinds of screening
59
+ # @return <Array[String]>
60
+ def variant
61
+ @variant.split(' ').map(&:downcase).sort
62
+ end
63
+
64
+ private
65
+
66
+ def self.cinema_name_hash(cinema_id)
67
+ { cinema_name: CineworldUk::Cinema.find(cinema_id).name }
68
+ end
69
+
70
+ def self.create_for_single_film(html, cinema_id)
71
+ screenings_parser(html).to_a.map do |attributes|
72
+ new cinema_name_hash(cinema_id).merge(attributes)
73
+ end
74
+ end
75
+
76
+ def self.screenings_parser(html)
77
+ CineworldUk::Internal::FilmWithScreeningsParser.new(html)
31
78
  end
32
79
 
33
- # @deprecated Please use {#variant} instead, I can't spell
34
- def varient
35
- warn "Please use #variant instead, I can't spell"
36
- variant
80
+ def self.whatson_parser(id)
81
+ CineworldUk::Internal::WhatsonParser.new(id)
37
82
  end
38
83
  end
39
84
  end
@@ -1,6 +1,6 @@
1
1
  # Ruby interface for http://www.cineworld.co.uk
2
- # @version 1.0.5
2
+ # @version 2.0.0
3
3
  module CineworldUk
4
4
  # Gem version
5
- VERSION = "1.0.5"
5
+ VERSION = '2.0.0'
6
6
  end
data/lib/cineworld_uk.rb CHANGED
@@ -8,10 +8,14 @@ require_relative './cineworld_uk/version'
8
8
  require_relative './cineworld_uk/internal/film_with_screenings_parser'
9
9
  require_relative './cineworld_uk/internal/name_parser'
10
10
  require_relative './cineworld_uk/internal/titleize'
11
+ require_relative './cineworld_uk/internal/screening_parser'
12
+ require_relative './cineworld_uk/internal/whatson_parser'
13
+ require_relative './cineworld_uk/internal/website'
11
14
 
12
15
  require_relative './cineworld_uk/cinema'
13
16
  require_relative './cineworld_uk/film'
14
17
  require_relative './cineworld_uk/screening'
15
18
 
19
+ # cineworld uk gem
16
20
  module CineworldUk
17
21
  end
@@ -0,0 +1,64 @@
1
+ require File.expand_path('../../lib/cineworld_uk.rb', __FILE__)
2
+
3
+ def fixture(name)
4
+ File.expand_path("../fixtures/#{name}.html", __FILE__)
5
+ end
6
+
7
+ File.open(fixture('cinemas'), 'w') do |file|
8
+ puts '* Cinemas'
9
+ file.write CineworldUk::Internal::Website.new.cinemas
10
+ end
11
+
12
+ # BRIGHTON
13
+
14
+ File.open(fixture('information/brighton'), 'w') do |file|
15
+ puts '* Brighton Information'
16
+ file.write CineworldUk::Internal::Website.new.cinema_information(3)
17
+ end
18
+
19
+ File.open(fixture('whatson/brighton'), 'w') do |file|
20
+ puts '* Brighton Whats On'
21
+ file.write CineworldUk::Internal::Website.new.whatson(3)
22
+ end
23
+
24
+ parser = CineworldUk::Internal::WhatsonParser.new(3)
25
+
26
+ File.open(fixture('whatson/brighton/film_first'), 'w') do |file|
27
+ puts '* Brighton Main Film'
28
+ file.write parser.films_with_screenings[0]
29
+ end
30
+
31
+ File.open(fixture('whatson/brighton/film_second'), 'w') do |file|
32
+ puts '* Brighton Second Film'
33
+ file.write parser.films_with_screenings[1]
34
+ end
35
+
36
+ File.open(fixture('whatson/brighton/film_last'), 'w') do |file|
37
+ puts '* Brighton Last Film'
38
+ file.write parser.films_with_screenings[-1]
39
+ end
40
+
41
+ # BRISTOL
42
+
43
+ File.open(fixture('information/bristol'), 'w') do |file|
44
+ puts '* Bristol Information'
45
+ file.write CineworldUk::Internal::Website.new.cinema_information(4)
46
+ end
47
+
48
+ # O2
49
+
50
+ parser = CineworldUk::Internal::WhatsonParser.new(79)
51
+
52
+ File.open(fixture('whatson/the-o2-greenwich/film_first'), 'w') do |file|
53
+ puts '* The O2 Greenwich Main Film'
54
+ file.write parser.films_with_screenings[0]
55
+ end
56
+
57
+ # GLASGOW IMAX
58
+
59
+ parser = CineworldUk::Internal::WhatsonParser.new(88)
60
+
61
+ File.open(fixture('whatson/glasgow-imax-at-gsc/film_first'), 'w') do |file|
62
+ puts '* Glasgow IMAX Main Film'
63
+ file.write parser.films_with_screenings[0]
64
+ end