import json import os from shutil import copytree, copy from pathlib import Path, PurePath import subprocess from jinja2 import Template, Environment, PackageLoader, select_autoescape from jinja2.utils import Markup import asstosrt import srt import webvtt class SongPage: THUMBNAIL_GEOMETRY = '200x200' def __init__(self, path, root): self.name = path.name self.original = None self.author = None self.path = path self.root = root self.video = None self.video_type = None self.vtt = None self.srt = None self.ass = None self.cover = None self.thumbnail = None self.files = [] self.search_media() def search_media(self): for entry in self.path.iterdir(): if entry.name == 'metadata.json': with entry.open('r') as metadatafile: self.metadata = json.load(metadatafile) if 'name' in self.metadata: self.name = self.metadata['name'] if 'original' in self.metadata: self.original = self.metadata['original'] if 'author' in self.metadata: self.author = self.metadata['author'] elif entry.name.endswith('mp4'): self.video = entry self.video_type = 'video/mp4' self.files.append(entry) elif entry.name.endswith('webm'): self.video = entry self.video_type = 'video/webm' self.files.append(entry) elif entry.name.endswith('ogv'): self.video = entry self.video_type = 'video/ogg' self.files.append(entry) elif entry.name.endswith('vtt'): self.vtt = entry elif entry.name == "{}.srt".format(self.path.name): self.srt = entry self.files.append(entry) elif entry.name.endswith('ass'): self.ass = entry self.files.append(entry) elif entry.name == 'thumb.jpg': self.thumbnail = entry elif entry.name == 'cover.jpg': self.cover = entry elif entry.name == 'index.html': continue else: self.files.append(entry) if self.srt is None and self.ass is not None: self.srt = self.path / "{}.srt".format(self.path.name) with self.ass.open('r') as assfile, self.srt.open('w') as srtfile: srtfile.write(asstosrt.convert(assfile)) self.files.append(self.srt) if self.vtt is None and self.srt is not None: srtfile = self.path / self.srt self.vtt = self.path / "{}.vtt".format(self.path.name) webvtt.from_srt(str(self.srt.absolute())).save(str(self.vtt.absolute())) if self.cover is None and self.video is not None: self.cover = self.path / "cover.jpg" command = [ 'ffmpeg', '-loglevel', 'quiet', '-i', str(self.video.absolute()), '-vcodec', 'mjpeg', '-vframes', '1', '-an', '-f', 'rawvideo', '-ss', '2', str(self.cover.absolute()), ] subprocess.check_call(command) if self.thumbnail is None and self.cover is not None: self.thumbnail = self.path / "thumb.jpg" subprocess.check_call([ 'convert', str(self.cover.absolute()), '-resize', self.THUMBNAIL_GEOMETRY, str(self.thumbnail.absolute()), ]) @property def publish(self): has_subtitles = self.ass or self.srt or self.vtt has_video = self.video return has_video and has_subtitles def get_context_data(self): parsed_srt = None if self.srt: with self.srt.open('r') as srtfile: try: srt_str = srtfile.read().encode('utf-8').decode('utf-8-sig') parsed_srt = list(srt.parse(srt_str)) except Exception as e: print("{}: srt parse error: {}".format(self.path.name, e)) root_path = os.path.relpath(self.root, self.path) return { 'song': self, 'parsed_srt': parsed_srt, 'root_path': root_path, } def render(self, builder, context): ctx = self.get_context_data() ctx.update(context) builder.render('song.html', self.path, ctx) class Builder: def __init__(self, root_folder, libreto): self.root_folder = root_folder self.libreto = libreto self.static_dir = Path(__file__).parent / 'static' self.env = Environment( loader=PackageLoader('negromateweb', 'templates'), autoescape=select_autoescape(['html']), ) self.env.filters['url'] = self.url self.env.filters['display_boolean'] = self.display_boolean self.current_path = self.root_folder def url(self, path): return os.path.relpath(path, self.current_path) def display_boolean(self, value): if value: return Markup('✓') else: return Markup('✗') def render(self, template, target, context): html_file = target / 'index.html' page_template = self.env.get_template(template) root_path = os.path.relpath(self.root_folder, target) context['root_path'] = root_path with html_file.open('w') as page: page.write(page_template.render(context)) def build(self): songs = [] pending_songs = [] for entry in self.root_folder.iterdir(): if entry.name in ['static', 'playlist', 'home', 'todo']: continue if entry.is_dir(): print("building {}".format(entry.name)) try: songpage = SongPage(entry, self.root_folder) except Exception as e: raise e print("Error: {}".format(e)) continue if songpage.publish: songs.append(songpage) else: pending_songs.append(songpage) songs.sort(key=lambda a: a.name) global_context = { 'songs': songs, 'root_folder': self.root_folder, } for song in songs: self.current_path = song.path song.render(self, global_context) self.render('index.html', self.root_folder, global_context) home = self.root_folder / 'home' self.current_path = home if not home.exists(): home.mkdir() self.render('home.html', home, global_context) playlist = self.root_folder / 'playlist' self.current_path = playlist if not playlist.exists(): playlist.mkdir() self.render('playlist.html', playlist, global_context) todo = self.root_folder / 'todo' self.current_path = todo if not todo.exists(): todo.mkdir() todo_context = { 'pending_songs': pending_songs, } todo_context.update(global_context) self.render('todo.html', todo, todo_context) static = self.root_folder / 'static' if not static.exists(): static.mkdir() subprocess.check_call([ 'rsync', '-ra', str(self.static_dir), str(self.root_folder.absolute()), ]) libreto = self.root_folder / 'static/libreto/libreto.pdf' copy(str(self.libreto.absolute()), str(libreto.absolute()))