odeon_uk 4.0.1 → 5.1.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 (73) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/release.yml +73 -0
  3. data/.github/workflows/test.yml +43 -0
  4. data/.gitignore +1 -1
  5. data/.ruby-version +1 -0
  6. data/CHANGELOG.md +36 -0
  7. data/README.md +100 -21
  8. data/Rakefile +14 -15
  9. data/lib/odeon_uk/cinema.rb +40 -22
  10. data/lib/odeon_uk/internal/api_response.rb +19 -39
  11. data/lib/odeon_uk/internal/guest_token.rb +23 -0
  12. data/lib/odeon_uk/internal/parser/api/showtime.rb +81 -0
  13. data/lib/odeon_uk/internal/title_sanitizer.rb +3 -8
  14. data/lib/odeon_uk/performance.rb +34 -41
  15. data/lib/odeon_uk/version.rb +3 -3
  16. data/lib/odeon_uk.rb +16 -2
  17. data/odeon_uk.gemspec +10 -6
  18. data/test/fixtures/api/showtimes-824.json +66 -0
  19. data/test/fixtures/api/sites.json +32 -0
  20. data/test/lib/odeon_uk/cinema_test.rb +66 -215
  21. data/test/lib/odeon_uk/internal/api_response_test.rb +48 -29
  22. data/test/lib/odeon_uk/internal/fixture_shape_test.rb +14 -0
  23. data/test/lib/odeon_uk/internal/guest_token_test.rb +84 -0
  24. data/test/lib/odeon_uk/internal/parser/api/showtime_test.rb +77 -0
  25. data/test/lib/odeon_uk/internal/title_sanitizer_test.rb +109 -89
  26. data/test/lib/odeon_uk/performance_test.rb +129 -146
  27. data/test/lib/odeon_uk/version_test.rb +3 -3
  28. data/test/live/api_shape_test.rb +19 -0
  29. data/test/live/integration_test.rb +21 -0
  30. data/test/live/live_helper.rb +11 -0
  31. data/test/package_test.rb +56 -0
  32. data/test/support/api_fixtures_helper.rb +5 -30
  33. data/test/support/api_shapes.rb +46 -0
  34. data/test/support/fake_api_response.rb +9 -6
  35. data/test/test_helper.rb +6 -3
  36. metadata +67 -89
  37. data/.travis.yml +0 -7
  38. data/lib/odeon_uk/internal/parser/api/film_lookup.rb +0 -34
  39. data/lib/odeon_uk/internal/parser/api/performance_day.rb +0 -92
  40. data/rake/fixture_creator.rb +0 -46
  41. data/test/fixtures/api/all_cinemas.plist +0 -0
  42. data/test/fixtures/api/app_init.plist +0 -0
  43. data/test/fixtures/api/film_times/71-14876.plist +0 -0
  44. data/test/fixtures/api/film_times/71-15408.plist +0 -0
  45. data/test/fixtures/api/film_times/71-15684.plist +0 -0
  46. data/test/fixtures/api/film_times/71-15866.plist +0 -0
  47. data/test/fixtures/api/film_times/71-15868.plist +0 -0
  48. data/test/fixtures/api/film_times/71-15961.plist +0 -0
  49. data/test/fixtures/api/film_times/71-15963.plist +0 -0
  50. data/test/fixtures/api/film_times/71-15965.plist +0 -0
  51. data/test/fixtures/api/film_times/71-15967.plist +0 -0
  52. data/test/fixtures/api/film_times/71-15969.plist +0 -0
  53. data/test/fixtures/api/film_times/71-15977.plist +0 -0
  54. data/test/fixtures/api/film_times/71-15980.plist +0 -0
  55. data/test/fixtures/api/film_times/71-16037.plist +0 -0
  56. data/test/fixtures/api/film_times/71-16136.plist +0 -0
  57. data/test/fixtures/api/film_times/71-16142.plist +0 -0
  58. data/test/fixtures/api/film_times/71-16143.plist +0 -0
  59. data/test/fixtures/api/film_times/71-16187.plist +0 -0
  60. data/test/fixtures/api/film_times/71-16369.plist +0 -0
  61. data/test/fixtures/api/film_times/71-16370.plist +0 -0
  62. data/test/fixtures/api/film_times/71-16374.plist +0 -0
  63. data/test/fixtures/api/film_times/71-16386.plist +0 -0
  64. data/test/fixtures/api/film_times/71-16387.plist +0 -0
  65. data/test/fixtures/api/film_times/71-16388.plist +0 -0
  66. data/test/fixtures/api/film_times/71-16438.plist +0 -0
  67. data/test/fixtures/api/film_times/71-16454.plist +0 -0
  68. data/test/fixtures/api/film_times/71-16457.plist +0 -0
  69. data/test/fixtures/api/film_times/71-16477.plist +0 -0
  70. data/test/fixtures/api/film_times/71-16478.plist +0 -0
  71. data/test/fixtures/api/film_times/71-16480.plist +0 -0
  72. data/test/fixtures/api/film_times/71-16482.plist +0 -0
  73. data/test/fixtures/api/film_times/71-16489.plist +0 -0
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module OdeonUk
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://www.odeon.co.uk/ticketing/seat-picker/?showtimeId="
13
+
14
+ # Vista attribute short names
15
+ VARIANTS = {
16
+ "AudioDes" => "audio_described",
17
+ "AutFriend" => "autism_friendly",
18
+ "EnglishSub" => "subtitled",
19
+ "EventCinem" => "arts",
20
+ "IMAX" => "imax",
21
+ "Newbies" => "baby",
22
+ "ODEON Kids" => "kids",
23
+ "OpenCapt" => "subtitled",
24
+ "SilverCine" => "senior"
25
+ }.freeze
26
+
27
+ # @param [Hash] data one showtime
28
+ # @param [Hash{String => Hash}] films related films keyed by id
29
+ # @param [Hash{String => Hash}] attributes related attributes keyed by id
30
+ def initialize(data, films:, attributes:)
31
+ @data = data
32
+ @films = films
33
+ @attributes = attributes
34
+ end
35
+
36
+ # @return [Hash] options for OdeonUk::Performance.new
37
+ def to_hash
38
+ {
39
+ booking_url: BOOKING_URL + @data.fetch("id"),
40
+ dimension: @data["requires3dGlasses"] ? "3d" : "2d",
41
+ festival: Cinebase::Festival.from_title(title, year: starting_at.year),
42
+ film_name: film_name,
43
+ film_year: Cinebase::TitleSanitizer.film_year(title),
44
+ language: language,
45
+ starting_at: starting_at,
46
+ variant: variant
47
+ }
48
+ end
49
+
50
+ private
51
+
52
+ def attribute_names(key)
53
+ @data.fetch("attributeIds", []).filter_map do |id|
54
+ @attributes.dig(id, key, "text")
55
+ end
56
+ end
57
+
58
+ def starting_at
59
+ Time.iso8601(@data.dig("schedule", "startsAt")).utc
60
+ end
61
+
62
+ def film_name
63
+ TitleSanitizer.new(title).sanitized
64
+ end
65
+
66
+ def title
67
+ @films.fetch(@data.fetch("filmId")).dig("title", "text")
68
+ end
69
+
70
+ def language
71
+ attribute_names("name").filter_map { |name| name[/\A(.+) \(Audio\)\z/, 1] }.first
72
+ end
73
+
74
+ def variant
75
+ attribute_names("shortName").filter_map { |name| VARIANTS[name] }.uniq
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -18,16 +18,11 @@ module OdeonUk
18
18
  # strings and regex to be removed
