| Stand | 2026-11-14 (nach #11–#20) |
|---|---|
| Bezug | 2026-11-14_newsletter, newsletter, edwiki_import |
Skripte, mit denen die Newsletter aus den Wayback-Kopien (HTML) zu Wiki-Seiten und Import-Paketen werden. Herunterladen: auf den Dateinamen über dem Code klicken.
newsletter_index.json (aus der Galnet-France-Liste).html2wiki.py <datei.html> <NNN> raw → raw/en_NNN.txt (Rohtext) + raw/images_NNN.json (Bilder in Reihenfolge, erstes = Kopfbild = Box-Bild).clean_en.py raw newsletter_index.json en → en/en_NNN.txt: Inhaltsverzeichnis als fette Zeile, Links auf andere Newsletter als …, Einzelkorrekturen in FIXES.de/de_NNN.txt – gleiche Bildzeilen und Links wie EN (wird geprüft), Begriffe nach glossar, Drabble-Titel englisch + deutsch in Klammern, Rubrik „Elite Fiction“ → „Elite-Fiction“.meta.json je Nummer: author, wayback, related (nur vorhandene Seiten), translation.build_newsletter.py . <von> <bis> → pakete/newsletter_paket_<von>-<bis>_tN.zip (≤ 3,7 MB, CSV im letzten Teil), dazu luecken_*.txt mit defekten Bildern.news:newsletter:NNN (DE, KI-Übersetzung mit Hinweis „ungeprüft“) und en:news:newsletter:NNN (Original). Medien: news:newsletter:NNN:<name> (Name ohne Wayback-Anhang, klein).&e=…), Werbe-Parameter (utm_…) und persönliche Anreden aus dem Seriendruck („Hi <Name>,“ → „Hi Commander,“) werden entfernt; Links auf die Mailchimp-Archivliste zeigen auf newsletter.newsletter: number, date (Anzeige d.m.Y), picture, author, url (Original), archive (Wayback), translation. Schiffe, Fraktionen, Konzerne, Systeme über relations.related.#!/usr/bin/env python3 """Mailchimp-Newsletter (Wayback-Kopie) -> DokuWiki-Rohtext (Englisch) + Bildliste. Aufruf: html2wiki.py <newsletter.html> <nr> <ausgabeordner> Schreibt <ausgabe>/en_<nr>.txt (Seitenrumpf ohne Rahmen) und <ausgabe>/images_<nr>.json """ import json import os import re import sys import urllib.parse from bs4 import BeautifulSoup, NavigableString, Tag SKIP_IMG = re.compile(r'(social|google|tumblr|like_|cdn-images|facebook|twitter)', re.I) WAYBACK = re.compile(r'^https?://web\.archive\.org/web/\d+[a-z_]*/', re.I) MEDIA_NS = 'news:newsletter:{nr}:' def clean_name(fname): """Wayback-Anhang entfernen, DokuWiki-taugliche Kleinschreibung.""" base, ext = os.path.splitext(fname) base = re.sub(r'_[A-Za-z0-9]{4}$', '', base) # _0oNk usw. base = re.sub(r'[^A-Za-z0-9]+', '_', base).strip('_').lower() ext = ext.lower().replace('jpeg', 'jpg') return base + ext def wrap(text, mk): """Formatierung je Absatz/Zeile, Leerraum außerhalb der Marker.""" parts = re.split(r'(\n+)', text) res = [] for p in parts: if not p.strip() or p.startswith('\n'): res.append(p) continue lead = p[:len(p) - len(p.lstrip())] trail = p[len(p.rstrip()):] res.append(lead + mk + p.strip() + mk + trail) return ''.join(res) def esc(text): """Sternchen-Folgen (z. B. Teaser 'the *****') vor DokuWiki schützen.""" return re.sub(r'(?<!\*)(\*{3,})(?!\*)', r'%%\1%%', text) def orig_url(href): href = WAYBACK.sub('', href or '') return href class Conv: def __init__(self, path, nr): self.path = path self.dir = os.path.dirname(path) self.nr = nr self.out = [] # Liste von Blöcken self.inline = [] # laufender Absatz self.images = [] self.seen = set() # ---------- Ausgabe def flush(self): txt = ''.join(self.inline) txt = re.sub(r'[ \t ]+', ' ', txt) txt = re.sub(r' *\n *', '\n', txt).strip() # leere Formatierungen entfernen prot = re.findall(r'%%.*?%%', txt) txt = re.sub(r'%%.*?%%', '\x00', txt) txt = txt.strip() for pr in prot: txt = txt.replace('\x00', pr, 1) if txt: for para in re.split(r'\n{2,}', txt): para = para.strip() if para: self.out.append(('p', para.replace('\n', ' \\\\ '))) self.inline = [] def add_img(self, img): src = urllib.parse.unquote(img.get('src', '')) if SKIP_IMG.search(src) or not src: return local = os.path.join(self.dir, src) if not os.path.exists(local): local = os.path.join(self.dir, os.path.basename(os.path.dirname(src)), os.path.basename(src)) name = clean_name(os.path.basename(src)) if name in self.seen: return self.seen.add(name) try: w = int(re.search(r'\d+', img.get('width') or '560').group()) except Exception: w = 560 link = img.parent.get('href') if img.parent and img.parent.name == 'a' else '' self.images.append({'name': name, 'local': local, 'width': w, 'orig': orig_url(link) if link and re.search(r'\.(jpe?g|png|gif)', link, re.I) else ''}) self.flush() self.out.append(('img', name, w)) # ---------- Durchlauf def walk(self, node, fmt=''): for ch in node.children: if isinstance(ch, NavigableString): if ch.__class__.__name__ == 'Comment': continue t = esc(str(ch).replace('\r', '').replace('\n', ' ')) self.inline.append(t) continue if not isinstance(ch, Tag): continue n = ch.name if n in ('script', 'style'): continue if n == 'img': self.add_img(ch) elif n == 'br': self.inline.append('\n') elif n in ('h1', 'h2', 'h3', 'h4'): self.flush() t = re.sub(r'\s+', ' ', ch.get_text()).strip() if t: self.out.append(('h', t)) for im in ch.find_all('img'): self.add_img(im) elif n in ('ul', 'ol'): self.flush() items = [] for li in ch.find_all('li', recursive=False): sub = Conv(self.path, self.nr) sub.seen = self.seen sub.walk(li) sub.flush() items.append(' '.join(b[1] for b in sub.out if b[0] == 'p')) self.images += sub.images self.out.append(('ul' if n == 'ul' else 'ol', items)) elif n in ('p', 'div', 'td', 'tr', 'table', 'tbody', 'blockquote', 'center'): self.flush() self.walk(ch) self.flush() elif n in ('strong', 'b', 'em', 'i'): mk = '**' if n in ('strong', 'b') else '//' inner = self.sub_inline(ch) self.inline.append(wrap(inner, mk)) elif n == 'a': href = ch.get('href', '') if ch.find('img'): for im in ch.find_all('img'): self.add_img(im) continue text = self.sub_inline(ch) if not href or href.startswith('#') or 'return to contents' in text.lower(): if 'return to contents' not in text.lower(): self.inline.append(text) continue url = orig_url(href) if url.startswith('mailto:'): self.inline.append(text) continue self.inline.append('[[%s|%s]]' % (url, text.strip()) if text.strip() else '') else: self.walk(ch) def sub_inline(self, node): sub = Conv(self.path, self.nr) sub.seen = self.seen sub.walk(node) self.images += sub.images paras = [] for b in sub.out: # Bilder in Formatierung if b[0] == 'img': self.flush() self.out.append(b) elif b[0] == 'p': paras.append(b[1].replace(' \\\\ ', '\n')) paras.append(''.join(sub.inline)) return re.sub(r'[ \t\u00a0]+', ' ', '\n\n'.join(x for x in paras if x.strip())) def main(): path, nr, outdir = sys.argv[1], sys.argv[2], sys.argv[3] html = open(path, encoding='utf-8', errors='replace').read() html = re.sub(r'<!-- BEGIN WAYBACK TOOLBAR INSERT -->.*?<!-- END WAYBACK TOOLBAR INSERT -->', '', html, flags=re.S) soup = BeautifulSoup(html, 'html.parser') body = soup.find(id='templateBody') or soup.body header = soup.find(id='templateHeader') c = Conv(path, nr) if header: for im in header.find_all('img'): c.add_img(im) c.walk(body) c.flush() # Ende abschneiden: "Follow Elite: Dangerous" und alles danach blocks = [] for b in c.out: if b[0] == 'h' and re.match(r'follow elite', b[1], re.I): break blocks.append(b) ns = MEDIA_NS.format(nr=nr) lines = [] # Kopfbild (mail_head / erstes Bild im Kopf) nicht in den Text, es ist das Box-Bild if blocks and blocks[0][0] == 'img': blocks = blocks[1:] grouped = [] for b in blocks: if b[0] == 'img' and b[2] < 500 and grouped and grouped[-1][0] == 'imgs': grouped[-1][1].append(b) elif b[0] == 'img' and b[2] < 500: grouped.append(('imgs', [b])) else: grouped.append(b) blocks = grouped for b in blocks: if b[0] == 'imgs': lines.append(' '.join('{{%s%s?%d|}}' % (ns, i[1], i[2]) for i in b[1]) + '\n') continue if b[0] == 'p': lines.append(b[1] + '\n') elif b[0] == 'h': lines.append('===== %s =====\n' % b[1]) elif b[0] == 'img': lines.append('{{%s%s?%d|}}\n' % (ns, b[1], min(b[2], 560))) elif b[0] in ('ul', 'ol'): mark = '*' if b[0] == 'ul' else '-' lines.append('\n'.join(' %s %s' % (mark, i) for i in b[1]) + '\n') os.makedirs(outdir, exist_ok=True) open(os.path.join(outdir, 'en_%s.txt' % nr), 'w').write('\n'.join(lines)) json.dump(c.images, open(os.path.join(outdir, 'images_%s.json' % nr), 'w'), indent=1) if __name__ == '__main__': main()
#!/usr/bin/env python3 """Rohtext (html2wiki) -> bereinigter englischer Seitenrumpf. - Inhaltsverzeichnis: Überschrift -> fette Zeile, ohne "Follow Elite: Dangerous", Banner bleibt - Links auf andere Newsletter (Mailchimp-IDs) -> interne Links - E-Mail-Adressen nicht fett, kleine Leerzeichen-/Satzzeichenfehler - gezielte Einzelkorrekturen (FIXES) Aufruf: clean_en.py <raw-ordner> <index.json> <ausgabe-ordner> """ import json import os import re import sys FIXES = { '015': [('**[[http://elite.frontier.co.uk/|http://elite.frontier.co.u]]k**', '**[[http://elite.frontier.co.uk/|http://elite.frontier.co.uk]]**'), ('**Engines**//: "When', '**Engines:** //"When')], '002': [('but**[[', 'but **[['), (']]**to check', ']]** to check')], '003': [('**[[http://i1305.photobucket.com/albums/s553/FrontierAshley/EmpireshipsketchesCropped_zpsc19947fb.jpg|smaller Imperial Ships]]**', '**[[NL:002|smaller Imperial Ships]]**')], '004': [('**[[http://s1305.photobucket.com/user/FrontierAshley/media/FedandEmpirefighters_zps35212663.jpg.html|early concept thumbnails]]**', '**[[NL:001|early concept thumbnails]]**'), ('\nShield Systems:\n', '\n**Shield Systems:**\n')], '007': [('&e=[UNIQID]', '')], '008': [(' \\\\ --- \\\\ This week’s drabble', '\n\n===== Elite Fiction: Childish Dreams =====\n\nThis week’s drabble'), ('**[[http://www.laveradio.com/|their site]]**now', '**[[http://www.laveradio.com/|their site]]** now')], '009': [('**“**"We don\'t need the whole universe, just the Milky Way." Where to start?', '“We don\'t need the whole universe, just the Milky Way.” Where to start?'), ('ring system(s). **”**', 'ring system(s).'), ('**[[http://elite.frontier.co.uk/images/newsletter_2013_07_05/schematic_flat_empfigher.png|Empire]][[http://elite.frontier.co.uk/images/newsletter_2013_07_05/schematic_flat_empfigher.png|Fighter]]**', '**[[http://elite.frontier.co.uk/images/newsletter_2013_07_05/schematic_flat_empfigher.png|Empire Fighter]]**'), ('\n- Elsewhere in the Elite galaxy', '\nElsewhere in the Elite galaxy')], } def main(): rawdir, index, outdir = sys.argv[1:4] idx = json.load(open(index)) by_id = {} for r in idx: m = re.search(r'id=([0-9a-f]{10})', r['url']) if m and re.fullmatch(r'\d+', r['nr']): by_id.setdefault(m.group(1), '%03d' % int(r['nr'])) os.makedirs(outdir, exist_ok=True) for fn in sorted(os.listdir(rawdir)): m = re.match(r'en_(\d{3})\.txt$', fn) if not m: continue nr = m.group(1) t = open(os.path.join(rawdir, fn)).read() # Inhaltsverzeichnis t = re.sub(r'===== Table of Contents[^=]*=====', '**Contents:**', t) t = re.sub(r'\n \* (\*\*)?Follow Elite: Dangerous(\*\*)?\n', '\n', t) # persönliche Anrede aus dem Mailchimp-Seriendruck (Name des Abonnenten) neutralisieren t = re.sub(r'^Hi (?!Commander)[^,\n]{1,40},', 'Hi Commander,', t) t = re.sub(r'^===== return to contents =====\n', '', t, flags=re.M | re.I) t = re.sub(r'^\{\{[^}]*transparent[^}]*\}\}\n', '', t, flags=re.M) # Platzhalter-GIF # Werbe-Parameter (utm_…) aus Links entfernen t = re.sub(r'[?&]utm_[^|\]]*?(?=[|\]])', '', t) # Einzelkorrekturen for a, b in FIXES.get(nr, []): assert a in t, (nr, a[:60]) t = t.replace(a, b) # Links auf andere Newsletter def nl_link(mm): url, text = mm.group(1), mm.group(2) i = re.search(r'id=([0-9a-f]{10})', url) if i and i.group(1) in by_id: target = by_id[i.group(1)] if target != nr: return '[[NL:%s|%s]]' % (target, text) return mm.group(0) t = re.sub(r'\[\[https?://us2\.campaign-archive\d?\.com/home/[^|\]]*\|', '[[NL:start|', t) t = re.sub(r'\[\[(https?://us2\.campaign-archive[^|\]]*)\|([^\]]*)\]\]', nl_link, t) # Fettung um Links mit eigener Fettung: [[url|**x**]] -> **[[url|x]]** t = re.sub(r'\[\[([^|\]]+)\|\*\*([^\]]*?)\*\*\]\]', r'**[[\1|\2]]**', t) # E-Mail-Adresse nicht fett t = re.sub(r'\*\*([\w.]+@frontier\.co\.uk)\*\*', r'\1', t) t = re.sub(r' +([.,!?])', r'\1', t) t = re.sub(r'\]\]\*\*(?=[A-Za-z])', ']]** ', t) t = re.sub(r'(?<=[A-Za-z])\*\*\[\[', ' **[[', t) t = re.sub(r'^If you do not wish to receive any more.*\n', '', t, flags=re.M) t = re.sub(r'\n{3,}', '\n\n', t).strip() + '\n' open(os.path.join(outdir, 'en_%s.txt' % nr), 'w').write(t) if __name__ == '__main__': main()
#!/usr/bin/env python3 """Newsletter-Paket für edwiki-import.php bauen. Eingaben (Arbeitsordner): en/en_NNN.txt bereinigter englischer Rumpf (clean_en.py) de/de_NNN.txt deutsche Übersetzung (Claude) raw/images_NNN.json Bildliste (html2wiki.py) newsletter_index.json Nummer, Datum, Original-Link (galnet.fr-Liste) meta.json je Nummer: author, wayback, related, translation Ausgabe: build/pages/..., build/media/..., build/newsletter_NNN-NNN.csv, pakete/newsletter_paket_<von>-<bis>_tN.zip (je <= 3,7 MB, CSV im letzten Teil) Aufruf: build_newsletter.py <arbeitsordner> <von> <bis> """ import csv import io import json import os import re import shutil import sys import zipfile from PIL import Image MAXPX = 1920 ZIPMAX = 3_700_000 KI_NOTE = '//Übersetzung aus dem Englischen: Claude, ungeprüft.//' LANGS = {'de': 'news:newsletter', 'en': 'en:news:newsletter'} def frame(title, body, footer): return f"""[<>] <WRAP centeralign> ====== {title} ====== </WRAP> ---- <WRAP group> <WRAP twothirds column box round> {body.strip()} {footer} </WRAP> <WRAP third column box round> {{{{structbox>newsletter}}}} </WRAP> </WRAP> ---- [<>] """ def de_date(iso): y, m, d = iso.split('-') return f'{d}.{m}.{y}' def links(text, lang): ns = LANGS[lang] text = text.replace('[[NL:start|', '[[news:newsletter:start|') # Übersicht gibt es nur auf Deutsch return re.sub(r'\[\[NL:(\d{3})\|', lambda m: f'[[{ns}:{m.group(1)}|', text) def save_image(src, dst): try: Image.open(src).verify() except Exception: return 'FEHLT (Wayback-Kopie defekt)' os.makedirs(os.path.dirname(dst), exist_ok=True) ext = os.path.splitext(dst)[1] if ext == '.jpg': im = Image.open(src) if max(im.size) > MAXPX: im.thumbnail((MAXPX, MAXPX), Image.LANCZOS) im.convert('RGB').save(dst, 'JPEG', quality=88, optimize=True) return 'verkleinert' shutil.copyfile(src, dst) return '' def main(): work, a, b = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]) idx = {r['nr']: r for r in json.load(open(os.path.join(work, 'newsletter_index.json')))} meta = json.load(open(os.path.join(work, 'meta.json'))) build = os.path.join(work, 'build') shutil.rmtree(build, ignore_errors=True) rows, report, gaps = [], [], [] for n in range(a, b + 1): nr = '%03d' % n info, m = idx[str(n)], meta[nr] date = info['date'] title = f'Elite: Dangerous Newsletter #{n}' src = f'//Quelle: Frontier Developments, {title}, {de_date(date)}//' de = links(open(os.path.join(work, 'de', f'de_{nr}.txt')).read(), 'de') en = links(open(os.path.join(work, 'en', f'en_{nr}.txt')).read(), 'en') pages = { f'pages/news/newsletter/{nr}.txt': frame(title, de, KI_NOTE + ' \\\\\n' + src), f'pages/en/news/newsletter/{nr}.txt': frame(title, en, f'//Source: Frontier Developments, {title}, {date}//'), } for p, t in pages.items(): os.makedirs(os.path.dirname(os.path.join(build, p)), exist_ok=True) open(os.path.join(build, p), 'w').write(t) # Bilder: alle, die im Text vorkommen + Kopfbild (Box) used = set(re.findall(r'\{\{news:newsletter:%s:([^?|}]+)' % nr, de)) imgs = json.load(open(os.path.join(work, 'raw', f'images_{nr}.json'))) head = imgs[0]['name'] if m.get('header_local'): # Wayback hat ein falsches Kopfbild geliefert (gleicher Dateiname bei Mailchimp) imgs[0]['local'] = m['header_local'] report.append(f'#{n} Kopfbild ersetzt durch {os.path.basename(m["header_local"])}') for im in imgs: if im['name'] in used or im['name'] == head: note = save_image(im['local'], os.path.join(build, 'media/news/newsletter', nr, im['name'])) if note.startswith('FEHLT'): gaps.append(f'#{n}\tnews:newsletter:{nr}:{im["name"]}\t{im["orig"] or "-"}') elif note: report.append(f'#{n} {im["name"]}: {note} auf {MAXPX} px') missing = used - {i['name'] for i in imgs} if missing: report.append(f'#{n} FEHLENDE BILDER: {sorted(missing)}') rows.append({ 'pid': f'news:newsletter:{nr}', 'number': str(n), 'date': date, 'picture': f'news:newsletter:{nr}:{head}', 'author': m['author'], 'url': re.sub(r'&e=[^&]*', '', info['url']), # Abonnenten-Kennung entfernen 'archive': m['wayback'], 'translation': m.get('translation', 'KI'), 'relations.related': ','.join(m.get('related', [])), }) # feste Seiten (Übersicht, Vorlage, news:start, Sidebar): static/pages/... 1:1 übernehmen shutil.copytree(os.path.join(work, 'static', 'pages'), os.path.join(build, 'pages'), dirs_exist_ok=True) if gaps: open(os.path.join(build, f'luecken_newsletter_{a:03d}-{b:03d}.txt'), 'w').write( 'Nr\tfehlendes Bild (Medien-ID)\tOriginal (über Wayback suchen)\n' + '\n'.join(gaps) + '\n') report.append(f'{len(gaps)} Bilder fehlen, siehe luecken_newsletter_{a:03d}-{b:03d}.txt') csvname = f'newsletter_{a:03d}-{b:03d}.csv' with open(os.path.join(build, csvname), 'w', newline='') as fh: w = csv.DictWriter(fh, fieldnames=list(rows[0])) w.writeheader() w.writerows(rows) # Pakete files = [] for root, _, fs in os.walk(build): for f in sorted(fs): full = os.path.join(root, f) rel = os.path.relpath(full, build) if rel.endswith('.csv'): continue files.append((rel, full)) files.sort() outdir = os.path.join(work, 'pakete') shutil.rmtree(outdir, ignore_errors=True) os.makedirs(outdir) parts, cur, size = [], [], 0 for rel, full in files: s = os.path.getsize(full) + 200 + len(rel) * 2 if cur and size + s > ZIPMAX: parts.append(cur) cur, size = [], 0 cur.append((rel, full)) size += s cur.append((csvname, os.path.join(build, csvname))) parts.append(cur) for i, part in enumerate(parts, 1): zn = os.path.join(outdir, f'newsletter_paket_{a:03d}-{b:03d}_t{i}.zip') with zipfile.ZipFile(zn, 'w', zipfile.ZIP_DEFLATED) as z: for rel, full in part: z.write(full, rel) report.append(f'{os.path.basename(zn)}: {len(part)} Dateien, {os.path.getsize(zn) / 1e6:.2f} MB') print('\n'.join(report)) if __name__ == '__main__': main()
{
"001": {
"author": "Michael Brookes",
"wayback": "https://web.archive.org/web/20190316200753/https://us2.campaign-archive.com/?u=dcbf6b86b4b0c7d1c21b73b1e&id=bafc8227dd",
"related": [
"hangar:ships:sidewinder",
"codex:federal:start",
"codex:imperial:start"
],
"translation": "KI"
},
"002": {
"author": "Ashley Barley",
"wayback": "https://web.archive.org/web/20150408094846/http://us2.campaign-archive2.com/?u=dcbf6b86b4b0c7d1c21b73b1e&id=b5519ec0fe",
"related": [
"hangar:ships:sidewinder",
"hangar:ships:anaconda",
"codex:imperial:start",
"codex:federal:start",
"galaxy:systems:lave"
],
"translation": "KI"
},
"003": {
"author": "Ashley Barley",
"wayback": "https://web.archive.org/web/20170901205441/http://us2.campaign-archive1.com/?u=dcbf6b86b4b0c7d1c21b73b1e&id=7b8136bd4a",
"related": [
"hangar:ships:sidewinder",
"codex:imperial:start",
"codex:federal:start"
],
"translation": "KI"
},
"004": {
"author": "Ashley Barley",
"wayback": "https://web.archive.org/web/20201112042442/https://us2.campaign-archive.com/?u=dcbf6b86b4b0c7d1c21b73b1e&id=d266df1c3b",
"related": [
"hangar:ships:sidewinder",
"hangar:ships:cobra_mk3",
"codex:federal:start",
"galaxy:systems:lave"
],
"translation": "KI"
},
"005": {
"author": "Ashley Barley",
"wayback": "https://web.archive.org/web/20151104160412/http://us2.campaign-archive1.com/?u=dcbf6b86b4b0c7d1c21b73b1e&id=a4bd777a7f",
"related": [
"codex:imperial:start",
"codex:federal:start",
"codex:corporations:lakon_spaceways",
"codex:corporations:saud_kruger",
"codex:corporations:gutamaya",
"codex:corporations:core_dynamics"
],
"translation": "KI"
},
"006": {
"author": "Ashley Barley",
"wayback": "https://web.archive.org/web/20151104144759/http://us2.campaign-archive1.com/?u=dcbf6b86b4b0c7d1c21b73b1e&id=94dad3b09c",
"related": [
"hangar:ships:eagle",
"hangar:ships:adder",
"hangar:ships:asp_explorer"
],
"translation": "KI"
},
"007": {
"author": "Ashley Barley",
"wayback": "https://web.archive.org/web/20151104144817/http://us2.campaign-archive1.com/?u=dcbf6b86b4b0c7d1c21b73b1e&id=c0b11fe947",
"related": [
"hangar:ships:imperial_courier",
"hangar:ships:imperial_cutter",
"codex:imperial:start"
],
"translation": "KI"
},
"008": {
"author": "Ashley Barley",
"wayback": "https://web.archive.org/web/20150130055157/http://us2.campaign-archive1.com/?u=dcbf6b86b4b0c7d1c21b73b1e&id=b3760cf65e",
"related": [
"hangar:ships:sidewinder",
"hangar:ships:anaconda",
"codex:federal:start",
"codex:imperial:start",
"codex:alliance:start"
],
"translation": "KI"
},
"009": {
"author": "Ashley Barley",
"wayback": "https://web.archive.org/web/20140901000640/http://us2.campaign-archive2.com/?u=dcbf6b86b4b0c7d1c21b73b1e&id=ffe2385d13",
"related": [
"codex:federal:start",
"codex:imperial:start",
"codex:alliance:start",
"codex:misc:pilotenvereinigung",
"galaxy:systems:sol",
"galaxy:systems:achenar",
"galaxy:systems:alioth"
],
"translation": "KI"
},
"010": {
"author": "Ashley Barley",
"wayback": "https://web.archive.org/web/20140901000639/http://us2.campaign-archive2.com/?u=dcbf6b86b4b0c7d1c21b73b1e&id=ee59a3bb5e",
"related": [
"hangar:ships:dolphin",
"hangar:ships:orca",
"codex:corporations:gutamaya"
],
"translation": "KI"
},
"011": {
"author": "Ashley Barley",
"wayback": "https://web.archive.org/web/20140723125234/http://us2.campaign-archive1.com/?u=dcbf6b86b4b0c7d1c21b73b1e&id=7157d0e3e1",
"related": [
"hangar:ships:cobra_mk3",
"codex:federal:start"
],
"translation": "KI"
},
"012": {
"author": "",
"wayback": "https://web.archive.org/web/20190316185013/https://us2.campaign-archive.com/?u=dcbf6b86b4b0c7d1c21b73b1e&id=d6ef3ab726",
"related": [],
"translation": "KI",
"header_local": "/home/claude/nl2_in/Elite_Dangerous Newsletter #017_files/mail_head_copy_jWHE.JPG",
"header_note": "Wayback-Kopien von #12 und #17 haben die Kopfbilder vertauscht (Mailchimp-Dateiname mail_head_copy.jpg doppelt)"
},
"013": {
"author": "",
"wayback": "https://web.archive.org/web/20150130220838/http://us2.campaign-archive2.com/?u=dcbf6b86b4b0c7d1c21b73b1e&id=94e38570c6",
"related": [],
"translation": "KI"
},
"014": {
"author": "",
"wayback": "https://web.archive.org/web/20140901000640/http://us2.campaign-archive2.com/?u=dcbf6b86b4b0c7d1c21b73b1e&id=f703580f39",
"related": [
"hangar:ships:sidewinder",
"codex:corporations:remlok"
],
"translation": "KI"
},
"015": {
"author": "",
"wayback": "https://web.archive.org/web/20140901000556/http://us2.campaign-archive1.com/?u=dcbf6b86b4b0c7d1c21b73b1e&id=4075b302f4",
"related": [
"hangar:ships:sidewinder",
"hangar:ships:anaconda",
"hangar:ships:cobra_mk3"
],
"translation": "KI"
},
"016": {
"author": "",
"wayback": "https://web.archive.org/web/20170901201148/http://us2.campaign-archive1.com/?u=dcbf6b86b4b0c7d1c21b73b1e&id=7d88ceec82",
"related": [
"hangar:ships:anaconda",
"hangar:ships:sidewinder",
"codex:federal:start"
],
"translation": "KI"
},
"017": {
"author": "Michael Brookes, Mark Boss",
"wayback": "https://web.archive.org/web/20150131230822/http://us2.campaign-archive1.com/?u=dcbf6b86b4b0c7d1c21b73b1e&id=f9f6f99d01",
"related": [
"codex:corporations:sirius_corporation"
],
"translation": "KI",
"header_local": "/home/claude/nl2_in/Elite_Dangerous Newsletter #012_files/mail_head_copy_vJtR.jpg",
"header_note": "Wayback-Kopien von #12 und #17 haben die Kopfbilder vertauscht (Mailchimp-Dateiname mail_head_copy.jpg doppelt)"
},
"018": {
"author": "",
"wayback": "https://web.archive.org/web/20140806215529/http://us2.campaign-archive2.com/?u=dcbf6b86b4b0c7d1c21b73b1e&id=89b597852e",
"related": [
"hangar:ships:cobra_mk3",
"hangar:ships:sidewinder",
"hangar:ships:python",
"codex:corporations:faulcon_delacy"
],
"translation": "KI"
},
"019": {
"author": "Frontier-Team",
"wayback": "https://web.archive.org/web/20140901000600/http://us2.campaign-archive1.com/?u=dcbf6b86b4b0c7d1c21b73b1e&id=d6f301a16e",
"related": [
"hangar:ships:cobra_mk3",
"hangar:ships:eagle",
"hangar:ships:sidewinder",
"codex:misc:pilotenvereinigung",
"galaxy:systems:sol",
"galaxy:systems:achenar",
"galaxy:systems:alioth"
],
"translation": "KI"
},
"020": {
"author": "Frontier-Team",
"wayback": "https://web.archive.org/web/20140901000638/http://us2.campaign-archive2.com/?u=dcbf6b86b4b0c7d1c21b73b1e&id=ee05d74c71",
"related": [
"hangar:ships:imperial_courier",
"hangar:ships:sidewinder",
"codex:federal:start",
"codex:imperial:start",
"codex:alliance:start"
],
"translation": "KI"
}
}