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.
- 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
|