"""Python 3.10+; pip install requests. Run: python integration.py manifest.json. Set MDG_BASE_URL (includes /api/v1), MDG_API_KEY and a durable MDG_INTEGRATION_ID. Never change the integration ID merely to retry an uncertain write. """ import hashlib import json import os from pathlib import Path import sys from urllib.parse import quote, urlsplit import requests BASE = os.environ.get('MDG_BASE_URL', '').rstrip('/') KEY = os.environ.get('MDG_API_KEY', '') RUN = os.environ.get('MDG_INTEGRATION_ID', '') def sha(value): return hashlib.sha256(value).hexdigest() def call(method, path, body=None, operation=None, extra=None): headers = {'Authorization': 'Bearer ' + KEY, **(extra or {})} if operation: material = json.dumps({'run': RUN, 'operation': operation, 'method': method, 'path': path, 'body': body}, sort_keys=True, separators=(',', ':'), ensure_ascii=False) headers['Idempotency-Key'] = 'mdg-example-' + sha(material.encode()) kwargs = {'json': body} if body is not None else {} response = requests.request(method, BASE + path, headers=headers, timeout=60, allow_redirects=False, **kwargs) result = response.json() if not 200 <= response.status_code < 300: raise RuntimeError('MDG {}: {} — {}; requestId={}. Preserve this integration ID and inspect uncertain writes.'.format(response.status_code, result.get('code'), result.get('message'), result.get('requestId'))) return result def upload(release_id, path, kind, track_id=None): source = Path(path) data = source.read_bytes() filename = ''.join(c if c.isascii() and (c.isalnum() or c in '._-') else '_' for c in source.name) content_type = 'audio/wav' if kind == 'audio' else {'.png': 'image/png', '.webp': 'image/webp'}.get(source.suffix.lower(), 'image/jpeg') metadata = {'releaseId': release_id, 'kind': kind, 'filename': filename, 'contentType': content_type, 'size': len(data), 'sha256': sha(data)} if track_id: metadata['trackId'] = track_id session = call('POST', '/uploads', metadata, 'upload:{}:{}'.format(kind, track_id or '')) headers = {'Authorization': 'Bearer ' + KEY, 'Upload-Token': session['token']} progress = call('GET', '/uploads/' + session['id'], extra={'Upload-Token': session['token']}) if progress['state'] == 'complete': return for index in range(session['partCount']): chunk = data[index * session['chunkSize']:(index + 1) * session['chunkSize']] if any(p['index'] == index and p['sha256'] == sha(chunk) for p in progress['received']): continue response = requests.put(BASE + '/uploads/{}/parts/{}'.format(session['id'], index), headers=headers, files={'file': ('part-' + str(index), chunk, 'application/octet-stream')}, timeout=60, allow_redirects=False) if not 200 <= response.status_code < 300: result = response.json() raise RuntimeError('Part upload failed: {}; requestId={}. Retry with the same integration ID.'.format(result.get('code'), result.get('requestId'))) call('POST', '/uploads/' + session['id'] + '/complete', {}, 'complete:{}:{}'.format(kind, track_id or ''), {'Upload-Token': session['token']}) def main(): if not BASE or not KEY or not RUN or len(sys.argv) != 2: raise RuntimeError('Set MDG_BASE_URL, MDG_API_KEY, MDG_INTEGRATION_ID and pass a real manifest JSON file.') url = urlsplit(BASE) if url.scheme != 'https' and not (url.scheme == 'http' and url.hostname in ('localhost', '127.0.0.1')): raise RuntimeError('HTTPS is required outside local development.') manifest = json.loads(Path(sys.argv[1]).read_text(encoding='utf-8')) identity = call('GET', '/identity') print('Connected to {}, partner {}.'.format(identity['environment'], identity['partnerId'])) artist_id = manifest.get('artistId') if not artist_id: artist = manifest['artist'] profiles = call('GET', '/artists?externalId=' + quote(artist['externalId'], safe='')) if not profiles['data'] and not profiles.get('onboarding'): call('POST', '/artists', artist, 'artist') profiles = call('GET', '/artists?externalId=' + quote(artist['externalId'], safe='')) if not profiles['data']: raise RuntimeError('Artist onboarding is pending. The artist must accept the MDG roster invitation; run again afterwards with the same integration ID.') artist_id = profiles['data'][0]['id'] draft = call('POST', '/releases', {**manifest['release'], 'artistId': artist_id}, 'release') release_id = draft['release']['_id'] for index, track in enumerate(manifest['tracks']): metadata = {k: v for k, v in track.items() if k != 'audioPath'} result = call('POST', '/releases/' + release_id + '/tracks', metadata, 'track:' + str(index)) created = next(t for t in result['release']['tracks'] if t['trackNumber'] == metadata['trackNumber']) current = call('GET', '/releases/' + release_id)['release'] current_track = next(t for t in current['tracks'] if t['trackId'] == created['trackId']) if not current_track.get('audioFile'): upload(release_id, track['audioPath'], 'audio', created['trackId']) current = call('GET', '/releases/' + release_id)['release'] if not current.get('artwork'): upload(release_id, manifest['artworkPath'], 'artwork') validation = call('POST', '/releases/' + release_id + '/validate', {}) blocking = [f for f in validation['findings'] if f['severity'] == 'error' and not f['code'].startswith(('fac.', 'producer.'))] if not validation['completion']['complete'] or blocking: print(json.dumps({'releaseId': release_id, 'completion': validation['completion'], 'findings': validation['findings']}, indent=2)) raise RuntimeError('Resolve findings through documented PATCH endpoints. The draft is retained.') call('POST', '/releases/' + release_id + '/submit', {}, 'submit') current = call('GET', '/releases/' + release_id)['release'] print(json.dumps({'releaseId': release_id, 'status': current['status'], 'reviewRequired': True}, indent=2)) if __name__ == '__main__': try: main() except Exception as error: print(str(error), file=sys.stderr) sys.exit(1)