berkeley_library-av-core 0.4.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 (104) hide show
  1. checksums.yaml +7 -0
  2. data/.dockerignore +391 -0
  3. data/.github/workflows/build.yml +30 -0
  4. data/.gitignore +388 -0
  5. data/.idea/av_core.iml +146 -0
  6. data/.idea/codeStyles/Project.xml +12 -0
  7. data/.idea/codeStyles/codeStyleConfig.xml +5 -0
  8. data/.idea/go.imports.xml +6 -0
  9. data/.idea/inspectionProfiles/Project_Default.xml +37 -0
  10. data/.idea/misc.xml +6 -0
  11. data/.idea/modules.xml +8 -0
  12. data/.idea/vcs.xml +6 -0
  13. data/.rubocop.yml +241 -0
  14. data/.ruby-version +1 -0
  15. data/.simplecov +8 -0
  16. data/CHANGES.md +38 -0
  17. data/Gemfile +3 -0
  18. data/Jenkinsfile +16 -0
  19. data/LICENSE.md +21 -0
  20. data/README.md +20 -0
  21. data/Rakefile +20 -0
  22. data/av_core.gemspec +49 -0
  23. data/lib/berkeley_library/av/config.rb +238 -0
  24. data/lib/berkeley_library/av/constants.rb +30 -0
  25. data/lib/berkeley_library/av/core/module_info.rb +18 -0
  26. data/lib/berkeley_library/av/core.rb +7 -0
  27. data/lib/berkeley_library/av/marc/util.rb +114 -0
  28. data/lib/berkeley_library/av/marc.rb +52 -0
  29. data/lib/berkeley_library/av/metadata/README.md +5 -0
  30. data/lib/berkeley_library/av/metadata/field.rb +110 -0
  31. data/lib/berkeley_library/av/metadata/fields.rb +130 -0
  32. data/lib/berkeley_library/av/metadata/link.rb +28 -0
  33. data/lib/berkeley_library/av/metadata/readers/alma.rb +54 -0
  34. data/lib/berkeley_library/av/metadata/readers/base.rb +53 -0
  35. data/lib/berkeley_library/av/metadata/readers/tind.rb +52 -0
  36. data/lib/berkeley_library/av/metadata/readers.rb +2 -0
  37. data/lib/berkeley_library/av/metadata/source.rb +93 -0
  38. data/lib/berkeley_library/av/metadata/tind_html_metadata_da.json +2076 -0
  39. data/lib/berkeley_library/av/metadata/value.rb +121 -0
  40. data/lib/berkeley_library/av/metadata.rb +103 -0
  41. data/lib/berkeley_library/av/record.rb +86 -0
  42. data/lib/berkeley_library/av/record_id.rb +121 -0
  43. data/lib/berkeley_library/av/record_not_found.rb +7 -0
  44. data/lib/berkeley_library/av/restrictions.rb +36 -0
  45. data/lib/berkeley_library/av/track.rb +132 -0
  46. data/lib/berkeley_library/av/types/duration.rb +67 -0
  47. data/lib/berkeley_library/av/types/file_type.rb +84 -0
  48. data/lib/berkeley_library/av/util.rb +65 -0
  49. data/rakelib/bundle.rake +8 -0
  50. data/rakelib/coverage.rake +11 -0
  51. data/rakelib/gem.rake +54 -0
  52. data/rakelib/rubocop.rake +18 -0
  53. data/rakelib/spec.rake +12 -0
  54. data/spec/.rubocop.yml +116 -0
  55. data/spec/data/10.23.19.JessieLaCavalier.02.mrc +3 -0
  56. data/spec/data/alma/991005939359706532-sru.xml +123 -0
  57. data/spec/data/alma/991034756419706532-sru.xml +162 -0
  58. data/spec/data/alma/991047179369706532-sru.xml +210 -0
  59. data/spec/data/alma/991054360089706532-sru.xml +186 -0
  60. data/spec/data/alma/b11082434-sru.xml +165 -0
  61. data/spec/data/alma/b18538031-sru.xml +123 -0
  62. data/spec/data/alma/b20786580-sru.xml +123 -0
  63. data/spec/data/alma/b22139647-sru.xml +171 -0
  64. data/spec/data/alma/b22139658-sru.xml +282 -0
  65. data/spec/data/alma/b23161018-sru.xml +182 -0
  66. data/spec/data/alma/b23305522-sru.xml +144 -0
  67. data/spec/data/alma/b24071548-sru.xml +136 -0
  68. data/spec/data/alma/b24659129-sru.xml +210 -0
  69. data/spec/data/alma/b25207857-sru.xml +217 -0
  70. data/spec/data/alma/b25716973-sru.xml +186 -0
  71. data/spec/data/alma/b25742488-sru.xml +246 -0
  72. data/spec/data/record-(cityarts)00002.xml +78 -0
  73. data/spec/data/record-(cityarts)00773.xml +94 -0
  74. data/spec/data/record-(clir)00020.xml +153 -0
  75. data/spec/data/record-(miscmat)00615.xml +45 -0
  76. data/spec/data/record-(pacradio)00107.xml +85 -0
  77. data/spec/data/record-(pacradio)01469.xml +82 -0
  78. data/spec/data/record-empty-result.xml +4 -0
  79. data/spec/data/record-multiple-998s-disordered.xml +178 -0
  80. data/spec/data/record-multiple-998s.xml +178 -0
  81. data/spec/data/record-physcolloquia-bk00169017b.xml +78 -0
  82. data/spec/data/record-ragged-998-subfields.xml +122 -0
  83. data/spec/data/record-ragged-998s-multiple-fields.xml +160 -0
  84. data/spec/data/record-redirect-to-login.html +288 -0
  85. data/spec/data/record_id/bibs_with_check_digits.txt +151 -0
  86. data/spec/data/search-1993.xml +158 -0
  87. data/spec/data/search-b23305516.xml +81 -0
  88. data/spec/lib/berkeley_library/av/av_spec.rb +12 -0
  89. data/spec/lib/berkeley_library/av/config_spec.rb +250 -0
  90. data/spec/lib/berkeley_library/av/marc/util_spec.rb +150 -0
  91. data/spec/lib/berkeley_library/av/marc_spec.rb +62 -0
  92. data/spec/lib/berkeley_library/av/metadata/field_spec.rb +81 -0
  93. data/spec/lib/berkeley_library/av/metadata/fields_spec.rb +180 -0
  94. data/spec/lib/berkeley_library/av/metadata/metadata_spec.rb +274 -0
  95. data/spec/lib/berkeley_library/av/metadata/source_spec.rb +261 -0
  96. data/spec/lib/berkeley_library/av/metadata/value_spec.rb +29 -0
  97. data/spec/lib/berkeley_library/av/record_id_spec.rb +72 -0
  98. data/spec/lib/berkeley_library/av/record_spec.rb +284 -0
  99. data/spec/lib/berkeley_library/av/track_spec.rb +335 -0
  100. data/spec/lib/berkeley_library/av/types/duration_spec.rb +91 -0
  101. data/spec/lib/berkeley_library/av/types/file_type_spec.rb +98 -0
  102. data/spec/lib/berkeley_library/av/util_spec.rb +30 -0
  103. data/spec/spec_helper.rb +63 -0
  104. metadata +499 -0
