cinera.css: Marker and categories style

cinera_player_pre.js: Episode keyboard navigation. Also swap out A for
K, and D for J
cinera_player_pre.js: Handle the case in onRefChanged() in which the
filter_container or filterState is not present
cinera.c: Refetch quotes when processing a set of annotations >60 mins
after the last fetch

Flags:
    -w Force quote cache rebuild
This commit is contained in:
Matt Mascarenhas 2018-02-28 01:04:06 +00:00
parent 2cac3ed03b
commit e7aefbada0
7 changed files with 261 additions and 249 deletions

192
README.md
View File

@ -45,22 +45,16 @@ directory. Typical operation will involve setting these flags:
-d Project Input Directory, the directory where the .hmml files reside
-r Root Directory, path shallower than or equal to the CSS, Images and JS
directories
-u Root URL, corresponding to the Root Directory (optional if the Output
-R Root URL, corresponding to the Root Directory (optional if the Output
Base Directory resides in the Root Directory)
-c CSS Directory, relative to Root
-i Images Directory, relative to Root
-j JS Directory, relative to Root
-b Output Base Directory, location of the table of contents / search page
-t Player Template Location
-x Index Template Location
#### Integration
CINERA_MODE=INTEGRATE cinera test.hmml
This will integrate into `template_player.html` (configurable with -t) the
player and related elements generated from `test.hmml` and output to
`out_integrated.html`
Feel free to play with `template_player.html` to your heart's content. If you do
anything invalid, `cinera` will tell you what's wrong
-B Output Base URL, corresponding to the Output Base Directory
-t Template Directory
-x Index Template Location, relative to Template Directory
-y Player Template Location, relative to Template Directory
#### Templates
@ -76,60 +70,128 @@ Valid tags:
- `<!-- __CINERA_INDEX__ -->` _the table of contents, and search functionality_
*Player Template*
- `<!-- __CINERA_INCLUDES__ -->` _the necessary `.css` and `.js` files, and charset setting_
- `<!-- __CINERA_MENUS__ -->` _ _the menu bar that typically appears above the player in my samples_
- `<!-- __CINERA_INCLUDES__ -->` _the necessary `.css` and `.js` files, and
charset setting_
- `<!-- __CINERA_MENUS__ -->` _ _the menu bar that typically appears above the
player in my samples_
- `<!-- __CINERA_PLAYER__ -->` _the player_
- `<!-- __CINERA_SCRIPT__ -->` _the filter state objects and `.js` file, which must come after both the MENUS and PLAYER tags_
- `<!-- __CINERA_SCRIPT__ -->` _the filter state objects and `.js` file, which
must come after both the MENUS and PLAYER tags_
*Optional tags available for use in your Player Template*
- `<!-- __CINERA_TITLE__ -->` _the title of the video, excluding numbering_
- `<!-- __CINERA_VIDEO_ID__ -->` _the unique identifer of the video as provided
by the VoD platform_
*Other available tags*
- `<!-- __CINERA_PROJECT__ -->` _the full name of the project_
- `<!-- __CINERA_URL__ -->` _the URL where we have derived the page will be
publically accessibly, only really usable if BaseURL is set (-B)_
- `<!-- __CINERA_CUSTOM0__ -->`
- `<!-- __CINERA_CUSTOM1__ -->`
- `<!-- __CINERA_CUSTOM2__ -->`
- `<!-- __CINERA_CUSTOM15__ -->`
_Freeform buffers for small snippets of localised information, e.g. a single
<a> element or perhaps a <!-- comment --> They correspond to the custom0 to
custom15 attributes in the [video] node in your .hmml files 0 to 11 may hold
up to 255 characters 12 to 15 may hold up to 1023 characters_
Feel free to play with templates to your heart's content. If you do anything
invalid, `cinera` will tell you what's wrong.
#### Arguments
Usage: ./cinera [option(s)] filename(s)
Usage: ./cinera [option(s)] filename(s)
Options:
Paths:
-r <root directory>
Override default root directory (".")
-u <root URL>
Override default root URL ("")
-b <base output directory>
Override project's default base output directory (".")
-c <CSS directory path>
Override default CSS directory (""), relative to root
-i <images directory path>
Override default images directory (""), relative to root
-j <JS directory path>
Override default JS directory (""), relative to root
-t <player template location>
Override default player template location ("template_player.html"), relative to root
and automatically enable integration
-x <index template location>
Override default index template location ("template_index.html"), relative to root
and automatically enable integration
-o <output location>
Override default output player location for SINGLE_EDITION ("out.html")
-d <project directory>
Override default project directory (".")
-f
Force integration with an incomplete template
-p <project ID>
Set the project ID, corresponding to the "project" field in the HMML files
-s <style>
Set the style / theme, corresponding to a cinera__*.css file
This is equal to the "project" field in the HMML files by default
-l <n>
Override default log level (0), where n is from 0 (terse) to 7 (verbose)
-m <default medium>
Override default default medium ("programming")
-U <seconds>
Override default update interval ("4")
Options:
Paths: (advisedly universal, but may be set per-(sub)project as required)
-r <root directory>
Override default root directory (".")
-R <root URL>
Override default root URL ("")
IMPORTANT: -r and -R must correspond to the same location
UNSUPPORTED: If you move files from RootDir, the RootURL should
correspond to the resulting location
-c <CSS directory path>
Override default CSS directory (""), relative to root
-i <images directory path>
Override default images directory (""), relative to root
-j <JS directory path>
Override default JS directory (""), relative to root
Project Settings:
-p <project ID>
Set the project ID, equal to the "project" field in the HMML files
NOTE: Setting the project ID triggers PROJECT EDITION
-m <default medium>
Override default default medium ("programming")
Known project defaults:
book: research
pcalc: programming
riscy: programming
chat: speech
code: programming
intro-to-c: programming
misc: admin
ray: programming
hmdshow: speech
lecture: speech
stream: programming
special: programming
obbg: programming
sysadmin: admin
-s <style>
Set the style / theme, corresponding to a cinera__*.css file
This is equal to the "project" field in the HMML files by default
-q
Quit after syncing with annotation files in project input directory
UNSUPPORTED: This is likely to be removed in the future
Project Input Paths
-d <annotations directory>
Override default annotations directory (".")
-t <templates directory>
Override default templates directory (".")
-x <index template location>
Set index template file path, either absolute or relative to
template directory, and enable integration
-y <player template location>
Set player template file path, either absolute or relative
to template directory, and enable integration
Project Output Paths
-b <base output directory>
Override project's default base output directory (".")
-B <base URL>
Override default base URL ("")
NOTE: This must be set, if -n or -a are to be used
-n <index location>
Override default index location (""), relative to base
-a <player location>
Override default player location (""), relative to base
NOTE: The PlayerURLPrefix is currently hardcoded in cinera.c but
will be configurable in the full configuration system
Single Edition Output Path
-o <output location>
Override default output player location ("out.html")
-e
Display (examine) index file and exit
-f
Force integration with an incomplete template
-w
Force quote cache rebuild (memory aid: "wget")
-l <n>
Override default log level (0), where n is from 0 (terse) to 7 (verbose)
-u <seconds>
Override default update interval (4)
-v
display version and exit
-h
display this help
#### Environment Variables
CINERA_MODE=INTEGRATE
Enable integration
Display version and exit
-h
Display this help

View File

@ -14,7 +14,7 @@ typedef struct
version CINERA_APP_VERSION = {
.Major = 0,
.Minor = 5,
.Patch = 35
.Patch = 36
};
// TODO(matt): Copy in the DB 3 stuff from cinera_working.c
@ -83,6 +83,7 @@ enum
{
MODE_ONESHOT = 1 << 0,
MODE_EXAMINE = 1 << 1,
MODE_NOCACHE = 1 << 2,
} modes;
enum
@ -114,6 +115,55 @@ typedef struct
int Size;
} arena;
typedef struct
{
// Universal
char CacheDir[256];
int Edition;
int LogLevel;
int Mode;
int UpdateInterval;
bool ForceIntegration;
// Advisedly universal, although could be per-project
char *RootDir; // Absolute
char *RootURL;
char *CSSDir; // Relative to Root{Dir,URL}
char *ImagesDir; // Relative to Root{Dir,URL}
char *JSDir; // Relative to Root{Dir,URL}
// Per Project
char *ProjectID;
char *Theme;
char *DefaultMedium;
// Per Project - Input
char *ProjectDir; // Absolute
char *TemplatesDir; // Absolute
char *TemplateIndexLocation; // Relative to TemplatesDir ???
char *TemplatePlayerLocation; // Relative to TemplatesDir ???
// Per Project - Output
char *BaseDir; // Absolute
char *BaseURL;
char *IndexLocation; // Relative to Base{Dir,URL}
char *PlayerLocation; // Relative to Base{Dir,URL}
char *PlayerURLPrefix; /* NOTE(matt): This will become a full blown customisable output URL.
For now it simply replaces the ProjectID */
// Single Edition - Input
char SingleHMMLFilePath[256];
// Single Edition - Output
char *OutLocation;
char *OutIntegratedLocation;
} config;
// NOTE(matt): Globals
config Config = {};
arena MemoryArena;
time_t LastQuoteFetch;
//
typedef struct
{
char *Location;
@ -308,53 +358,6 @@ typedef struct
buffer Buffer;
} template;
typedef struct
{
// Universal
char CacheDir[256];
int Edition;
int LogLevel;
int Mode;
int UpdateInterval;
bool ForceIntegration;
// Advisedly universal, although could be per-project
char *RootDir; // Absolute
char *RootURL;
char *CSSDir; // Relative to Root{Dir,URL}
char *ImagesDir; // Relative to Root{Dir,URL}
char *JSDir; // Relative to Root{Dir,URL}
// Per Project
char *ProjectID;
char *Theme;
char *DefaultMedium;
// Per Project - Input
char *ProjectDir; // Absolute
char *TemplatesDir; // Absolute
char *TemplateIndexLocation; // Relative to TemplatesDir ???
char *TemplatePlayerLocation; // Relative to TemplatesDir ???
// Per Project - Output
char *BaseDir; // Absolute
char *BaseURL;
char *IndexLocation; // Relative to Base{Dir,URL}
char *PlayerLocation; // Relative to Base{Dir,URL}
char *PlayerURLPrefix; /* NOTE(matt): This will become a full blown customisable output URL.
For now it simply replaces the ProjectID */
// Single Edition - Input
char SingleHMMLFilePath[256];
// Single Edition - Output
char *OutLocation;
char *OutIntegratedLocation;
} config;
// NOTE(matt): Globals
config Config = {};
arena MemoryArena;
// TODO(matt): Consider putting the ref_info and quote_info into linked lists on the heap, just to avoid all the hardcoded sizes
typedef struct
@ -1735,6 +1738,7 @@ CurlIntoBuffer(char *InPtr, size_t CharLength, size_t Chars, char **OutputPtr)
void
CurlQuotes(buffer *QuoteStaging, char *QuotesURL)
{
fprintf(stderr, "\e[0;35mFetching\e[0m quotes: %s\n", QuotesURL);
CURL *curl = curl_easy_init();
if(curl) {
CURLcode res;
@ -1800,14 +1804,18 @@ SearchQuotes(buffer *QuoteStaging, int CacheSize, quote_info *Info, int ID)
}
int
BuildQuote(quote_info *Info, char *Speaker, int ID)
BuildQuote(quote_info *Info, char *Speaker, int ID, bool ShouldFetchQuotes)
{
// TODO(matt): Rebuild cache option
char QuoteCacheDir[256];
CopyString(QuoteCacheDir, "%s/quotes", Config.CacheDir);
char QuoteCachePath[256];
CopyString(QuoteCachePath, "%s/%s", QuoteCacheDir, Speaker);
if(ShouldFetchQuotes)
{
remove(QuoteCachePath);
}
FILE *QuoteCache;
char QuotesURL[256];
// TODO(matt): Make the URL configurable
@ -1819,7 +1827,7 @@ BuildQuote(quote_info *Info, char *Speaker, int ID)
if(MakeDir(QuoteCacheDir) == RC_SUCCESS)
{
CacheAvailable = TRUE;
};
}
if(!(QuoteCache = fopen(QuoteCachePath, "a+")))
{
fprintf(stderr, "Unable to open quote cache %s: %s\n", QuoteCachePath, strerror(errno));
@ -2103,6 +2111,8 @@ PrintUsage(char *BinaryLocation, config *DefaultConfig)
" Display (examine) index file and exit\n"
" -f\n"
" Force integration with an incomplete template\n"
" -w\n"
" Force quote cache rebuild \e[1;30m(memory aid: \"wget\")\e[0m\n"
"\n"
" -l <n>\n"
" Override default log level (%d), where n is from 0 (terse) to 7 (verbose)\n"
@ -2115,12 +2125,12 @@ PrintUsage(char *BinaryLocation, config *DefaultConfig)
" Display this help\n"
"\n"
"Template:\n"
" A complete index template shall contain exactly one each of the following tags:\n"
" A complete Index Template shall contain exactly one each of the following tags:\n"
" <!-- __CINERA_INCLUDES__ -->\n"
" to put inside your own <head></head>\n"
" <!-- __CINERA_INDEX__ -->\n"
"\n"
" A complete player template shall contain exactly one each of the following tags:\n"
" A complete Player Template shall contain exactly one each of the following tags:\n"
" <!-- __CINERA_INCLUDES__ -->\n"
" to put inside your own <head></head>\n"
" <!-- __CINERA_MENUS__ -->\n"
@ -2128,7 +2138,7 @@ PrintUsage(char *BinaryLocation, config *DefaultConfig)
" <!-- __CINERA_SCRIPT__ -->\n"
" must come after <!-- __CINERA_MENUS__ --> and <!-- __CINERA_PLAYER__ -->\n"
"\n"
" Optional tags available for use in your player template:\n"
" Optional tags available for use in your Player Template:\n"
" <!-- __CINERA_TITLE__ -->\n"
" <!-- __CINERA_VIDEO_ID__ -->\n"
"\n"
@ -3232,9 +3242,15 @@ AppendedIdentifier:
HasQuote = TRUE;
char *Speaker = Anno->quote.author ? Anno->quote.author : HMML.metadata.stream_username ? HMML.metadata.stream_username : HMML.metadata.member;
bool ShouldFetchQuotes = FALSE;
if(Config.Mode & MODE_NOCACHE || (Config.Edition != EDITION_SINGLE && time(0) - LastQuoteFetch > 60*60))
{
ShouldFetchQuotes = TRUE;
LastQuoteFetch = time(0);
}
if(BuildQuote(&QuoteInfo,
Speaker,
Anno->quote.id) == RC_UNFOUND)
Anno->quote.id, ShouldFetchQuotes) == RC_UNFOUND)
{
LogError(LOG_ERROR, "Quote #%s %d not found: %s:%d", Speaker, Anno->quote.id, Filename, Anno->line);
Filename[StringLength(Filename) - StringLength(".hmml")] = '\0';
@ -3650,7 +3666,8 @@ AppendedIdentifier:
" <span class=\"help_key\">?</span><h1>Keyboard Navigation</h1>\n"
"\n"
" <h2>Global Keys</h2>\n"
" <span class=\"help_key\">W</span>, <span class=\"help_key\">A</span>, <span class=\"help_key\">P</span> / <span class=\"help_key\">S</span>, <span class=\"help_key\">D</span>, <span class=\"help_key\">N</span> <span class=\"help_text\">Jump to previous / next marker</span><br>\n"
" <span class=\"help_key\">[</span>, <span class=\"help_key\">&lt;</span> / <span class=\"help_key\">]</span>, <span class=\"help_key\">&gt;</span> <span class=\"help_text\">Jump to previous / next episode</span><br>\n"
" <span class=\"help_key\">W</span>, <span class=\"help_key\">K</span>, <span class=\"help_key\">P</span> / <span class=\"help_key\">S</span>, <span class=\"help_key\">J</span>, <span class=\"help_key\">N</span> <span class=\"help_text\">Jump to previous / next marker</span><br>\n"
" <span class=\"help_key\">t</span> / <span class=\"help_key\">T</span> <span class=\"help_text\">Toggle theatre / SUPERtheatre mode</span><br>\n"
);
@ -3715,7 +3732,7 @@ AppendedIdentifier:
CopyStringToBuffer(&CollationBuffers->Menus,
"\n"
" <h2>Movement</h2>\n"
" <h2>In-Menu Movement</h2>\n"
" <div class=\"help_paragraph\">\n"
" <div class=\"key_block\">\n"
" <div class=\"key_column\" style=\"flex-grow: 1\">\n"
@ -4784,8 +4801,8 @@ int LinkNeighbours(index *Index, char *BaseFilename, int LinkType)
break;
}
index_header Header = *(index_header *)Index->Metadata.Buffer.Ptr;
Index->Metadata.Buffer.Ptr += sizeof(Header);
Index->Header = *(index_header *)Index->Metadata.Buffer.Ptr;
Index->Metadata.Buffer.Ptr += sizeof(Index->Header);
index_metadata *Prev = { 0 };
index_metadata *This = { 0 };
index_metadata *Next = { 0 };
@ -4795,12 +4812,12 @@ int LinkNeighbours(index *Index, char *BaseFilename, int LinkType)
{
case LINK_INCLUDE:
{
for(EntryIndex = 0; EntryIndex < Header.EntryCount; ++EntryIndex)
for(EntryIndex = 0; EntryIndex < Index->Header.EntryCount; ++EntryIndex)
{
This = (index_metadata *)Index->Metadata.Buffer.Ptr;
if(!StringsDiffer(This->BaseFilename, BaseFilename))
{
if(EntryIndex < (Header.EntryCount - 1))
if(EntryIndex < (Index->Header.EntryCount - 1))
{
Next = (index_metadata *)(Index->Metadata.Buffer.Ptr + sizeof(index_metadata));
}
@ -4812,17 +4829,13 @@ int LinkNeighbours(index *Index, char *BaseFilename, int LinkType)
} break;
case LINK_EXCLUDE:
{
if(Index->Header.EntryCount == 1)
This = (index_metadata *)Index->Metadata.Buffer.Ptr;
if(Index->Header.EntryCount != 1)
{
This = (index_metadata *)Index->Metadata.Buffer.Ptr;
}
else
{
This = (index_metadata *)Index->Metadata.Buffer.Ptr;
Index->Metadata.Buffer.Ptr += sizeof(index_metadata);
Next = (index_metadata *)Index->Metadata.Buffer.Ptr;
for(EntryIndex = 1; EntryIndex < Header.EntryCount; ++EntryIndex)
for(EntryIndex = 1; EntryIndex < Index->Header.EntryCount; ++EntryIndex)
{
Prev = This;
This = Next;
@ -5892,7 +5905,7 @@ main(int ArgC, char **Args)
}
char CommandLineArg;
while((CommandLineArg = getopt(ArgC, Args, "a:b:B:c:d:efhi:j:l:m:n:o:p:qr:R:s:t:u:vx:y:")) != -1)
while((CommandLineArg = getopt(ArgC, Args, "a:b:B:c:d:efhi:j:l:m:n:o:p:qr:R:s:t:u:vwx:y:")) != -1)
{
switch(CommandLineArg)
{
@ -5961,6 +5974,9 @@ main(int ArgC, char **Args)
case 'v':
PrintVersions();
return RC_SUCCESS;
case 'w':
Config.Mode |= MODE_NOCACHE;
break;
case 'x':
Config.TemplateIndexLocation = optarg;
break;
@ -6226,6 +6242,15 @@ main(int ArgC, char **Args)
while(MonitorDirectory(&Index, &CollationBuffers, IndexTemplate, PlayerTemplate, BespokeTemplate, inotifyInstance, WatchDescriptor) != RC_ERROR_FATAL)
{
// TODO(matt): Refetch the quotes and rebuild player pages if needed
//
// Every sixty mins, redownload the quotes and, I suppose, SyncIndexWithInput(). But here we still don't even know
// who the speaker is. To know, we'll probably have to store all quoted speakers in the project's .metadata. Maybe
// postpone this for now, but we will certainly need this to happen
//
// The most ideal solution is possibly that we store quote numbers in the Metadata->Entry, listen for and handle a
// REST PUT request from insobot when a quote changes (unless we're supposed to poll insobot for them?), and rebuild
// the player page(s) accordingly.
sleep(Config.UpdateInterval);
}
}

View File

@ -528,7 +528,7 @@
}
.cineraPlayerContainer .markers_container > .markers .marker .cineraContent {
display: block;
display: inline-block;
font-size: 14px;
}
@ -575,7 +575,8 @@
}
.cineraPlayerContainer .markers_container > .markers .marker .cineraContent .cineraCategories {
margin: 4px;
float: right;
min-height: 1em;
}
.cineraMenus > .menu > .filter_container .filter_content {

View File

@ -220,6 +220,8 @@ for(var i = 0; i < sourceMenus.length; ++i)
})
};
var prevEpisode = playerContainer.querySelector(".episodeMarker.prev");
var nextEpisode = playerContainer.querySelector(".episodeMarker.next");
var testMarkers = playerContainer.querySelectorAll(".marker");
window.addEventListener("blur", function(){

View File

@ -56,8 +56,6 @@ function Player(htmlContainer, refsCallback) {
Player.initializeYoutube(this.onYoutubeReady.bind(this));
this.updateSize();
this.resume();
}
// Start playing the video from the current position.
@ -68,6 +66,7 @@ Player.prototype.play = function() {
this.youtubePlayer.playVideo();
}
this.pauseAfterBuffer = false;
this.resume();
} else {
this.shouldPlay = true;
}
@ -119,6 +118,7 @@ Player.prototype.updateSize = function() {
this.markersContainer.style.height = height;
if (this.youtubePlayerReady) {
this.youtubePlayer.setSize(Math.floor(width), Math.floor(height));
this.resume();
}
}
@ -929,16 +929,30 @@ function handleKey(key) {
} break;
case 'N':
case 'D':
case 'J':
case 'S': {
player.jumpToNextMarker();
} break;
case 'P':
case 'A':
case 'K':
case 'W': {
player.jumpToPrevMarker();
} break;
case '[':
case '<': {
if(prevEpisode)
{
location = prevEpisode.href;
}
} break;
case ']':
case '>': {
if(nextEpisode)
{
location = nextEpisode.href;
}
} break;
default: {
gotKey = false;
} break;
@ -1165,6 +1179,22 @@ function resetFade() {
function onRefChanged(ref, element) {
if(element.classList.contains("skip"))
{
var filterState;
var ErrorCount = 0;
if(!filter) { console.log("Missing filter_container div"); ErrorCount++; }
if(!filterState) { console.log("Missing filterState object"); ErrorCount++; }
if(ErrorCount > 0)
{
switch(ErrorCount)
{
case 1:
{ console.log("This should have been generated by Cinera along with the following element containing the \"skip\" class:"); } break;
default:
{ console.log("These should have been generated by Cinera along with the following element containing the \"skip\" class:"); } break;
}
console.log(element); return;
}
if(!filter.classList.contains("responsible"))
{
filter.classList.add("responsible");

Binary file not shown.

View File

@ -1,108 +0,0 @@
#ifndef HMML_H_
#define HMML_H_
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <stdio.h>
// Data structures
typedef struct {
char* member;
char* stream_platform;
char* stream_username;
char* project;
char* title;
char* vod_platform;
char* id;
char** co_hosts;
size_t co_host_count;
char** guests;
size_t guest_count;
char** annotators;
size_t annotator_count;
char* template;
char* medium;
} HMML_VideoMetaData;
typedef struct {
char* site;
char* page;
char* url;
char* title;
char* article;
char* author;
char* editor;
char* publisher;
char* isbn;
int offset;
} HMML_Reference;
typedef enum {
HMML_CATEGORY,
HMML_MEMBER,
HMML_PROJECT,
HMML_MARKER_COUNT,
} HMML_MarkerType;
typedef struct {
HMML_MarkerType type;
char* marker;
char* parameter;
char* episode;
int offset;
} HMML_Marker;
typedef struct {
int id;
char* author;
} HMML_Quote;
typedef struct {
int line;
char* time;
char* text;
char* author;
HMML_Reference* references;
size_t reference_count;
HMML_Marker* markers;
size_t marker_count;
HMML_Quote quote;
bool is_quote;
} HMML_Annotation;
typedef struct {
int line;
char* message;
} HMML_Error;
typedef struct {
bool well_formed;
HMML_VideoMetaData metadata;
HMML_Annotation* annotations;
size_t annotation_count;
HMML_Error error;
} HMML_Output;
// Functions
HMML_Output hmml_parse_file (FILE* file);
void hmml_dump (HMML_Output* output);
void hmml_free (HMML_Output* output);
// Version
extern const struct HMML_Version {
int Major, Minor, Patch;
} hmml_version;
#endif