19
19
  def remove
20
20
  [
21
- /\s+[23][dD]/, # dimension
22
21
  'Autism Friendly Screening -', # autism screening
23
- /\ACinemagic \d{1,4} \-/, # cinemagic
24
22
  /\(encore.+\)/i, # encore for NT Live
25
23
  /\(live\)/i, # live in brackets
26
- 'UKJFF -', # UK Jewish festival prefix
27
- /\bsing\-?a\-?long\b/i, # singalong
28
- 'Autism Friendly - ', # autism friendly
29
- 'amp;', # html ampersands
30
- ]
24
+ 'Autism Friendly - ' # autism friendly
25
+ ] + COMMON_REMOVE
31
26
  end
32
27
 
33
28
  # regexes and their replacements
@@ -40,7 +35,7 @@ module OdeonUk
40
35
  /NT Live - (.*)/ => 'National Theatre: ',
41
36
  /ROH - (.*)/ => 'Royal Opera House: ',
42
37
  /(.*)\- RSC Live \d{1,4}/ => 'Royal Shakespeare Company: '
43
- }
38
+ }.merge(COMMON_REPLACE)
44
39
  end
45
40
  end
46
41
  end
@@ -20,19 +20,25 @@ module OdeonUk
20
20
  # @option options [String] :dimension ('2d') dimension of the screening
