1# isan.py - functions for handling International Standard Audiovisual Numbers 2# (ISANs) 3# 4# Copyright (C) 2010, 2011, 2012, 2013 Arthur de Jong 5# 6# This library is free software; you can redistribute it and/or 7# modify it under the terms of the GNU Lesser General Public 8# License as published by the Free Software Foundation; either 9# version 2.1 of the License, or (at your option) any later version. 10# 11# This library is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY; without even the implied warranty of 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14# Lesser General Public License for more details. 15# 16# You should have received a copy of the GNU Lesser General Public 17# License along with this library; if not, write to the Free Software 18# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 19# 02110-1301 USA 20 21"""ISAN (International Standard Audiovisual Number). 22 23The ISAN (International Standard Audiovisual Number) is used to identify 24audiovisual works. 25 26The number is hexadecimal and can consists of at least a root identifier, 27and an episode or part. After that an optional check digit, optional 28version and optionally another check digit can be provided. The check 29digits are validated using the ISO 7064 Mod 37, 36 algorithm. 30 31>>> validate('000000018947000000000000') 32'000000018947000000000000' 33>>> compact('0000-0000-D07A-0090-Q-0000-0000-X') 34'00000000D07A009000000000' 35>>> validate('0000-0001-8CFA-0000-I-0000-0000-K') 36'000000018CFA0000I00000000K' 37>>> validate('0000-0001-8CFA-0000-A-0000-0000-K') 38Traceback (most recent call last): 39 ... 40InvalidChecksum: ... 41>>> format('000000018947000000000000') 42'0000-0001-8947-0000-8-0000-0000-D' 43 44>>> to_urn('00000000D07A009000000000') 45'URN:ISAN:0000-0000-D07A-0090-Q-0000-0000-X' 46>>> to_xml('1881-66C7-3420-6541-Y-9F3A-0245-O') 47'<ISAN root="1881-66C7-3420" episode="6541" version="9F3A-0245" />' 48""" 49 50from stdnum.exceptions import * 51from stdnum.iso7064 import mod_37_36 52from stdnum.util import clean 53 54 55def split(number): 56 """Split the number into a root, an episode or part, a check digit a 57 version and another check digit. If any of the parts are missing an empty 58 string is returned.""" 59 number = clean(number, ' -').strip().upper() 60 if len(number) == 17 or len(number) == 26: 61 return number[0:12], number[12:16], number[16], number[17:25], number[25:] 62 elif len(number) > 16: 63 return number[0:12], number[12:16], '', number[16:24], number[24:] 64 else: 65 return number[0:12], number[12:16], number[16:], '', '' 66 67 68def compact(number, strip_check_digits=True): 69 """Convert the ISAN to the minimal representation. This strips the number 70 of any valid separators and removes surrounding whitespace. The check 71 digits are removed by default.""" 72 number = list(split(number)) 73 if strip_check_digits: 74 number[2] = number[4] = '' 75 return ''.join(number) 76 77 78def validate(number, strip_check_digits=False, add_check_digits=False): 79 """Check if the number provided is a valid ISAN. If check digits are 80 present in the number they are validated. If strip_check_digits is True 81 any existing check digits will be removed (after checking). If 82 add_check_digits is True the check digit will be added if they are not 83 present yet.""" 84 (root, episode, check1, version, check2) = split(number) 85 # check digits used 86 for x in root + episode + version: 87 if x not in '0123456789ABCDEF': 88 raise InvalidFormat() 89 # check length of all components 90 if len(root) != 12 or len(episode) != 4 or len(check1) not in (0, 1) or \ 91 len(version) not in (0, 8) or len(check1) not in (0, 1): 92 raise InvalidLength() 93 # allow removing check digits 94 if strip_check_digits: 95 check1 = check2 = '' 96 # check check digits 97 if check1: 98 mod_37_36.validate(root + episode + check1) 99 if check2: 100 mod_37_36.validate(root + episode + version + check2) 101 # add check digits 102 if add_check_digits and not check1: 103 check1 = mod_37_36.calc_check_digit(root + episode) 104 if add_check_digits and not check2 and version: 105 check2 = mod_37_36.calc_check_digit(root + episode + version) 106 return root + episode + check1 + version + check2 107 108 109def is_valid(number): 110 """Check if the number provided is a valid ISAN. If check digits are 111 present in the number they are validated.""" 112 try: 113 return bool(validate(number)) 114 except ValidationError: 115 return False 116 117 118def format(number, separator='-', strip_check_digits=False, add_check_digits=True): 119 """Reformat the number to the standard presentation format. If 120 add_check_digits is True the check digit will be added if they are not 121 present yet. If both strip_check_digits and add_check_digits are True the 122 check digits will be recalculated.""" 123 (root, episode, check1, version, check2) = split(number) 124 if strip_check_digits: 125 check1 = check2 = '' 126 if add_check_digits and not check1: 127 check1 = mod_37_36.calc_check_digit(root + episode) 128 if add_check_digits and not check2 and version: 129 check2 = mod_37_36.calc_check_digit(root + episode + version) 130 number = [root[i:i + 4] for i in range(0, 12, 4)] + [episode] 131 if check1: 132 number.append(check1) 133 if version: 134 number.extend((version[0:4], version[4:])) 135 if check2: 136 number.append(check2) 137 return separator.join(number) 138 139 140def to_binary(number): 141 """Convert the number to its binary representation (without the check 142 digits).""" 143 from binascii import a2b_hex 144 return a2b_hex(compact(number, strip_check_digits=True)) 145 146 147def to_xml(number): 148 """Return the XML form of the ISAN as a string.""" 149 number = format(number, strip_check_digits=True, add_check_digits=False) 150 return '<ISAN root="%s" episode="%s" version="%s" />' % ( 151 number[0:14], number[15:19], number[20:]) 152 153 154def to_urn(number): 155 """Return the URN representation of the ISAN.""" 156 return 'URN:ISAN:' + format(number, add_check_digits=True) 157