diff --git a/README.md b/README.md index b74cacf..8a922f2 100644 --- a/README.md +++ b/README.md @@ -4,3 +4,9 @@ > and a comfortable digital "living room" experience for lumbung[dot]space. > This is a work in progress. The end goal of this is to have a new design and > implementation for [`lumbung.space`](https://lumbung.space) for January 2022. + +## SYN ACK + +Huge thanks to all the folks who did work to make this possible. + +- [`@rra`](https://test.roelof.info/) prototypin' in [git.vvvvvvaria.org/rra](https://git.vvvvvvaria.org/rra) diff --git a/lumbung-calendar-prototype/.gitignore b/lumbung-calendar-prototype/.gitignore new file mode 100644 index 0000000..54d69e5 --- /dev/null +++ b/lumbung-calendar-prototype/.gitignore @@ -0,0 +1,2 @@ +event_feed_config.py +__pycache__ diff --git a/lumbung-calendar-prototype/README.md b/lumbung-calendar-prototype/README.md new file mode 100644 index 0000000..62fe8ff --- /dev/null +++ b/lumbung-calendar-prototype/README.md @@ -0,0 +1,9 @@ +# Calendar Feed +Generate HUGO posts based on a publicly accessible ICS calendar. + +## Use +Fill in your details in `calendar_feed_config.py` + +## TODO / FIXME + + * Multiple calendars to multiple hugo categories diff --git a/lumbung-calendar-prototype/event_feed.py b/lumbung-calendar-prototype/event_feed.py new file mode 100644 index 0000000..d11fb1c --- /dev/null +++ b/lumbung-calendar-prototype/event_feed.py @@ -0,0 +1,194 @@ +#!/bin/python3 + +#lumbung.space calendar feed generator +#© 2021 roel roscam abbing gplv3 etc + +from ics import Calendar +import requests +import jinja2 +import os +import shutil +from slugify import slugify +from natural import date +from event_feed_config import calendar_url, output_dir +from urllib.parse import urlparse +import arrow +import re + +cal = Calendar(requests.get(calendar_url).text) + +env = jinja2.Environment( + loader=jinja2.FileSystemLoader(os.path.curdir) + ) + +if not os.path.exists(output_dir): + os.mkdir(output_dir) + +template = env.get_template('event_template.md') + +existing_posts = os.listdir(output_dir) + +def findURLs(string): + """ + return all URLs in a given string + """ + regex = r"(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'\".,<>?«»“”‘’]))" + url = re.findall(regex,string) + return [x[0] for x in url] + +def find_imageURLS(string): + """ + return all image URLS in a given string + """ + regex = r"(?:http\:|https\:)?\/\/.*?\.(?:png|jpg|jpeg|gif|svg)" + + img_urls = re.findall(regex, string, flags=re.IGNORECASE) + return img_urls + +def create_metadata(event): + """ + construct a formatted dict of event metadata for use as frontmatter for HUGO post + """ + + if event.location: + location_urls = findURLs(event.location) + + if location_urls: + location_url = location_urls[0] + event.location = '[{}]({})'.format(urlparse(location_url).netloc, location_url) + + + event_metadata = { + 'name':event.name, + 'created':event.created.format(), + 'description': event.description, + 'localized_begin': '           '.join(localize_time(event.begin)), #non-breaking space characters to defeat markdown + 'begin': event.begin.format(), + 'end': event.end.format(), + 'duration': date.compress(event.duration), + 'location': event.location, + 'uid': event.uid, + 'images' : find_imageURLS(event.description) # currently not used in template + } + + return event_metadata + +def localize_time(date): + """ + Turn a given date into various timezones + Takes arrow objects + """ + + # 3 PM Kassel, Germany, 4 PM Ramallah/Jerusalem, Palestina (QoF), + # 8 AM Bogota, Colombia (MaMa), 8 PM Jakarta, Indonesia (Gudskul), + # 1 PM (+1day) Wellington, New Zealand (Fafswag), 9 AM Havana, Cuba (Instar). + + + tzs = [ + ('Kassel','Europe/Berlin'), + ('Bamako', 'Europe/London'), + ('Palestine','Asia/Jerusalem'), + ('Bogota','America/Bogota'), + ('Jakarta','Asia/Jakarta'), + ('Makassar','Asia/Makassar'), + ('Wellington', 'Pacific/Auckland') + ] + + localized_begins =[] + for location, tz in tzs: + localized_begins.append( #javascript formatting because of string creation from hell + '__{}__ {}'.format( + str(location), + str(date.to(tz).format("YYYY-MM-DD __HH:mm__")) + ) + ) + return localized_begins + +def create_event_post(post_dir, event): + """ + Create HUGO post based on calendar event metadata + Searches for image URLS in description and downloads them + Function is also called when post is in need of updating + In that case it will also delete images no longer in metadata + TODO: split this up into more functions for legibility + """ + + if not os.path.exists(post_dir): + os.mkdir(post_dir) + + event_metadata = create_metadata(event) + + #list already existing images + #so we can later delete them if we dont find them in the event metadata anymore + existing_images = os.listdir(post_dir) + try: + existing_images.remove('index.md') + existing_images.remove('.timestamp') + except: + pass + + for img in event_metadata['images']: + + #parse img url to safe local image name + img_name = img.split('/')[-1] + fn, ext = img_name.split('.') + img_name = slugify(fn) + '.' + ext + + local_image = os.path.join(post_dir, img_name) + + if not os.path.exists(local_image): + #download preview image + response = requests.get(img, stream=True) + with open(local_image, 'wb') as img_file: + shutil.copyfileobj(response.raw, img_file) + print('Downloaded image for event "{}"'.format(event.name)) + event_metadata['description'] = event_metadata['description'].replace(img, '![]({})'.format(img_name)) + if img_name in existing_images: + existing_images.remove(img_name) + + for left_over_image in existing_images: + #remove images we found, but which are no longer in remote event + os.remove(os.path.join(post_dir,left_over_image)) + print('deleted image', left_over_image) + + with open(os.path.join(post_dir,'index.md'),'w') as f: + post = template.render(event = event_metadata) + f.write(post) + print('created post for', event.name, '({})'.format(event.uid)) + + with open(os.path.join(post_dir,'.timestamp'),'w') as f: + f.write(event_metadata['created']) + + +def update_event_post(post_dir, event): + """ + Update a post based on the VCARD event 'created' field which changes when updated + """ + if os.path.exists(post_dir): + old_timestamp = open(os.path.join(post_dir,'.timestamp')).read() + if event.created > arrow.get(old_timestamp): + print('Updating', event.name, '({})'.format(event.uid)) + create_event_post(post_dir, event) + else: + print('Event current: ', event.name, '({})'.format(event.uid)) + +for event in list(cal.events): + + post_dir = os.path.join(output_dir, event.uid) + + if event.uid not in existing_posts: + #if there is an event we dont already have, make it + create_event_post(post_dir, event) + + elif event.uid in existing_posts: + #if we already have it, update + update_event_post(post_dir, event) + existing_posts.remove(event.uid) # create list of posts which have not been returned by the calendar + + +for post in existing_posts: + #remove events not returned by the calendar (deletion) + print('deleted', post) + shutil.rmtree(os.path.join(output_dir,post)) + + diff --git a/lumbung-calendar-prototype/event_template.md b/lumbung-calendar-prototype/event_template.md new file mode 100644 index 0000000..441f3da --- /dev/null +++ b/lumbung-calendar-prototype/event_template.md @@ -0,0 +1,21 @@ +--- +title: "{{ event.name }}" +date: "{{ event.begin }}" #2021-06-10T10:46:33+02:00 +draft: false +categories: "calendar" +event_begin: "{{ event.begin }}" +event_end: "{{ event.end }}" +duration: "{{ event.duration }}" +localized_begin: "{{ event.localized_begin }}" +uid: "{{ event.uid }}" +{% if event.location %} +location: "{{ event.location }}" +{% endif %} + + +--- +{% if event.description %} + +{{ event.description }} + +{% endif %} diff --git a/lumbung-calendar-prototype/requirements.txt b/lumbung-calendar-prototype/requirements.txt new file mode 100644 index 0000000..356637c --- /dev/null +++ b/lumbung-calendar-prototype/requirements.txt @@ -0,0 +1,16 @@ +# Automatically generated by https://github.com/damnever/pigar. + +# calendar-feed/event_feed.py: 3 +Jinja2 == 2.10 + +# calendar-feed/event_feed.py: 1 +ics == 0.7 + +# calendar-feed/event_feed.py: 6 +natural == 0.2.0 + +# calendar-feed/event_feed.py: 5 +python_slugify == 5.0.2 + +# calendar-feed/event_feed.py: 2 +requests == 2.21.0 diff --git a/lumbung-feed-aggregator/.gitignore b/lumbung-feed-aggregator/.gitignore new file mode 100644 index 0000000..ecf1da3 --- /dev/null +++ b/lumbung-feed-aggregator/.gitignore @@ -0,0 +1,2 @@ +network/ +etags/ diff --git a/lumbung-feed-aggregator/README.md b/lumbung-feed-aggregator/README.md new file mode 100644 index 0000000..97d32e9 --- /dev/null +++ b/lumbung-feed-aggregator/README.md @@ -0,0 +1,11 @@ +# lumbung feed aggregator + +* Grab feeds listed in `feeds_list.txt` +* Parse feed for blogpost entries +* * Download images linked in blogposts +* Turn blogpost entries into HUGO posts + +# TODO/FIXME + +* only include posts with a certain tag + diff --git a/lumbung-feed-aggregator/feeds_list.txt b/lumbung-feed-aggregator/feeds_list.txt new file mode 100644 index 0000000..7334acb --- /dev/null +++ b/lumbung-feed-aggregator/feeds_list.txt @@ -0,0 +1,11 @@ +https://www.masartemasaccion.org/feed/ +https://fafswag.wordpress.com/feed/ +https://wajukuuarts.wordpress.com/feed/ +https://inland.org/feed/ +https://jatiwangiartfactory.tumblr.com/rss/ +https://brittoartstrust.org/feed/ +https://artivismo.org/feed/ +http://www.festivalsegou.org/spip.php?page=backend&lang=fr +https://gudskul.art/feed/ +https://projectartworks.org/feed/ +https://ruangrupa.id/feed/ \ No newline at end of file diff --git a/lumbung-feed-aggregator/post_template.md b/lumbung-feed-aggregator/post_template.md new file mode 100644 index 0000000..9dbc449 --- /dev/null +++ b/lumbung-feed-aggregator/post_template.md @@ -0,0 +1,13 @@ +--- +title: "{{ frontmatter.title }}" +date: "{{ frontmatter.date }}" #2021-06-10T10:46:33+02:00 +draft: false +summary: "{{ frontmatter.summary }}" +author: "{{ frontmatter.author }}" +original_link: "{{ frontmatter.original_link }}" +feed_name: "{{ frontmatter.feed_name}}" +categories: ["network", "{{ frontmatter.feed_name}}"] +tags: {{ frontmatter.tags }} +--- + +{{ content }} \ No newline at end of file diff --git a/lumbung-feed-aggregator/rss_aggregator.py b/lumbung-feed-aggregator/rss_aggregator.py new file mode 100644 index 0000000..0f65c93 --- /dev/null +++ b/lumbung-feed-aggregator/rss_aggregator.py @@ -0,0 +1,248 @@ +#!/bin/python3 + +#lumbung.space rss feed aggregator +#© 2021 roel roscam abbing gplv3 etc + +import requests +import jinja2 +import os +import shutil +import feedparser +from urllib.parse import urlparse +from ast import literal_eval as make_tuple +from slugify import slugify +from bs4 import BeautifulSoup +import time +import arrow + + +def write_etag(feed_name, feed_data): + """ + save timestamp of when feed was last modified + """ + etag = '' + modified = '' + + if 'etag' in feed_data: + etag = feed_data.etag + if 'modified' in feed_data: + modified = feed_data.modified + + if etag or modified: + with open(os.path.join('etags',feed_name +'.txt'),'w') as f: + f.write(str((etag, modified))) + +def get_etag(feed_name): + """ + return timestamp of when feed was last modified + """ + fn = os.path.join('etags',feed_name +'.txt') + etag = '' + modified = '' + + if os.path.exists(fn): + etag, modified = make_tuple(open(fn,'r').read()) + + return etag, modified + +def create_frontmatter(entry): + """ + parse RSS metadata and return as frontmatter + """ + if 'published' in entry: + published = entry.published_parsed + if 'updated' in entry: + published = entry.updated_parsed + + published = arrow.get(published) + + if 'author' in entry: + author = entry.author + else: + author = '' + + tags = [] + if 'tags' in entry: + #TODO finish categories + for t in entry.tags: + tags.append(t['term']) + + frontmatter = { + 'title':entry.title, + 'date': published.format(), + 'summary': '', + 'author': author, + 'original_link': entry.link, + 'feed_name': entry['feed_name'], + 'tags': str(tags) + } + + return frontmatter + +def create_post(post_dir, entry): + """ + write hugo post based on RSS entry + """ + frontmatter = create_frontmatter(entry) + + if not os.path.exists(post_dir): + os.makedirs(post_dir) + + if 'content' in entry: + post_content = entry.content[0].value + else: + post_content = entry.summary + + parsed_content = parse_posts(post_dir, post_content) + + with open(os.path.join(post_dir,'index.html'),'w') as f: #n.b. .html + post = template.render(frontmatter=frontmatter, content=parsed_content) + f.write(post) + print('created post for', entry.title, '({})'.format(entry.link)) + +def grab_media(post_directory, url): + """ + download media linked in post to have local copy + if download succeeds return new local path otherwise return url + """ + image = urlparse(url).path.split('/')[-1] + + try: + if not os.path.exists(os.path.join(post_directory, image)): + #TODO: stream is true is a conditional so we could check the headers for things, mimetype etc + response = requests.get(url, stream=True) + if response.ok: + with open(os.path.join(post_directory, image), 'wb') as img_file: + shutil.copyfileobj(response.raw, img_file) + print('Downloaded cover image', image) + return image + return image + elif os.path.exists(os.path.join(post_directory, image)): + return image + + except Exception as e: + print('Failed to download image', url) + print(e) + return url + + +def parse_posts(post_dir, post_content): + """ + parse the post content to for media items + replace foreign image with local copy + filter out iframe sources not in allowlist + """ + soup = BeautifulSoup(post_content, "html.parser") + allowed_iframe_sources = ['youtube.com', 'vimeo.com', 'tv.lumbung.space'] + media = [] + + for img in soup(['img','object']): + local_image = grab_media(post_dir, img['src']) + if img['src'] != local_image: + img['src'] = local_image + + for iframe in soup(['iframe']): + if not any(source in iframe['src'] for source in allowed_iframe_sources): + print('filtered iframe: {}...'.format(iframe['src'][:25])) + iframe.decompose() + return soup.decode() + +def grab_feed(feed_url): + """ + check whether feed has been updated + download & return it if it has + """ + feed_name = urlparse(feed_url).netloc + + etag, modified = get_etag(feed_name) + + try: + if modified: + data = feedparser.parse(feed_url, modified=modified) + elif etag: + data = feedparser.parse(feed_url, etag=etag) + else: + data = feedparser.parse(feed_url) + except Exception as e: + print('Error grabbing feed') + print(feed_name) + print(e) + return False + + print(data.status, feed_url) + if data.status == 200: + #304 means the feed has not been modified since we last checked + write_etag(feed_name, data) + return data + return False + + +feed_urls = open('feeds_list.txt','r').read().splitlines() + +start = time.time() + +if not os.path.exists('etags'): + os.mkdir('etags') + + +env = jinja2.Environment( + loader=jinja2.FileSystemLoader(os.path.curdir) + ) + +output_dir = os.environ.get('OUTPUT_DIR', '/home/r/Programming/lumbung.space/lumbung.space-web/content/posts/') +#output_dir = os.environ.get('OUTPUT_DIR', 'network/') + +if not os.path.exists(output_dir): + os.makedirs(output_dir) + +template = env.get_template('post_template.md') + +#add iframe to the allowlist of feedparser's sanitizer, +#this is now handled in parse_post() +feedparser.sanitizer._HTMLSanitizer.acceptable_elements |= {'iframe'} + +for feed_url in feed_urls: + + feed_name = urlparse(feed_url).netloc + + feed_dir = os.path.join(output_dir, feed_name) + + if not os.path.exists(feed_dir): + os.makedirs(feed_dir) + + existing_posts = os.listdir(feed_dir) + + data = grab_feed(feed_url) + + if data: + for entry in data.entries: + # if 'tags' in entry: + # for tag in entry.tags: + # for x in ['lumbung.space', 'D15', 'lumbung']: + # if x in tag['term']: + # print(entry.title) + entry['feed_name'] = feed_name + + post_name = slugify(entry.title) + post_dir = os.path.join(output_dir, feed_name, post_name) + + if post_name not in existing_posts: + #if there is a blog entry we dont already have, make it + create_post(post_dir, entry) + + elif post_name in existing_posts: + #if we already have it, update it + create_post(post_dir, entry) + existing_posts.remove(post_name) # create list of posts which have not been returned by the feed + + for post in existing_posts: + #remove blog posts no longer returned by the RSS feed + print('deleted', post) + shutil.rmtree(os.path.join(feed_dir, slugify(post))) + + + +end = time.time() + +print(end - start) + diff --git a/lumbung-hashtag-bot/.gitignore b/lumbung-hashtag-bot/.gitignore new file mode 100644 index 0000000..8afa646 --- /dev/null +++ b/lumbung-hashtag-bot/.gitignore @@ -0,0 +1,3 @@ +config_hashtag_bot.py +*.secret +__pycache__/* diff --git a/lumbung-hashtag-bot/README.md b/lumbung-hashtag-bot/README.md new file mode 100644 index 0000000..618a3ac --- /dev/null +++ b/lumbung-hashtag-bot/README.md @@ -0,0 +1,30 @@ +# lumbung.space hashtag publishing bot + +This script makes [Hugo page bundles](https://gohugo.io/content-management/page-bundles/) out of Hashtag feeds on a Mastodon Hometown or Glitchsoc instance. + +## Install requirements + +`pip3 install Mastodon.py jinja2` + +## Setup + +This script requires access to an account on said Mastodon instance. This instance and the credentials can be set in `config_hashtag_bot.py`. + +If it is the first time you are running the script, you need to register the application on the Mastodon instance. Have a look at the [Mastodon.py documentation](https://mastodonpy.readthedocs.io/en/stable/#module-mastodon) for how to do that. + +This bot only uses read permissions. + +Set which hashtags you want to publish by adding them to the list `hashtags` in `config_hashtag_bot.py`. Omit the '#'. + +## What it does + +* The Bot only looks at the **local timeline** for posts under each hashtag configured in `config_hashtag_bot.py`. +* This means posts need to be **public** or directly addressed to the bot +* This script respects the mental model of 'local only' posts in the sense that people do not expect them to appear elsewhere. So **local only posts are ignored** +* It takes only posts with Media attached and then only those with images + +## What it doesn't do + +* Different types of media or embeds +* No thread recreation, each post is treated as a top level post + diff --git a/lumbung-hashtag-bot/post_template.md b/lumbung-hashtag-bot/post_template.md new file mode 100644 index 0000000..6aeff3e --- /dev/null +++ b/lumbung-hashtag-bot/post_template.md @@ -0,0 +1,14 @@ +--- +date: "{{ post_metadata.created_at }}" #2021-06-10T10:46:33+02:00 +draft: false +author: "{{ post_metadata.account.display_name }}" +avatar: "{{ post_metadata.account.avatar }}" +categories: ["shouts"] +tags: [{% for i in post_metadata.tags %} "{{ i.name }}", {% endfor %}] +--- + +{% for item in post_metadata.media_attachments %} +{{item.description}} +{% endfor %} + +{{ post_metadata.content | filter_mastodon_urls }} \ No newline at end of file diff --git a/lumbung-hashtag-bot/publish_hashtags.py b/lumbung-hashtag-bot/publish_hashtags.py new file mode 100644 index 0000000..09e09d7 --- /dev/null +++ b/lumbung-hashtag-bot/publish_hashtags.py @@ -0,0 +1,137 @@ +# lumbung.space hashtag publishing bot +# © 2021 roel roscam abbing agplv3 +# Makes Hugo posts out of hashtag feeds on Mastodon. +# Requires an account on the Mastodon instance configured. +# Currently does not do any thread recreation and only handles images + +import os +import requests +import shutil + +import jinja2 + +from mastodon import Mastodon +import config_hashtag_bot + +def login_mastodon_bot(): + mastodon = Mastodon( + client_id = 'publishbot_clientcred.secret', + api_base_url = config_hashtag_bot.instance + ) + + mastodon.log_in( + config_hashtag_bot.email, + config_hashtag_bot.password, + to_file = 'publishbot_usercred.secret', scopes=['read'] + ) + + return mastodon + +def create_frontmatter(post_metadata): + """ + Parse post metadata and return it as HUGO frontmatter + """ + + frontmatter = "" + return frontmatter + +def download_media(post_directory, media_attachments): + """ + Download media attached to posts. N.b. currently only images + See: https://mastodonpy.readthedocs.io/en/stable/#media-dicts + """ + + for item in media_attachments: + if item['type'] == 'image': + image = localize_media_url(item['url']) + #TODO check whether this needs to handle delete & redraft with different images + if not os.path.exists(os.path.join(post_directory, image)): + #download image + response = requests.get(item['url'], stream=True) + with open(os.path.join(post_directory, image), 'wb') as img_file: + shutil.copyfileobj(response.raw, img_file) + print('Downloaded cover image', image) + +def create_post(post_directory, post_metadata): + """ + Create Hugo posts based on Toots/posts retuned in timeline. + See: https://mastodonpy.readthedocs.io/en/stable/#toot-dicts + """ + + if not os.path.exists(post_directory): + os.mkdir(post_directory) + + with open(os.path.join(post_directory,'index.html'),'w') as f: + post = template.render(post_metadata=post_metadata) + f.write(post) + + download_media(post_directory, post_metadata['media_attachments']) + +def localize_media_url(url): + """ + Returns the filename, used also as custom jinja filter + """ + return url.split('/')[-1] + + +def filter_mastodon_urls(content): + """ + Filters out Mastodon generated URLS for tags + e.g.
+
+{{ end }} diff --git a/lumbung-theme/layouts/index.html b/lumbung-theme/layouts/index.html new file mode 100644 index 0000000..cc048bb --- /dev/null +++ b/lumbung-theme/layouts/index.html @@ -0,0 +1,37 @@ +{{ define "main" }} +
+
+ {{ range (.Paginator 13).Pages }} + + {{ if in .Params.categories "tv"}} + + {{- partial "video_box.html" . -}} + + {{ else if in .Params.categories "calendar" }} + + {{- partial "calendar_card.html" . -}} + + {{ else if in .Params.categories "network" }} + + {{- partial "network_card.html" . -}} + + {{ else if in .Params.categories "shouts" }} + + {{- partial "shout_card.html" . -}} + + {{ else }} + + {{- partial "card.html" . -}} + + {{ end }} + + {{ end }} +
+ + + + +{{ end }} + diff --git a/lumbung-theme/layouts/partials/calendar_card.html b/lumbung-theme/layouts/partials/calendar_card.html new file mode 100644 index 0000000..6c65f76 --- /dev/null +++ b/lumbung-theme/layouts/partials/calendar_card.html @@ -0,0 +1,30 @@ +{{ $t := (time .Params.event_end) }} +
+
+
+

{{ .Title }}

+
+ {{ range first 1 (.Resources.ByType "image") }} + + {{ end }} +
+ + {{ .Params.localized_begin | markdownify }} + +
+ + + + +
+
+ \ No newline at end of file diff --git a/lumbung-theme/layouts/partials/card.html b/lumbung-theme/layouts/partials/card.html new file mode 100644 index 0000000..9154a32 --- /dev/null +++ b/lumbung-theme/layouts/partials/card.html @@ -0,0 +1,34 @@ +
+
+
+

{{ .Title }}

+ +
+ + {{ $img := (.Resources.ByType "image").GetMatch "*featured*" }} + +
+
+ {{ .Summary }} +
+ {{ with $img }} + {{ $thumb := .Resize "400x300"}} +
+ {{ .Title }} +
+ {{ end }} +
+ {{ if .Truncated }} + + {{ end }} + +
+
\ No newline at end of file diff --git a/lumbung-theme/layouts/partials/footer.html b/lumbung-theme/layouts/partials/footer.html new file mode 100644 index 0000000..e033aca --- /dev/null +++ b/lumbung-theme/layouts/partials/footer.html @@ -0,0 +1,5 @@ +
+
+ Imprint - Privacy Policy - Copyright +
+
\ No newline at end of file diff --git a/lumbung-theme/layouts/partials/head.html b/lumbung-theme/layouts/partials/head.html new file mode 100644 index 0000000..b539c40 --- /dev/null +++ b/lumbung-theme/layouts/partials/head.html @@ -0,0 +1,29 @@ + + + + + {{ if .IsHome }} {{ .Site.Title }} {{ else }} {{ .Title }} | {{ .Site.Title }} {{ end }} + + {{- if or .Description .Site.Params.description }} + + {{- end }} + {{- if or .Description .Site.Params.description }} + + {{- end }} + + {{ template "_internal/opengraph.html" . }} + {{ template "_internal/twitter_cards.html" . }} + + + + + + + {{ with .Site.Params.favicon }} + + {{ end }} + + {{ with .OutputFormats.Get "rss" -}} + {{ printf `` .Rel .MediaType.Type .Permalink $.Site.Title | safeHTML }} + {{ end -}} + diff --git a/lumbung-theme/layouts/partials/header.html b/lumbung-theme/layouts/partials/header.html new file mode 100644 index 0000000..2f5ff63 --- /dev/null +++ b/lumbung-theme/layouts/partials/header.html @@ -0,0 +1,34 @@ +
+

{{ .Site.Title }}

+ +
\ No newline at end of file diff --git a/lumbung-theme/layouts/partials/network_card.html b/lumbung-theme/layouts/partials/network_card.html new file mode 100644 index 0000000..db6f877 --- /dev/null +++ b/lumbung-theme/layouts/partials/network_card.html @@ -0,0 +1,37 @@ +
+
+
+ {{ $postPermalink := .Permalink}} +

{{ .Title }}

+ + +
+ +
From {{ if .Params.author }}{{.Params.author}} at {{ end }}{{ .Params.feed_name }}
+ {{ with (index (.Resources.ByType "image") 0) }} + {{ $height := add .Height 0.0}} + {{ $ratio := div $height .Width}} + {{ $thumb := .Fit "540x360"}} +
+
+ {{ .Title }} +
+ {{ else }} +
+ {{ end }} +
+ {{ .Summary }} +
+
+ +
+ + {{ if .Truncated }} + + + {{ end }} +
+
+
\ No newline at end of file diff --git a/lumbung-theme/layouts/partials/shout_card.html b/lumbung-theme/layouts/partials/shout_card.html new file mode 100644 index 0000000..83dfc77 --- /dev/null +++ b/lumbung-theme/layouts/partials/shout_card.html @@ -0,0 +1,36 @@ +
+
+
+ {{ $postPermalink := .Permalink}} +

{{ .Title }}

+
+ + {{ with (index (.Resources.ByType "image") 0) }} + {{ $height := add .Height 0.0}} + {{ $ratio := div $height .Width}} + {{ $thumb := .Fit "540x360"}} +
+
+ {{ .Title }} +
+ {{ else }} +
+ {{ end }} +
+ {{ .Summary }} +
+
+
+ +
+ + {{ if .Truncated }} + + + {{ end }} +
+
+
\ No newline at end of file diff --git a/lumbung-theme/layouts/partials/video_box.html b/lumbung-theme/layouts/partials/video_box.html new file mode 100644 index 0000000..711800e --- /dev/null +++ b/lumbung-theme/layouts/partials/video_box.html @@ -0,0 +1,37 @@ +
+
+ + + + {{ if .Params.is_live}} +
LIVE
+ {{ else }} +
{{.Params.video_duration}}
+ {{ end }} + +
+
+
+
+
+ +
+ diff --git a/lumbung-theme/static/css/fonts/Anonymous_Pro_Regular.woff b/lumbung-theme/static/css/fonts/Anonymous_Pro_Regular.woff new file mode 100644 index 0000000..220852e Binary files /dev/null and b/lumbung-theme/static/css/fonts/Anonymous_Pro_Regular.woff differ diff --git a/lumbung-theme/static/css/fonts/Barrio_Regular.woff b/lumbung-theme/static/css/fonts/Barrio_Regular.woff new file mode 100644 index 0000000..d066b82 Binary files /dev/null and b/lumbung-theme/static/css/fonts/Barrio_Regular.woff differ diff --git a/lumbung-theme/static/css/fonts/Zen_Maru_Gothic_Regular.woff b/lumbung-theme/static/css/fonts/Zen_Maru_Gothic_Regular.woff new file mode 100644 index 0000000..ca6a3eb Binary files /dev/null and b/lumbung-theme/static/css/fonts/Zen_Maru_Gothic_Regular.woff differ diff --git a/lumbung-theme/static/css/main.css b/lumbung-theme/static/css/main.css new file mode 100644 index 0000000..c457077 --- /dev/null +++ b/lumbung-theme/static/css/main.css @@ -0,0 +1,603 @@ +/*nice body-border color combos + +antiquewhite - burlywood +peachpuff - tomato +lightpink - crimson +lightblue - cornflowerblue +palegreen - lightseagreen +steelblue - aliceblue + +fonts +bungeeshade +allerta +*/ + +@font-face { + font-family: BarrioRegular; + src: url(fonts/Barrio_Regular.woff); +} + +@font-face { + font-family: ZenMaruGothic; + src: url(fonts/Zen_Maru_Gothic_Regular.woff); +} + +@font-face { + font-family: AnonymousPro; + src: url(fonts/Anonymous_Pro_Regular.woff); +} + +h1, h2, h3 { + font-family: BarrioRegular; +} + +:root { + --border-color: tomato; +} + +/*Main Stuff*/ +body { + font-size:24px; + font-family: ZenMaruGothic; + color: maroon; +} + + a { + color: #1B4C8A; + } + +* { + box-sizing: border-box; +} + +#content { + margin: 2em auto; + max-width: 80%; + margin-bottom: 0; + } + +.card{ + + border: 2px solid var(--border-color); + box-shadow:1em 1em 0 #d2d1c8; + background-color: #fff09d; + max-width: 600px; + margin-bottom: 2em; + flex: auto; + margin: 0 3em 3em 0; + align-self: start; + + } + + .card{ + background-color: peachpuff; + } + + .side-bar { + border: 2px solid var(--border-color); + max-width: 400px; + } + + + .card:nth-child(even){ + transform: rotate(-1deg); + } + + .card:nth-child(odd){ + transform: rotate(1deg); + } + + .card:nth-child(5){ + transform: rotate(2deg); + } + + + .video.box{ + margin-top:3em; + } + + .bar{ + border: 2px solid var(--border-color); + box-shadow: 0.6em 0.6em 0 #d2d1c8; + margin-bottom: 2em; + margin-top:3em; + display: inline-block; + background-color: #fff09d; + } + +.h-feed{ + display: flex; + flex-flow: row wrap; + width: 100%; + +} + +.entries{ + padding-top: 10%; +} + + +/* base header & menu */ + +#top-menu{ + position: fixed; + left: 5%; + transform: translate(-50%); + width: 30%; + z-index: 1; + margin-top: 1em; + transform: rotate(-3deg); +} + +.menu-dropdown summary{ + list-style: none; + cursor: pointer; +} + +.dropdown-menu summary::-webkit-details-marker { + display: none; +} + +.logo { + margin-left: 0.5em; + margin-right: 0.5em; + margin-top: 0.2em; + margin-bottom: 0.2em; +} + +.logo a { + text-decoration: none; +} + +.menu { + border-top: 2px solid var(--border-color); + margin: 0px; + padding: 0px; + +} + +.menu ul{ + list-style-type: none; + margin: 0; + padding: 0; + display: flex; +} + +.menu-nav-item { + border-right: 2px solid var(--border-color); + padding: 0.5em; +} + +/*Article Summary Cards*/ + +.h-entry header { + display: flex; + border-bottom: 2px solid var(--border-color); + justify-content: space-between; +} + +.h-entry header h2{ + padding: 0.2em; + margin: 0; + padding-right: 0.3em; + padding-left: 0.3em; + border-left: 2px solid var(--border-color); + flex-grow: 1; +} + +.h-entry header h2:hover{ + box-shadow: inset 4px 4px 0px tomato; + cursor: pointer; +} + +.h-entry header h2 a { + text-decoration: none; + color: var(--border-color); +} + + +.h-entry header .header-metadata{ + margin: 0; + display: flex; + flex-flow: column wrap; + font-size: 0.8rem; +} + +.header-metadata .dt-published{ + padding: 0.5em 1.2em 0.5em 1.2em; +} + +.author.p-author { + border-top: 2px solid var(--border-color); + padding: 0.5em 1.2em 0.5em 1.2em; +} + + +.p-summary.truncated.image { + display: flex; + flex-direction: row-reverse; +} + +.p-summary.truncated { + display: flex; + flex-direction: column; +} + +.summary-text { + flex: 1; + padding: 1em; + min-width: 34ch; + text-overflow: ellipsis; + overflow: hidden; +} + +.summary-image > img { +/* height: 100%; + object-fit: cover; + max-width: 100%;*/ + +} + +.summary-image > a { + display: flex; +} + +.summary-image{ + border-right: 2px solid var(--border-color); +} + +footer.post-footer { + display: flex; + flex-flow: row-reverse; +} + +.footer-filler{ + border-top: 2px solid var(--border-color); + flex-grow: 1; +} +.read-more { + border-top: 2px solid var(--border-color); + border-left: 2px solid var(--border-color); + align-content: flex-end; + padding: 0.2em 1em 0.2em 1em; + font-size: 0.9rem; +} + +/* network cards */ + +.card.network{ + + border: 2px solid darkcyan; + box-shadow:1em 1em 0 #d2d1c8; + background-color: lightgreen; + max-width: min-content; + margin-bottom: 2em; + flex: auto; + margin: 0 3em 3em 0; + align-self: start; + color: darkcyan; + } + +.h-entry.network header { + display: flex; + border-bottom: 2px solid darkcyan; + flex-direction: row-reverse; +} + +.h-entry.network header h2{ + padding: 0.2em 0.5em 0.2em 0.5em; + margin: 0; + border: none; +} + +.h-entry.network header h2:hover{ + box-shadow: inset 4px 4px 0px darkcyan; + cursor: pointer; +} + +.h-entry.network header h2 a { + text-decoration: none; + color: darkcyan; +} + +.network .header-metadata { + align-items: center; +} + +.network .header-metadata .dt-published{ + + border-bottom: 2px solid darkcyan; +} + +.network .filler { + min-height: 1rem; +} + +.network .author.p-author { + border-color: darkcyan; + padding: 0.5em 1.2em 0.5em 1.2em; +} + +.network .p-summary { + display: flex; +} + +.network .p-summary.portrait { + flex-direction: row; +} +.network .p-summary.landscape{ + flex-direction: column; +} +.network .summary-image.portrait { + border-right: 2px solid darkcyan; +} +.network .summary-image.landscape { + border-bottom: 2px solid darkcyan; + border-right: none; +} + + +.network .summary-image > img { + display: inherit; +} +.network .summary-text { + font-size: 18px; +} + +div.network-source{ + padding: 0.5em 1em 0.5em 1em; + border-bottom: 2px solid darkcyan; + font-size: 14px; + display: flex; + justify-content: space-between; +} + +.network-source a { + font-weight: bold; + color: darkcyan; +} + +.network .footer-filler{ + border-left: 2px solid darkcyan; + border-top: none; +} + +.network footer.post-footer{ + border-top: 2px solid darkcyan; + flex-flow: row; + font-size: 0.9rem; +} + +.network .read-more { + border: none; + border-left: 2px solid darkcyan; + padding: 0.5em 1.2em 0.5em 1.2em; +} + +.network .footer-metadata { + padding: 0.5em 1.2em 0.5em 1.2em; +} + +/* shouts cards */ + +.card.shout{ + border-color: steelblue; + border: 2px solid; + box-shadow:1em 1em 0 #d2d1c8; + background-color: aliceblue; + max-width: min-content; + margin-bottom: 2em; + flex: auto; + margin: 0 3em 3em 0; + align-self: start; + color: steelblue; + } + +/* calendar cards */ + +.card.calendar { + border: 2px solid cornflowerblue; + box-shadow:1em 1em 0 #d2d1c8; + background-color: lightblue; + max-width: 360px; + margin-bottom: 2em; + flex: auto; + margin: 0 3em 3em 0; + align-self: start; + color: royalblue; + +} + +.card.calendar.past { + opacity: 0.3; +} + +.card.calendar.past:hover { + opacity: 1; +} + +.h-event.calendar header { + display: flex; + border-bottom: 2px solid cornflowerblue; +} + +.h-event.calendar header h2{ + padding: 0.2em 0.5em 0.2em 0.5em; + margin: 0; + border-right: none; +} + +.h-event.calendar header h2:hover{ + box-shadow: inset 4px 4px 0px royalblue; + cursor: pointer; +} + +.h-event.calendar header h2 a { + text-decoration: none; + color: royalblue; +} + +.header-filler { + min-width: 10%; +} + +.calendar-location{ + font-size: 0.8rem; + min-width: 20%; + padding: 0.5em 0.9em 0.5em 0.9em; + border-left: 2px solid cornflowerblue; +} + +.calendar-duration{ + font-size: 0.8rem; + border-right: 2px solid cornflowerblue; + padding: 0.5em 0.9em 0.5em 0.9em; +} + +.start-scroller { + display: flex; + flex-flow: row-reverse; + border-bottom: 2px solid cornflowerblue; +} +.start-scroller marquee{ + font-size: 0.8rem; + padding-top: 0.2em; + padding-bottom: 0.2em; +} + +.calendar .description { + border-top: 2px solid cornflowerblue; +} + +.calendar-image-holder{ + border-bottom: 2px solid cornflowerblue; +} + +.calendar-image-holder a { + display: inherit; +} + +.calendar-image{ + max-width: 100%; + display: inherit; +} + +/* Card metadata (video & calendar) */ + +.metadata { + display: flex; + justify-content: space-between; + flex-wrap: wrap; +} + +.description p:first-of-type { + margin:0; +} + +.description ul:first-of-type { + margin:0; +} + +input + label +.calendar-location+.description{ + display: none; + } + +input:checked + label +.calendar-location+.description { + display: block; + transition: ease .5s; + } + +.metadata label { + text-align: center; + vertical-align: sub; + flex-grow: 1; + font-weight: normal; + cursor: pointer; + padding: 0.4em 0.9em 0.4em 0.9em; + font-size: 0.9em; +} + +label:hover { + box-shadow: inset 2px 2px 0px #95948c; +} + +.description{ + padding: 0.5em 0.7em 0.7em 0.5em; + overflow: hidden; + flex-basis: 100%; +} + +.descr_button { + cursor: pointer; + flex-grow: 1; + text-align: center; +} + +/* Paginator */ + +nav.pagination{ + width: 60%; + margin: auto; + margin-top: 2em; + margin-bottom: 2em; +} + +ul.pagination{ + display: flex; + justify-content: space-evenly; /* align horizontal */ + align-items: center; /* align vertical */ +} + +.page-item{ + display: block; + text-align: center; + vertical-align: middle; + font-size: 38px; + border: 2px solid #1B4C8A; + box-shadow:0.4em 0.4em 0 #d2d1c8; + +} + +li.page-item{ + background-color: lightblue; + padding: 0.4em; +} + +li.page-item.active{ + background-color: peachpuff; + border: 2px solid tomato; + padding: 0.4em; +} + +li.page-item.disabled{ + display: none; +} + + li.page-item:nth-child(even){ + transform: rotate(-1deg); + } + + li.page-item:nth-child(odd){ + transform: rotate(1deg); + } + + li.page-item:nth-child(5){ + transform: rotate(2deg); + } + + li.page-item:nth-child(8){ + transform: rotate(-3deg); + } + + + +/* Page footer */ + +footer.bar { + margin-top: 0; + width: 80%; + margin-left: auto; + display: block; + margin-right: auto; + margin-bottom: 2em; +} \ No newline at end of file diff --git a/lumbung-theme/static/css/print.css b/lumbung-theme/static/css/print.css new file mode 100644 index 0000000..e69de29 diff --git a/lumbung-theme/static/css/video-box.css b/lumbung-theme/static/css/video-box.css new file mode 100644 index 0000000..051cd70 --- /dev/null +++ b/lumbung-theme/static/css/video-box.css @@ -0,0 +1,155 @@ +:root { + --video-border-color: burlywood; + --video-background-color: antiquewhite; +} + .video-box { + border:2px solid var(--video-border-color); + max-width:560px; + margin:auto; + box-shadow:1em 1em 0 #d2d1c8; + margin-bottom: 2em; + color: chocolate; + } + + .video-box:nth-child(even){ + transform: rotate(-1deg); + } + + .video-box:nth-child(odd){ + transform: rotate(1deg); + } + + .video-box:nth-child(5){ + transform: rotate(3deg); + } + + + .video-box img { + max-width: 100%; + } + + .video-box iframe { + max-width: 100%; +} + + .video-box .media { + line-height: 0; + } + + .video { + background-color: var(--video-background-color); + } + + .video .metadata{ + font-size:0.9rem; + justify-content: space-between; + flex-wrap: wrap; + } + + .metadata .title{ + margin-top:0; + border-top: 2px solid var(--video-border-color); + border-bottom: 2px solid var(--video-border-color); + padding:0.5em; + font-weight:700; + font-size:1.3rem; + flex-basis: 100%; + } + + .video.channel{ + border-right: 2px solid var(--video-border-color); + padding: 0.5em 0.9em 0.5em 0.9em; + font-size: 0.8rem; + } + + .video.date { + float:right; + border-left: 2px solid var(--video-border-color); + padding: 0.5em 0.9em 0.5em 0.9em; + font-size: 0.8rem; + } + + .video.description { + border-top: 2px solid var(--video-border-color); + padding: 0.8em 0.8em 0.8em 0.8em; + + } + .descr_button a { + color:inherit; + text-decoration: inherit; + } + + input.descr_button { + display: none; + } + + input + label + .video.date + .description{ + display: none; + } + + input:checked + label + .video.date +.description { + display: block; + } + + .play-icon { + width: 0; + height: 0; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%,-50%) scale(.5); + border-top: 13px solid transparent; + border-bottom: 13px solid transparent; + border-left: 18px solid hsla(0,0%,100%,.95); + } + + .video-thumbnail { + position: absolute; + width: 100%; + height: 100%; + top: 0; + } + .video-thumbnail { + display: flex; + flex-direction: column; + position: relative; + overflow: hidden; + background-color: #ececec; + transition: filter .2s ease; + } + +.video-thumbnail-duration-overlay { + display: inline-block; + background-color: var(--video-background-color); + color: chocolate; + font-size: 14px; + line-height: 1.1; + z-index: 10; + position: absolute; + padding: 1px 3px 1px 3px; + right: 5px; + bottom: 5px; + border: 2px solid var(--video-border-color); + } + + .play-overlay { + transition: all .2s ease; + position: absolute; + right: 0; + bottom: 0; + width: inherit; + height: inherit; + opacity: 0; + background-color: rgba(0,0,0,.3); + cursor: pointer; + } + +.video-thumbnail:hover { + text-decoration:none!important +} +.video-thumbnail:hover .play-overlay { +opacity:1 +} +.video-thumbnail:hover .play-overlay .play-icon { +transform:translate(-50%,-50%) scale(1) +} diff --git a/lumbung-theme/theme.toml b/lumbung-theme/theme.toml new file mode 100644 index 0000000..72ac31f --- /dev/null +++ b/lumbung-theme/theme.toml @@ -0,0 +1,21 @@ +# theme.toml template for a Hugo theme +# See https://github.com/gohugoio/hugoThemes#themetoml for an example + +name = "Lumbung" +license = "AGPL3" +licenselink = "https://github.com/yourname/yourtheme/blob/master/LICENSE" +description = "" +homepage = "http://example.com/" +tags = [] +features = [] +min_version = "0.41.0" + +[author] + name = "" + homepage = "" + +# If porting an existing theme +[original] + name = "" + homepage = "" + repo = "" diff --git a/lumbung-video-prototype/README.md b/lumbung-video-prototype/README.md new file mode 100644 index 0000000..a5d9115 --- /dev/null +++ b/lumbung-video-prototype/README.md @@ -0,0 +1,27 @@ +# video feed prototypes + +These scripts poll a peertube instance to return a list of videos and construct a static page for it using jinja2. + +See it in action on + +## video-feed.py + +Utility that returns Peertube videos tagged as `publish` and turns them in to `hugo` page bundles. Videos no longer tagged as `publish` are deleted. + +### index-template.md + +Jinja2 template of a hugo post for use with the above. + +## streams-feed.py + +Returns only livestreams and displays them differently depending on the tags associated with the video. E.g. audio stream or video stream. WIP. + +### video-feed.html +The jinja template for creating video feeds. This is now used in the HUGO theme. + +### video-feed-prototype.html +rendered example of above + + + + diff --git a/lumbung-video-prototype/index_template.md b/lumbung-video-prototype/index_template.md new file mode 100644 index 0000000..898746b --- /dev/null +++ b/lumbung-video-prototype/index_template.md @@ -0,0 +1,15 @@ +--- +title: "{{ v.name }}" +date: "{{ v.published_at }}" #2021-06-10T10:46:33+02:00 +draft: false +uuid: "{{v.uuid}}" +video_duration: "{{ v.duration | duration }} " +video_channel: "{{ v.channel.display_name }}" +channel_url: "{{ v.channel.url }}" +preview_image: "{{ preview_image }}" +categories: ["tv","{{ v.channel.display_name }}"] +is_live: {{ v.is_live }} + +--- + +{{ v.description }} diff --git a/lumbung-video-prototype/requirements.txt b/lumbung-video-prototype/requirements.txt new file mode 100644 index 0000000..eaec3c5 --- /dev/null +++ b/lumbung-video-prototype/requirements.txt @@ -0,0 +1,12 @@ +# Automatically generated by https://github.com/damnever/pigar. + +# video_feed/streams-feed.py: 7 +# video_feed/video-feed.py: 7 +Jinja2 == 2.10 + +# video_feed/streams-feed.py: 6 +# video_feed/video-feed.py: 6 +git+https://framagit.org/framasoft/peertube/clients/python.git + +# video_feed/video-feed.py: 12 +requests == 2.21.0 diff --git a/lumbung-video-prototype/video-feed.html b/lumbung-video-prototype/video-feed.html new file mode 100644 index 0000000..5ce0551 --- /dev/null +++ b/lumbung-video-prototype/video-feed.html @@ -0,0 +1,251 @@ + + + + + + + lumbung.space video archive prototype + + + + + + + + + + + + + + + + + + + + + + + +
+ {% for video in videos %} +
+
+ + +
+ {% if video.is_live %} + LIVE + {% else %} + {{ video.duration | duration }} + {% endif %} +
+
+
+
+ +
+ +
+ {% endfor %} +
+ + + + diff --git a/lumbung-video-prototype/video-feed.py b/lumbung-video-prototype/video-feed.py new file mode 100644 index 0000000..15f7da3 --- /dev/null +++ b/lumbung-video-prototype/video-feed.py @@ -0,0 +1,131 @@ +#!/bin/python3 + +#lumbung.space video feed generator +#c 2021 roel roscam abbing gpvl3 etc + +import peertube +import jinja2 +import json +import os +import datetime +import shutil +import requests +import ast +import arrow + + +#jinja filters & config +def duration(n): + """ + convert '6655' in '1:50:55' + + """ + return str(datetime.timedelta(seconds = n)) + +def linebreaks(text): + if not text: + return text + else: + import re + br = re.compile(r"(\r\n|\r|\n)") + return br.sub(r"
\n", text) + + +env = jinja2.Environment( + loader=jinja2.FileSystemLoader(os.path.curdir) + ) +env.filters['duration'] = duration +env.filters['linebreaks'] = linebreaks + +host = 'https://tv.lumbung.space' + +configuration = peertube.Configuration( + host = host+"/api/v1" +) + +client = peertube.ApiClient(configuration) + +v = peertube.VideoApi(client) + +response = v.videos_get(count=100, filter='local', tags_one_of='publish') + +videos = response.to_dict() +videos = videos['data'] + + +def create_post(post_directory, video_metadata): + global client #lazy + + if not os.path.exists(post_dir): + os.mkdir(post_directory) + + preview_image = video_metadata['preview_path'].split('/')[-1] + + if not os.path.exists(os.path.join(post_directory, preview_image)): + #download preview image + response = requests.get(host+video_metadata['preview_path'], stream=True) + with open(os.path.join(post_directory, preview_image), 'wb') as img_file: + shutil.copyfileobj(response.raw, img_file) + print('Downloaded cover image') + + #replace the truncated description with the full video description + #peertube api is some broken thing in between a py dict and a json file + api_response = peertube.VideoApi(client).videos_id_description_get(video_metadata['uuid']) + long_description = ast.literal_eval(api_response) + video_metadata['description'] = long_description['description'] + + + with open(os.path.join(post_directory,'index.md'),'w') as f: + post = template.render(v=video_metadata, host=host, preview_image=preview_image) + f.write(post) + + + with open(os.path.join(post_directory, '.timestamp'), 'w') as f: + timestamp = arrow.get(video_metadata['updated_at']) + f.write(timestamp.format('X')) + +def update_post(post_directory, video_metadata): + if os.path.exists(post_directory): + if os.path.exists(os.path.join(post_directory,'.timestamp')): + old_timestamp = open(os.path.join(post_directory,'.timestamp')).read() + + #FIXME: this is ugly but I need to do this because arrow removes miliseconds + current_timestamp = arrow.get(video_metadata['updated_at']) + current_timestamp = arrow.get(current_timestamp.format('X')) + + if current_timestamp > arrow.get(old_timestamp): + print('Updating', video_metadata['name'], '({})'.format(video_metadata['uuid'])) + create_post(post_dir, video_metadata) + else: + print('Video current: ', video_metadata['name'], '({})'.format(video_metadata['uuid'])) + else: + #compat for when there is no timestamp yet.. + create_post(post_dir, video_metadata) + + +output_dir = os.environ.get('OUTPUT_DIR', '/home/r/Programming/lumbung.space/lumbung.space-web/content/video') + +if not os.path.exists(output_dir): + os.mkdir(output_dir) + +template = env.get_template('index_template.md') + +existing_posts = os.listdir(output_dir) + +for video_metadata in videos: + post_dir = os.path.join(output_dir, video_metadata['uuid']) + + if video_metadata['uuid'] not in existing_posts: #if there is a video we dont already have, make it + print('New: ', video_metadata['name'], '({})'.format(video_metadata['uuid'])) + create_post(post_dir, video_metadata) + + elif video_metadata['uuid'] in existing_posts: # if we already have the video do nothing, possibly update + update_post(post_dir, video_metadata) + existing_posts.remove(video_metadata['uuid']) # create list of posts which have not been returned by peertube + +for post in existing_posts: + print('deleted', post) #rm posts not returned + shutil.rmtree(os.path.join(output_dir,post)) + + +