21
21
  # @option options [String] :film_name name of the film
22
22
  # @option options [Time] :starting_at listed start time of the performance
23
+ # @option options [Cinebase::Festival] :festival (nil) festival edition
24
+ # @option options [String] :language (nil) audio language of the screening
23
25
 
24
- # All currently listed films showing at a cinema
25
- # @param [Integer] cinema_id id of the cinema on the website
26
+ # Performances within an inclusive range of cinema business dates.
27
+ # @param [Integer, String] cinema_id id of the cinema on the website
28
+ # @param [Date] from first date (defaults to today in London)
29
+ # @param [Date] to last date (defaults to 13 days after from)
26
30
  # @return [Array<OdeonUk::Performance>]
27
- def self.at(cinema_id)
28
- cinema_id = cinema_id.to_i
29
- film_ids_at(cinema_id).flat_map do |film_id|
30
- api_response.film_times(cinema_id, film_id).flat_map do |day|
31
- performance_days(day).map do |hash|
32
- new(hash.merge(cinema_hash(cinema_id))
33
- .merge(film_name: film_name(film_id)))
34
- end
35
- end
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 = OdeonUk::Internal::ApiResponse.new
39
+ cinema = Cinema.new(cinema_id)
40
+ (from..to).flat_map do |date|
41
+ performances_on(api, cinema, date)
36
42
  end
37
43
  end
38
44
 
@@ -48,41 +54,28 @@ module OdeonUk
48
54
  # The kinds of screening (IMAX, kids, baby, senior)
49
55
  # @return <Array[String]>
50
56
 
51
- # private
52
-
53
- def self.api_response
54
- @api_response ||= OdeonUk::Internal::ApiResponse.new
55
- end
56
- private_class_method :api_response
57
-
58
- def self.cinema_hash(cinema_id)
59
- { cinema_id: cinema_id, cinema_name: Cinema.new(cinema_id).name }
60
- end
61
- private_class_method :cinema_hash
62
-
63
- def self.films_at(cinema_id)
64
- film_lookup.at(cinema_id)
65
- end
66
- private_class_method :films_at
67
57
 
68
- def self.film_lookup
69
- @film_lookup ||= OdeonUk::Internal::Parser::Api::FilmLookup.new
70
- end
71
- private_class_method :film_lookup
58
+ # private
72
59
 
73
- def self.film_ids_at(cinema_id)
74
- films_at(cinema_id).keys
75
- end
76
- private_class_method :film_ids_at
60
+ def self.performances_on(api, cinema, date)
61
+ response = api.showtimes(cinema.id, date)
62
+ related = response.fetch('relatedData')
63
+ films = by_id(related.fetch('films'))
64
+ attributes = by_id(related.fetch('attributes'))
77
65
 
78
- def self.film_name(film_id)
79
- film_lookup.to_hash[film_id]['title']
66
+ response.fetch('showtimes').map do |data|
67
+ showtime = OdeonUk::Internal::Parser::Api::Showtime.new(
68
+ data, films: films, attributes: attributes
69
+ )
70
+ new(showtime.to_hash.merge(cinema_id: cinema.id,
71
+ cinema_name: cinema.name))
72
+ end
80
73
  end
81
- private_class_method :film_name
74
+ private_class_method :performances_on
82
75
 
