berkeley_library-av-core 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.dockerignore +391 -0
- data/.github/workflows/build.yml +30 -0
- data/.gitignore +388 -0
- data/.idea/av_core.iml +146 -0
- data/.idea/codeStyles/Project.xml +12 -0
- data/.idea/codeStyles/codeStyleConfig.xml +5 -0
- data/.idea/go.imports.xml +6 -0
- data/.idea/inspectionProfiles/Project_Default.xml +37 -0
- data/.idea/misc.xml +6 -0
- data/.idea/modules.xml +8 -0
- data/.idea/vcs.xml +6 -0
- data/.rubocop.yml +241 -0
- data/.ruby-version +1 -0
- data/.simplecov +8 -0
- data/CHANGES.md +38 -0
- data/Gemfile +3 -0
- data/Jenkinsfile +16 -0
- data/LICENSE.md +21 -0
- data/README.md +20 -0
- data/Rakefile +20 -0
- data/av_core.gemspec +49 -0
- data/lib/berkeley_library/av/config.rb +238 -0
- data/lib/berkeley_library/av/constants.rb +30 -0
- data/lib/berkeley_library/av/core/module_info.rb +18 -0
- data/lib/berkeley_library/av/core.rb +7 -0
- data/lib/berkeley_library/av/marc/util.rb +114 -0
- data/lib/berkeley_library/av/marc.rb +52 -0
- data/lib/berkeley_library/av/metadata/README.md +5 -0
- data/lib/berkeley_library/av/metadata/field.rb +110 -0
- data/lib/berkeley_library/av/metadata/fields.rb +130 -0
- data/lib/berkeley_library/av/metadata/link.rb +28 -0
- data/lib/berkeley_library/av/metadata/readers/alma.rb +54 -0
- data/lib/berkeley_library/av/metadata/readers/base.rb +53 -0
- data/lib/berkeley_library/av/metadata/readers/tind.rb +52 -0
- data/lib/berkeley_library/av/metadata/readers.rb +2 -0
- data/lib/berkeley_library/av/metadata/source.rb +93 -0
- data/lib/berkeley_library/av/metadata/tind_html_metadata_da.json +2076 -0
- data/lib/berkeley_library/av/metadata/value.rb +121 -0
- data/lib/berkeley_library/av/metadata.rb +103 -0
- data/lib/berkeley_library/av/record.rb +86 -0
- data/lib/berkeley_library/av/record_id.rb +121 -0
- data/lib/berkeley_library/av/record_not_found.rb +7 -0
- data/lib/berkeley_library/av/restrictions.rb +36 -0
- data/lib/berkeley_library/av/track.rb +132 -0
- data/lib/berkeley_library/av/types/duration.rb +67 -0
- data/lib/berkeley_library/av/types/file_type.rb +84 -0
- data/lib/berkeley_library/av/util.rb +65 -0
- data/rakelib/bundle.rake +8 -0
- data/rakelib/coverage.rake +11 -0
- data/rakelib/gem.rake +54 -0
- data/rakelib/rubocop.rake +18 -0
- data/rakelib/spec.rake +12 -0
- data/spec/.rubocop.yml +116 -0
- data/spec/data/10.23.19.JessieLaCavalier.02.mrc +3 -0
- data/spec/data/alma/991005939359706532-sru.xml +123 -0
- data/spec/data/alma/991034756419706532-sru.xml +162 -0
- data/spec/data/alma/991047179369706532-sru.xml +210 -0
- data/spec/data/alma/991054360089706532-sru.xml +186 -0
- data/spec/data/alma/b11082434-sru.xml +165 -0
- data/spec/data/alma/b18538031-sru.xml +123 -0
- data/spec/data/alma/b20786580-sru.xml +123 -0
- data/spec/data/alma/b22139647-sru.xml +171 -0
- data/spec/data/alma/b22139658-sru.xml +282 -0
- data/spec/data/alma/b23161018-sru.xml +182 -0
- data/spec/data/alma/b23305522-sru.xml +144 -0
- data/spec/data/alma/b24071548-sru.xml +136 -0
- data/spec/data/alma/b24659129-sru.xml +210 -0
- data/spec/data/alma/b25207857-sru.xml +217 -0
- data/spec/data/alma/b25716973-sru.xml +186 -0
- data/spec/data/alma/b25742488-sru.xml +246 -0
- data/spec/data/record-(cityarts)00002.xml +78 -0
- data/spec/data/record-(cityarts)00773.xml +94 -0
- data/spec/data/record-(clir)00020.xml +153 -0
- data/spec/data/record-(miscmat)00615.xml +45 -0
- data/spec/data/record-(pacradio)00107.xml +85 -0
- data/spec/data/record-(pacradio)01469.xml +82 -0
- data/spec/data/record-empty-result.xml +4 -0
- data/spec/data/record-multiple-998s-disordered.xml +178 -0
- data/spec/data/record-multiple-998s.xml +178 -0
- data/spec/data/record-physcolloquia-bk00169017b.xml +78 -0
- data/spec/data/record-ragged-998-subfields.xml +122 -0
- data/spec/data/record-ragged-998s-multiple-fields.xml +160 -0
- data/spec/data/record-redirect-to-login.html +288 -0
- data/spec/data/record_id/bibs_with_check_digits.txt +151 -0
- data/spec/data/search-1993.xml +158 -0
- data/spec/data/search-b23305516.xml +81 -0
- data/spec/lib/berkeley_library/av/av_spec.rb +12 -0
- data/spec/lib/berkeley_library/av/config_spec.rb +250 -0
- data/spec/lib/berkeley_library/av/marc/util_spec.rb +150 -0
- data/spec/lib/berkeley_library/av/marc_spec.rb +62 -0
- data/spec/lib/berkeley_library/av/metadata/field_spec.rb +81 -0
- data/spec/lib/berkeley_library/av/metadata/fields_spec.rb +180 -0
- data/spec/lib/berkeley_library/av/metadata/metadata_spec.rb +274 -0
- data/spec/lib/berkeley_library/av/metadata/source_spec.rb +261 -0
- data/spec/lib/berkeley_library/av/metadata/value_spec.rb +29 -0
- data/spec/lib/berkeley_library/av/record_id_spec.rb +72 -0
- data/spec/lib/berkeley_library/av/record_spec.rb +284 -0
- data/spec/lib/berkeley_library/av/track_spec.rb +335 -0
- data/spec/lib/berkeley_library/av/types/duration_spec.rb +91 -0
- data/spec/lib/berkeley_library/av/types/file_type_spec.rb +98 -0
- data/spec/lib/berkeley_library/av/util_spec.rb +30 -0
- data/spec/spec_helper.rb +63 -0
- metadata +499 -0
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'berkeley_library/util/uris'
|
2
|
+
require 'berkeley_library/av/util'
|
3
|
+
require 'berkeley_library/av/metadata/link'
|
4
|
+
|
5
|
+
module BerkeleyLibrary
|
6
|
+
module AV
|
7
|
+
class Metadata
|
8
|
+
class Value
|
9
|
+
include AV::Util
|
10
|
+
include Comparable
|
11
|
+
|
12
|
+
# ------------------------------------------------------------
|
13
|
+
# Constants
|
14
|
+
|
15
|
+
URL_CODE = :u
|
16
|
+
BODY_CODES = %i[y z].freeze
|
17
|
+
|
18
|
+
# ------------------------------------------------------------
|
19
|
+
# Accessors
|
20
|
+
|
21
|
+
# TODO: find uses of :entries & replace them with standardized string value method here
|
22
|
+
attr_reader :tag, :label, :entries, :order
|
23
|
+
|
24
|
+
# ------------------------------------------------------------
|
25
|
+
# Initializers
|
26
|
+
|
27
|
+
def initialize(tag:, label:, entries:, order:)
|
28
|
+
raise ArgumentError, 'Entries cannot be empty' if entries.empty?
|
29
|
+
|
30
|
+
@tag = tag
|
31
|
+
@label = label
|
32
|
+
@order = order
|
33
|
+
@entries = entries
|
34
|
+
end
|
35
|
+
|
36
|
+
# ------------------------------------------------------------
|
37
|
+
# Public methods
|
38
|
+
|
39
|
+
def includes_link?(link_body)
|
40
|
+
entries.any? do |entry|
|
41
|
+
entry.is_a?(Link) && entry.body == link_body
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def as_string
|
46
|
+
entries.join(' ').gsub(/[[:space:]]+/, ' ')
|
47
|
+
end
|
48
|
+
|
49
|
+
# ------------------------------
|
50
|
+
# Object overrides
|
51
|
+
|
52
|
+
def to_s
|
53
|
+
StringIO.new.tap do |out|
|
54
|
+
out << "#{label} (#{tag}): "
|
55
|
+
out << entries.join(' ')
|
56
|
+
end.string
|
57
|
+
end
|
58
|
+
|
59
|
+
# @param other [Value] the Value to compare
|
60
|
+
def <=>(other)
|
61
|
+
compare_by_attributes(self, other, :order, :tag, :entries, :label)
|
62
|
+
end
|
63
|
+
|
64
|
+
# ------------------------------------------------------------
|
65
|
+
# Class methods
|
66
|
+
|
67
|
+
class << self
|
68
|
+
include AV::Util
|
69
|
+
include BerkeleyLibrary::Util::URIs
|
70
|
+
|
71
|
+
def value_for(field, subfield_groups)
|
72
|
+
return if subfield_groups.empty?
|
73
|
+
return if (all_entries = entries_from_groups(subfield_groups, field.subfields_separator)).empty?
|
74
|
+
|
75
|
+
Value.new(tag: field.tag, label: field.label, entries: all_entries, order: field.order)
|
76
|
+
end
|
77
|
+
|
78
|
+
def link_value(field, link)
|
79
|
+
Value.new(tag: field.tag, label: field.label, order: field.order, entries: [link])
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def entries_from_groups(subfield_groups, separator)
|
85
|
+
subfield_groups.each_with_object([]) do |sg, entries|
|
86
|
+
entries.concat(entries_from_group(sg, separator))
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def entries_from_group(subfield_group, separator)
|
91
|
+
[].tap do |entries|
|
92
|
+
link = extract_link(subfield_group)
|
93
|
+
entries << link if link
|
94
|
+
|
95
|
+
subfield_values = subfield_group.values.map { |sf| tidy_value(sf.value) }
|
96
|
+
entries << subfield_values.join(separator) unless subfield_values.empty?
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def extract_link(subfield_group)
|
101
|
+
return unless (url_sf = subfield_group.delete(URL_CODE))
|
102
|
+
|
103
|
+
url = url_sf.value
|
104
|
+
body = link_body_from(subfield_group) || url
|
105
|
+
AV::Metadata::Link.new(url:, body:)
|
106
|
+
end
|
107
|
+
|
108
|
+
def link_body_from(subfield_group)
|
109
|
+
body_sf = BODY_CODES.lazy.filter_map { |code| subfield_group.delete(code) }.first
|
110
|
+
return unless body_sf
|
111
|
+
|
112
|
+
body_value = tidy_value(body_sf.value)
|
113
|
+
body_value unless body_value.empty?
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'berkeley_library/av/constants'
|
2
|
+
require 'berkeley_library/av/metadata/fields'
|
3
|
+
require 'berkeley_library/av/metadata/source'
|
4
|
+
require 'berkeley_library/av/restrictions'
|
5
|
+
|
6
|
+
module BerkeleyLibrary
|
7
|
+
module AV
|
8
|
+
class Metadata
|
9
|
+
include AV::Constants
|
10
|
+
|
11
|
+
attr_reader :record_id, :source
|
12
|
+
|
13
|
+
# TODO: can we stop passing in record ID / stop lazy-loading MARC?
|
14
|
+
def initialize(record_id:, source:, marc_record: nil)
|
15
|
+
@record_id = record_id
|
16
|
+
@source = source
|
17
|
+
@marc_record = marc_record
|
18
|
+
end
|
19
|
+
|
20
|
+
def bib_number
|
21
|
+
return @bib_number if instance_variable_defined?(:@bib_number)
|
22
|
+
|
23
|
+
@bib_number = source.find_bib_number(self)
|
24
|
+
end
|
25
|
+
|
26
|
+
def tind_id
|
27
|
+
id_001 if source == Source::TIND
|
28
|
+
end
|
29
|
+
|
30
|
+
def alma_id
|
31
|
+
id_001 if source == Source::ALMA
|
32
|
+
end
|
33
|
+
|
34
|
+
def marc_record
|
35
|
+
@marc_record ||= source.record_for(record_id)
|
36
|
+
end
|
37
|
+
|
38
|
+
def values_by_field
|
39
|
+
@values_by_field ||= Fields.default_values_from(marc_record).tap { |values| ensure_catalog_link(values) }
|
40
|
+
end
|
41
|
+
|
42
|
+
def each_value(&)
|
43
|
+
values_by_field.each_value(&)
|
44
|
+
end
|
45
|
+
|
46
|
+
def title
|
47
|
+
@title ||= (title_value = values_by_field[Fields::TITLE]) ? title_value.entries.first : UNKNOWN_TITLE
|
48
|
+
end
|
49
|
+
|
50
|
+
def description
|
51
|
+
@description ||= (desc_value = values_by_field[Fields::DESCRIPTION]) ? desc_value.as_string : ''
|
52
|
+
end
|
53
|
+
|
54
|
+
def calnet_or_ip?
|
55
|
+
restrictions.calnet_or_ip?
|
56
|
+
end
|
57
|
+
|
58
|
+
def calnet_only?
|
59
|
+
restrictions.calnet_only?
|
60
|
+
end
|
61
|
+
|
62
|
+
def display_uri
|
63
|
+
@display_uri ||= source.display_uri_for(self)
|
64
|
+
end
|
65
|
+
|
66
|
+
class << self
|
67
|
+
def for_record(record_id:)
|
68
|
+
source = Source.for_record_id(record_id)
|
69
|
+
raise AV::RecordNotFound, "Unable to determine metadata source for record ID: #{record_id.inspect}" unless source
|
70
|
+
|
71
|
+
Metadata.new(record_id:, source:)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def restrictions
|
78
|
+
@restrictions ||= Restrictions.new(marc_record)
|
79
|
+
end
|
80
|
+
|
81
|
+
def id_001
|
82
|
+
return @id_001 if instance_variable_defined?(:@id_001)
|
83
|
+
return (@id_001 = nil) unless (cf_001 = marc_record['001'])
|
84
|
+
|
85
|
+
@id_001 = RecordId.ensure_record_id(cf_001.value)
|
86
|
+
end
|
87
|
+
|
88
|
+
def ensure_catalog_link(values_by_field)
|
89
|
+
catalog_value = values_by_field[Fields::CATALOG_LINK]
|
90
|
+
return if catalog_value && catalog_value.includes_link?(source.catalog_link_text)
|
91
|
+
|
92
|
+
catalog_link = Link.new(url: display_uri.to_s, body: source.catalog_link_text)
|
93
|
+
|
94
|
+
if catalog_value
|
95
|
+
catalog_value.entries << catalog_link
|
96
|
+
else
|
97
|
+
values_by_field[Fields::CATALOG_LINK] = Value.link_value(Fields::CATALOG_LINK, catalog_link)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'berkeley_library/util'
|
2
|
+
|
3
|
+
require 'berkeley_library/av/track'
|
4
|
+
require 'berkeley_library/av/metadata'
|
5
|
+
require 'berkeley_library/av/metadata/source'
|
6
|
+
|
7
|
+
module BerkeleyLibrary
|
8
|
+
module AV
|
9
|
+
class Record
|
10
|
+
include BerkeleyLibrary::Util
|
11
|
+
|
12
|
+
attr_reader :collection, :tracks, :metadata
|
13
|
+
|
14
|
+
# TODO: stop passing in track list & just get it from metadata & collection
|
15
|
+
def initialize(collection:, tracks:, metadata:)
|
16
|
+
@collection = collection
|
17
|
+
@tracks = tracks.sort
|
18
|
+
@metadata = metadata
|
19
|
+
end
|
20
|
+
|
21
|
+
def title
|
22
|
+
metadata.title
|
23
|
+
end
|
24
|
+
|
25
|
+
def bib_number
|
26
|
+
metadata.bib_number
|
27
|
+
end
|
28
|
+
|
29
|
+
def tind_id
|
30
|
+
metadata.tind_id
|
31
|
+
end
|
32
|
+
|
33
|
+
def record_id
|
34
|
+
metadata.record_id
|
35
|
+
end
|
36
|
+
|
37
|
+
def calnet_or_ip?
|
38
|
+
metadata.calnet_or_ip?
|
39
|
+
end
|
40
|
+
|
41
|
+
def calnet_only?
|
42
|
+
metadata.calnet_only?
|
43
|
+
end
|
44
|
+
|
45
|
+
def type_label
|
46
|
+
@type_label ||= begin
|
47
|
+
file_types = Set.new(tracks.map(&:file_type)).to_a.sort
|
48
|
+
file_types = AV::Types::FileType.to_a if file_types.empty?
|
49
|
+
|
50
|
+
file_types.map(&:label).join(' / ')
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def player_uri
|
55
|
+
@player_uri ||= URIs.append(AV::Config.avplayer_base_uri, collection, record_id)
|
56
|
+
end
|
57
|
+
|
58
|
+
def display_uri
|
59
|
+
metadata.display_uri
|
60
|
+
end
|
61
|
+
|
62
|
+
def description
|
63
|
+
metadata.description
|
64
|
+
end
|
65
|
+
|
66
|
+
class << self
|
67
|
+
# Loads the metadata for the specified record and creates a record object from it.
|
68
|
+
#
|
69
|
+
# Note that for TIND records the record ID is *not* the TIND internal ID
|
70
|
+
# (MARC field 001) but rather the ID assigned by the UC Berkeley Library
|
71
|
+
# (MARC field 035).
|
72
|
+
#
|
73
|
+
# @param collection [String] The collection name (Wowza application id).
|
74
|
+
# @param record_id [String] The record ID.
|
75
|
+
def from_metadata(collection:, record_id:)
|
76
|
+
metadata = Metadata.for_record(record_id:)
|
77
|
+
Record.new(
|
78
|
+
collection:,
|
79
|
+
metadata:,
|
80
|
+
tracks: Track.tracks_from(metadata.marc_record, collection:)
|
81
|
+
)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'typesafe_enum'
|
2
|
+
require 'berkeley_library/av/constants'
|
3
|
+
|
4
|
+
# TODO: use berkeley_library/alma to replace as much of this as possible
|
5
|
+
module BerkeleyLibrary
|
6
|
+
module AV
|
7
|
+
class RecordId
|
8
|
+
include AV::Util
|
9
|
+
include Comparable
|
10
|
+
|
11
|
+
# ------------------------------------------------------------
|
12
|
+
# Fields
|
13
|
+
|
14
|
+
attr_reader :id, :type
|
15
|
+
|
16
|
+
# ------------------------------------------------------------
|
17
|
+
# Initializer
|
18
|
+
|
19
|
+
def initialize(id)
|
20
|
+
@id = id.to_s
|
21
|
+
@type = Type.for_id(@id)
|
22
|
+
end
|
23
|
+
|
24
|
+
# ------------------------------------------------------------
|
25
|
+
# Class methods
|
26
|
+
|
27
|
+
class << self
|
28
|
+
include AV::Constants
|
29
|
+
|
30
|
+
def ensure_record_id(record_id)
|
31
|
+
return record_id if record_id.is_a?(RecordId)
|
32
|
+
|
33
|
+
RecordId.new(record_id)
|
34
|
+
end
|
35
|
+
|
36
|
+
def ensure_check_digit(bib_number)
|
37
|
+
digit_str, check_str = split_bib(bib_number)
|
38
|
+
|
39
|
+
digits = digit_str.chars.map(&:to_i)
|
40
|
+
check_digit = calculate_check_digit(digits)
|
41
|
+
|
42
|
+
return "b#{digit_str}#{check_digit}" if check_str.nil? || check_str == 'a'
|
43
|
+
return bib_number if check_str == check_digit
|
44
|
+
|
45
|
+
raise ArgumentError, "#{bib_number} check digit invalid: expected #{check_digit}, got #{check_str}"
|
46
|
+
end
|
47
|
+
|
48
|
+
def strip_check_digit(bib_number)
|
49
|
+
digit_str, = split_bib(bib_number)
|
50
|
+
"b#{digit_str}"
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def split_bib(bib_number)
|
56
|
+
raise ArgumentError, "Not a Millennium bib number: #{bib_number.inspect}" unless (md = MILLENNIUM_RECORD_RE.match(bib_number.to_s))
|
57
|
+
|
58
|
+
%i[digits check].map { |part| md[part] }
|
59
|
+
end
|
60
|
+
|
61
|
+
def calculate_check_digit(digits)
|
62
|
+
raise ArgumentError, "Not an 8-digit array : #{digits.inspect}" unless digits.is_a?(Array) && digits.size == 8
|
63
|
+
|
64
|
+
# From: http://liwong.blogspot.com/2018/04/recipe-computing-millennium-checkdigit.html
|
65
|
+
mod = digits.reverse.each_with_index.inject(0) { |sum, (v, i)| sum + (v * (i + 2)) } % 11
|
66
|
+
mod == 10 ? 'x' : mod.to_s
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# ------------------------------------------------------------
|
71
|
+
# Comparable
|
72
|
+
|
73
|
+
def <=>(other)
|
74
|
+
compare_by_attributes(self, other, :id, :type)
|
75
|
+
end
|
76
|
+
|
77
|
+
# ------------------------------------------------------------
|
78
|
+
# Object overrides
|
79
|
+
|
80
|
+
def hash
|
81
|
+
[id, type].hash
|
82
|
+
end
|
83
|
+
|
84
|
+
def to_s
|
85
|
+
id
|
86
|
+
end
|
87
|
+
|
88
|
+
def inspect
|
89
|
+
"#{self.class}(#{id} [#{type}])@#{object_id}"
|
90
|
+
end
|
91
|
+
|
92
|
+
# ------------------------------------------------------------
|
93
|
+
# Helper classes
|
94
|
+
|
95
|
+
class Type < TypesafeEnum::Base
|
96
|
+
new :ALMA
|
97
|
+
new :MILLENNIUM
|
98
|
+
new :OCLC
|
99
|
+
new :TIND
|
100
|
+
|
101
|
+
def to_s
|
102
|
+
key.to_s
|
103
|
+
end
|
104
|
+
|
105
|
+
class << self
|
106
|
+
include AV::Constants
|
107
|
+
|
108
|
+
def for_id(id)
|
109
|
+
return unless id
|
110
|
+
return id.type if id.is_a?(RecordId)
|
111
|
+
return Type::ALMA if ALMA_RECORD_RE =~ id
|
112
|
+
return Type::MILLENNIUM if MILLENNIUM_RECORD_RE =~ id
|
113
|
+
return Type::OCLC if OCLC_RECORD_RE =~ id
|
114
|
+
|
115
|
+
Type::TIND
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module BerkeleyLibrary
|
2
|
+
module AV
|
3
|
+
class Restrictions
|
4
|
+
|
5
|
+
# TODO: remove 85642$y and 95640$z once we confirm all records have 998$r
|
6
|
+
SUBFIELD_VALUE_SPECS = %w[998$r/#-# 856$y/#-#{^1=\4}{^2=\2} 956$z/#-#{^1=\4}{^2=\0}].freeze
|
7
|
+
|
8
|
+
RE_CALNET_ONLY = /CalNet/i
|
9
|
+
RE_CALNET_OR_IP = /UCB access|UCB only/i
|
10
|
+
|
11
|
+
attr_reader :marc_record
|
12
|
+
|
13
|
+
def initialize(marc_record)
|
14
|
+
@marc_record = marc_record
|
15
|
+
end
|
16
|
+
|
17
|
+
def calnet_only?
|
18
|
+
@calnet ||= any_field_value_matches?(RE_CALNET_ONLY)
|
19
|
+
end
|
20
|
+
|
21
|
+
def calnet_or_ip?
|
22
|
+
@ucb_ip ||= any_field_value_matches?(RE_CALNET_OR_IP)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def any_field_value_matches?(re)
|
28
|
+
field_values.any? { |v| re =~ v }
|
29
|
+
end
|
30
|
+
|
31
|
+
def field_values
|
32
|
+
@field_values ||= SUBFIELD_VALUE_SPECS.flat_map { |spec| MARC::Spec.find(spec, marc_record) }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'berkeley_library/av/constants'
|
2
|
+
require 'berkeley_library/av/types/duration'
|
3
|
+
require 'berkeley_library/av/types/file_type'
|
4
|
+
require 'berkeley_library/av/marc/util'
|
5
|
+
require 'marc'
|
6
|
+
|
7
|
+
module BerkeleyLibrary
|
8
|
+
module AV
|
9
|
+
class Track
|
10
|
+
include Comparable
|
11
|
+
include AV::Constants
|
12
|
+
include AV::Util
|
13
|
+
|
14
|
+
attr_reader :sort_order, :title, :path, :duration, :file_type
|
15
|
+
|
16
|
+
def initialize(sort_order:, path:, title: nil, duration: nil)
|
17
|
+
@sort_order = sort_order
|
18
|
+
@title = title
|
19
|
+
@path = path
|
20
|
+
@duration = duration_or_nil(duration)
|
21
|
+
@file_type = AV::Types::FileType.for_path(path)
|
22
|
+
end
|
23
|
+
|
24
|
+
def <=>(other)
|
25
|
+
compare_by_attributes(self, other, :sort_order, :title, :duration, :path)
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_s
|
29
|
+
''.tap do |s|
|
30
|
+
s << "#{sort_order}: " if sort_order
|
31
|
+
s << path
|
32
|
+
s << " #{title.inspect}" if title
|
33
|
+
s << " (#{duration})" if duration
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def inspect
|
38
|
+
"\#<#{self.class.name} #{self}>"
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [Array<MARC::Subfield>]
|
42
|
+
def to_marc_subfields
|
43
|
+
[].tap do |subfields|
|
44
|
+
subfields << MARC::Subfield.new(SUBFIELD_CODE_DURATION, duration.to_s) if duration
|
45
|
+
subfields << MARC::Subfield.new(SUBFIELD_CODE_TITLE, title) if title
|
46
|
+
subfields << MARC::Subfield.new(SUBFIELD_CODE_PATH, path)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def duration_or_nil(duration)
|
53
|
+
return duration if duration.is_a?(AV::Types::Duration)
|
54
|
+
|
55
|
+
AV::Types::Duration.from_string(duration)
|
56
|
+
end
|
57
|
+
|
58
|
+
class << self
|
59
|
+
include AV::Constants
|
60
|
+
include AV::Marc::Util
|
61
|
+
|
62
|
+
LABELS = {
|
63
|
+
SUBFIELD_CODE_DURATION => 'duration',
|
64
|
+
SUBFIELD_CODE_TITLE => 'title',
|
65
|
+
SUBFIELD_CODE_PATH => 'path'
|
66
|
+
}.freeze
|
67
|
+
|
68
|
+
# Note that if multiple tracks are encoded in the same 998 field, the
|
69
|
+
# subfields **must** be in the order :a, :t, :g (duration, title, path),
|
70
|
+
# as documented in {https://docs.google.com/document/d/1gRWsaSoerSvadNlYR-zbYOjgj0geLxV41bBC0rm5nHE/edit
|
71
|
+
# "How to add media to the AV System"}.
|
72
|
+
#
|
73
|
+
# @param marc_record [MARC::Record] the MARC record
|
74
|
+
# @param collection [String] the collection
|
75
|
+
def tracks_from(marc_record, collection:)
|
76
|
+
track_fields = marc_record.fields(TAG_TRACK_FIELD)
|
77
|
+
track_fields.each_with_object([]) do |df, all_tracks|
|
78
|
+
value_groups = group_values(df.subfields)
|
79
|
+
value_groups.each do |group|
|
80
|
+
all_tracks << from_value_group(group, collection:, sort_order: all_tracks.size)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def group_values(subfields)
|
88
|
+
filtered = subfields.select { |sf| SUBFIELD_CODES_TRACKS.include?(sf.code.to_sym) }
|
89
|
+
group_subfields(filtered).map { |sfg| to_value_group(sfg) }
|
90
|
+
end
|
91
|
+
|
92
|
+
def group_subfields(subfields)
|
93
|
+
single_track = subfields.lazy.select { |sf| sf.code.to_sym == SUBFIELD_CODE_PATH }.one?
|
94
|
+
return [group_together(subfields)] if single_track
|
95
|
+
|
96
|
+
group_on_paths(subfields)
|
97
|
+
end
|
98
|
+
|
99
|
+
def group_together(subfields)
|
100
|
+
subfields.each_with_object({}) { |sf, grp| grp[sf.code.to_sym] = sf }
|
101
|
+
end
|
102
|
+
|
103
|
+
def group_on_paths(subfields)
|
104
|
+
current_group = {}
|
105
|
+
subfields.each_with_object([]) do |subfield, groups|
|
106
|
+
code_sym = subfield.code.to_sym
|
107
|
+
|
108
|
+
current_group[code_sym] = subfield
|
109
|
+
if code_sym == SUBFIELD_CODE_PATH
|
110
|
+
groups << current_group
|
111
|
+
current_group = {}
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def to_value_group(subfield_group)
|
117
|
+
subfield_group.transform_values { |sf| tidy_value(sf.value) }
|
118
|
+
end
|
119
|
+
|
120
|
+
def from_value_group(group, collection:, sort_order:)
|
121
|
+
Track.new(
|
122
|
+
sort_order:,
|
123
|
+
title: group[SUBFIELD_CODE_TITLE],
|
124
|
+
path: "#{collection}/#{group[SUBFIELD_CODE_PATH]}",
|
125
|
+
duration: group[SUBFIELD_CODE_DURATION]
|
126
|
+
)
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module BerkeleyLibrary
|
2
|
+
module AV
|
3
|
+
module Types
|
4
|
+
class Duration
|
5
|
+
include Comparable
|
6
|
+
|
7
|
+
DURATION_RE = /^([0-9]{1,2})?:?([0-9]{2}):?([0-9]{2})$/
|
8
|
+
|
9
|
+
attr_reader :total_seconds
|
10
|
+
|
11
|
+
def initialize(hours: 0, minutes: 0, seconds: 0)
|
12
|
+
@total_seconds = (3600 * hours) + (60 * minutes) + seconds
|
13
|
+
end
|
14
|
+
|
15
|
+
def hours
|
16
|
+
@hours ||= (total_seconds / 3600).floor
|
17
|
+
end
|
18
|
+
|
19
|
+
def minutes
|
20
|
+
remainder = total_seconds % 3600
|
21
|
+
(remainder / 60).floor
|
22
|
+
end
|
23
|
+
|
24
|
+
def seconds
|
25
|
+
remainder = total_seconds % 60
|
26
|
+
remainder.floor
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_s
|
30
|
+
format('%02d:%02d:%02d', hours, minutes, seconds)
|
31
|
+
end
|
32
|
+
|
33
|
+
def <=>(other)
|
34
|
+
return 0 if equal?(other)
|
35
|
+
return unless other
|
36
|
+
return unless other.respond_to?(:total_seconds)
|
37
|
+
|
38
|
+
total_seconds <=> other.total_seconds
|
39
|
+
end
|
40
|
+
|
41
|
+
class << self
|
42
|
+
# @return [Duration] the duration, or nil
|
43
|
+
def from_string(s)
|
44
|
+
return unless s
|
45
|
+
|
46
|
+
value = clean_value(s)
|
47
|
+
return unless (md = DURATION_RE.match(value))
|
48
|
+
|
49
|
+
Duration.new(
|
50
|
+
hours: md[1].to_i,
|
51
|
+
minutes: md[2].to_i,
|
52
|
+
seconds: md[3].to_i
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def clean_value(value)
|
59
|
+
raise ArgumentError, "Not a string: #{value.inspect}" unless value.is_a?(String)
|
60
|
+
|
61
|
+
value.gsub(/[[:space:]]/, '')
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|