LunaGen/LunaGen/src/lunagen/main.py

445 lines
18 KiB
Python

'''
Created on Oct 23, 2021
@author: terry
'''
import os, random, importlib
#from shutil import copytree, rmtree
from distutils.dir_util import remove_tree, copy_tree
from collections import OrderedDict
import yaml
import jinja2
import lunagen.addon
print("Addon variable:")
print(repr(lunagen.addon))
def demo():
print("Am able to load and run code from 'lunagen' library.")
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, seed=None):
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')
if verbosity:
self.verbose = verbosity
else:
self.verbose = 0
if seed is not None:
if self.verbose: print("Setting random number seed to %d", seed)
# Useful for testing
random.seed(seed)
if self.verbose:
print("Verbose mode %d" % 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
# Create the internal "queues" dictionary
self._queues = {}
# Load up the data from YAML files:
self._load_sitedata()
self._load_theme()
self.template_paths = [
self.templates, # Designer's own templates override all
self.theme['path'], # Theme templates
]
self.load_site_addons()
self.template_paths.append(
os.path.join(os.path.dirname(__file__), 'templates')
)
if self.verbose:
print("Template source paths:")
for template_path in self.template_paths:
print(" %s" % template_path)
print("\n")
# TODO: The following are the loaders we want to move into add-ons
#self._load_affiliates()
#self._load_softwarelist()
#self._load_products()
self._load_serieslist()
super().__init__(
loader=jinja2.ChoiceLoader(
[jinja2.FileSystemLoader(p) for p in self.template_paths]
),
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 template 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 = []
if 'stylesheets' in self.theme:
stylesheets.extend(self.theme['stylesheets'])
if 'stylesheets' in self.sitedata:
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.safe_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.safe_load(themedatafile))
self.theme['path'] = themedir
def load_site_addons(self):
"""
Load the addons listed in the site.yaml file, in
the order specified.
"""
self.addons = []
if 'addons' in self.sitedata and self.sitedata['addons']:
for addon_name in self.sitedata['addons']:
# Import the add-on module
if self.verbose: print("Add-on to load: %s" % addon_name)
addon_mod = importlib.import_module(
'.' + '.'.join(('addons', addon_name, addon_name)),
package='lunagen')
# Register location for templates:
self.template_paths.append(os.path.join(os.path.dirname(__file__),
'addons', addon_name))
# Find, instantiate, register, and initialize addon classes
for name in addon_mod.__dict__:
ob = addon_mod.__dict__[name]
if (isinstance(ob, type) and
issubclass(ob, lunagen.addon.LunaGenAddOn) and
ob.name in self.sitedata['addons']):
self.addons.append(ob())
for ob in self.addons:
ob.load(self)
ob.render(self)
if self.verbose:
for addon in self.addons:
print ("Add-on '%s' data loaded and rendered." % addon.name)
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.safe_load(seriesfile)['serieslist']
#
print("serieslist: ", self.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.safe_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'] = []
self.serieslist = []
def _copy_skeleton(self, skeleton=None):
if not skeleton:
skeleton = self.skeleton
if os.path.exists(self.tgtdir):
remove_tree(self.tgtdir, verbose=self.verbose)
if self.verbose: print("Copying source skeleton.")
copy_tree(os.path.join(self.srcdir, 'skeleton'), self.tgtdir, verbose=self.verbose)
if self.verbose: print("Copying add-on skeletons.")
for ao in self.addons:
ao.copy_skeleton(self)
if self.verbose: print("Copying the theme skeleton.")
copy_tree(os.path.join(self.theme['path'], 'skeleton'), self.tgtdir, verbose=self.verbose)
if self.verbose: print("Copying the skeleton site.")
copy_tree(skeleton, self.tgtdir, verbose=self.verbose)
if self.verbose: print("Copying add-on skeletons.")
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.safe_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.")
#
if 'affiliates' in self.sitedata:
print("affiliates = %s" % repr(self.sitedata['affiliates']))
else:
print("No 'affiliates' value in sitedata.")
#
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 # Expose built-in function
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()
self.regen_site()
def regen_site(self):
if 'simple_pages' in self.sitedata and self.sitedata['simple_pages']:
for page in self.sitedata['simple_pages']:
self._gen_simple_page(page)
#if 'serieslist' in self.sitedata and self.sitedata['serieslist']:
if self.serieslist:
self._gen_episode_list_page()
self._gen_episode_pages()
else:
print("Not generating series & episode pages: serieslist empty.")
self._gen_index()
if __name__ == '__main__':
pass