@@ -0,0 +1,18 @@
1
+ module BerkeleyLibrary
2
+ module AV
3
+ module Core
4
+ class ModuleInfo
5
+ NAME = 'berkeley_library-av-core'.freeze
6
+ AUTHOR = 'David Moles'.freeze
7
+ AUTHOR_EMAIL = 'dmoles@berkeley.edu'.freeze
8
+ SUMMARY = 'UC Berkeley Library audio/video core code'.freeze
9
+ DESCRIPTION = 'Gem for UC Berkeley Library shared audio/video code'.freeze
10
+ LICENSE = 'MIT'.freeze
11
+ VERSION = '0.4.0'.freeze
12
+ HOMEPAGE = 'https://git.lib.berkeley.edu/lap/av_core'.freeze
13
+
14
+ private_class_method :new
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,7 @@
1
+ require 'berkeley_library/av/config'
2
+ require 'berkeley_library/av/marc'
3
+ require 'berkeley_library/av/metadata'
4
+ require 'berkeley_library/av/record'
5
+ require 'berkeley_library/av/record_id'
6
+ require 'berkeley_library/av/track'
7
+ require 'berkeley_library/av/util'
@@ -0,0 +1,114 @@
1
+ require 'marc'
2
+ require 'berkeley_library/av/util'
3
+
4
+ module BerkeleyLibrary
5
+ module AV
6
+ module Marc
7
+ module Util
8
+ include AV::Util
9
+
10
+ # Extracts the subfield values from the specifed MARC data field and
11
+ # returns the groups of related subfield values in order as they
12
+ # appear. For instance, for the following data field:
13
+ #
14
+ # ```
15
+ # 505 00$tQuatrain II$g(16:35) --$tWater ways$g(1:57) --$tWaves$g(10:49).
16
+ # ```
17
+ #
18
+ # this method would return:
19
+ #
20
+ # ```
21
+ # [
22
+ # {t: 'Quatrain II', g: '(16:35)'},
23
+ # {t: 'Water ways', g: '(01:57)'},
24
+ # {t: 'Waves', g: '(10:49)'}
25
+ # ]
26
+ # ```
27
+ #
28
+ # If an order is provided, each group will be reordered according to
29
+ # that order. E.g., given the order `[:g, :t]`, the above would be
30
+ # returned instead as:
31
+ #
32
+ # ```
33
+ # [
34
+ # {g: '(16:35)', t: 'Quatrain II'},
35
+ # {g: '(01:57)', t: 'Water ways'},
36
+ # {g: '(10:49)', t: 'Waves'},
37
+ # ]
38
+ # ```
39
+ #
40
+ # @param data_field [MARC::DataField] the data field
41
+ # @param order [Array<Symbol>, nil] the order of subfield codes
42
+ # @return [Array<Hash<Symbol, String>>] the grouped values
43
+ def group_subfield_values(data_field, order: nil)
44
+ # TODO: do we still need this?
45
+ grouped_subfields = group_subfields(data_field.subfields, order:)
46
+ grouped_subfields.each_with_object([]) do |subfield_group, value_groups|
47
+ value_group = subfield_group.transform_values { |sf| tidy_value(sf.value) }
48
+ value_groups << value_group
49
+ end
50
+ end
51
+
52
+ # Extracts the subfieldsfrom the specifed MARC data field and
53
+ # returns the groups of related subfieldsin order as they
54
+ # appear. For instance, for the following data field:
55
+ #
56
+ # ```
57
+ # 505 00$tQuatrain II$g(16:35) --$tWater ways$g(1:57) --$tWaves$g(10:49).
58
+ # ```
59
+ #
60
+ # this method would return:
61
+ #
62
+ # ```
63
+ # [
64
+ # {t: #<Subfield @code='t', @value='Quatrain II'>, g: #<Subfield @code='g', @value='(16:35)'>},
65
+ # {t: #<Subfield @code='t', @value='Water ways'>, g: #<Subfield @code='g', @value='(01:57)'>},
66
+ # {t: #<Subfield @code='t', @value='Waves'>, g: #<Subfield @code='g', @value='(10:49)'>}
67
+ # ]
68
+ # ```
69
+ #
70
+ # If an order is provided, each group will be reordered according to
71
+ # that order. E.g., given the order `[:g, :t]`, the above would be
72
+ # returned instead as:
73
+ #
74
+ # ```
75
+ # [
76
+ # {g: #<Subfield @code='g', @value='(16:35)'>, t: #<Subfield @code='t', @value='Quatrain II'>},
77
+ # {g: #<Subfield @code='g', @value='(01:57)'>, t: #<Subfield @code='t', @value='Water ways'>},
78
+ # {g: #<Subfield @code='g', @value='(10:49)'>, t: #<Subfield @code='t', @value='Waves'>},
79
+ # ]
80
+ # ```
81
+ #
82
+ # @param data_field [MARC::DataField] the data field
83
+ # @param order [Array<Symbol>, nil] the order of subfield codes
84
+ # @return [Array<Hash<Symbol, String>>] the grouped values
85
+ def group_subfields(subfields, order: nil)
86
+ by_code = subfields_by_code(subfields)
87
+ order = by_code.keys if order.nil? || order.empty?
88
+ group_by_code(by_code, order)
89
+ end
90
+
91
+ private
92
+
93
+ def subfields_by_code(subfields)
94
+ subfields.each_with_object({}) do |sf, h|
95
+ (h[sf.code.to_sym] ||= []) << sf
96
+ end
97
+ end
98
+
99
+ def group_by_code(subfields_by_code, order)
100
+ order.each_with_object([]) do |code, groups|
101
+ csym = code.to_sym
102
+ next unless (subfields = subfields_by_code[csym])
103
+
104
+ subfields.each_with_index { |v, i| (groups[i] ||= {})[csym] = v }
105
+ end
106
+ end
107
+
108
+ class << self
109
+ include AV::Marc::Util
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,52 @@
1
+ require 'marc'
2
+
3
+ module BerkeleyLibrary
4
+ module AV
5
+ module Marc
6
+ class << self
7
+ # Parses MARCXML.
8
+ #
9
+ # @param xml [String] the XML to parse
10
+ # @return [MARC::Record, nil] the MARC record from the specified XML
11
+ def from_xml(xml)
12
+ # noinspection RubyYardReturnMatch,RubyMismatchedReturnType
13
+ all_from_xml(xml).first
14
+ end
15
+
16
+ # Parses MARCXML.
17
+ #
18
+ # @param xml [String] the XML to parse
19
+ # @return [MARC::XMLReader] the MARC records
20
+ def all_from_xml(xml)
21
+ input = StringIO.new(xml.scrub)
22
+ MARC::XMLReader.new(input)
23
+ end
24
+
25
+ # Returns a reader for the specified MARC file.
26
+ #
27
+ # @param marc_path [String] the path to a MARC file in XML or binary format
28
+ # @param external_encoding [String] the encoding, for binary files
29
+ # @return [MARC::XMLReader] if the file path ends in `.xml`
30
+ # @return [MARC::Reader] if the file path ends in `.mrc`
31
+ def reader_for(marc_path, external_encoding: 'MARC-8')
32
+ downcased_path = marc_path.downcase
33
+ return MARC::XMLReader.new(marc_path) if downcased_path.end_with?('.xml')
34
+ return MARC::Reader.new(marc_path, external_encoding:) if downcased_path.end_with?('.mrc')
35
+
36
+ raise ArgumentError, "Unable to determine reader needed for MARC file #{marc_path.inspect}"
37
+ end
38
+
39
+ # Returns a MARC record read from the specified file (or the first record
40
+ # if the file contains multiple records).
41
+ #
42
+ # @param marc_path [String] the path to a MARC file in XML or binary format
43
+ # @param external_encoding [String] the encoding, for binary files
44
+ # @return [MARC::Record] the MARC record
45
+ def read(marc_path, external_encoding: 'MARC-8')
46
+ # noinspection RubyYardReturnMatch
47
+ reader_for(marc_path, external_encoding:).first
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,5 @@
1
+ # `tind_html_metadata_da.json`
2
+
3
+ This file contains a snapshot of JSON data extracted from the HTML source of the
4
+ [TIND Record Page Configuration Interface](https://digicoll.lib.berkeley.edu/displays/?ln=en&section=tind_html_metadata_da),
5
+ defining the labels, visibility, and order of the MARC fields on the TIND record display page.
@@ -0,0 +1,110 @@
1
+ require 'berkeley_library/av/constants'
2
+ require 'berkeley_library/av/marc/util'
3
+ require 'berkeley_library/av/metadata/value'
4
+ require 'marc/spec'
5
+
6
+ module BerkeleyLibrary
7
+ module AV
8
+ class Metadata
9
+ class Field
10
+ include AV::Marc::Util
11
+ include Comparable
12
+
13
+ # ------------------------------------------------------------
14
+ # Constants
15
+
16
+ SPEC_TAG_RE = /^([0-9a-z.]{3})/
17
+
18
+ # ------------------------------------------------------------
19
+ # Accessors
20
+
21
+ attr_reader :order, :label, :tag, :spec, :query, :subfields_separator, :subfield_order
22
+
23
+ # ------------------------------------------------------------
24
+ # Initializer
25
+
26
+ # rubocop:disable Metrics/ParameterLists
27
+ def initialize(order:, label:, spec:, tag: nil, subfields_separator: ' ', subfield_order: [])
28
+ @order = order
29
+ @label = label
30
+ @spec = spec
31
+ @query = MARC::Spec.parse_query(spec)
32
+ @tag = tag || query.tag_str
33
+ @subfields_separator = subfields_separator
34
+ @subfield_order = subfield_order
35
+ end
36
+ # rubocop:enable Metrics/ParameterLists
37
+
38
+ # ------------------------------------------------------------
39
+ # Public methods
40
+
41
+ def value_from(marc_record)
42
+ results = MARC::Spec.execute_query(query, marc_record)
43
+ subfield_groups = subfield_groups_from_result(results)
44
+ Value.value_for(self, subfield_groups)
45
+ end
46
+
47
+ def same_metadata?(other)
48
+ raise ArgumentError, "Not a #{class_name(self)}: #{other}" unless other.is_a?(Field)
49
+
50
+ %i[tag query subfields_separator subfield_order].all? do |attr|
51
+ (other.respond_to?(attr) && send(attr) == other.send(attr))
52
+ end
53
+ end
54
+
55
+ # ------------------------------
56
+ # Object
57
+
58
+ def to_s
59
+ "#{order}. #{query} #{label.inspect}".tap do |str|
60
+ str << " #{subfields_separator.inspect}" unless subfields_separator == ' '
61
+ str << " $#{subfield_order.join('$')}" unless subfield_order.empty?
62
+ end
63
+ end
64
+
65
+ def inspect
66
+ "#<#{class_name(self)} #{self}>"
67
+ end
68
+
69
+ def hash
70
+ sort_attrs.map { |a| send(a) }.hash
71
+ end
72
+
73
+ # ------------------------------
74
+ # Comparable
75
+
76
+ def <=>(other)
77
+ compare_by_attributes(self, other, *sort_attrs)
78
+ end
79
+
80
+ # ------------------------------------------------------------
81
+ # Private
82
+
83
+ private
84
+
85
+ def sort_attrs
86
+ %i[order tag query subfields_separator label subfield_order]
87
+ end
88
+
89
+ def subfield_groups_from_result(marc_result)
90
+ if marc_result.is_a?(Array)
91
+ return [] if marc_result.empty?
92
+ return marc_result.map { |r| subfield_groups_from_result(r) }.flatten unless marc_result[0].is_a?(MARC::Subfield)
93
+ end
94
+
95
+ subfields = subfields_from_result(marc_result)
96
+ group_subfields(subfields, order: subfield_order)
97
+ end
98
+
99
+ def subfields_from_result(marc_result)
100
+ return marc_result if marc_result.is_a?(Array) && marc_result[0].is_a?(MARC::Subfield)
101
+ return [marc_result] if marc_result.is_a?(MARC::Subfield)
102
+ return [] unless marc_result.respond_to?(:subfields)
103
+
104
+ marc_result.subfields
105
+ end
106
+
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,130 @@
1
+ require 'berkeley_library/av/metadata/field'
2
+
3
+ module BerkeleyLibrary
4
+ module AV
5
+ class Metadata
6
+ module Fields
7
+ include AV::Constants
8
+
9
+ TITLE = Field.new(order: 1, label: 'Title', spec: "#{TAG_TITLE_FIELD}$a")
10
+
11
+ DESCRIPTION = Field.new(order: 2, label: 'Description', spec: '520$a')
12
+ CREATOR_PERSONAL = Field.new(order: 2, label: 'Creator', spec: '700')
13
+ CREATOR_CORPORATE = Field.new(order: 2, label: 'Creator', spec: '710')
14
+ TRACKS = Field.new(order: 99, label: 'Tracks', spec: TAG_TRACK_FIELD, subfield_order: %w[g t a])
15
+ CATALOG_LINK = Field.new(order: 999, label: 'Linked Resources', spec: "#{TAG_LINK_FIELD}{^1=\\4}{^2=\\1}")
16
+
17
+ STANDARD_FIELDS = [
18
+ TITLE,
19
+ DESCRIPTION,
20
+ CREATOR_PERSONAL,
21
+ CREATOR_CORPORATE,
22
+ TRACKS,
23
+ CATALOG_LINK
24
+ ].freeze
25
+
26
+ TIND_CONFIG = File.join(__dir__, 'tind_html_metadata_da.json')
27
+
28
+ REQ_ATTRS = %w[visible params labels order].freeze
29
+ TAG_ATTRS = %w[tag fields tag_1 tag_2].freeze
30
+ TAG_RE = /(?<tag>[0-9]{3})(?<ind1>[a-z0-9_%])(?<ind2>[a-z0-9_%])(?<subfield>[a-z0-9])?/
31
+
32
+ class << self
33
+
34
+ def default_fields
35
+ @default_fields ||= begin
36
+ default_fields = STANDARD_FIELDS + from_tind_config(TIND_CONFIG)
37
+ unique_by_metadata(default_fields)
38
+ end
39
+ end
40
+
41
+ def from_tind_config(json_config)
42
+ json_config_hash = ensure_hash(json_config)
43
+ json_config_hash['config'].filter_map { |jf| to_field(jf) }
44
+ end
45
+
46
+ def default_values_from(marc_record)
47
+ default_fields.each_with_object({}) do |f, vv|
48
+ field_value = f.value_from(marc_record)
49
+ vv[f] = field_value if field_value
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ # @return [Hash] the config hash
56
+ def ensure_hash(json_config)
57
+ json_config = File.read(json_config) if File.file?(json_config)
58
+ json_config.is_a?(Hash) ? json_config : JSON.parse(json_config)
59
+ end
60
+
61
+ # rubocop:disable Metrics/MethodLength
62
+ def to_field(json_field)
63
+ return unless can_display?(json_field)
64
+
65
+ params = json_field['params']
66
+ tag, marc_spec = tag_and_spec_from(params)
67
+ return unless tag
68
+
69
+ Field.new(
70
+ order: json_field['order'].to_i,
71
+ label: json_field['labels']['en'],
72
+ tag:,
73
+ spec: marc_spec,
74
+ subfields_separator: (params['subfields_separator'] || ' '),
75
+ subfield_order: params['subfield_order'].to_s.split(',')
76
+ )
77
+ end
78
+ # rubocop:enable Metrics/MethodLength
79
+
80
+ def unique_by_metadata(fields)
81
+ fields.sort.each_with_object([]) do |f, uniques|
82
+ already_present = uniques.any? { |u| u.same_metadata?(f) }
83
+ uniques << f unless already_present
84
+ end
85
+ end
86
+
87
+ def can_display?(json_field)
88
+ REQ_ATTRS.all? { |f| json_field[f] } &&
89
+ json_field['labels'].key?('en') &&
90
+ json_field['machine_name'] != 'local_245_880_linking' # extra title
91
+ end
92
+
93
+ def tag_and_spec_from(params)
94
+ tag_and_spec = TAG_ATTRS.lazy.filter_map { |attr| parse_tag_and_spec(params[attr]) }.first
95
+ return tag_and_spec if tag_and_spec
96
+
97
+ return unless (input_tag = params['input_tag'])
98
+
99
+ input_subfield = params['input_subfield']
100
+ parse_tag_and_spec("#{input_tag}#{input_subfield}")
101
+ end
102
+
103
+ def parse_tag_and_spec(val)
104
+ return unless (md = TAG_RE.match(val))
105
+
106
+ tag = md[:tag]
107
+ [tag, to_marc_spec(tag, md[:ind1], md[:ind2], md[:subfield])]
108
+ end
109
+
110
+ def to_marc_spec(tag, ind1_val, ind2_val, subfield)
111
+ [
112
+ tag,
113
+ to_subfield_spec(subfield),
114
+ to_ind_spec(1, ind1_val),
115
+ to_ind_spec(2, ind2_val)
116
+ ].join
117
+ end
118
+
119
+ def to_subfield_spec(sf)
120
+ "$#{sf}" if sf
121
+ end
122
+
123
+ def to_ind_spec(i, ind_val)
124
+ "{^#{i}=\\#{ind_val}}" unless [nil, '', '%', '_'].include?(ind_val)
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,28 @@
1
+ module BerkeleyLibrary
2
+ module AV
3
+ class Metadata
4
+ class Link
5
+ include Comparable
6
+
7
+ attr_reader :body, :url
8
+
9
+ def initialize(body:, url:)
10
+ @body = body
11
+ @url = url
12
+ end
13
+
14
+ def to_s
15
+ "[#{body}](#{url})"
16
+ end
17
+
18
+ def <=>(other)
19
+ return unless other
20
+ return 0 if equal?(other)
21
+ return unless other.is_a?(Link)
22
+
23
+ to_s <=> other.to_s
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,54 @@
1
+ require 'berkeley_library/util/uris'
2
+
3
+ require 'berkeley_library/av/config'
4
+ require 'berkeley_library/av/record_id'
5
+ require 'berkeley_library/av/metadata/readers/base'
6
+
7
+ module BerkeleyLibrary
8
+ module AV
9
+ class Metadata
10
+ module Readers
11
+ module Alma
12
+ include Base
13
+
14
+ def marc_uri_for(record_id)
15
+ query_string = URI.encode_www_form(
16
+ 'version' => '1.2',
17
+ 'operation' => 'searchRetrieve',
18
+ 'query' => sru_query_value_for(record_id)
19
+ )
20
+
21
+ URIs.append(AV::Config.alma_sru_base_uri, '?', query_string)
22
+ end
23
+
24
+ protected
25
+
26
+ def _display_uri_for(record_id)
27
+ URIs.append(AV::Config.alma_permalink_base_uri, "alma#{record_id}")
28
+ end
29
+
30
+ private
31
+
32
+ def sru_query_value_for(record_id)
33
+ id_type = AV::RecordId::Type.for_id(record_id)
34
+ return "alma.mms_id=#{record_id}" if id_type == AV::RecordId::Type::ALMA
35
+ return millennium_query_value(record_id) if id_type == AV::RecordId::Type::MILLENNIUM
36
+
37
+ raise ArgumentError, "Invalid record type: #{id_type}"
38
+ end
39
+
40
+ def millennium_query_value(bib_number)
41
+ other_system_number = other_system_number_for_bib(bib_number)
42
+ "alma.other_system_number=#{other_system_number}"
43
+ end
44
+
45
+ def other_system_number_for_bib(bib_number)
46
+ bib_number = RecordId.ensure_check_digit(bib_number)
47
+ "UCB-#{bib_number}-#{AV::Config.alma_institution_code.downcase}"
48
+ end
49
+
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,53 @@
1
+ require 'berkeley_library/av/util'
2
+ require 'berkeley_library/av/record_not_found'
3
+
4
+ module BerkeleyLibrary
5
+ module AV
6
+ class Metadata
7
+ module Readers
8
+ module Base
9
+ include AV::Util
10
+
11
+ # @return [MARC::Record] the MARC record
12
+ def record_for(record_id)
13
+ first_record_for(record_id)
14
+ rescue AV::RecordNotFound
15
+ raise
16
+ rescue StandardError => e
17
+ raise not_found(record_id, e.message)
18
+ end
19
+
20
+ private
21
+
22
+ # @return [MARC::Record] the MARC record
23
+ def first_record_for(record_id)
24
+ marc_uri = marc_uri_for(record_id)
25
+ record_from(marc_uri, record_id)
26
+ end
27
+
28
+ def record_from(marc_uri, record_id)
29
+ xml = do_get(marc_uri)
30
+ AV::Marc.from_xml(xml).tap do |record|
31
+ raise not_found(record_id, "GET #{marc_uri} returned: #{xml}", marc_uri:) unless record
32
+ end
33
+ end
34
+
35
+ def not_found(record_id, details, marc_uri: nil)
36
+ msg = "Can't find #{name} record for ID #{record_id}: #{details}."
37
+
38
+ marc_uri_msg = marc_uri_message(record_id, marc_uri)
39
+ msg = [msg, marc_uri_msg].join(' ') if marc_uri_msg
40
+
41
+ AV::RecordNotFound.new(msg)
42
+ end
43
+
44
+ def marc_uri_message(record_id, marc_uri)
45
+ " MARC URI: #{marc_uri | marc_uri_for(record_id)}"
46
+ rescue StandardError
47
+ # nil
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,52 @@
1
+ require 'berkeley_library/util/uris'
2
+
3
+ require 'berkeley_library/av/config'
4
+ require 'berkeley_library/av/record_id'
5
+ require 'berkeley_library/av/metadata/readers/base'
6
+
7
+ module BerkeleyLibrary
8
+ module AV
9
+ class Metadata
10
+ module Readers
11
+ module TIND
12
+ include Base
13
+
14
+ TIND_ID_FIELD = '035__a'.freeze
15
+
16
+ ID_FIELDS = {
17
+ AV::RecordId::Type::MILLENNIUM => '901__m'.freeze,
18
+ AV::RecordId::Type::OCLC => '901__o'.freeze
19
+ }.freeze
20
+
21
+ def marc_uri_for(record_id)
22
+ id_field = id_field_for(record_id)
23
+ query_string = URI.encode_www_form(
24
+ 'p' => "#{id_field}:\"#{record_id}\"",
25
+ 'of' => 'xm'
26
+ )
27
+ URIs.append(base_uri, 'search', '?', query_string)
28
+ end
29
+
30
+ protected
31
+
32
+ def _display_uri_for(record_id)
33
+ URIs.append(base_uri, 'record', record_id)
34
+ end
35
+
36
+ private
37
+
38
+ def base_uri
39
+ AV::Config.tind_base_uri
40
+ end
41
+
42
+ def id_field_for(record_id)
43
+ id_type = AV::RecordId::Type.for_id(record_id)
44
+ raise ArgumentError, "Can't look up Alma record #{record_id} in TIND" if id_type == AV::RecordId::Type::ALMA
45
+
46
+ ID_FIELDS[id_type] || TIND_ID_FIELD
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,2 @@
1
+ require 'berkeley_library/av/metadata/readers/alma'
2
+ require 'berkeley_library/av/metadata/readers/tind'