diff --git a/archetypes/default.md b/archetypes/default.md
new file mode 100644
index 0000000..00e77bd
--- /dev/null
+++ b/archetypes/default.md
@@ -0,0 +1,6 @@
+---
+title: "{{ replace .Name "-" " " | title }}"
+date: {{ .Date }}
+draft: true
+---
+
diff --git a/archetypes/post.md b/archetypes/post.md
new file mode 100644
index 0000000..32c599e
--- /dev/null
+++ b/archetypes/post.md
@@ -0,0 +1,75 @@
+---
+
+# Set the correct title here
+title: "2003-11-02"
+
+# Date of the event, will be set via script. Format like "2003-12-31"
+date: 2003-11-02
+
+# Set the correct sports kind here (single value)
+# It's taxonomy term: look at existing posts, to find the valid values
+sports: "MTB"
+
+# Set the correct event type here (single value)
+# It's taxonomy term: look at existing posts, to find the valid values
+eventtypes: "single"
+
+# Set the correct participants here (list values)
+# It's taxonomy term: lLook at existing posts, to find the valid values.
+# For new participants set "FirstName LastName"
+# Unknown names have to be set as "Gast"
+members: [
+ "Peter",
+ "Gregor",
+ "Edmund",
+ "Gerald",
+ "Christian"
+ ]
+
+# City name of start point
+# It's taxonomy term: look at existing posts, to find the valid values
+# If it's a new location: Take a simple city name
+locations: "Somewhere"
+
+# false to hide it in production
+draft: false
+
+# If one of the following values are not given, delete the default value
+# Set the correct value here, Example 78.3
+distance_km: 0.0
+# Set the correct value here, Example 3:58:59
+duration_h: 0:00:00
+# Set the correct value here, Example 23.2
+average_speed_kmh: 0
+# Set the correct value here, Example 1234
+ascent_m: 0
+# Set the correct value here, Example 24.2
+temperature_c:
+
+# All image paths are relative paths and have to start with "images/"
+
+# Image for the post's header e.g. header_image: images/img123.jpg. Can be empty.
+header_image:
+
+# Image for the summary list e.g. featured_image: images/img123.jpg. Can be empty.
+featured_image:
+
+# Set captions for specific images (optional)
+# A caption item has two entries: -name: "images/IMAGE_NAME" and -text: "YOUR DESCRIPTION"
+# Caption names will be generated by the script, add text or let it empty.
+captions:
+
+# Should not be changed
+# Be careful: src value must be unique
+resources:
+ - src: images/**
+
+# Links to activity on social platforms
+# Example velohero_activity: https://app.velohero.com/activity/364363
+# velohero_activity:
+# strava_activity:
+
+---
+
+
+
diff --git a/redaktion/.gitignore b/redaktion/.gitignore
new file mode 100644
index 0000000..2c66778
--- /dev/null
+++ b/redaktion/.gitignore
@@ -0,0 +1,6 @@
+in/
+.idea/
+.settings
+.venv/lib/python3.8/site-packages/
+.venv/lib64/python3.8/site-packages/
+.venv
diff --git a/redaktion/assets/logo-inkscape.svg b/redaktion/assets/logo-inkscape.svg
new file mode 100644
index 0000000..459d784
--- /dev/null
+++ b/redaktion/assets/logo-inkscape.svg
@@ -0,0 +1,163 @@
+
+
+
+
diff --git a/redaktion/assets/logo.jpg b/redaktion/assets/logo.jpg
new file mode 100644
index 0000000..fc43bb2
Binary files /dev/null and b/redaktion/assets/logo.jpg differ
diff --git a/redaktion/assets/logo.png b/redaktion/assets/logo.png
new file mode 100644
index 0000000..1039096
Binary files /dev/null and b/redaktion/assets/logo.png differ
diff --git a/redaktion/assets/logo40-dark.png b/redaktion/assets/logo40-dark.png
new file mode 100644
index 0000000..4755efa
Binary files /dev/null and b/redaktion/assets/logo40-dark.png differ
diff --git a/redaktion/assets/logo40-t.png b/redaktion/assets/logo40-t.png
new file mode 100644
index 0000000..db9dcbd
Binary files /dev/null and b/redaktion/assets/logo40-t.png differ
diff --git a/redaktion/assets/logo40.png b/redaktion/assets/logo40.png
new file mode 100644
index 0000000..c75a0bd
Binary files /dev/null and b/redaktion/assets/logo40.png differ
diff --git a/redaktion/assets/stravainkscape.svg b/redaktion/assets/stravainkscape.svg
new file mode 100644
index 0000000..2324a72
--- /dev/null
+++ b/redaktion/assets/stravainkscape.svg
@@ -0,0 +1,114 @@
+
+
diff --git a/redaktion/backup.sh b/redaktion/backup.sh
new file mode 100755
index 0000000..b42cb1a
--- /dev/null
+++ b/redaktion/backup.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+rsync -avz --delete --exclude kollegenrunde/.git --exclude kollegenrunde/public/ --exclude *swp /home/chris/hugo /media/diskstation/externalAccess/rsyncs
diff --git a/redaktion/load.sh b/redaktion/load.sh
new file mode 100755
index 0000000..38c6e5f
--- /dev/null
+++ b/redaktion/load.sh
@@ -0,0 +1,77 @@
+#!/bin/bash
+
+ROOT_DST="/home/chris/hugo/kollegenrunde/redaktion/in/posts"
+
+# case in-sensitive matching
+shopt -s nocaseglob
+shopt -s nullglob
+
+if [ "$1" == '-h' ]
+ then
+ echo "Copy all images "
+ echo " from DIR"
+ echo " to $ROOT_DST/"
+ echo "DIR must be a path to a DATE base directory like 2006010213_STAUFEN"
+ echo "Destination directory will be deleted an recreated"
+ echo "Usage:"
+ echo " load DIR"
+ echo ""
+ echo " DATE must be an existing directory"
+ echo " Images will be copied into $ROOT_DST/"
+ echo " For instance: load /mnt/pict/20200912_STAUFEN"
+ exit 0
+fi
+
+if [ -z "$1" ]
+ then echo "Missing source directory, e. g. call 'load /mnt/pict/20200912_STAUFEN'"
+ exit 1
+fi
+
+DIR=$(basename "$1")
+#echo dir=$DIR
+
+DATE=${DIR:0:8}
+#echo date=$DATE
+
+if [ ! -e "$1" ]
+ then echo "Directory '$1' not found !"
+ exit 1
+fi
+
+if [ ! -e $ROOT_DST ]
+ then echo "Destination root directory '$ROOT_DST' not found !"
+ exit 1
+fi
+
+DST="$ROOT_DST/$DATE"
+
+if [ ! -e $DST ]
+ then echo "Create $DST"
+ mkdir "$DST"
+fi
+
+echo "Processing $DATE..."
+
+if [ -e "$DST" ]
+ then
+ echo "Delete $DST"
+ rm -r "$DST"
+fi
+
+echo "Create $DST"
+mkdir "$DST"
+
+# It's tricky to copy dirs with whitespaces
+shopt -s extglob # turn on extended globbing
+SAVEIFS=$IFS
+IFS=$(echo -en "\n\b")
+for f in $1?(*.jpg|*.jpeg|*.png)
+do
+ echo "$f"
+ cp -v "$f" "$DST"/
+done
+IFS=$SAVEIFS
+
+
+
+
diff --git a/redaktion/publish.sh b/redaktion/publish.sh
new file mode 100755
index 0000000..1c4068f
--- /dev/null
+++ b/redaktion/publish.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+echo Publish to Uberspace...
+
+rsync -avv --delete /home/chris/hugo/kollegenrunde/public/ kollegen@despina.uberspace.de:html/kollegenrunde
diff --git a/redaktion/red.py b/redaktion/red.py
new file mode 100644
index 0000000..2e25e8e
--- /dev/null
+++ b/redaktion/red.py
@@ -0,0 +1,597 @@
+import argparse
+import re
+import json
+import sys
+from os import path, mkdir, remove, listdir
+from shutil import copyfile, rmtree
+from PIL import Image
+
+import yaml
+
+try:
+ from yaml import CLoader as Loader, CDumper as Dumper
+except ImportError:
+ from yaml import Loader, Dumper
+
+args = None
+log_switch = False
+
+post_file_name = "index.md"
+post_file_backup_name = "~index.md"
+post_file_archetype = path.join("..", "archetypes", "post.md")
+settings_path = ".settings"
+
+
+def execute_info():
+
+ if not path.exists(settings_path):
+ print("No actual post.")
+ return
+
+ print(f"Actual post is: '{settings_get_date()}'")
+
+ exit_invalid_initialization()
+
+
+def execute_update():
+ exit_invalid_initialization()
+
+ yml = yaml.load(get_frontmatters(), Loader=Loader)
+
+ previous_images = settings_get_images()
+
+ convert(get_image_path(yml))
+
+ remove_previous_images(previous_images, yml)
+
+ yml = cleanup_zombies(yml)
+
+ yml = merge_images(yml)
+
+ update_frontmatters(yml)
+
+ print("Done.")
+
+
+def remove_previous_images(previous_images, yml):
+ converted_images = settings_get_images()
+
+ for image in [i for i in previous_images if i not in converted_images]:
+ image_path = path.join(get_image_path(yml), image)
+ log(f"Removing previous image '{image}'")
+ remove(image_path)
+
+
+def init_date():
+
+ yml = yaml.load(get_frontmatters(), Loader=Loader)
+
+ yml = set_date(yml)
+
+ lines = get_frontmatters()
+
+ lines = update_date(yml, lines)
+
+ content = get_content()
+ with open(get_post_file_path(), "w") as f:
+ f.write("---\n")
+ f.write(lines)
+ f.write("---\n")
+ f.write(content)
+
+
+def init_title():
+
+ yml = yaml.load(get_frontmatters(), Loader=Loader)
+
+ yml = set_title(yml)
+
+ lines = get_frontmatters()
+
+ lines = update_title(yml, lines)
+
+ content = get_content()
+ with open(get_post_file_path(), "w") as f:
+ f.write("---\n")
+ f.write(lines)
+ f.write("---\n")
+ f.write(content)
+
+
+def update_frontmatters(yml):
+ copyfile(get_post_file_path(), get_post_backup_file_path())
+
+ lines = get_frontmatters()
+
+ lines = update_title(yml, lines)
+ lines = update_date(yml, lines)
+ lines = update_header_image(yml, lines)
+ lines = update_featured_image(yml, lines)
+ lines = update_captions(yml, lines)
+
+ content = get_content()
+ with open(get_post_file_path(), "w") as f:
+ f.write("---\n")
+ f.write(lines)
+ f.write("---\n")
+ f.write(content)
+
+
+def set_title(yml):
+ yml['title'] = "Tour " + settings_get_date()[0:4] + "-" + settings_get_date()[4:6] + "-" + settings_get_date()[6:8]
+
+ return yml
+
+
+def set_date(yml):
+ yml['date'] = settings_get_date()[0:4] + "-" + settings_get_date()[4:6] + "-" + settings_get_date()[6:8]
+
+ return yml
+
+
+def get_image_path(yml):
+ return path.join(get_out_path(), get_image_resource_path(yml))
+
+
+def get_image_resource_path(yml):
+ items = [i for i in yml['resources'] if 'src' in i]
+
+ ret = ""
+
+ if len(items) > 0:
+ path = items[0]['src']
+ # Should never happens
+ else:
+ path = ''
+
+
+ reg = re.match("(.*)/\*\*", path)
+ if reg is not None and len(reg.groups()) >= 1:
+ ret = reg.group(1)
+
+ # log(f"get_image_path={ret}")
+
+ return ret
+
+
+def merge_images(yml):
+ files = get_image_resources(yml)
+
+ if len(files) == 0:
+ return yml
+
+ key = 'header_image'
+ if key in yml and yml[key] is None:
+ yml[key] = files[0]
+
+ key = 'featured_image'
+ if key in yml and yml[key] is None:
+ yml[key] = files[0]
+
+ # log(f"merge_images {yml}")
+
+ key = 'captions'
+ key2 = 'name'
+ if key in yml:
+ if yml[key] is None:
+ yml[key] = []
+ for file in files:
+ hit = False
+ for item in yml[key]:
+ if key2 in item and item[key2] == file:
+ hit = True
+ break
+ if not hit:
+ # log(f"merge_images added {file}")
+ yml[key].append(dict(name=file, text=""))
+
+ yml[key] = sorted(yml[key], key=lambda k: k['name'])
+
+ # log(f"merge_images {yml}")
+ return yml
+
+
+def cleanup_zombies(yml):
+ files = get_image_resources(yml)
+
+ key = 'header_image'
+ if key in yml:
+ if yml[key] is not None and not yml[key] in files:
+ yml[key] = ""
+ else:
+ yml[key] = ""
+
+ key = 'featured_image'
+ if key in yml:
+ if yml[key] is not None and not yml[key] in files:
+ yml[key] = ""
+ else:
+ yml[key] = ""
+
+ # log(f"cleanup_zombies={yml}")
+
+ key = 'captions'
+ if key in yml:
+ if yml[key] is not None:
+ key2 = 'name'
+ # log(f"cleanup_zombies={yml[key]}")
+ items = yml[key].copy()
+ for item in items:
+ if not item[key2] in files:
+ # log(f"cleanup_zombies remove item {item[key2]} ")
+ yml[key].remove(item)
+ else:
+ yml[key] = []
+
+ # log(f"cleanup_zombies ret = {yml}")
+ return yml
+
+
+def get_image_resources(yml):
+ files = get_images(get_image_path(yml))
+ sub = get_image_resource_path(yml)
+
+ ret = []
+ for name in files:
+ ret.append(path.join(sub, name))
+
+ return ret
+
+
+def get_images(dir):
+ ret = [f for f in listdir(dir) if f.lower().endswith(".jpg")
+ or f.lower().endswith(".jpeg") or f.lower().endswith(".png")]
+
+ ret.sort()
+
+ return ret
+
+
+def convert(image_path):
+ names = []
+ for f in get_images(get_in_path()):
+
+ in_path = path.join(get_in_path(), f)
+ out_path = path.join(image_path, f)
+
+ log(f"Converting '{in_path}' to '{out_path}'")
+
+ im = Image.open(in_path)
+
+ if im.width <= 1200 and im.height <= 1200:
+ copyfile(in_path, out_path)
+ continue
+
+ if im.width > im.height:
+ new_image = im.resize((1200, round(1200*im.height/im.width)))
+ else:
+ new_image = im.resize((round(1200*im.width/im.height), 1200))
+
+ if 'exif' in im.info:
+ new_image.save(out_path, exif=im.info['exif'], quality=80)
+ else:
+ new_image.save(out_path, quality=80)
+
+
+ names.append(f)
+
+ settings_save_images(names)
+
+
+def update_date(yml, lines):
+ return re.sub("\ndate:.*\n", f"\ndate: {yml['date']}\n",lines)
+
+
+def update_featured_image(yml, lines):
+ return re.sub("\nfeatured_image:.*\n", f"\nfeatured_image: {yml['featured_image']}\n",lines)
+
+
+def update_header_image(yml, lines):
+ return re.sub("\nheader_image:.*\n", f"\nheader_image: {yml['header_image']}\n",lines)
+
+
+def update_captions(yml, lines):
+ data = yml['captions']
+
+ lines = clear_captions(lines)
+ # log(f"update_captions lines={lines}")
+
+ if data is None:
+ output = ""
+ else:
+ output = yaml.dump(data, Dumper=Dumper)
+
+ # log(f"update_captions output={output}")
+ lines = re.sub("\ncaptions:.*\n", f"\ncaptions:\n{output}", lines)
+
+ # log(f"update_captions {lines}")
+ return lines
+
+
+def clear_captions(lines):
+ arr = lines.split('\n')
+ captions = False
+ arr2 = []
+
+ for item in arr:
+ if item.startswith("captions:"):
+ captions = True
+ arr2.append(item)
+ continue
+
+ if captions:
+ if len(item.strip()) == 0:
+ captions = False
+ arr2.append(item)
+
+ else:
+ arr2.append(item)
+
+ ret = ""
+ for item in arr2:
+ if len(ret) == 0:
+ ret = item
+ else:
+ ret = ret + '\n' + item
+
+ return ret
+
+
+def update_title(yml, lines):
+ return re.sub("\ntitle:.*\n", f"\ntitle: \"{yml['title']}\"\n",lines)
+
+
+def get_content():
+ lines = ""
+
+ # log(f"get_content get_post_file_path={get_post_file_path()}")
+ pattern = re.compile("^---$")
+ cnt = 0
+
+ with open(get_post_file_path(), "r") as f:
+ # log(f"get_content with f={f}")
+
+ for line in f:
+ # log(f"get_content {line}")
+
+ if pattern.match(line):
+ cnt += 1
+ continue
+
+ if cnt < 2:
+ continue
+
+ lines = lines + line
+
+ # log(f"get_content return={lines}")
+ return lines
+
+
+def get_frontmatters():
+ lines = ""
+ with open(get_post_file_path(), "r") as f:
+ # log("open")
+ pattern = re.compile("^---$")
+ cnt = 0
+
+ for line in f:
+ # log(line)
+ if pattern.match(line):
+ cnt += 1
+ # log(f"a: {cnt}")
+ continue
+
+ if cnt == 0:
+ # log(f"b: {cnt}")
+ continue
+
+ if cnt == 2:
+ # log(f"c: {cnt}")
+ break
+
+ # log(line)
+ lines = lines + line
+
+ return lines
+
+
+def execute_cleanup():
+
+ if path.exists(settings_path):
+ date = settings_get_date()
+ else:
+ print("Not initialized: nothing to do.")
+ return
+
+ if path.exists(get_in_path()):
+ rmtree(get_in_path())
+ print(f"Removed '{get_in_path()}'")
+
+ remove(settings_path)
+
+ print(f"{date} closed.")
+
+
+def settings_get_date():
+ with open(settings_path) as f:
+ settings = json.load(f)
+ return settings['date']
+
+
+def settings_get_images():
+ with open(settings_path) as f:
+ settings = json.load(f)
+ return settings['images']
+
+
+def settings_init(date):
+ with open(settings_path, 'w') as f:
+ json.dump(dict(date=date, images=[]), f)
+
+
+def settings_save_images(images):
+
+ date = settings_get_date()
+
+ with open(settings_path, 'w') as f:
+ json.dump(dict(date=date, images=images), f)
+
+
+def execute_init():
+
+ # date must match YYYYMMDD pattern
+ pattern = re.compile("^\d{8}$")
+
+ if not pattern.match(args.date):
+ exit_on_error("Wrong date format, must be ")
+
+ settings_init(args.date)
+
+ # ### in ####
+ if path.exists(get_in_path()):
+ print(f"Existing in path found. Put your original images here: '{get_in_path()}'")
+ else:
+ mkdir(get_in_path())
+ print(f"Post directory created. Put your original images here: '{get_in_path()}'")
+
+ # ### post dir ####
+ if path.exists(get_out_path()):
+ if args.clean:
+ rmtree(get_out_path())
+ mkdir(get_out_path())
+ print(f"Existing post directory cleaned: '{get_out_path()}'")
+ else:
+ print(f"Existing post directory found: '{get_out_path()}'")
+ else:
+ mkdir(get_out_path())
+ print(f"Post' directory created: '{get_out_path()}' ")
+
+ # ### post file ####
+ if path.exists(get_post_file_path()):
+ print(f"Existing post file found: '{get_post_file_path()}'")
+ else:
+ copyfile(post_file_archetype, get_post_file_path())
+ init_title()
+ print(f"New post file created: '{get_post_file_path()}'")
+
+ yml = yaml.load(get_frontmatters(), Loader=Loader)
+ image_path = get_image_path(yml)
+ if not path.exists(image_path):
+ mkdir(image_path)
+ print(f"Image directory created: '{get_image_resource_path(yml)}' ")
+
+ init_date()
+
+ print(f"The next step would be to add your images into the '{get_in_path()}' directory. "
+ f"After that you can update the post directory '{get_out_path()}' and the post file '{post_file_name}': "
+ f"call 'update'.")
+
+
+def get_post_file_path():
+ return path.join(get_out_path(), post_file_name)
+
+
+def get_post_backup_file_path():
+ return path.join(get_out_path(), post_file_backup_name)
+
+
+def get_in_path():
+ with open('.settings') as f:
+ settings = json.load(f)
+ return path.join("in", "posts", settings['date'])
+
+
+def log(message):
+ """
+ Only switched on for development
+ """
+ if log_switch:
+ print(message)
+
+
+def exit_invalid_initialization():
+ if not path.exists(settings_path):
+ exit_on_error("Editorial seems not to be initialized.")
+
+ if not path.exists(get_in_path()):
+ exit_on_error(f"Invalid in path: '{get_in_path()}'.")
+
+ if not path.exists(get_out_path()):
+ exit_on_error(f"Invalid post path: '{get_out_path()}'.")
+
+ if not path.exists(get_post_file_path()):
+ exit_on_error(f"No post file found: '{get_post_file_path()}'.")
+
+
+def get_out_path():
+ with open(settings_path) as f:
+ settings = json.load(f)
+ return path.join("..", "content", "posts", settings['date'])
+
+
+def parse_args():
+
+ global args
+
+ parser = argparse.ArgumentParser(description='Bring your content into the Hugo blog')
+
+ parser.add_argument("-v", "--verbose",
+ action='store_true',
+ help="Print more info")
+
+ sub_parsers = parser.add_subparsers()
+
+ # ######### init #########
+ init_parser = sub_parsers.add_parser('init',
+ help="Setup a new post or edit an existing one",
+ description=""
+ )
+ init_parser.add_argument('date', metavar='DATE', type=str,
+ help="Date of the post as , example: 'init 20201231'")
+
+ init_parser.add_argument('--clean', action="store_true",
+ help="Removes existing post content, if exists."
+ "Be careful with this option! Particularly, if there are multiple editors working "
+ "on this blog."
+ )
+
+ init_parser.set_defaults(func=execute_init)
+
+ # ######### update #########
+ update_parser = sub_parsers.add_parser('update',
+ help="Converts images into a proper size and update the post file",
+ description="Images must be in in/post/"
+ )
+
+ update_parser.set_defaults(func=execute_update)
+
+ # ######### cleanup #########
+ cleanup_parser = sub_parsers.add_parser('cleanup',
+ help=f"cleanup editorial by clean up the in directory",
+ description=f"Removes in directory (with your original images). "
+ )
+
+ cleanup_parser.set_defaults(func=execute_cleanup)
+
+ args = parser.parse_args()
+
+ if args.verbose:
+ set_log_switch(True)
+
+ if len(sys.argv) > 1:
+ args.func()
+ else:
+ execute_info()
+
+def set_log_switch(value):
+ global log_switch
+ log_switch = value
+
+
+def exit_on_error(message):
+ print(message)
+ exit(1)
+
+
+if __name__ == '__main__':
+ parse_args()
\ No newline at end of file
diff --git a/redaktion/requirements.txt b/redaktion/requirements.txt
new file mode 100644
index 0000000..e3af201
--- /dev/null
+++ b/redaktion/requirements.txt
@@ -0,0 +1,2 @@
+Pillow==7.2.0
+PyYAML==5.3.1