LunaGen/LunaGen/src/lunagen_backup.py

479 lines
19 KiB
Python
Executable File

#!/usr/bin/python3
# encoding: utf-8
'''
lunagen -- "Lunatics!" Project Release Site Generator
LunaGen generates a small static HTML website based on YAML
data documents containing the key release information. This
generates an index.html file containing the episode lists
for all series, individual release pages for each episode,
and additional pages for character list and story background.
This is meant to be a fan-focused site.
It also contains affiliate link, sponsor, advertising, and
other fundraising elements.
@author: Terry Hancock
@copyright: 2019 Anansi Spaceworks.
@license: GNU General Public License, version 2.0 or later. (Python code)
Creative Commons Attribution-ShareAlike, version 3.0 or later. (Website Templates).
@contact: digitante@gmail.com
'''
import sys
import os
#from shutil import copytree, rmtree
from distutils.dir_util import remove_tree, copy_tree
from optparse import OptionParser
import random
from collections import OrderedDict
import yaml
import jinja2
import lunagen
lunagen.demo()
class Config(object):
"""
Installation configuration variables.
TODO: Probably change this to load from a YAML file in the future.
"""
__all__ = []
__version__ = 0.1
__date__ = '2019-09-19'
__updated__ = '2019-09-19'
DEBUG = True
TESTRUN = False
PROFILE = False
THEMES = '../themes'
class LunaGen(jinja2.Environment):
"""
Generator for a LunaGen site, based on contents of srcdir:
<srcdir>/data - YAML files (handwritten content)
<srcdir>/templates - Jinja2 site templates
<srcdir>/skeleton - unchanging parts of the site (copied)
The new site is created in <tgtdir>.
"""
def __init__(self, srcdir, tgtdir=None, verbosity=0):
self.srcdir = os.path.abspath(srcdir)
if not tgtdir:
self.tgtdir = os.path.join(self.srcdir, 'site')
else:
self.tgtdir = os.path.abspath(tgtdir)
self.datadir = os.path.join(self.srcdir, 'data')
self.templates = os.path.join(self.srcdir, 'templates')
self.skeleton = os.path.join(self.srcdir, 'skeleton')
self.verbose = verbosity
if self.verbose:
print("Source directory: %s" % self.srcdir)
print("Target directory: %s" % self.tgtdir)
print("YAML content should be in: %s" % self.datadir)
print("Jinja2 templates should be in: %s" % self.templates)
print("Skeleton website should be in: %s" % self.skeleton)
#TODO: Could make sure these directories exist
# Load up the data from YAML files:
self._load_sitedata()
self._load_theme()
self._load_affiliates()
self._load_softwarelist()
self._load_products()
self._load_serieslist()
super().__init__(
loader=jinja2.ChoiceLoader([
jinja2.FileSystemLoader(os.path.join(self.datadir, 'templates')),
jinja2.FileSystemLoader(self.theme['path']),
jinja2.FileSystemLoader(self.templates)]),
autoescape=jinja2.select_autoescape(['html','xml']))
@staticmethod
def _paginate(seq, pagesize):
"""
Given a sequence of objects, break it into a book of
pages, each containing no more than pagesize objects:
>>> test = [1,'2','three', 4, 5, 'six', 'seven', 8, 9, True, False, None, 0]
>>> LunaGen._paginate(test, 4)
[[1, '2', 'three', 4], [5, 'six', 'seven', 8], [9, True, False, None], [0]]
>>>
"""
book = []
page = []
for i,ob in enumerate(seq):
if i%pagesize==0:
if i>0: book.append(page)
page = []
page.append(ob)
if len(page)>0:
book.append(page)
return book
@staticmethod
def _paginate_sponsors(series, episode):
"""
Regroup sponsors into pages which:
- Contain only one kind of sponsor
- Contain no more than the 'page' limit number of sponsors per page
- Are tagged with the sponsortype so we can find the right tempate for them:
>>> series = {'sponsortypes':
... {'A':{'page':1,'limit':1},
... 'B':{'page':3, 'limit':10},
... 'C':{'page':4, 'limit':20}}}
...
>>> episode = {'sponsors':
... {'A':list(range(2)),
... 'B':list(range(7)),
... 'C':list(range(22))}}
...
>>> LunaGen._paginate_sponsors(series, episode)
[('A', [0]), ('B', [0, 1, 2]), ('B', [3, 4, 5]), ('B', [6]), ('C', [0, 1, 2, 3]), ('C', [4, 5, 6, 7]), ('C', [8, 9, 10, 11]), ('C', [12, 13, 14, 15]), ('C', [16, 17, 18, 19])]
>>>
"""
paged_sponsors = []
for spkey, sponsortype in series['sponsortypes'].items():
if spkey not in episode['sponsors']:
episode['sponsors'][spkey] = []
#if 'excludes' in sponsortype:
# for excluded in sponsortype['excludes']:
# if excluded in episode['sponsors'] and episode['sponsors'][excluded]:
# print("WARNING: excluded sponsortype %s will be ignored, because of existing %s." %
# (excluded, spkey))
if 'page'in sponsortype:
paged = LunaGen._paginate(
episode['sponsors'][spkey][:sponsortype['limit']],
sponsortype['page'])
tags = [spkey] * len(paged)
paged_sponsors.extend(zip(tags, paged))
return paged_sponsors
@staticmethod
def _fix_series(series):
"""
Modify series data to correct certain datatypes that are not
natively supported by YAML (like OrderedDict):
>>> series = {'credits':{
... 'a':{'labels':'ordered'},
... 'b':{'labels':[['A','-A-'],['B','-B-']]}}}
...
>>> LunaGen._fix_series(series)
>>> series['credits']['b']['labels']
OrderedDict([('A', '-A-'), ('B', '-B-')])
>>>
"""
for key, credit in series['credits'].items():
if type(credit['labels']) != type(''):
credit['labels']=OrderedDict(credit['labels'])
def _collect_stylesheets(self, *extras):
"""
Collect a list of unique stylesheets from various stylesheet
requirements from theme, site, and data from extra pages.
"""
stylesheets = []
stylesheets.extend(self.theme['stylesheets'])
stylesheets.extend(self.sitedata['stylesheets'])
for extra in extras:
if 'stylesheets' in extra:
stylesheets.extend(extra['stylesheets'])
stylesheets = [s for i,s in enumerate(stylesheets) if s not in stylesheets[:i]]
return stylesheets
def _load_sitedata(self):
if self.verbose: print("Loading global site data.")
with open(os.path.join(self.datadir, 'site.yaml'), 'rt') as sitedatafile:
self.sitedata = yaml.load(sitedatafile)
def _load_theme(self):
if self.verbose: print("Loading theme data.")
self.theme = { 'stylesheets':[] } # Default values
themedir = os.path.join(Config.THEMES, self.sitedata['theme'])
if not os.path.exists(themedir):
raise FileNotFoundError("Theme directory %s not found!" % themedir)
with open(os.path.join(themedir, 'theme.yaml'), 'rt') as themedatafile:
self.theme.update(yaml.load(themedatafile))
self.theme['path'] = themedir
def _load_affiliates(self):
if self.verbose: print("Loading affiliates data.")
try:
with open(os.path.join(self.datadir, 'affiliates.yaml')) as aff_file:
affiliates = yaml.load(aff_file)
stylesheets = self.sitedata['stylesheets']
self.sitedata.update(affiliates)
self.sitedata['stylesheets'] = self._collect_stylesheets(affiliates)
self.sitedata['affiliates'] = random.sample(
affiliates['affiliates'], min( int(affiliates['affiliates_at_once']),
len(affiliates['affiliates'])))
except FileNotFoundError:
print("No affiliates.yaml file, so affiliates list is empty.")
self.sitedata['affiliates'] = []
def _load_softwarelist(self):
if self.verbose: print("Loading software data.")
try:
with open(os.path.join(self.datadir, 'software.yaml')) as sw_file:
softwarelist = yaml.load(sw_file)
stylesheets = self.sitedata['stylesheets']
self.sitedata.update(softwarelist)
self.sitedata['stylesheets'] = self._collect_stylesheets(softwarelist)
except FileNotFoundError:
print("No software.yaml file, so software list is empty.")
self.sitedata['softwarelist'] = []
def _load_products(self):
if self.verbose: print("Loading store products data.")
try:
with open(os.path.join(self.datadir, 'products.yaml')) as prod_file:
products = yaml.load(prod_file)
stylesheets = self.sitedata['stylesheets']
self.sitedata.update(products)
self.sitedata['stylesheets'] = self._collect_stylesheets(products)
except FileNotFoundError:
print("No products.yaml file, so software list is empty.")
self.sitedata['products'] = []
def _load_serieslist(self):
if self.verbose: print("Loading series data")
try:
with open(os.path.join(self.datadir, 'episodes', 'series.yaml'),'rt') as seriesfile:
self.serieslist = yaml.load(seriesfile)['serieslist']
for series in self.serieslist:
self._fix_series(series)
episodes = []
seriesdir = os.path.join(self.datadir, 'episodes', series['directory'])
episode_filenames = [f for f in os.listdir(seriesdir) if f.endswith('.yaml')]
for episode_filename in episode_filenames:
if self.verbose: print("Loading episode from %s" % episode_filename)
with open(os.path.join(seriesdir, episode_filename), 'rt') as episode_file:
episodes.append(yaml.load(episode_file))
# Sort by episode number specified in the files:
try:
episodes.sort(key=lambda a: int(a['episode']))
except KeyError:
print("Some episode YAML files may not have an 'episode' number entry?")
series['episodes'] = episodes
except FileNotFoundError:
print("No series.yaml file, so no series loaded.")
self.sitedata['serieslist'] = []
def _copy_skeleton(self):
if os.path.exists(self.tgtdir):
remove_tree(self.tgtdir, verbose=self.verbose)
if self.verbose: print("Copying the theme base.")
copy_tree(os.path.join(self.theme['path'], 'base'), self.tgtdir, verbose=self.verbose)
if self.verbose: print("Copying the skeleton site.")
copy_tree(self.skeleton, self.tgtdir, verbose=self.verbose)
def _gen_simple_page(self, pagename, stylesheets=()):
"""
Generates a simple page with 1:1:1 Jinja2+YAML+CSS.
The YAML and CSS pages are optional. If no file exists
for them, they will be ignored and the page generated
with only the template and global data and/or style.
"""
if self.verbose: print("Creating '%s' page" % pagename)
jinja2_name = pagename + '.j2'
yaml_path = os.path.join(self.datadir, pagename+'.yaml')
css_path = os.path.join(self.tgtdir, pagename+'.css')
# Assumes skeleton already copied
data = {} # Defaults can be set here
data.update(self.sitedata) # Global data
if os.path.exists(yaml_path):
with open(yaml_path, 'rt') as yaml_file:
data.update(yaml.load(yaml_file)) # Page data
data['stylesheets'] = self._collect_stylesheets(data)
# Add CSS if not already present:
if os.path.exists(css_path) and pagename not in data['stylesheets']:
data['stylesheets'].append(pagename)
if self.verbose: print("Generating '%s.html' from template." % pagename)
html = self.get_template(jinja2_name).render(data)
with open(os.path.join(self.tgtdir, pagename+'.html'), 'wt') as page:
page.write(html)
def _gen_index(self):
"""
Generate an index page, if the skeleton doesn't already have one.
"""
if not os.path.exists(os.path.join(self.tgtdir, 'index.html')):
if self.verbose: print("Generating the Index page.")
data = {}
data.update(self.sitedata)
data['next'] = next # Adds iterator capability
data['stylesheets'] = self._collect_stylesheets(self.sitedata['stylesheets'])
if 'episode_as_index' in self.sitedata and self.sitedata['episode_as_index']:
data['serieslist'] = self.serieslist
data['banners'] = iter(['affiliates_banner.j2',
'store_banner.j2',
'sponsoropps_banner.j2'])
data['stylesheets'].extend(self._collect_stylesheets(
self.sitedata['episode_list_page']))
html = self.get_template('index.j2').render(data)
with open(os.path.join(self.tgtdir, 'index.html'), 'wt') as page:
page.write(html)
else:
if self.verbose: print("Found 'index.html', so not generated.")
def _gen_episode_list_page(self):
"""
Generate a page linking to all of the individual episodes,
grouped into "series" (or "seasons").
#Currently hard-coded to be saved as 'index.html' for the site.
"""
if self.verbose: print("Generating the Index (Episode List) page.")
if 'render_as' in self.sitedata['episode_list_page']:
render_as = self.sitedata['episode_list_page']['render_as']
else:
render_as = 'index.html'
data = {}
data.update(self.sitedata)
data['serieslist'] = self.serieslist
data['banners'] = iter(['affiliates_banner.j2', 'store_banner.j2', 'sponsoropps_banner.j2'])
data['stylesheets'] = self._collect_stylesheets(self.sitedata['episode_list_page'])
data['next'] = next # Adds iterator capability
html = self.get_template('episode_list.j2').render(data)
with open(os.path.join(self.tgtdir, render_as), 'wt') as page:
page.write(html)
def _gen_episode_pages(self):
"""
Generate a page for each episode in each series.
"""
if self.verbose: print("Generating episode pages...")
if 'stylesheets' in self.sitedata['episode_pages']:
stylesheets = self.sitedata['episode_pages']['stylesheets']
else:
stylesheets = []
for series in self.serieslist:
for episode in series['episodes']:
paged_sponsors = self._paginate_sponsors(series, episode)
episode['paged_sponsors'] = iter(paged_sponsors)
data = {}
data.update(self.sitedata)
data['series'] = series
data['episode'] = episode
data['stylesheets'] = self._collect_stylesheets(self.sitedata['episode_pages'],episode)
data['next'] = next
data['banners'] = ['affiliates_banner.j2']
html = self.get_template('episode_page.j2').render(data)
filename = episode['series'] +'E' + ('%2.2d' % int(episode['episode'])) + '.html'
os.makedirs(os.path.join(self.tgtdir, series['directory']), exist_ok=True)
with open(os.path.join(self.tgtdir, series['directory'], filename), 'wt') as page:
page.write(html)
def gensite(self):
"""
Generate the site, using the data we've accumulated.
"""
self._copy_skeleton()
for page in self.sitedata['simple_pages']:
self._gen_simple_page(page)
if self.sitedata['serieslist']:
self._gen_episode_list_page()
self._gen_episode_pages()
else:
print("Not generating series & episode pages: serieslist empty.")
self._gen_index()
def main(argv=None):
'''Command line options.'''
program_name = os.path.basename(sys.argv[0])
program_version = "v0.1"
program_build_date = "%s" % Config.__updated__
program_version_string = '%%prog %s (%s)' % (program_version, program_build_date)
program_longdesc = '''\
LunaGen is a static HTML website generator designed for releasing
a series of episodes (technically: a series of series of episodes).
Data is authored using the YAML structured data language, which allows
for episode metadata and descriptions to be written in a human-friendly
format, which is then formatted into HTML using Jinja2 templates.
Once generated, the site is static and can simply be uploaded to a
standard web server with minimal or no configuration (like a static web host).
It was originally created to generate the release pages for
Anansi Spaceworks' "Lunatics!" series.
For details, please see the 'examples' and 'doc' directories.
'''
program_license = "Copyright 2019 Terry Hancock (Anansi Spaceworks) \
Licensed under the GNU General Public License, version 2.0\n"
if argv is None:
argv = sys.argv[1:]
#try:
# setup option parser
parser = OptionParser(version=program_version_string, epilog=program_longdesc, description=program_license)
parser.add_option("-i", "--in", dest="src", help="set input path [default: %default]", metavar="FILE")
parser.add_option("-o", "--out", dest="tgt", help="set output path [default: %default]", metavar="FILE")
parser.add_option("-v", "--verbose", dest="verbose", action="count", help="set verbosity level [default: %default]")
# set defaults
parser.set_defaults(tgt="./site", src=".")
# process options
(opts, args) = parser.parse_args(argv)
if opts.verbose > 0:
print("verbosity level = %d" % opts.verbose)
if opts.src:
print("src = %s" % opts.src)
if opts.tgt:
print("tgt = %s" % opts.tgt)
lunagen = LunaGen(opts.src, opts.tgt, opts.verbose)
lunagen.gensite()
# except Exception as e:
# indent = len(program_name) * " "
# sys.stderr.write(program_name + ": " + repr(e) + "\n")
# sys.stderr.write(indent + " for help use --help")
# return 2
if __name__ == "__main__":
if Config.DEBUG:
#sys.argv.append("-h")
sys.argv.append("-v")
if Config.TESTRUN:
import doctest
doctest.testmod()
if Config.PROFILE:
import cProfile
import pstats
profile_filename = 'lunagen_profile.txt'
cProfile.run('main()', profile_filename)
statsfile = open("profile_stats.txt", "wb")
p = pstats.Stats(profile_filename, stream=statsfile)
stats = p.strip_dirs().sort_stats('cumulative')
stats.print_stats()
statsfile.close()
sys.exit(0)
sys.exit(main())