83
- def self.performance_days(data)
84
- OdeonUk::Internal::Parser::Api::PerformanceDay.new(data).to_a
76
+ def self.by_id(items)
77
+ items.to_h { |item| [item.fetch('id'), item] }
85
78
  end
86
- private_class_method :performance_days
79
+ private_class_method :by_id
87
80
  end
88
81
  end
@@ -1,6 +1,6 @@
1
- # Ruby interface for http://www.odeon.co.uk
2
- # @version 4.0.1
1
+ # Ruby interface for https://www.odeon.co.uk
2
+ # @version 5.1.0
3
3
  module OdeonUk
4
4
  # Gem version
5
- VERSION = '4.0.1'.freeze
5
+ VERSION = '5.1.0'.freeze
6
6
  end
data/lib/odeon_uk.rb CHANGED
@@ -1,16 +1,30 @@
1
+ require 'json'
2
+
1
3
  require 'cinebase'
4
+ require 'cinebase/vista'
2
5
 
3
6
  require_relative './odeon_uk/version'
4
7
 
5
8
  require_relative './odeon_uk/internal/api_response'
9
+ require_relative './odeon_uk/internal/guest_token'
6
10
  require_relative './odeon_uk/internal/title_sanitizer'
7
11
 
8
- require_relative './odeon_uk/internal/parser/api/film_lookup'
9
- require_relative './odeon_uk/internal/parser/api/performance_day'
12
+ require_relative './odeon_uk/internal/parser/api/showtime'
10
13
 
11
14
  require_relative './odeon_uk/cinema'
12
15
  require_relative './odeon_uk/performance'
13
16
 
14
17
  # odeon_uk gem
15
18
  module OdeonUk
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
16
30
  end
data/odeon_uk.gemspec CHANGED
@@ -7,21 +7,25 @@ Gem::Specification.new do |spec|
7
7
  spec.version = OdeonUk::VERSION
8
8
  spec.authors = ['Andy Croll']
9
9
  spec.email = ['andy@goodscary.com']
10
- spec.description = 'Pull movie & cinema information from the Odeon iOS API'
10
+ spec.description = 'Pull movie & cinema information from the ODEON Vista API'
11
11
  spec.summary = "It's a scraper, but a nice one"
12
- spec.homepage = 'http://github.com/andycroll/odeon_uk'
12
+ spec.homepage = 'https://github.com/andycroll/odeon_uk'
13
13
  spec.licenses = %w(AGPL MIT)
14
14
 
15
- spec.files = `git ls-files`.split($RS)
15
+ spec.required_ruby_version = '>= 3.4'
16
+
17
+ spec.files = `git ls-files`.split("\n")
16
18
  spec.executables = spec.files.grep(%r{^bin/}).map { |f| File.basename(f) }
17
19
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
20
  spec.require_paths = ['lib']
19
21
 
20
- spec.add_development_dependency 'codeclimate-test-reporter'
22
+ spec.add_development_dependency 'base64'
23
+ spec.add_development_dependency 'bundler', '>= 2.0'
24
+ spec.add_development_dependency 'minitest-mock'
21
25
  spec.add_development_dependency 'minitest-reporters'
22
26
  spec.add_development_dependency 'rake'
27
+ spec.add_development_dependency 'simplecov', '< 1'
23
28
  spec.add_development_dependency 'webmock'
24
29
 
25
- spec.add_runtime_dependency 'CFPropertyList'
26
- spec.add_runtime_dependency 'cinebase', '~> 3.0'
30
+ spec.add_runtime_dependency 'cinebase', '~> 5.4'
27
31
  end
