| 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | # Copyright (C) 2007 Samalyse SARL |
|---|
| 3 | |
|---|
| 4 | # This software is a computer program whose purpose is to backup, analyse, |
|---|
| 5 | # transcode and stream any audio content with its metadata over a web frontend. |
|---|
| 6 | |
|---|
| 7 | # This software is governed by the CeCILL license under French law and |
|---|
| 8 | # abiding by the rules of distribution of free software. You can use, |
|---|
| 9 | # modify and/ or redistribute the software under the terms of the CeCILL |
|---|
| 10 | # license as circulated by CEA, CNRS and INRIA at the following URL |
|---|
| 11 | # "http://www.cecill.info". |
|---|
| 12 | |
|---|
| 13 | # As a counterpart to the access to the source code and rights to copy, |
|---|
| 14 | # modify and redistribute granted by the license, users are provided only |
|---|
| 15 | # with a limited warranty and the software's author, the holder of the |
|---|
| 16 | # economic rights, and the successive licensors have only limited |
|---|
| 17 | # liability. |
|---|
| 18 | |
|---|
| 19 | # In this respect, the user's attention is drawn to the risks associated |
|---|
| 20 | # with loading, using, modifying and/or developing or reproducing the |
|---|
| 21 | # software by the user in light of its specific status of free software, |
|---|
| 22 | # that may mean that it is complicated to manipulate, and that also |
|---|
| 23 | # therefore means that it is reserved for developers and experienced |
|---|
| 24 | # professionals having in-depth computer knowledge. Users are therefore |
|---|
| 25 | # encouraged to load and test the software's suitability as regards their |
|---|
| 26 | # requirements in conditions enabling the security of their systems and/or |
|---|
| 27 | # data to be ensured and, more generally, to use and operate it in the |
|---|
| 28 | # same conditions as regards security. |
|---|
| 29 | |
|---|
| 30 | # The fact that you are presently reading this means that you have had |
|---|
| 31 | # knowledge of the CeCILL license and that you accept its terms. |
|---|
| 32 | # |
|---|
| 33 | # Author: Olivier Guilyardi <olivier@samalyse.com> |
|---|
| 34 | |
|---|
| 35 | from telemeta.models.core import Duration |
|---|
| 36 | from telemeta.models.media import MediaItem, MediaCollection, MediaItemAnalysis |
|---|
| 37 | from django.contrib.sites.models import Site |
|---|
| 38 | from django.conf import settings |
|---|
| 39 | |
|---|
| 40 | |
|---|
| 41 | class Resource(object): |
|---|
| 42 | "Represent a Dublin Core resource" |
|---|
| 43 | |
|---|
| 44 | def __init__(self, *args): |
|---|
| 45 | self.elements = [] |
|---|
| 46 | self.add(*args) |
|---|
| 47 | |
|---|
| 48 | def flatten(self): |
|---|
| 49 | """Convert the resource to a dictionary with element names as keys. |
|---|
| 50 | |
|---|
| 51 | Warnings: |
|---|
| 52 | - refinements are lost during this conversion, |
|---|
| 53 | - if there are several occurences of the same element, only the first is |
|---|
| 54 | used, others are lost. |
|---|
| 55 | - all values are converted to strings |
|---|
| 56 | """ |
|---|
| 57 | result = {} |
|---|
| 58 | for element in self.elements: |
|---|
| 59 | if not result.has_key(element.name): |
|---|
| 60 | result[element.name] = unicode(element.value) |
|---|
| 61 | return result |
|---|
| 62 | |
|---|
| 63 | def to_list(self): |
|---|
| 64 | """Convert the resource to unqualified dublin core, as a list of the form: |
|---|
| 65 | [(key, value), ...]""" |
|---|
| 66 | result = [] |
|---|
| 67 | for element in self.elements: |
|---|
| 68 | result.append((element.name, unicode(element.value))) |
|---|
| 69 | return result |
|---|
| 70 | |
|---|
| 71 | def add(self, *elements): |
|---|
| 72 | for e in elements: |
|---|
| 73 | if isinstance(e, Element): |
|---|
| 74 | if not e in self.elements: |
|---|
| 75 | self.elements.append(e) |
|---|
| 76 | else: |
|---|
| 77 | try: |
|---|
| 78 | iter(e) |
|---|
| 79 | except TypeError: |
|---|
| 80 | raise Exception("add() only accepts elements or sequences of elements") |
|---|
| 81 | |
|---|
| 82 | self.add(*e) |
|---|
| 83 | |
|---|
| 84 | def __unicode__(self): |
|---|
| 85 | dump = u'' |
|---|
| 86 | for e in self.elements: |
|---|
| 87 | key = unicode(e.name) |
|---|
| 88 | if e.refinement: |
|---|
| 89 | key += u'.' + unicode(e.refinement) |
|---|
| 90 | dump += u'%s:\t%s\n' % (key, unicode(e.value)) |
|---|
| 91 | return dump |
|---|
| 92 | |
|---|
| 93 | |
|---|
| 94 | class Element(object): |
|---|
| 95 | "Represent a Dublin Core element" |
|---|
| 96 | |
|---|
| 97 | def __init__(self, name, value=None, refinement=None, related=None): |
|---|
| 98 | self.name = name |
|---|
| 99 | self.value = value |
|---|
| 100 | self.refinement = refinement |
|---|
| 101 | self.related = related |
|---|
| 102 | |
|---|
| 103 | def __eq__(self, other): |
|---|
| 104 | return self.name == other.name and self.value == other.value and self.refinement == self.refinement |
|---|
| 105 | |
|---|
| 106 | def __ne__(self, other): |
|---|
| 107 | return not (self == other) |
|---|
| 108 | |
|---|
| 109 | @staticmethod |
|---|
| 110 | def multiple(name, values, refinement=None): |
|---|
| 111 | elements = [] |
|---|
| 112 | if values: |
|---|
| 113 | for v in values: |
|---|
| 114 | elements.append(Element(name, v, refinement)) |
|---|
| 115 | return elements |
|---|
| 116 | |
|---|
| 117 | class Date(Element): |
|---|
| 118 | "Dublin Core date element formatted according to W3C-DTF or DCMI Period" |
|---|
| 119 | |
|---|
| 120 | def __init__(self, start, end=None, refinement=None): |
|---|
| 121 | value = '' |
|---|
| 122 | if start: |
|---|
| 123 | value = start |
|---|
| 124 | elif end: |
|---|
| 125 | value = end |
|---|
| 126 | else: |
|---|
| 127 | value = '' |
|---|
| 128 | if isinstance(value, long): |
|---|
| 129 | # start is a year |
|---|
| 130 | value = unicode(value) + '-01-01T00:00:00Z' |
|---|
| 131 | elif value: |
|---|
| 132 | value = value.strftime('%Y-%m-%dT%H:%M:%SZ') |
|---|
| 133 | |
|---|
| 134 | super(Date, self).__init__('date', value, refinement) |
|---|
| 135 | |
|---|
| 136 | def media_access_rights(media): |
|---|
| 137 | if media.public_access == 'full': |
|---|
| 138 | return 'public' |
|---|
| 139 | if media.public_access == 'metadata': |
|---|
| 140 | return 'restricted' |
|---|
| 141 | return 'private' |
|---|
| 142 | |
|---|
| 143 | def media_identifier(media): |
|---|
| 144 | sites = Site.objects.all() |
|---|
| 145 | domain = sites[0].domain |
|---|
| 146 | return 'http://' + domain + '/' + media.element_type + 's/' + unicode(media.id) |
|---|
| 147 | |
|---|
| 148 | def express_collection(collection): |
|---|
| 149 | "Express a collection as a Dublin Core resource" |
|---|
| 150 | |
|---|
| 151 | if collection.collector: |
|---|
| 152 | creator = (Element('creator', collection.collector), |
|---|
| 153 | Element('contributor', collection.creator)) |
|---|
| 154 | else: |
|---|
| 155 | creator = Element('creator', collection.creator) |
|---|
| 156 | |
|---|
| 157 | duration = max(collection.approx_duration, collection.computed_duration()) |
|---|
| 158 | parts = [] |
|---|
| 159 | for item in collection.items.all(): |
|---|
| 160 | id = media_identifier(item) |
|---|
| 161 | if id: |
|---|
| 162 | parts.append(Element('relation', id, 'hasPart', item)) |
|---|
| 163 | |
|---|
| 164 | resource = Resource( |
|---|
| 165 | Element('identifier', media_identifier(collection), related=collection), |
|---|
| 166 | Element('type', 'Collection'), |
|---|
| 167 | Element('title', collection.title), |
|---|
| 168 | Element('title', collection.alt_title), |
|---|
| 169 | creator, |
|---|
| 170 | Element('contributor', collection.metadata_author), |
|---|
| 171 | Element.multiple('subject', settings.TELEMETA_SUBJECTS), |
|---|
| 172 | Element('publisher', collection.publisher), |
|---|
| 173 | Element('publisher', settings.TELEMETA_ORGANIZATION), |
|---|
| 174 | Date(collection.recorded_from_year, collection.recorded_to_year, refinement='created'), |
|---|
| 175 | Date(collection.year_published, refinement='issued'), |
|---|
| 176 | Element('rights', collection.legal_rights, 'license'), |
|---|
| 177 | Element('rights', media_access_rights(collection), 'accessRights'), |
|---|
| 178 | Element('format', duration, 'extent'), |
|---|
| 179 | Element('format', collection.physical_format, 'medium'), |
|---|
| 180 | parts |
|---|
| 181 | ) |
|---|
| 182 | |
|---|
| 183 | return resource |
|---|
| 184 | |
|---|
| 185 | def express_item(item): |
|---|
| 186 | "Express a media item as a Dublin Core resource" |
|---|
| 187 | |
|---|
| 188 | if item.collector: |
|---|
| 189 | creator = (Element('creator', item.collector), |
|---|
| 190 | Element('contributor', item.collection.creator)) |
|---|
| 191 | elif item.collection.collector: |
|---|
| 192 | creator = (Element('creator', item.collection.collector), |
|---|
| 193 | Element('contributor', item.collection.creator)) |
|---|
| 194 | else: |
|---|
| 195 | creator = Element('creator', item.collection.creator) |
|---|
| 196 | |
|---|
| 197 | if item.recorded_from_date: |
|---|
| 198 | date = Date(item.recorded_from_date, item.recorded_to_date, refinement='created') |
|---|
| 199 | else: |
|---|
| 200 | date = Date(item.collection.recorded_from_year, item.collection.recorded_to_year, refinement='created'), |
|---|
| 201 | |
|---|
| 202 | if item.title: |
|---|
| 203 | title = item.title |
|---|
| 204 | else: |
|---|
| 205 | title = item.collection.title |
|---|
| 206 | if item.track: |
|---|
| 207 | title += u' - ' + item.track |
|---|
| 208 | |
|---|
| 209 | try: |
|---|
| 210 | analysis = MediaItemAnalysis(item=item, analyzer_id='mime_type') |
|---|
| 211 | mime_type = analysis.value |
|---|
| 212 | except: |
|---|
| 213 | mime_type = 'unknown' |
|---|
| 214 | |
|---|
| 215 | resource = Resource( |
|---|
| 216 | Element('identifier', media_identifier(item), related=item), |
|---|
| 217 | Element('type', 'Sound'), |
|---|
| 218 | Element('title', title), |
|---|
| 219 | Element('title', item.alt_title), |
|---|
| 220 | creator, |
|---|
| 221 | Element('contributor', item.collection.metadata_author), |
|---|
| 222 | Element.multiple('subject', settings.TELEMETA_SUBJECTS), |
|---|
| 223 | Element.multiple('subject', item.keywords()), |
|---|
| 224 | Element('description', item.context_comment, 'abstract'), |
|---|
| 225 | Element('publisher', item.collection.publisher), |
|---|
| 226 | Element('publisher', settings.TELEMETA_ORGANIZATION), |
|---|
| 227 | date, |
|---|
| 228 | Date(item.collection.year_published, refinement='issued'), |
|---|
| 229 | Element.multiple('coverage', item.location and item.location.fullnames(), 'spatial'), |
|---|
| 230 | Element('coverage', item.location_comment, 'spatial'), |
|---|
| 231 | Element('rights', item.collection.legal_rights, 'license'), |
|---|
| 232 | Element('rights', media_access_rights(item.collection), 'accessRights'), |
|---|
| 233 | Element('format', max(item.approx_duration, item.computed_duration()), 'extent'), |
|---|
| 234 | Element('format', item.collection.physical_format, 'medium'), |
|---|
| 235 | Element('format', mime_type, 'MIME type'), |
|---|
| 236 | Element('relation', media_identifier(item.collection), 'isPartOf', item.collection) |
|---|
| 237 | ) |
|---|
| 238 | |
|---|
| 239 | return resource |
|---|
| 240 | |
|---|
| 241 | def express_resource(res): |
|---|
| 242 | if isinstance(res, MediaItem): |
|---|
| 243 | return express_item(res) |
|---|
| 244 | elif isinstance(res, MediaCollection): |
|---|
| 245 | return express_collection(res) |
|---|
| 246 | |
|---|
| 247 | raise Exception("Invalid resource type") |
|---|
| 248 | |
|---|
| 249 | def lookup_resource(media_id): |
|---|
| 250 | try: |
|---|
| 251 | type, code = media_id.split(':', 1) |
|---|
| 252 | except ValueError: |
|---|
| 253 | raise MalformedMediaIdentifier("Media identifier must be in type:code format") |
|---|
| 254 | |
|---|
| 255 | if (type == 'collection'): |
|---|
| 256 | try: |
|---|
| 257 | return MediaCollection.objects.get(code=code) |
|---|
| 258 | except MediaCollection.DoesNotExist: |
|---|
| 259 | return None |
|---|
| 260 | elif (type == 'item'): |
|---|
| 261 | try: |
|---|
| 262 | return MediaItem.objects.get(code=code) |
|---|
| 263 | except MediaItem.DoesNotExist: |
|---|
| 264 | try: |
|---|
| 265 | return MediaItem.objects.get(old_code=code) |
|---|
| 266 | except MediaItem.DoesNotExist: |
|---|
| 267 | return None |
|---|
| 268 | else: |
|---|
| 269 | raise MalformedMediaIdentifier("No such type in media identifier: " + type) |
|---|
| 270 | |
|---|
| 271 | class MalformedMediaIdentifier(Exception): |
|---|
| 272 | pass |
|---|