@@ -0,0 +1,66 @@
1
+ {
2
+ "businessDate": "2026-10-24",
3
+ "showtimes": [
4
+ {
5
+ "id": "824-1001",
6
+ "filmId": "FILM-1",
7
+ "schedule": { "startsAt": "2026-10-24T09:00:00+01:00" },
8
+ "attributeIds": ["A-AD"],
9
+ "requires3dGlasses": false
10
+ },
11
+ {
12
+ "id": "824-1002",
13
+ "filmId": "FILM-1",
14
+ "schedule": { "startsAt": "2026-10-24T14:30:00+01:00" },
15
+ "attributeIds": ["A-3D", "A-IMAX"],
16
+ "requires3dGlasses": true
17
+ },
18
+ {
19
+ "id": "824-1003",
20
+ "filmId": "FILM-2",
21
+ "schedule": { "startsAt": "2026-10-24T18:00:00+01:00" },
22
+ "attributeIds": ["A-KOREAN", "A-SUB", "A-EVENT"],
23
+ "requires3dGlasses": false
24
+ },
25
+ {
26
+ "id": "824-1004",
27
+ "filmId": "FILM-3",
28
+ "schedule": { "startsAt": "2026-10-24T20:15:00+01:00" },
29
+ "attributeIds": ["A-TAMIL", "A-SUB"],
30
+ "requires3dGlasses": false
31
+ },
32
+ {
33
+ "id": "824-1005",
34
+ "filmId": "FILM-4",
35
+ "schedule": { "startsAt": "2026-10-24T10:00:00+01:00" },
36
+ "attributeIds": ["A-KIDS", "A-CAPT"],
37
+ "requires3dGlasses": false
38
+ },
39
+ {
40
+ "id": "824-1006",
41
+ "filmId": "FILM-1",
42
+ "schedule": { "startsAt": "2026-10-25T00:15:00+01:00" },
43
+ "attributeIds": [],
44
+ "requires3dGlasses": false
45
+ }
46
+ ],
47
+ "relatedData": {
48
+ "films": [
49
+ { "id": "FILM-1", "title": { "text": "The Example Film 3D" } },
50
+ { "id": "FILM-2", "title": { "text": "Made Up Concert: Live" } },
51
+ { "id": "FILM-3", "title": { "text": "Imaginary Picture (2025) (Tamil)" } },
52
+ { "id": "FILM-4", "title": { "text": "Pretend Pups" } }
53
+ ],
54
+ "attributes": [
55
+ { "id": "A-AD", "shortName": { "text": "AudioDes" }, "name": { "text": "Audio Described" } },
56
+ { "id": "A-3D", "shortName": { "text": "3D" }, "name": { "text": "3D" } },
57
+ { "id": "A-IMAX", "shortName": { "text": "IMAX" }, "name": { "text": "IMAX" } },
58
+ { "id": "A-KOREAN", "shortName": { "text": "KoreanLang" }, "name": { "text": "Korean (Audio)" } },
59
+ { "id": "A-TAMIL", "shortName": { "text": "TamilA" }, "name": { "text": "Tamil (Audio)" } },
60
+ { "id": "A-SUB", "shortName": { "text": "EnglishSub" }, "name": { "text": "English (Sub)" } },
61
+ { "id": "A-EVENT", "shortName": { "text": "EventCinem" }, "name": { "text": "Event Cinema" } },
62
+ { "id": "A-KIDS", "shortName": { "text": "ODEON Kids" }, "name": { "text": "ODEON Kids" } },
63
+ { "id": "A-CAPT", "shortName": { "text": "OpenCapt" }, "name": { "text": "Open Captioned" } }
64
+ ]
65
+ }
66
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "sites": [
3
+ {
4
+ "id": "017",
5
+ "name": { "text": "Birmingham New Street" },
6
+ "contactDetails": {
7
+ "address": { "line1": "1 Example Arcade", "line2": "New Street", "city": "Birmingham" }
8
+ }
9
+ },
10
+ {
11
+ "id": "153",
12
+ "name": { "text": "London Leicester Square" },
13
+ "contactDetails": {
14
+ "address": { "line1": "2 Example Square", "line2": "", "city": "London" }
15
+ }
16
+ },
17
+ {
18
+ "id": "824",
19
+ "name": { "text": "Brighton" },
20
+ "contactDetails": {
21
+ "address": { "line1": "3 Example Parade", "line2": "Seafront", "city": "Brighton" }
22
+ }
23
+ },
24
+ {
25
+ "id": "955",
26
+ "name": { "text": "Trafford Centre" },
27
+ "contactDetails": {
28
+ "address": { "line1": "4 Example Dome", "line2": "", "city": "Manchester" }
29
+ }
30
+ }
31
+ ]
32
+ }