9187 lines
356 KiB
C
9187 lines
356 KiB
C
#if 0
|
|
ctime -begin ${0%.*}.ctm
|
|
#gcc -g -fsanitize=address -Wall -std=c99 -pipe $0 -o ${0%.*} hmml.a -lcurl
|
|
gcc -O2 -Wall -std=c99 -pipe $0 -o ${0%.*} hmml.a -lcurl
|
|
#clang -fsanitize=address -g -Wall -std=c99 -pipe $0 -o ${0%.*} hmml.a -lcurl
|
|
#clang -O2 -Wall -std=c99 -pipe $0 -o ${0%.*} hmml.a -lcurl
|
|
ctime -end ${0%.*}.ctm
|
|
exit
|
|
#endif
|
|
|
|
#include <stdint.h>
|
|
typedef struct
|
|
{
|
|
uint32_t Major, Minor, Patch;
|
|
} version;
|
|
|
|
version CINERA_APP_VERSION = {
|
|
.Major = 0,
|
|
.Minor = 6,
|
|
.Patch = 3
|
|
};
|
|
|
|
#include <stdarg.h> // NOTE(matt): varargs
|
|
#include <stdio.h> // NOTE(matt): printf, sprintf, vsprintf, fprintf, perror
|
|
#include <stdlib.h> // NOTE(matt): calloc, malloc, free
|
|
#include "hmmlib.h"
|
|
#include <getopt.h> // NOTE(matt): getopts
|
|
//#include "config.h" // TODO(matt): Implement config.h
|
|
#include <curl/curl.h>
|
|
#include <time.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <dirent.h>
|
|
#include <string.h> // NOTE(matt): strerror
|
|
#include <errno.h> //NOTE(matt): errno
|
|
#include <sys/inotify.h> // NOTE(matt): inotify
|
|
#include <wordexp.h>
|
|
|
|
#define __USE_XOPEN2K8 // NOTE(matt): O_NOFOLLOW
|
|
#include <fcntl.h> // NOTE(matt): open()
|
|
#define __USE_XOPEN2K // NOTE(matt): readlink()
|
|
#include <unistd.h> // NOTE(matt): sleep()
|
|
|
|
typedef unsigned int bool;
|
|
#define TRUE 1
|
|
#define FALSE 0
|
|
|
|
#define enum8(type) int8_t
|
|
#define enum16(type) int16_t
|
|
#define enum32(type) int32_t
|
|
|
|
#define DEBUG 0
|
|
#define DEBUG_MEM 0
|
|
|
|
bool PROFILING = 0;
|
|
clock_t TIMING_START;
|
|
#define START_TIMING_BLOCK(...) if(PROFILING) { printf(__VA_ARGS__); TIMING_START = clock(); }
|
|
#define END_TIMING_BLOCK() if(PROFILING) { printf("\e[1;34m%ld\e[0m\n", clock() - TIMING_START);}
|
|
|
|
#define Kilobytes(Bytes) Bytes << 10
|
|
#define Megabytes(Bytes) Bytes << 20
|
|
|
|
#define MAX_PROJECT_ID_LENGTH 31
|
|
#define MAX_PROJECT_NAME_LENGTH 63
|
|
#define MAX_BASE_DIR_LENGTH 127
|
|
#define MAX_BASE_URL_LENGTH 127
|
|
#define MAX_RELATIVE_PAGE_LOCATION_LENGTH 31
|
|
#define MAX_PLAYER_URL_PREFIX_LENGTH 15
|
|
|
|
#define MAX_ROOT_DIR_LENGTH 127
|
|
#define MAX_ROOT_URL_LENGTH 127
|
|
#define MAX_RELATIVE_ASSET_LOCATION_LENGTH 31
|
|
|
|
#define MAX_BASE_FILENAME_LENGTH 31
|
|
#define MAX_TITLE_LENGTH 128 - (MAX_BASE_FILENAME_LENGTH + 1) - (int)sizeof(link_insertion_offsets) - (int)sizeof(unsigned short int) - 1 // NOTE(matt): We size this such that db_entry is 128 bytes total
|
|
|
|
#define MAX_ASSET_FILENAME_LENGTH 63
|
|
|
|
// TODO(matt): Stop distinguishing between short / long and lift the size limit once we're on the LUT
|
|
#define MAX_CUSTOM_SNIPPET_SHORT_LENGTH 255
|
|
#define MAX_CUSTOM_SNIPPET_LONG_LENGTH 1023
|
|
|
|
#define ArrayCount(A) sizeof(A)/sizeof(*(A))
|
|
#define Assert(Expression) if(!(Expression)) { printf("l.%d: \e[1;31mAssertion failure\e[0m\n", __LINE__); __asm__("int3"); }
|
|
#define FOURCC(String) ((uint32_t)(String[0] << 0) | (uint32_t)(String[1] << 8) | (uint32_t)(String[2] << 16) | (uint32_t)(String[3] << 24))
|
|
|
|
enum
|
|
{
|
|
EDITION_SINGLE,
|
|
EDITION_PROJECT,
|
|
EDITION_NETWORK
|
|
} editions;
|
|
|
|
enum
|
|
{
|
|
// NOTE(matt): https://tools.ietf.org/html/rfc5424#section-6.2.1
|
|
LOG_EMERGENCY,
|
|
LOG_ALERT,
|
|
LOG_CRITICAL,
|
|
LOG_ERROR,
|
|
LOG_WARNING,
|
|
LOG_NOTICE,
|
|
LOG_INFORMATIONAL,
|
|
LOG_DEBUG
|
|
} log_levels;
|
|
|
|
enum
|
|
{
|
|
MODE_FORCEINTEGRATION = 1 << 0,
|
|
MODE_ONESHOT = 1 << 1,
|
|
MODE_EXAMINE = 1 << 2,
|
|
MODE_NOCACHE = 1 << 3,
|
|
MODE_NOPRIVACY = 1 << 4,
|
|
MODE_SINGLETAB = 1 << 5,
|
|
MODE_NOREVVEDRESOURCE = 1 << 6
|
|
} modes;
|
|
|
|
enum
|
|
{
|
|
RC_ARENA_FULL,
|
|
RC_ERROR_DIRECTORY,
|
|
RC_ERROR_FATAL,
|
|
RC_ERROR_FILE,
|
|
RC_ERROR_HMML,
|
|
RC_ERROR_MAX_REFS,
|
|
RC_ERROR_MEMORY,
|
|
RC_ERROR_PARSING,
|
|
RC_ERROR_PROJECT,
|
|
RC_ERROR_QUOTE,
|
|
RC_ERROR_SEEK,
|
|
RC_FOUND,
|
|
RC_UNFOUND,
|
|
RC_INVALID_REFERENCE,
|
|
RC_INVALID_TEMPLATE,
|
|
RC_PRIVATE_VIDEO,
|
|
RC_NOOP,
|
|
RC_RIP,
|
|
RC_SUCCESS
|
|
} returns;
|
|
|
|
typedef struct
|
|
{
|
|
void *Location;
|
|
void *Ptr;
|
|
char *ID;
|
|
int Size;
|
|
} arena;
|
|
|
|
typedef struct
|
|
{
|
|
// Universal
|
|
char CacheDir[256];
|
|
enum8(editions) Edition;
|
|
enum8(log_levels) LogLevel;
|
|
enum8(modes) Mode;
|
|
int UpdateInterval;
|
|
|
|
// 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}
|
|
char *QueryString;
|
|
|
|
// Per Project
|
|
char *ProjectID;
|
|
char *Theme;
|
|
char *DefaultMedium;
|
|
|
|
// Per Project - Input
|
|
char *ProjectDir; // Absolute
|
|
char *TemplatesDir; // Absolute
|
|
char *TemplateSearchLocation; // Relative to TemplatesDir ???
|
|
char *TemplatePlayerLocation; // Relative to TemplatesDir ???
|
|
|
|
// Per Project - Output
|
|
char *BaseDir; // Absolute
|
|
char *BaseURL;
|
|
char *SearchLocation; // 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;
|
|
|
|
typedef struct
|
|
{
|
|
char *Location;
|
|
char *Ptr;
|
|
char *ID;
|
|
int Size;
|
|
} buffer;
|
|
|
|
typedef struct
|
|
{
|
|
buffer Buffer;
|
|
FILE *Handle;
|
|
char Path[256]; // NOTE(matt): Could this just be a char *?
|
|
int FileSize;
|
|
} file_buffer;
|
|
|
|
char *AssetTypeNames[] =
|
|
{
|
|
"Generic",
|
|
"CSS",
|
|
"Image",
|
|
"JavaScript"
|
|
};
|
|
|
|
enum
|
|
{
|
|
ASSET_GENERIC,
|
|
ASSET_CSS,
|
|
ASSET_IMG,
|
|
ASSET_JS,
|
|
ASSET_TYPE_COUNT
|
|
} asset_types;
|
|
|
|
typedef struct asset
|
|
{
|
|
int32_t Hash;
|
|
enum8(asset_types) Type;
|
|
char Filename[MAX_ASSET_FILENAME_LENGTH + 1];
|
|
int32_t FilenameAt:29;
|
|
int32_t Known:1;
|
|
int32_t OffsetLandmarks:1;
|
|
int32_t DeferredUpdate:1;
|
|
uint32_t SearchLandmarkCapacity;
|
|
uint32_t SearchLandmarkCount;
|
|
uint32_t *SearchLandmark;
|
|
uint32_t PlayerLandmarkCapacity;
|
|
uint32_t PlayerLandmarkCount;
|
|
uint32_t *PlayerLandmark;
|
|
} asset;
|
|
|
|
asset BuiltinAssets[] =
|
|
{
|
|
{ 0, ASSET_CSS, "cinera.css" },
|
|
{ 0, ASSET_CSS }, // NOTE(matt): .Filename set by InitBuiltinAssets()
|
|
{ 0, ASSET_CSS, "cinera_topics.css" },
|
|
{ 0, ASSET_IMG, "cinera_icon_filter.png" },
|
|
{ 0, ASSET_JS, "cinera_search.js" },
|
|
{ 0, ASSET_JS, "cinera_player_pre.js" },
|
|
{ 0, ASSET_JS, "cinera_player_post.js" },
|
|
};
|
|
|
|
enum
|
|
{
|
|
ASSET_CSS_CINERA,
|
|
ASSET_CSS_THEME,
|
|
ASSET_CSS_TOPICS,
|
|
ASSET_IMG_FILTER,
|
|
ASSET_JS_SEARCH,
|
|
ASSET_JS_PLAYER_PRE,
|
|
ASSET_JS_PLAYER_POST,
|
|
BUILTIN_ASSETS_COUNT,
|
|
} builtin_assets;
|
|
|
|
typedef struct
|
|
{
|
|
int Count;
|
|
int Capacity;
|
|
asset *Asset;
|
|
} assets;
|
|
|
|
enum
|
|
{
|
|
WT_HMML,
|
|
WT_ASSET
|
|
} watch_types;
|
|
|
|
typedef struct
|
|
{
|
|
int Descriptor;
|
|
enum8(watch_types) Type;
|
|
char Path[MAX_ROOT_DIR_LENGTH + 1 + MAX_RELATIVE_ASSET_LOCATION_LENGTH];
|
|
} watch_handle;
|
|
|
|
typedef struct
|
|
{
|
|
int Count;
|
|
int Capacity;
|
|
watch_handle *Handle;
|
|
} watch_handles;
|
|
|
|
// DBVersion 1
|
|
typedef struct { unsigned int DBVersion; version AppVersion; version HMMLVersion; unsigned int EntryCount; } db_header1;
|
|
typedef struct { int Size; char BaseFilename[32]; } db_entry1;
|
|
typedef struct { file_buffer File; file_buffer Metadata; db_header1 Header; db_entry1 Entry; } database1;
|
|
//
|
|
|
|
// DBVersion 2
|
|
typedef struct { unsigned int DBVersion; version AppVersion; version HMMLVersion; unsigned int EntryCount; char SearchLocation[32]; char PlayerLocation[32]; } db_header2;
|
|
typedef db_entry1 db_entry2;
|
|
typedef struct { file_buffer File; file_buffer Metadata; db_header2 Header; db_entry2 Entry; } database2;
|
|
//
|
|
|
|
// TODO(matt): Increment CINERA_DB_VERSION!
|
|
typedef struct
|
|
{
|
|
unsigned int CurrentDBVersion;
|
|
version CurrentAppVersion;
|
|
version CurrentHMMLVersion;
|
|
|
|
unsigned int InitialDBVersion;
|
|
version InitialAppVersion;
|
|
version InitialHMMLVersion;
|
|
|
|
unsigned short int EntryCount;
|
|
char ProjectID[MAX_PROJECT_ID_LENGTH + 1];
|
|
char ProjectName[MAX_PROJECT_NAME_LENGTH + 1];
|
|
char BaseURL[MAX_BASE_URL_LENGTH + 1];
|
|
char SearchLocation[MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1];
|
|
char PlayerLocation[MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1];
|
|
char PlayerURLPrefix[MAX_PLAYER_URL_PREFIX_LENGTH + 1];
|
|
} db_header3;
|
|
|
|
typedef struct
|
|
{
|
|
unsigned int PrevStart, NextStart;
|
|
unsigned short int PrevEnd, NextEnd;
|
|
} link_insertion_offsets; // NOTE(matt): PrevStart is Absolute (or relative to start of file), the others are Relative to PrevStart
|
|
|
|
typedef struct
|
|
{
|
|
link_insertion_offsets LinkOffsets;
|
|
unsigned short int Size;
|
|
char BaseFilename[MAX_BASE_FILENAME_LENGTH + 1];
|
|
char Title[MAX_TITLE_LENGTH + 1];
|
|
} db_entry3;
|
|
|
|
typedef struct
|
|
{
|
|
file_buffer File;
|
|
file_buffer Metadata;
|
|
db_header3 Header;
|
|
db_entry3 Entry;
|
|
} database3;
|
|
|
|
#pragma pack(push, 1)
|
|
typedef struct
|
|
{
|
|
uint32_t HexSignature; // 'CNRA'
|
|
uint32_t CurrentDBVersion;
|
|
version CurrentAppVersion;
|
|
version CurrentHMMLVersion;
|
|
|
|
uint32_t InitialDBVersion;
|
|
version InitialAppVersion;
|
|
version InitialHMMLVersion;
|
|
|
|
uint32_t BlockCount;
|
|
} db_header4;
|
|
|
|
typedef struct
|
|
{
|
|
uint32_t BlockID; // 'NTRY'
|
|
uint16_t Count;
|
|
char ProjectID[MAX_PROJECT_ID_LENGTH + 1];
|
|
char ProjectName[MAX_PROJECT_NAME_LENGTH + 1];
|
|
char BaseURL[MAX_BASE_URL_LENGTH + 1];
|
|
char SearchLocation[MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1];
|
|
char PlayerLocation[MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1];
|
|
char PlayerURLPrefix[MAX_PLAYER_URL_PREFIX_LENGTH + 1]; // TODO(matt): Replace this with the OutputPath, when we add that
|
|
} db_header_entries4;
|
|
|
|
typedef db_entry3 db_entry4;
|
|
|
|
typedef struct
|
|
{
|
|
uint32_t BlockID; // 'ASET'
|
|
uint16_t Count;
|
|
char RootDir[MAX_ROOT_DIR_LENGTH + 1];
|
|
char RootURL[MAX_ROOT_URL_LENGTH + 1];
|
|
char CSSDir[MAX_RELATIVE_ASSET_LOCATION_LENGTH + 1];
|
|
char ImagesDir[MAX_RELATIVE_ASSET_LOCATION_LENGTH + 1];
|
|
char JSDir[MAX_RELATIVE_ASSET_LOCATION_LENGTH + 1];
|
|
} db_header_assets4;
|
|
|
|
typedef struct
|
|
{
|
|
int32_t Hash;
|
|
uint32_t LandmarkCount;
|
|
enum8(asset_types) Type;
|
|
char Filename[MAX_ASSET_FILENAME_LENGTH + 1];
|
|
} db_asset4;
|
|
|
|
typedef struct
|
|
{
|
|
int32_t EntryIndex;
|
|
uint32_t Position;
|
|
} db_landmark4;
|
|
|
|
typedef struct
|
|
{
|
|
file_buffer File;
|
|
file_buffer Metadata;
|
|
|
|
db_header4 Header;
|
|
db_header_entries4 EntriesHeader;
|
|
db_entry4 Entry;
|
|
db_header_assets4 AssetsHeader;
|
|
db_asset4 Asset;
|
|
db_landmark4 Landmark;
|
|
} database4;
|
|
#pragma pack(pop)
|
|
|
|
#define CINERA_DB_VERSION 4
|
|
|
|
#define db_header db_header4
|
|
#define db_header_entries db_header_entries4
|
|
#define db_entry db_entry4
|
|
#define db_header_assets db_header_assets4
|
|
#define db_asset db_asset4
|
|
#define db_landmark db_landmark4
|
|
#define database database4
|
|
// TODO(matt): Increment CINERA_DB_VERSION!
|
|
|
|
// NOTE(matt): Globals
|
|
arena MemoryArena;
|
|
config Config;
|
|
assets Assets;
|
|
int inotifyInstance;
|
|
watch_handles WatchHandles;
|
|
database DB;
|
|
time_t LastPrivacyCheck;
|
|
time_t LastQuoteFetch;
|
|
//
|
|
|
|
enum
|
|
{
|
|
PAGE_TYPE_SEARCH = -1,
|
|
} page_type_indices;
|
|
|
|
typedef struct
|
|
{
|
|
buffer IncludesSearch;
|
|
buffer SearchEntry;
|
|
buffer Search; // NOTE(matt): This buffer is malloc'd separately, rather than claimed from the memory_arena
|
|
buffer IncludesPlayer;
|
|
buffer Menus;
|
|
buffer Player;
|
|
buffer ScriptPlayer;
|
|
|
|
char Custom0[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1];
|
|
char Custom1[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1];
|
|
char Custom2[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1];
|
|
char Custom3[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1];
|
|
char Custom4[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1];
|
|
char Custom5[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1];
|
|
char Custom6[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1];
|
|
char Custom7[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1];
|
|
char Custom8[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1];
|
|
char Custom9[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1];
|
|
char Custom10[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1];
|
|
char Custom11[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1];
|
|
char Custom12[MAX_CUSTOM_SNIPPET_LONG_LENGTH + 1];
|
|
char Custom13[MAX_CUSTOM_SNIPPET_LONG_LENGTH + 1];
|
|
char Custom14[MAX_CUSTOM_SNIPPET_LONG_LENGTH + 1];
|
|
char Custom15[MAX_CUSTOM_SNIPPET_LONG_LENGTH + 1];
|
|
|
|
char ProjectID[MAX_PROJECT_ID_LENGTH + 1];
|
|
char ProjectName[MAX_PROJECT_NAME_LENGTH + 1];
|
|
char Theme[MAX_PROJECT_NAME_LENGTH + 1];
|
|
char Title[MAX_TITLE_LENGTH + 1];
|
|
char URLSearch[MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1];
|
|
char URLPlayer[MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH + 1];
|
|
char VideoID[16];
|
|
char VODPlatform[16];
|
|
} buffers;
|
|
|
|
enum
|
|
{
|
|
// Contents and Player Pages Mandatory
|
|
TAG_INCLUDES,
|
|
|
|
// Contents Page Mandatory
|
|
TAG_SEARCH,
|
|
|
|
// Player Page Mandatory
|
|
TAG_MENUS,
|
|
TAG_PLAYER,
|
|
TAG_SCRIPT,
|
|
|
|
// Player Page Optional
|
|
TAG_CUSTOM0,
|
|
TAG_CUSTOM1,
|
|
TAG_CUSTOM2,
|
|
TAG_CUSTOM3,
|
|
TAG_CUSTOM4,
|
|
TAG_CUSTOM5,
|
|
TAG_CUSTOM6,
|
|
TAG_CUSTOM7,
|
|
TAG_CUSTOM8,
|
|
TAG_CUSTOM9,
|
|
TAG_CUSTOM10,
|
|
TAG_CUSTOM11,
|
|
|
|
TAG_CUSTOM12,
|
|
TAG_CUSTOM13,
|
|
TAG_CUSTOM14,
|
|
TAG_CUSTOM15,
|
|
|
|
TAG_TITLE,
|
|
TAG_VIDEO_ID,
|
|
TAG_VOD_PLATFORM,
|
|
|
|
// Anywhere Optional
|
|
TAG_ASSET,
|
|
TAG_CSS,
|
|
TAG_IMAGE,
|
|
TAG_JS,
|
|
TAG_PROJECT,
|
|
TAG_PROJECT_ID,
|
|
TAG_SEARCH_URL,
|
|
TAG_THEME,
|
|
TAG_URL,
|
|
TEMPLATE_TAG_COUNT,
|
|
} template_tag_codes;
|
|
|
|
char *TemplateTags[] = {
|
|
"__CINERA_INCLUDES__",
|
|
|
|
"__CINERA_SEARCH__",
|
|
|
|
"__CINERA_MENUS__",
|
|
"__CINERA_PLAYER__",
|
|
"__CINERA_SCRIPT__",
|
|
|
|
"__CINERA_CUSTOM0__",
|
|
"__CINERA_CUSTOM1__",
|
|
"__CINERA_CUSTOM2__",
|
|
"__CINERA_CUSTOM3__",
|
|
"__CINERA_CUSTOM4__",
|
|
"__CINERA_CUSTOM5__",
|
|
"__CINERA_CUSTOM6__",
|
|
"__CINERA_CUSTOM7__",
|
|
"__CINERA_CUSTOM8__",
|
|
"__CINERA_CUSTOM9__",
|
|
"__CINERA_CUSTOM10__",
|
|
"__CINERA_CUSTOM11__",
|
|
|
|
"__CINERA_CUSTOM12__",
|
|
"__CINERA_CUSTOM13__",
|
|
"__CINERA_CUSTOM14__",
|
|
"__CINERA_CUSTOM15__",
|
|
|
|
"__CINERA_TITLE__",
|
|
"__CINERA_VIDEO_ID__",
|
|
"__CINERA_VOD_PLATFORM__",
|
|
|
|
"__CINERA_ASSET__",
|
|
"__CINERA_CSS__",
|
|
"__CINERA_IMAGE__",
|
|
"__CINERA_JS__",
|
|
"__CINERA_PROJECT__",
|
|
"__CINERA_PROJECT_ID__",
|
|
"__CINERA_SEARCH_URL__",
|
|
"__CINERA_THEME__",
|
|
"__CINERA_URL__",
|
|
};
|
|
|
|
typedef struct
|
|
{
|
|
int Offset;
|
|
uint32_t AssetIndex;
|
|
enum8(template_tag_codes) TagCode;
|
|
} tag_offset;
|
|
|
|
typedef struct
|
|
{
|
|
int Validity; // NOTE(matt): Bitmask describing which page the template is valid for, i.e. contents and / or player page
|
|
int TagCapacity;
|
|
int TagCount;
|
|
tag_offset *Tags;
|
|
} template_metadata;
|
|
|
|
typedef struct
|
|
{
|
|
file_buffer File;
|
|
template_metadata Metadata;
|
|
} template;
|
|
|
|
// 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
|
|
{
|
|
char Date[32];
|
|
char Text[512];
|
|
} quote_info;
|
|
|
|
typedef struct
|
|
{
|
|
char Timecode[8];
|
|
int Identifier;
|
|
} identifier;
|
|
|
|
#define MAX_REF_IDENTIFIER_COUNT 64
|
|
typedef struct
|
|
{
|
|
char RefTitle[620];
|
|
char ID[512];
|
|
char URL[512];
|
|
char Source[256];
|
|
identifier Identifier[MAX_REF_IDENTIFIER_COUNT];
|
|
int IdentifierCount;
|
|
} ref_info;
|
|
|
|
typedef struct
|
|
{
|
|
char Marker[32];
|
|
char WrittenText[32];
|
|
} category_info;
|
|
|
|
typedef struct
|
|
{
|
|
category_info Category[64];
|
|
int Count;
|
|
} categories;
|
|
|
|
char *SupportIcons[] =
|
|
{
|
|
"cinera_sprite_patreon.png",
|
|
"cinera_sprite_sendowl.png",
|
|
};
|
|
|
|
typedef enum
|
|
{
|
|
ICON_PATREON = BUILTIN_ASSETS_COUNT,
|
|
ICON_SENDOWL,
|
|
SUPPORT_ICON_COUNT,
|
|
} support_icons;
|
|
|
|
// TODO(matt): Parse this stuff out of a config file
|
|
typedef struct
|
|
{
|
|
char *Username;
|
|
char *CreditedName;
|
|
char *HomepageURL;
|
|
enum8(support_icons) SupportIconIndex;
|
|
char *SupportURL;
|
|
} credential_info;
|
|
|
|
credential_info Credentials[] =
|
|
{
|
|
{ "a_waterman", "Andrew Waterman", "https://www.linkedin.com/in/andrew-waterman-76805788" },
|
|
{ "y_lee", "Yunsup Lee", "https://www.linkedin.com/in/yunsup-lee-385b692b/" },
|
|
{ "AndrewJDR", "Andrew Johnson" },
|
|
{ "AsafGartner", "Asaf Gartner" },
|
|
{ "BretHudson", "Bret Hudson", "http://www.brethudson.com/", ICON_PATREON, "https://www.patreon.com/indieFunction"},
|
|
{ "ChronalDragon", "Andrew Chronister", "http://chronal.net/" },
|
|
{ "Kelimion", "Jeroen van Rijn", "https://handmade.network/home" },
|
|
{ "Mannilie", "Emmanuel Vaccaro", "http://emmanuelvaccaro.com/" },
|
|
{ "Miblo", "Matt Mascarenhas", "https://miblodelcarpio.co.uk/", ICON_SENDOWL, "https://miblodelcarpio.co.uk/cinera#pledge"},
|
|
{ "Mr4thDimention", "Allen Webster", "http://www.4coder.net/" },
|
|
{ "Pseudonym73", "Andrew Bromage", "https://twitter.com/deguerre" },
|
|
{ "Quel_Solaar", "Eskil Steenberg", "http://quelsolaar.com/" },
|
|
{ "ZedZull", "Jay Waggle" },
|
|
{ "abnercoimbre", "Abner Coimbre", "https://handmade.network/m/abnercoimbre" },
|
|
{ "brianwill", "Brian Will", "http://brianwill.net/blog/" },
|
|
{ "cbloom", "Charles Bloom", "http://cbloomrants.blogspot.co.uk/" },
|
|
{ "cmuratori", "Casey Muratori", "https://handmadehero.org", ICON_SENDOWL, "https://handmadehero.org/fund"},
|
|
{ "csnover", "Colin Snover", "https://zetafleet.com/" },
|
|
{ "debiatan", "Miguel Lechón", "http://blog.debiatan.net/" },
|
|
{ "dspecht", "Dustin Specht" },
|
|
{ "effect0r", "Cory Henderlite" },
|
|
{ "ffsjs", "ffsjs" },
|
|
{ "fierydrake", "Mike Tunnicliffe" },
|
|
{ "garlandobloom", "Matthew VanDevander", "https://lowtideproductions.com/", ICON_PATREON, "https://www.patreon.com/mv"},
|
|
{ "ikerms", "Iker Murga" },
|
|
{ "insofaras", "Alex Baines", "https://abaines.me.uk/" },
|
|
{ "jacebennett", "Jace Bennett" },
|
|
{ "jon", "Jonathan Blow", "http://the-witness.net/news/" },
|
|
{ "jpike", "Jacob Pike" },
|
|
{ "martincohen", "Martin Cohen", "http://blog.coh.io/" },
|
|
{ "miotatsu", "Mio Iwakura", "http://riscy.tv/", ICON_PATREON, "https://patreon.com/miotatsu"},
|
|
{ "nothings", "Sean Barrett", "https://nothings.org/" },
|
|
{ "pervognsen", "Per Vognsen", "https://github.com/pervognsen/bitwise/" },
|
|
{ "philipbuuck", "Philip Buuck", "http://philipbuuck.com/" },
|
|
{ "powerc9000", "Clay Murray", "http://claymurray.website/" },
|
|
{ "rygorous", "Fabian Giesen", "https://fgiesen.wordpress.com/" },
|
|
{ "schme", "Kasper Sauramo" },
|
|
{ "sssmcgrath", "Shawn McGrath", "http://www.dyadgame.com/" },
|
|
{ "thehappiecat", "Anne", "https://www.youtube.com/c/TheHappieCat", ICON_PATREON, "https://www.patreon.com/thehappiecat"},
|
|
{ "theinternetftw", "Ben Craddock" },
|
|
{ "wheatdog", "Tim Liou", "http://stringbulbs.com/" },
|
|
{ "williamchyr", "William Chyr", "http://williamchyr.com/" },
|
|
{ "wonchun", "Won Chun", "https://twitter.com/won3d" },
|
|
};
|
|
|
|
typedef struct
|
|
{
|
|
char *Medium;
|
|
char *Icon;
|
|
char *WrittenName;
|
|
} category_medium;
|
|
|
|
category_medium CategoryMedium[] =
|
|
{
|
|
// medium icon written name
|
|
{ "admin", "🗹", "Administrivia"},
|
|
{ "afk", "…" , "Away from Keyboard"},
|
|
{ "authored", "🗪", "Chat Comment"},
|
|
{ "blackboard", "🖌", "Blackboard"},
|
|
{ "drawing", "🎨", "Drawing"},
|
|
{ "experience", "🍷", "Experience"},
|
|
{ "hat", "🎩", "Hat"},
|
|
{ "multimedia", "🎬", "Media Clip"},
|
|
{ "owl", "🦉", "Owl of Shame"},
|
|
{ "programming", "🖮", "Programming"},
|
|
{ "rant", "💢", "Rant"},
|
|
{ "research", "📖", "Research"},
|
|
{ "run", "🏃", "In-Game"}, // TODO(matt): Potentially make this written name configurable per project
|
|
{ "speech", "🗩", "Speech"},
|
|
{ "trivia", "🎲", "Trivia"},
|
|
};
|
|
|
|
enum
|
|
{
|
|
NS_CALENDRICAL,
|
|
NS_LINEAR,
|
|
NS_SEASONAL,
|
|
} numbering_schemes;
|
|
|
|
typedef struct
|
|
{
|
|
char *ProjectID;
|
|
char *FullName;
|
|
char *Unit; // e.g. Day, Episode, Session
|
|
enum8(numbering_schemes) NumberingScheme; // numbering_schemes
|
|
char *Medium;
|
|
char *AltURLPrefix; // NOTE(matt): This currently just straight up replaces the ProjectID in the player
|
|
// pages' output directories
|
|
} project_info;
|
|
|
|
project_info ProjectInfo[] =
|
|
{
|
|
{ "bitwise", "Bitwise", "Day", NS_LINEAR, "programming", "" },
|
|
|
|
{ "book", "Book Club", "Day", NS_LINEAR, "research", "" },
|
|
{ "coad", "Computer Organization and Design", "", NS_LINEAR, "research", "" },
|
|
{ "reader", "RISC-V Reader", "", NS_LINEAR, "research", "" },
|
|
{ "riscy", "RISCY BUSINESS", "Day", NS_LINEAR, "programming", "" },
|
|
{ "risc", "RISCellaneous", "", NS_CALENDRICAL, "speech", "" },
|
|
|
|
{ "chat", "Handmade Chat", "Day", NS_LINEAR, "speech", "" },
|
|
{ "code", "Handmade Hero", "Day", NS_LINEAR, "programming", "day" },
|
|
{ "intro-to-c", "Intro to C on Windows", "Day", NS_LINEAR, "programming", "day" },
|
|
{ "misc", "Handmade Miscellany", "", NS_LINEAR, "admin", "" },
|
|
{ "ray", "Handmade Ray", "Day", NS_LINEAR, "programming", "" },
|
|
|
|
{ "hmdshow", "HandmadeDev Show", "", NS_SEASONAL, "speech", "ep" },
|
|
{ "lecture", "Abner Talks", "", NS_SEASONAL, "speech", "" },
|
|
{ "stream", "Abner Programs", "", NS_SEASONAL, "programming", "" },
|
|
{ "special", "Abner Show Special", "", NS_SEASONAL, "programming", "" },
|
|
|
|
{ "obbg", "Open Block Building Game", "Episode", NS_LINEAR, "programming", "" },
|
|
|
|
{ "sysadmin", "SysAdmin", "Session", NS_LINEAR, "admin", "" },
|
|
};
|
|
|
|
char *ColourStrings[] =
|
|
{
|
|
"\e[0m",
|
|
"\e[0;33m",
|
|
"\e[0;34m",
|
|
"\e[0;35m", "\e[0;35m",
|
|
"\e[1;30m", "\e[1;30m",
|
|
"\e[1;31m",
|
|
"\e[1;32m", "\e[1;32m",
|
|
"\e[1;33m",
|
|
};
|
|
|
|
enum
|
|
{
|
|
CS_END,
|
|
CS_WARNING,
|
|
CS_PRIVATE,
|
|
CS_ONGOING, CS_PURPLE,
|
|
CS_COMMENT, CS_DELETION,
|
|
CS_ERROR,
|
|
CS_ADDITION, CS_SUCCESS,
|
|
CS_REINSERTION,
|
|
} colour_strings;
|
|
|
|
typedef struct
|
|
{
|
|
char *Name;
|
|
enum8(colour_strings) Colour;
|
|
} edit_type;
|
|
|
|
enum
|
|
{
|
|
EDIT_INSERTION,
|
|
EDIT_APPEND,
|
|
EDIT_REINSERTION,
|
|
EDIT_DELETION,
|
|
EDIT_ADDITION,
|
|
} edit_types;
|
|
|
|
edit_type EditTypes[] =
|
|
{
|
|
{ "Inserted", CS_ADDITION },
|
|
{ "Appended", CS_ADDITION },
|
|
{ "Reinserted", CS_REINSERTION },
|
|
{ "Deleted", CS_DELETION },
|
|
{ "Added", CS_ADDITION }
|
|
};
|
|
|
|
void
|
|
Clear(char *String, int Size)
|
|
{
|
|
for(int i = 0; i < Size; ++i)
|
|
{
|
|
String[i] = 0;
|
|
}
|
|
}
|
|
|
|
int
|
|
StringLength(char *String)
|
|
{
|
|
int i = 0;
|
|
while(String[i])
|
|
{
|
|
++i;
|
|
}
|
|
return i;
|
|
}
|
|
|
|
#define CopyString(Dest, DestSize, Format, ...) CopyString_(__LINE__, (Dest), (DestSize), (Format), ##__VA_ARGS__)
|
|
__attribute__ ((format (printf, 4, 5)))
|
|
int
|
|
CopyString_(int LineNumber, char Dest[], int DestSize, char *Format, ...)
|
|
{
|
|
int Length = 0;
|
|
va_list Args;
|
|
va_start(Args, Format);
|
|
Length = vsnprintf(Dest, DestSize, Format, Args);
|
|
if(Length >= DestSize)
|
|
{
|
|
printf("CopyString() call on line %d has been passed a buffer too small (%d bytes) to contain null-terminated %d(+1)-character string\n", LineNumber, DestSize, Length);
|
|
__asm__("int3");
|
|
}
|
|
va_end(Args);
|
|
return Length;
|
|
}
|
|
|
|
#define CopyStringNoFormat(Dest, DestSize, String) CopyStringNoFormat_(__LINE__, Dest, DestSize, String)
|
|
int
|
|
CopyStringNoFormat_(int LineNumber, char *Dest, int DestSize, char *String)
|
|
{
|
|
int Length = 0;
|
|
char *Start = String;
|
|
while(*String)
|
|
{
|
|
*Dest++ = *String++;
|
|
++Length;
|
|
}
|
|
if(Length >= DestSize)
|
|
{
|
|
printf("CopyStringNoFormat() call on line %d has been passed a buffer too small (%d bytes) to contain null-terminated %d(+1)-character string:\n"
|
|
"%s\n", LineNumber, DestSize, Length, Start);
|
|
__asm__("int3");
|
|
}
|
|
*Dest = '\0';
|
|
return Length;
|
|
}
|
|
|
|
#define ClearCopyStringNoFormat(Dest, DestSize, String) ClearCopyStringNoFormat_(__LINE__, Dest, DestSize, String)
|
|
int
|
|
ClearCopyStringNoFormat_(int LineNumber, char *Dest, int DestSize, char *String)
|
|
{
|
|
Clear(Dest, DestSize);
|
|
int Length = 0;
|
|
char *Start = String;
|
|
while(*String)
|
|
{
|
|
*Dest++ = *String++;
|
|
++Length;
|
|
}
|
|
if(Length >= DestSize)
|
|
{
|
|
printf("ClearCopyStringNoFormat() call on line %d has been passed a buffer too small (%d bytes) to contain null-terminated %d(+1)-character string:\n"
|
|
"%s\n", LineNumber, DestSize, Length, Start);
|
|
__asm__("int3");
|
|
}
|
|
*Dest = '\0';
|
|
return Length;
|
|
}
|
|
|
|
// TODO(matt): Maybe do a version of this that takes a string as a Terminator
|
|
#define CopyStringNoFormatT(Dest, DestSize, String, Terminator) CopyStringNoFormatT_(__LINE__, Dest, DestSize, String, Terminator)
|
|
int
|
|
CopyStringNoFormatT_(int LineNumber, char *Dest, int DestSize, char *String, char Terminator)
|
|
{
|
|
int Length = 0;
|
|
char *Start = String;
|
|
while(*String != Terminator)
|
|
{
|
|
*Dest++ = *String++;
|
|
++Length;
|
|
}
|
|
if(Length >= DestSize)
|
|
{
|
|
printf("CopyStringNoFormatT() call on line %d has been passed a buffer too small (%d bytes) to contain %c-terminated %d(+1)-character string:\n"
|
|
"%.*s\n", LineNumber, DestSize, Terminator == 0 ? '0' : Terminator, Length, Length, Start);
|
|
__asm__("int3");
|
|
}
|
|
*Dest = '\0';
|
|
return Length;
|
|
}
|
|
|
|
#define CopyStringToBuffer(Dest, Format, ...) CopyStringToBuffer_(__LINE__, Dest, Format, ##__VA_ARGS__)
|
|
__attribute__ ((format (printf, 3, 4)))
|
|
void
|
|
CopyStringToBuffer_(int LineNumber, buffer *Dest, char *Format, ...)
|
|
{
|
|
va_list Args;
|
|
va_start(Args, Format);
|
|
int Length = vsnprintf(Dest->Ptr, Dest->Size - (Dest->Ptr - Dest->Location), Format, Args);
|
|
va_end(Args);
|
|
if(Length + (Dest->Ptr - Dest->Location) >= Dest->Size)
|
|
{
|
|
fprintf(stderr, "CopyStringToBuffer(%s) call on line %d cannot accommodate null-terminated %d(+1)-character string:\n"
|
|
"%s\n", Dest->ID, LineNumber, Length, Format);
|
|
__asm__("int3");
|
|
}
|
|
Dest->Ptr += Length;
|
|
}
|
|
|
|
#define CopyStringToBufferNoFormat(Dest, String) CopyStringToBufferNoFormat_(__LINE__, Dest, String)
|
|
void
|
|
CopyStringToBufferNoFormat_(int LineNumber, buffer *Dest, char *String)
|
|
{
|
|
char *Start = String;
|
|
while(*String)
|
|
{
|
|
*Dest->Ptr++ = *String++;
|
|
}
|
|
if(Dest->Ptr - Dest->Location >= Dest->Size)
|
|
{
|
|
fprintf(stderr, "CopyStringToBufferNoFormat(%s) call on line %d cannot accommodate %d-character string:\n"
|
|
"%s\n", Dest->ID, LineNumber, StringLength(Start), Start);
|
|
__asm__("int3");
|
|
}
|
|
*Dest->Ptr = '\0';
|
|
}
|
|
|
|
#define CopyStringToBufferNoFormatL(Dest, Length, String) CopyStringToBufferNoFormatL_(__LINE__, Dest, Length, String)
|
|
void
|
|
CopyStringToBufferNoFormatL_(int LineNumber, buffer *Dest, int Length, char *String)
|
|
{
|
|
char *Start = String;
|
|
for(int i = 0; i < Length; ++i)
|
|
{
|
|
*Dest->Ptr++ = *String++;
|
|
}
|
|
if(Dest->Ptr - Dest->Location >= Dest->Size)
|
|
{
|
|
fprintf(stderr, "CopyStringToBufferNoFormat(%s) call on line %d cannot accommodate %d-character string:\n"
|
|
"%s\n", Dest->ID, LineNumber, StringLength(Start), Start);
|
|
__asm__("int3");
|
|
}
|
|
*Dest->Ptr = '\0';
|
|
}
|
|
|
|
#define CopyStringToBufferHTMLSafe(Dest, String) CopyStringToBufferHTMLSafe_(__LINE__, Dest, String)
|
|
void
|
|
CopyStringToBufferHTMLSafe_(int LineNumber, buffer *Dest, char *String)
|
|
{
|
|
char *Start = String;
|
|
int Length = StringLength(String);
|
|
while(*String)
|
|
{
|
|
switch(*String)
|
|
{
|
|
case '<': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'l'; *Dest->Ptr++ = 't'; *Dest->Ptr++ = ';'; Length += 3; break;
|
|
case '>': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'g'; *Dest->Ptr++ = 't'; *Dest->Ptr++ = ';'; Length += 3; break;
|
|
case '&': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'a'; *Dest->Ptr++ = 'm'; *Dest->Ptr++ = 'p'; *Dest->Ptr++ = ';'; Length += 4; break;
|
|
case '\"': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'q'; *Dest->Ptr++ = 'u'; *Dest->Ptr++ = 'o'; *Dest->Ptr++ = 't'; *Dest->Ptr++ = ';'; Length += 5; break;
|
|
case '\'': *Dest->Ptr++ = '&'; *Dest->Ptr++ = '#'; *Dest->Ptr++ = '3'; *Dest->Ptr++ = '9'; *Dest->Ptr++ = ';'; Length += 4; break;
|
|
default: *Dest->Ptr++ = *String; break;
|
|
}
|
|
++String;
|
|
}
|
|
if(Dest->Ptr - Dest->Location >= Dest->Size)
|
|
{
|
|
fprintf(stderr, "CopyStringToBufferHTMLSafe(%s) call on line %d cannot accommodate %d(+1)-character HTML-sanitised string:\n"
|
|
"%s\n", Dest->ID, LineNumber, Length, Start);
|
|
__asm__("int3");
|
|
}
|
|
*Dest->Ptr = '\0';
|
|
}
|
|
|
|
#define CopyStringToBufferHTMLSafeBreakingOnSlash(Dest, String) CopyStringToBufferHTMLSafeBreakingOnSlash_(__LINE__, Dest, String)
|
|
void
|
|
CopyStringToBufferHTMLSafeBreakingOnSlash_(int LineNumber, buffer *Dest, char *String)
|
|
{
|
|
char *Start = String;
|
|
int Length = StringLength(String);
|
|
while(*String)
|
|
{
|
|
switch(*String)
|
|
{
|
|
case '<': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'l'; *Dest->Ptr++ = 't'; *Dest->Ptr++ = ';'; Length += 3; break;
|
|
case '>': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'g'; *Dest->Ptr++ = 't'; *Dest->Ptr++ = ';'; Length += 3; break;
|
|
case '&': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'a'; *Dest->Ptr++ = 'm'; *Dest->Ptr++ = 'p'; *Dest->Ptr++ = ';'; Length += 4; break;
|
|
case '\'': *Dest->Ptr++ = '&'; *Dest->Ptr++ = '#'; *Dest->Ptr++ = '3'; *Dest->Ptr++ = '9'; *Dest->Ptr++ = ';'; Length += 4; break;
|
|
case '\"': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'q'; *Dest->Ptr++ = 'u'; *Dest->Ptr++ = 'o'; *Dest->Ptr++ = 't'; *Dest->Ptr++ = ';'; Length += 5; break;
|
|
case '/': *Dest->Ptr++ = '/'; *Dest->Ptr++ = '&'; *Dest->Ptr++ = '#'; *Dest->Ptr++ = '8'; *Dest->Ptr++ = '2'; *Dest->Ptr++ = '0'; *Dest->Ptr++ = '3'; *Dest->Ptr++ = ';'; Length += 7; break;
|
|
default: *Dest->Ptr++ = *String; break;
|
|
}
|
|
++String;
|
|
}
|
|
if(Dest->Ptr - Dest->Location >= Dest->Size)
|
|
{
|
|
fprintf(stderr, "CopyStringToBufferHTMLSafeBreakingOnSlash(%s) call on line %d cannot accommodate %d(+1)-character HTML-sanitised string:\n"
|
|
"%s\n", Dest->ID, LineNumber, Length, Start);
|
|
__asm__("int3");
|
|
}
|
|
*Dest->Ptr = '\0';
|
|
}
|
|
|
|
#define CopyStringToBufferHTMLPercentEncoded(Dest, String) CopyStringToBufferHTMLPercentEncoded_(__LINE__, Dest, String)
|
|
void
|
|
CopyStringToBufferHTMLPercentEncoded_(int LineNumber, buffer *Dest, char *String)
|
|
{
|
|
char *Start = String;
|
|
int Length = StringLength(String);
|
|
while(*String)
|
|
{
|
|
switch(*String)
|
|
{
|
|
case ' ': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '2'; *Dest->Ptr++ = '0'; Length += 2; break;
|
|
case '\"': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '2'; *Dest->Ptr++ = '2'; Length += 2; break;
|
|
case '%': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '2'; *Dest->Ptr++ = '5'; Length += 2; break;
|
|
case '&': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '2'; *Dest->Ptr++ = '6'; Length += 2; break;
|
|
case '<': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '3'; *Dest->Ptr++ = 'C'; Length += 2; break;
|
|
case '>': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '3'; *Dest->Ptr++ = 'E'; Length += 2; break;
|
|
case '?': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '3'; *Dest->Ptr++ = 'F'; Length += 2; break;
|
|
case '\\': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '5'; *Dest->Ptr++ = 'C'; Length += 2; break;
|
|
case '^': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '5'; *Dest->Ptr++ = 'E'; Length += 2; break;
|
|
case '`': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '6'; *Dest->Ptr++ = '0'; Length += 2; break;
|
|
case '{': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '7'; *Dest->Ptr++ = 'B'; Length += 2; break;
|
|
case '|': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '7'; *Dest->Ptr++ = 'C'; Length += 2; break;
|
|
case '}': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '7'; *Dest->Ptr++ = 'D'; Length += 2; break;
|
|
default: *Dest->Ptr++ = *String; break;
|
|
}
|
|
++String;
|
|
}
|
|
if(Dest->Ptr - Dest->Location >= Dest->Size)
|
|
{
|
|
fprintf(stderr, "CopyStringToBufferHTMLPercentEncodedL(%s) call on line %d cannot accommodate %d(+1)-character percent-encoded string:\n"
|
|
"%s\n", Dest->ID, LineNumber, Length, Start);
|
|
__asm__("int3");
|
|
}
|
|
*Dest->Ptr = '\0';
|
|
}
|
|
|
|
#define CopyBuffer(Dest, Src) CopyBuffer_(__LINE__, Dest, Src)
|
|
void
|
|
CopyBuffer_(int LineNumber, buffer *Dest, buffer *Src)
|
|
{
|
|
Src->Ptr = Src->Location;
|
|
while(*Src->Ptr)
|
|
{
|
|
*Dest->Ptr++ = *Src->Ptr++;
|
|
}
|
|
if(Dest->Ptr - Dest->Location >= Dest->Size)
|
|
{
|
|
fprintf(stderr, "CopyBuffer(%s) call on line %d cannot accommodate %d(+1)-character %s\n", Dest->ID, LineNumber, StringLength(Src->Location), Src->ID);
|
|
__asm__("int3");
|
|
}
|
|
*Dest->Ptr = '\0';
|
|
}
|
|
|
|
#define CopyBufferSized(Dest, Src, Size) CopyBufferSized_(__LINE__, Dest, Src, Size)
|
|
void
|
|
CopyBufferSized_(int LineNumber, buffer *Dest, buffer *Src, int Size)
|
|
{
|
|
// NOTE(matt): Similar to CopyBuffer(), just without null-terminating
|
|
Src->Ptr = Src->Location;
|
|
while(Src->Ptr - Src->Location < Size)
|
|
{
|
|
*Dest->Ptr++ = *Src->Ptr++;
|
|
}
|
|
if(Dest->Ptr - Dest->Location >= Dest->Size)
|
|
{
|
|
fprintf(stderr, "CopyBufferNoNull(%s) call on line %d cannot accommodate %d(+1)-character %s\n", Dest->ID, LineNumber, StringLength(Src->Location), Src->ID);
|
|
__asm__("int3");
|
|
}
|
|
}
|
|
|
|
int
|
|
StringsDiffer(char *A, char *B) // NOTE(matt): Two null-terminated strings
|
|
{
|
|
while(*A && *B && *A == *B)
|
|
{
|
|
++A, ++B;
|
|
}
|
|
return *A - *B;
|
|
}
|
|
|
|
int
|
|
StringsDifferCaseInsensitive(char *A, char *B) // NOTE(matt): Two null-terminated strings
|
|
{
|
|
while(*A && *B &&
|
|
((*A >= 'A' && *A <= 'Z') ? *A + ('a' - 'A') : *A) ==
|
|
((*B >= 'A' && *B <= 'Z') ? *B + ('a' - 'A') : *B))
|
|
{
|
|
++A, ++B;
|
|
}
|
|
return *A - *B;
|
|
}
|
|
|
|
bool
|
|
StringsDifferT(char *A, // NOTE(matt): Null-terminated string
|
|
char *B, // NOTE(matt): Not null-terminated string (e.g. one mid-buffer)
|
|
char Terminator // NOTE(matt): Caller definable terminator. Pass 0 to only match on the extent of A
|
|
)
|
|
{
|
|
// TODO(matt): Make sure this can't crash upon reaching the end of B's buffer
|
|
int ALength = StringLength(A);
|
|
int i = 0;
|
|
while(i < ALength && A[i] && A[i] == B[i])
|
|
{
|
|
++i;
|
|
}
|
|
if((!Terminator && !A[i] && ALength == i) ||
|
|
(!A[i] && ALength == i && (B[i] == Terminator)))
|
|
{
|
|
return FALSE;
|
|
}
|
|
else
|
|
{
|
|
return TRUE;
|
|
}
|
|
}
|
|
|
|
enum
|
|
{
|
|
C_SEEK_FORWARDS,
|
|
C_SEEK_BACKWARDS
|
|
} seek_directions;
|
|
|
|
enum
|
|
{
|
|
C_SEEK_START, // First character of string
|
|
C_SEEK_BEFORE, // Character before first character
|
|
C_SEEK_END, // Last character of string
|
|
C_SEEK_AFTER // Character after last character
|
|
} seek_positions;
|
|
|
|
int
|
|
SeekBufferForString(buffer *Buffer, char *String,
|
|
enum8(seek_directions) Direction,
|
|
enum8(seek_positions) Position)
|
|
{
|
|
// TODO(matt): Optimise? Some means of analysing the String to increment
|
|
// the pointer in bigger strides
|
|
|
|
// Perhaps count up runs of consecutive chars and seek for the char with
|
|
// the longest run, in strides of that run-length
|
|
|
|
char *InitialLocation = Buffer->Ptr;
|
|
if(Direction == C_SEEK_FORWARDS)
|
|
{
|
|
while(Buffer->Ptr - Buffer->Location < Buffer->Size - StringLength(String)
|
|
&& StringsDifferT(String, Buffer->Ptr, 0))
|
|
{
|
|
++Buffer->Ptr;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
while(Buffer->Ptr > Buffer->Location
|
|
&& StringsDifferT(String, Buffer->Ptr, 0))
|
|
{
|
|
--Buffer->Ptr;
|
|
}
|
|
}
|
|
|
|
if(StringsDifferT(String, Buffer->Ptr, 0))
|
|
{
|
|
Buffer->Ptr = InitialLocation;
|
|
return RC_UNFOUND;
|
|
}
|
|
|
|
switch(Position)
|
|
{
|
|
case C_SEEK_START:
|
|
break;
|
|
case C_SEEK_BEFORE:
|
|
if(Buffer->Ptr > Buffer->Location)
|
|
{
|
|
--Buffer->Ptr;
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
return RC_ERROR_SEEK; // Ptr remains at string start
|
|
}
|
|
case C_SEEK_END:
|
|
Buffer->Ptr += StringLength(String) - 1;
|
|
break;
|
|
case C_SEEK_AFTER:
|
|
if(Buffer->Size >= Buffer->Ptr - Buffer->Location + StringLength(String))
|
|
{
|
|
Buffer->Ptr += StringLength(String);
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
return RC_ERROR_SEEK; // Ptr remains at string start
|
|
// NOTE(matt): Should it, however, be left at the end of the string?
|
|
}
|
|
}
|
|
return RC_SUCCESS;
|
|
}
|
|
|
|
int
|
|
StripComponentFromPath(char *Path)
|
|
{
|
|
char *Ptr = Path + StringLength(Path) - 1;
|
|
if(Ptr < Path) { return RC_ERROR_DIRECTORY; }
|
|
while(Ptr > Path && *Ptr != '/')
|
|
{
|
|
--Ptr;
|
|
}
|
|
*Ptr = '\0';
|
|
return RC_SUCCESS;
|
|
}
|
|
|
|
int ClaimBuffer(buffer *Buffer, char *ID, int Size);
|
|
void DeclaimBuffer(buffer *Buffer);
|
|
|
|
int
|
|
ResolvePath(char *Path)
|
|
{
|
|
buffer B;
|
|
ClaimBuffer(&B, "ResolvedPath", StringLength(Path) + 1);
|
|
CopyStringToBufferNoFormat(&B, Path);
|
|
|
|
B.Ptr = B.Location;
|
|
while(SeekBufferForString(&B, "/../", C_SEEK_FORWARDS, C_SEEK_END) == RC_SUCCESS)
|
|
{
|
|
char *NextComponentHead = B.Ptr;
|
|
int RemainingChars = StringLength(NextComponentHead);
|
|
--B.Ptr;
|
|
SeekBufferForString(&B, "/", C_SEEK_BACKWARDS, C_SEEK_BEFORE);
|
|
SeekBufferForString(&B, "/", C_SEEK_BACKWARDS, C_SEEK_START);
|
|
CopyStringToBufferNoFormat(&B, NextComponentHead);
|
|
Clear(B.Ptr, B.Size - (B.Ptr - B.Location));
|
|
B.Ptr -= RemainingChars;
|
|
}
|
|
|
|
CopyStringNoFormat(Path, StringLength(Path) + 1, B.Location);
|
|
|
|
DeclaimBuffer(&B);
|
|
return RC_SUCCESS;
|
|
}
|
|
|
|
int
|
|
MakeDir(char *Path)
|
|
{
|
|
// TODO(matt): Correctly check for permissions
|
|
int i = StringLength(Path);
|
|
int Ancestors = 0;
|
|
while(mkdir(Path, 00755) == -1)
|
|
{
|
|
if(errno == EACCES)
|
|
{
|
|
return RC_ERROR_DIRECTORY;
|
|
}
|
|
if(StripComponentFromPath(Path) == RC_ERROR_DIRECTORY) { return RC_ERROR_DIRECTORY; }
|
|
++Ancestors;
|
|
}
|
|
while(Ancestors > 0)
|
|
{
|
|
while(Path[i] != '\0')
|
|
{
|
|
++i;
|
|
}
|
|
Path[i] = '/';
|
|
--Ancestors;
|
|
if((mkdir(Path, 00755)) == -1)
|
|
{
|
|
return RC_ERROR_DIRECTORY;
|
|
}
|
|
}
|
|
return RC_SUCCESS;
|
|
}
|
|
|
|
void
|
|
LogUsage(buffer *Buffer)
|
|
{
|
|
#if DEBUG
|
|
char LogPath[256];
|
|
CopyString(LogPath, "%s/%s", Config.CacheDir, "buffers.log");
|
|
FILE *LogFile;
|
|
if(!(LogFile = fopen(LogPath, "a+")))
|
|
{
|
|
MakeDir(Config.CacheDir);
|
|
if(!(LogFile = fopen(LogPath, "a+")))
|
|
{
|
|
perror("LogUsage");
|
|
return;
|
|
}
|
|
}
|
|
|
|
fprintf(LogFile, "%s,%ld,%d\n",
|
|
Buffer->ID,
|
|
Buffer->Ptr - Buffer->Location,
|
|
Buffer->Size);
|
|
fclose(LogFile);
|
|
#endif
|
|
}
|
|
|
|
__attribute__ ((format (printf, 2, 3)))
|
|
void
|
|
LogError(int LogLevel, char *Format, ...)
|
|
{
|
|
if(Config.LogLevel >= LogLevel)
|
|
{
|
|
char LogPath[256];
|
|
CopyString(LogPath, sizeof(LogPath), "%s/%s", Config.CacheDir, "errors.log");
|
|
FILE *LogFile;
|
|
if(!(LogFile = fopen(LogPath, "a+")))
|
|
{
|
|
MakeDir(Config.CacheDir);
|
|
if(!(LogFile = fopen(LogPath, "a+")))
|
|
{
|
|
perror("LogUsage");
|
|
return;
|
|
}
|
|
}
|
|
|
|
va_list Args;
|
|
va_start(Args, Format);
|
|
vfprintf(LogFile, Format, Args);
|
|
va_end(Args);
|
|
// TODO(matt): Include the LogLevel "string" and the current wall time
|
|
fprintf(LogFile, "\n");
|
|
fclose(LogFile);
|
|
}
|
|
}
|
|
|
|
int
|
|
ReadFileIntoBuffer(file_buffer *File, int BufferPadding)
|
|
{
|
|
if(!(File->Handle = fopen(File->Path, "r"))) // TODO(matt): Fuller error handling
|
|
{
|
|
return RC_ERROR_FILE;
|
|
}
|
|
|
|
fseek(File->Handle, 0, SEEK_END);
|
|
File->FileSize = ftell(File->Handle);
|
|
File->Buffer.Size = File->FileSize + 1 + BufferPadding; // NOTE(matt): +1 to accommodate a NULL terminator
|
|
fseek(File->Handle, 0, SEEK_SET);
|
|
|
|
// TODO(matt): Consider using the MemoryArena? Maybe have separate ReadFileIntoMemory() and ReadFileIntoArena()
|
|
if(!(File->Buffer.Location = malloc(File->Buffer.Size)))
|
|
{
|
|
fclose(File->Handle);
|
|
return RC_ERROR_MEMORY;
|
|
}
|
|
File->Buffer.Ptr = File->Buffer.Location;
|
|
|
|
fread(File->Buffer.Location, File->FileSize, 1, File->Handle);
|
|
File->Buffer.Location[File->FileSize] = '\0';
|
|
fclose(File->Handle);
|
|
File->Buffer.ID = File->Path;
|
|
return RC_SUCCESS;
|
|
}
|
|
|
|
void
|
|
FreeBuffer(buffer *Buffer)
|
|
{
|
|
free(Buffer->Location);
|
|
Buffer->Location = 0;
|
|
Buffer->Ptr = 0;
|
|
Buffer->Size = 0;
|
|
#if DEBUG_MEM
|
|
FILE *MemLog = fopen("/home/matt/cinera_mem", "a+");
|
|
fprintf(MemLog, " Freed %s\n", Buffer->ID);
|
|
fclose(MemLog);
|
|
printf(" Freed %s\n", Buffer->ID);
|
|
#endif
|
|
Buffer->ID = 0;
|
|
}
|
|
|
|
int
|
|
ClaimBuffer(buffer *Buffer, char *ID, int Size)
|
|
{
|
|
if(MemoryArena.Ptr - MemoryArena.Location + Size > MemoryArena.Size)
|
|
{
|
|
return RC_ARENA_FULL;
|
|
}
|
|
Buffer->Location = (char *)MemoryArena.Ptr;
|
|
Buffer->Size = Size;
|
|
Buffer->ID = ID;
|
|
MemoryArena.Ptr += Buffer->Size;
|
|
*Buffer->Location = '\0';
|
|
Buffer->Ptr = Buffer->Location;
|
|
#if DEBUG
|
|
float PercentageUsed = (float)(MemoryArena.Ptr - MemoryArena.Location) / MemoryArena.Size * 100;
|
|
printf(" ClaimBuffer(%s): %d\n"
|
|
" Total ClaimedMemory: %ld (%.2f%%, leaving %ld free)\n\n", Buffer->ID, Buffer->Size, MemoryArena.Ptr - MemoryArena.Location, PercentageUsed, MemoryArena.Size - (MemoryArena.Ptr - MemoryArena.Location));
|
|
#endif
|
|
return RC_SUCCESS;
|
|
}
|
|
|
|
void
|
|
DeclaimBuffer(buffer *Buffer)
|
|
{
|
|
*Buffer->Location = '\0';
|
|
MemoryArena.Ptr -= Buffer->Size;
|
|
float PercentageUsed = (float)(Buffer->Ptr - Buffer->Location) / Buffer->Size * 100;
|
|
#if DEBUG
|
|
printf("DeclaimBuffer(%s)\n"
|
|
" Used: %ld / %d (%.2f%%)\n"
|
|
"\n"
|
|
" Total ClaimedMemory: %ld\n\n",
|
|
Buffer->ID,
|
|
Buffer->Ptr - Buffer->Location,
|
|
Buffer->Size,
|
|
PercentageUsed,
|
|
MemoryArena.Ptr - MemoryArena.Location);
|
|
#endif
|
|
LogUsage(Buffer);
|
|
if(PercentageUsed >= 95.0f)
|
|
{
|
|
// TODO(matt): Implement either dynamically growing buffers, or phoning home to miblodelcarpio@gmail.com
|
|
LogError(LOG_ERROR, "%s used %.2f%% of its allotted memory\n", Buffer->ID, PercentageUsed);
|
|
fprintf(stderr, "%sWarning%s: %s used %.2f%% of its allotted memory\n",
|
|
ColourStrings[CS_ERROR], ColourStrings[CS_END],
|
|
Buffer->ID, PercentageUsed);
|
|
}
|
|
else if(PercentageUsed >= 80.0f)
|
|
{
|
|
// TODO(matt): Implement either dynamically growing buffers, or phoning home to miblodelcarpio@gmail.com
|
|
LogError(LOG_ERROR, "%s used %.2f%% of its allotted memory\n", Buffer->ID, PercentageUsed);
|
|
fprintf(stderr, "%sWarning%s: %s used %.2f%% of its allotted memory\n",
|
|
ColourStrings[CS_WARNING], ColourStrings[CS_END],
|
|
Buffer->ID, PercentageUsed);
|
|
}
|
|
Buffer->Size = 0;
|
|
}
|
|
|
|
void
|
|
RewindBuffer(buffer *Buffer)
|
|
{
|
|
#if DEBUG
|
|
float PercentageUsed = (float)(Buffer->Ptr - Buffer->Location) / Buffer->Size * 100;
|
|
printf("Rewinding %s\n"
|
|
" Used: %ld / %d (%.2f%%)\n\n",
|
|
Buffer->ID,
|
|
Buffer->Ptr - Buffer->Location,
|
|
Buffer->Size,
|
|
PercentageUsed);
|
|
#endif
|
|
Buffer->Ptr = Buffer->Location;
|
|
}
|
|
|
|
enum
|
|
{
|
|
TEMPLATE_SEARCH,
|
|
TEMPLATE_PLAYER,
|
|
TEMPLATE_BESPOKE
|
|
} template_types;
|
|
|
|
char *
|
|
GetDirectoryPath(char *Filepath)
|
|
{
|
|
char *Ptr = Filepath + StringLength(Filepath) - 1;
|
|
while(Ptr > Filepath && *Ptr != '/')
|
|
{
|
|
--Ptr;
|
|
}
|
|
if(Ptr == Filepath)
|
|
{
|
|
*Ptr++ = '.';
|
|
}
|
|
*Ptr = '\0';
|
|
return Filepath;
|
|
}
|
|
|
|
char *
|
|
GetBaseFilename(char *Filepath,
|
|
char *Extension // Including the "."
|
|
// Pass 0 to retain the whole file path, only without its parent directories
|
|
)
|
|
{
|
|
char *BaseFilename = Filepath + StringLength(Filepath) - 1;
|
|
while(BaseFilename > Filepath && *BaseFilename != '/')
|
|
{
|
|
--BaseFilename;
|
|
}
|
|
if(*BaseFilename == '/')
|
|
{
|
|
++BaseFilename;
|
|
}
|
|
BaseFilename[StringLength(BaseFilename) - StringLength(Extension)] = '\0';
|
|
return BaseFilename;
|
|
}
|
|
|
|
void
|
|
ConstructTemplatePath(template *Template, enum8(template_types) Type)
|
|
{
|
|
// NOTE(matt): Bespoke template paths are set relative to:
|
|
// in Project Edition: ProjectDir
|
|
// in Single Edition: Parent directory of .hmml file
|
|
|
|
if(Template->File.Path[0] != '/')
|
|
{
|
|
char Temp[256];
|
|
CopyString(Temp, sizeof(Temp), "%s", Template->File.Path);
|
|
char *Ptr = Template->File.Path;
|
|
char *End = Template->File.Path + sizeof(Template->File.Path);
|
|
if(Type == TEMPLATE_BESPOKE)
|
|
{
|
|
if(Config.Edition == EDITION_SINGLE)
|
|
{
|
|
Ptr += CopyString(Ptr, End - Ptr, "%s/", GetDirectoryPath(Config.SingleHMMLFilePath));
|
|
}
|
|
else
|
|
{
|
|
Ptr += CopyString(Ptr, End - Ptr, "%s/", Config.ProjectDir);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Ptr += CopyString(Ptr, End - Ptr, "%s/", Config.TemplatesDir);
|
|
}
|
|
CopyString(Ptr, End - Ptr, "%s", Temp);
|
|
}
|
|
}
|
|
|
|
void
|
|
FitTemplateTag(template *Template)
|
|
{
|
|
int BlockSize = 16;
|
|
if(Template->Metadata.TagCount == Template->Metadata.TagCapacity)
|
|
{
|
|
Template->Metadata.TagCapacity += BlockSize;
|
|
if(Template->Metadata.Tags)
|
|
{
|
|
Template->Metadata.Tags = realloc(Template->Metadata.Tags, Template->Metadata.TagCapacity * sizeof(*Template->Metadata.Tags));
|
|
}
|
|
else
|
|
{
|
|
Template->Metadata.Tags = calloc(Template->Metadata.TagCapacity, sizeof(*Template->Metadata.Tags));
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
PushTemplateTag(template *Template, int Offset, enum8(template_tag_types) TagType, int AssetIndex)
|
|
{
|
|
FitTemplateTag(Template);
|
|
Template->Metadata.Tags[Template->Metadata.TagCount].Offset = Offset;
|
|
Template->Metadata.Tags[Template->Metadata.TagCount].TagCode = TagType;
|
|
Template->Metadata.Tags[Template->Metadata.TagCount].AssetIndex = AssetIndex;
|
|
++Template->Metadata.TagCount;
|
|
}
|
|
|
|
void
|
|
ClearTemplateMetadata(template *Template)
|
|
{
|
|
Template->Metadata.TagCapacity = 0;
|
|
Template->Metadata.TagCount = 0;
|
|
Template->Metadata.Validity = 0;
|
|
}
|
|
|
|
void
|
|
InitTemplate(template *Template, char *Location, enum8(template_types) Type)
|
|
{
|
|
CopyStringNoFormat(Template->File.Path, sizeof(Template->File.Path), Location);
|
|
ConstructTemplatePath(Template, Type);
|
|
printf("%sPacking%s template: %s\n", ColourStrings[CS_ONGOING], ColourStrings[CS_END], Template->File.Path);
|
|
ReadFileIntoBuffer(&Template->File, 0);
|
|
ClearTemplateMetadata(Template);
|
|
}
|
|
|
|
void
|
|
FreeTemplate(template *Template)
|
|
{
|
|
FreeBuffer(&Template->File.Buffer);
|
|
Clear(Template->File.Path, sizeof(Template->File.Path));
|
|
Template->File.FileSize = 0;
|
|
|
|
free(Template->Metadata.Tags);
|
|
Template->Metadata.Tags = 0;
|
|
ClearTemplateMetadata(Template);
|
|
}
|
|
|
|
int
|
|
TimecodeToSeconds(char *Timecode)
|
|
{
|
|
int HMS[3] = { 0, 0, 0 }; // 0 == Seconds; 1 == Minutes; 2 == Hours
|
|
int Colons = 0;
|
|
while(*Timecode)
|
|
{
|
|
//if((*Timecode < '0' || *Timecode > '9') && *Timecode != ':') { return FALSE; }
|
|
|
|
if(*Timecode == ':')
|
|
{
|
|
++Colons;
|
|
//if(Colons > 2) { return FALSE; }
|
|
for(int i = 0; i < Colons; ++i)
|
|
{
|
|
HMS[Colons - i] = HMS[Colons - (i + 1)];
|
|
}
|
|
HMS[0] = 0;
|
|
}
|
|
else
|
|
{
|
|
HMS[0] = HMS[0] * 10 + *Timecode - '0';
|
|
}
|
|
|
|
++Timecode;
|
|
}
|
|
|
|
//if(HMS[0] > 59 || HMS[1] > 59 || Timecode[-1] == ':') { return FALSE; }
|
|
|
|
return HMS[2] * 60 * 60 + HMS[1] * 60 + HMS[0];
|
|
}
|
|
|
|
typedef struct
|
|
{
|
|
unsigned int Hue:16;
|
|
unsigned int Saturation:8;
|
|
unsigned int Lightness:8;
|
|
} hsl_colour;
|
|
|
|
hsl_colour
|
|
CharToColour(char Char)
|
|
{
|
|
hsl_colour Colour;
|
|
if(Char >= 'a' && Char <= 'z')
|
|
{
|
|
Colour.Hue = (((float)Char - 'a') / ('z' - 'a') * 360);
|
|
Colour.Saturation = (((float)Char - 'a') / ('z' - 'a') * 26 + 74);
|
|
}
|
|
else if(Char >= 'A' && Char <= 'Z')
|
|
{
|
|
Colour.Hue = (((float)Char - 'A') / ('Z' - 'A') * 360);
|
|
Colour.Saturation = (((float)Char - 'A') / ('Z' - 'A') * 26 + 74);
|
|
}
|
|
else if(Char >= '0' && Char <= '9')
|
|
{
|
|
Colour.Hue = (((float)Char - '0') / ('9' - '0') * 360);
|
|
Colour.Saturation = (((float)Char - '0') / ('9' - '0') * 26 + 74);
|
|
}
|
|
else
|
|
{
|
|
Colour.Hue = 180;
|
|
Colour.Saturation = 50;
|
|
}
|
|
|
|
return Colour;
|
|
}
|
|
|
|
void
|
|
StringToColourHash(hsl_colour *Colour, char *String)
|
|
{
|
|
Colour->Hue = 0;
|
|
Colour->Saturation = 0;
|
|
Colour->Lightness = 74;
|
|
|
|
int i;
|
|
for(i = 0; String[i]; ++i)
|
|
{
|
|
Colour->Hue += CharToColour(String[i]).Hue;
|
|
Colour->Saturation += CharToColour(String[i]).Saturation;
|
|
}
|
|
|
|
Colour->Hue = Colour->Hue % 360;
|
|
Colour->Saturation = Colour->Saturation % 26 + 74;
|
|
}
|
|
|
|
char *
|
|
SanitisePunctuation(char *String)
|
|
{
|
|
char *Ptr = String;
|
|
while(*Ptr)
|
|
{
|
|
if(*Ptr == ' ')
|
|
{
|
|
*Ptr = '_';
|
|
}
|
|
if((*Ptr < '0' || *Ptr > '9') &&
|
|
(*Ptr < 'a' || *Ptr > 'z') &&
|
|
(*Ptr < 'A' || *Ptr > 'Z'))
|
|
{
|
|
*Ptr = '-';
|
|
}
|
|
++Ptr;
|
|
}
|
|
return String;
|
|
}
|
|
|
|
enum
|
|
{
|
|
PAGE_PLAYER = 1 << 0,
|
|
PAGE_SEARCH = 1 << 1
|
|
} pages;
|
|
|
|
void
|
|
ConstructURLPrefix(buffer *URLPrefix, enum8(asset_types) AssetType, enum8(pages) PageType)
|
|
{
|
|
RewindBuffer(URLPrefix);
|
|
if(StringsDiffer(Config.RootURL, ""))
|
|
{
|
|
CopyStringToBufferHTMLPercentEncoded(URLPrefix, Config.RootURL);
|
|
CopyStringToBuffer(URLPrefix, "/");
|
|
}
|
|
else
|
|
{
|
|
if(Config.Edition == EDITION_PROJECT)
|
|
{
|
|
if(PageType == PAGE_PLAYER)
|
|
{
|
|
CopyStringToBuffer(URLPrefix, "../");
|
|
}
|
|
CopyStringToBuffer(URLPrefix, "../");
|
|
}
|
|
}
|
|
|
|
switch(AssetType)
|
|
{
|
|
case ASSET_CSS:
|
|
if(StringsDiffer(Config.CSSDir, ""))
|
|
{
|
|
CopyStringToBufferHTMLPercentEncoded(URLPrefix, Config.CSSDir);
|
|
CopyStringToBuffer(URLPrefix, "/");
|
|
}
|
|
break;
|
|
case ASSET_IMG:
|
|
if(StringsDiffer(Config.ImagesDir, ""))
|
|
{
|
|
CopyStringToBufferHTMLPercentEncoded(URLPrefix, Config.ImagesDir);
|
|
CopyStringToBuffer(URLPrefix, "/");
|
|
}
|
|
break;
|
|
case ASSET_JS:
|
|
if(StringsDiffer(Config.JSDir, ""))
|
|
{
|
|
CopyStringToBufferHTMLPercentEncoded(URLPrefix, Config.JSDir);
|
|
CopyStringToBuffer(URLPrefix, "/");
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
typedef struct
|
|
{
|
|
char Abbreviation[32];
|
|
hsl_colour Colour;
|
|
credential_info *Credential;
|
|
bool Seen;
|
|
} speaker;
|
|
|
|
typedef struct
|
|
{
|
|
speaker Speaker[16];
|
|
int Count;
|
|
} speakers;
|
|
|
|
enum
|
|
{
|
|
CreditsError_NoHost,
|
|
CreditsError_NoAnnotator,
|
|
CreditsError_NoCredentials
|
|
} credits_errors;
|
|
|
|
size_t
|
|
StringToFletcher32(const char *Data, size_t Length)
|
|
{// https://en.wikipedia.org/wiki/Fletcher%27s_checksum
|
|
size_t c0, c1;
|
|
unsigned short int i;
|
|
|
|
for(c0 = c1 = 0; Length >= 360; Length -= 360)
|
|
{
|
|
for(i = 0; i < 360; ++i)
|
|
{
|
|
c0 += *Data++;
|
|
c1 += c0;
|
|
}
|
|
c0 %= 65535;
|
|
c1 %= 65535;
|
|
}
|
|
|
|
for(i = 0; i < Length; ++i)
|
|
{
|
|
c0 += *Data++;
|
|
c1 += c0;
|
|
}
|
|
|
|
c0 %= 65535;
|
|
c1 %= 65535;
|
|
return (c1 << 16 | c0);
|
|
}
|
|
|
|
void
|
|
ConstructAssetPath(file_buffer *AssetFile, char *Filename, int Type)
|
|
{
|
|
buffer Path;
|
|
ClaimBuffer(&Path, "Path", Kilobytes(4));
|
|
if(StringsDiffer(Config.RootDir, ""))
|
|
{
|
|
CopyStringToBuffer(&Path, "%s", Config.RootDir);
|
|
}
|
|
char *AssetDir = 0;
|
|
switch(Type)
|
|
{
|
|
case ASSET_CSS: AssetDir = Config.CSSDir; break;
|
|
case ASSET_IMG: AssetDir = Config.ImagesDir; break;
|
|
case ASSET_JS: AssetDir = Config.JSDir; break;
|
|
}
|
|
if(AssetDir && StringsDiffer(AssetDir, "")) { CopyStringToBuffer(&Path, "/%s", AssetDir); }
|
|
if(Filename) { CopyStringToBuffer(&Path, "/%s", Filename); }
|
|
|
|
CopyString(AssetFile->Path, sizeof(AssetFile->Path), Path.Location);
|
|
DeclaimBuffer(&Path);
|
|
}
|
|
|
|
void
|
|
CycleFile(file_buffer *File)
|
|
{
|
|
fclose(File->Handle);
|
|
// TODO(matt): Rather than freeing the buffer, why not just realloc it to fit the new file? Couldn't that work easily?
|
|
// The reason we Free / Reread is to save us having to shuffle the buffer contents around, basically
|
|
// If we switch the whole database over to use linked lists - the database being the only file we actually cycle
|
|
// at the moment - then we may actually obviate the need to cycle at all
|
|
FreeBuffer(&File->Buffer);
|
|
ReadFileIntoBuffer(File, 0);
|
|
}
|
|
|
|
void
|
|
ConstructDirectoryPath(buffer *DirectoryPath, int PageType, char *PageLocation, char *BaseFilename)
|
|
{
|
|
RewindBuffer(DirectoryPath);
|
|
CopyStringToBuffer(DirectoryPath, "%s", Config.BaseDir);
|
|
switch(PageType)
|
|
{
|
|
case PAGE_SEARCH:
|
|
if(StringsDiffer(PageLocation, ""))
|
|
{
|
|
CopyStringToBuffer(DirectoryPath, "/%s", PageLocation);
|
|
}
|
|
break;
|
|
case PAGE_PLAYER:
|
|
if(StringsDiffer(PageLocation, ""))
|
|
{
|
|
CopyStringToBuffer(DirectoryPath, "/%s", PageLocation);
|
|
}
|
|
if(BaseFilename)
|
|
{
|
|
if(StringsDiffer(Config.PlayerURLPrefix, ""))
|
|
{
|
|
char *Ptr = BaseFilename + StringLength(Config.ProjectID);
|
|
CopyStringToBuffer(DirectoryPath, "/%s%s", Config.PlayerURLPrefix, Ptr);
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(DirectoryPath, "/%s", BaseFilename);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
int
|
|
ReadSearchPageIntoBuffer(file_buffer *File)
|
|
{
|
|
buffer SearchPagePath;
|
|
ClaimBuffer(&SearchPagePath, "SearchPagePath", MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + 10);
|
|
|
|
ConstructDirectoryPath(&SearchPagePath, PAGE_SEARCH, Config.SearchLocation, 0);
|
|
CopyString(File->Path, sizeof(File->Path), "%s/index.html", SearchPagePath.Location);
|
|
DeclaimBuffer(&SearchPagePath);
|
|
return(ReadFileIntoBuffer(File, 0));
|
|
}
|
|
|
|
int
|
|
ReadPlayerPageIntoBuffer(file_buffer *File, db_entry *Entry)
|
|
{
|
|
buffer PlayerPagePath;
|
|
ClaimBuffer(&PlayerPagePath, "PlayerPagePath", MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH + 1 + 10);
|
|
|
|
ConstructDirectoryPath(&PlayerPagePath, PAGE_PLAYER, Config.PlayerLocation, Entry->BaseFilename);
|
|
CopyString(File->Path, sizeof(File->Path), "%s/index.html", PlayerPagePath.Location);
|
|
DeclaimBuffer(&PlayerPagePath);
|
|
return(ReadFileIntoBuffer(File, 0));
|
|
}
|
|
|
|
void
|
|
ClearTerminalRow(int Length)
|
|
{
|
|
fprintf(stderr, "\r");
|
|
for(int i = 0; i < Length; ++i)
|
|
{
|
|
fprintf(stderr, " ");
|
|
}
|
|
fprintf(stderr, "\r");
|
|
}
|
|
|
|
typedef struct
|
|
{
|
|
uint32_t First;
|
|
uint32_t Length;
|
|
} landmark_range;
|
|
|
|
uint32_t
|
|
GetIndexRangeLength(void *FirstLandmark, int EntryIndex, int LandmarkIndex, int LandmarkCount)
|
|
{
|
|
uint32_t Result = 1;
|
|
db_landmark Landmark = *(db_landmark*)(FirstLandmark + sizeof(Landmark) * LandmarkIndex);
|
|
while(EntryIndex == Landmark.EntryIndex && LandmarkIndex < LandmarkCount - 1)
|
|
{
|
|
++LandmarkIndex;
|
|
Landmark = *(db_landmark*)(FirstLandmark + sizeof(Landmark) * LandmarkIndex);
|
|
if(EntryIndex == Landmark.EntryIndex)
|
|
{
|
|
++Result;
|
|
}
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
landmark_range
|
|
GetIndexRange(void *FirstLandmark, int EntryIndex, int LandmarkIndex, int LandmarkCount)
|
|
{
|
|
landmark_range Result = {};
|
|
db_landmark Landmark = *(db_landmark*)(FirstLandmark + sizeof(Landmark) * LandmarkIndex);
|
|
while(EntryIndex == Landmark.EntryIndex && LandmarkIndex > 0)
|
|
{
|
|
--LandmarkIndex;
|
|
Landmark = *(db_landmark*)(FirstLandmark + sizeof(Landmark) * LandmarkIndex);
|
|
}
|
|
if(Landmark.EntryIndex != EntryIndex)
|
|
{
|
|
++LandmarkIndex;
|
|
}
|
|
|
|
Landmark = *(db_landmark*)(FirstLandmark + sizeof(Landmark) * LandmarkIndex);
|
|
|
|
Result.First = LandmarkIndex;
|
|
Result.Length = GetIndexRangeLength(FirstLandmark, EntryIndex, LandmarkIndex, LandmarkCount);
|
|
return Result;
|
|
}
|
|
|
|
landmark_range
|
|
BinarySearchForMetadataLandmark(void *FirstLandmark, int EntryIndex, int LandmarkCount)
|
|
{
|
|
// NOTE(matt): Depends on FirstLandmark being positioned after an Asset "header"
|
|
landmark_range Result = {};
|
|
if(LandmarkCount > 0)
|
|
{
|
|
int Lower = 0;
|
|
db_landmark *LowerLandmark = (db_landmark*)(FirstLandmark + sizeof(LowerLandmark) * Lower);
|
|
if(EntryIndex < LowerLandmark->EntryIndex)
|
|
{
|
|
Result.First = 0;
|
|
Result.Length = 0;
|
|
return Result;
|
|
}
|
|
|
|
int Upper = LandmarkCount - 1;
|
|
db_landmark *UpperLandmark;
|
|
// TODO(matt): Is there a slicker way of doing this?
|
|
if(Upper >= 0)
|
|
{
|
|
UpperLandmark = (db_landmark*)(FirstLandmark + sizeof(db_landmark) * Upper);
|
|
if(EntryIndex > UpperLandmark->EntryIndex)
|
|
{
|
|
Result.First = LandmarkCount;
|
|
Result.Length = 0;
|
|
return Result;
|
|
}
|
|
}
|
|
|
|
int Pivot = Upper - ((Upper - Lower) >> 1);
|
|
db_landmark *PivotLandmark;
|
|
|
|
do {
|
|
LowerLandmark = (db_landmark*)(FirstLandmark + sizeof(db_landmark) * Lower);
|
|
PivotLandmark = (db_landmark*)(FirstLandmark + sizeof(db_landmark) * Pivot);
|
|
UpperLandmark = (db_landmark*)(FirstLandmark + sizeof(db_landmark) * Upper);
|
|
|
|
if(EntryIndex == LowerLandmark->EntryIndex)
|
|
{
|
|
return GetIndexRange(FirstLandmark, EntryIndex, Lower, LandmarkCount);
|
|
}
|
|
|
|
if(EntryIndex == PivotLandmark->EntryIndex)
|
|
{
|
|
return GetIndexRange(FirstLandmark, EntryIndex, Pivot, LandmarkCount);
|
|
}
|
|
|
|
if(EntryIndex == UpperLandmark->EntryIndex)
|
|
{
|
|
return GetIndexRange(FirstLandmark, EntryIndex, Upper, LandmarkCount);
|
|
}
|
|
|
|
if(EntryIndex < PivotLandmark->EntryIndex) { Upper = Pivot; }
|
|
else { Lower = Pivot; }
|
|
Pivot = Upper - ((Upper - Lower) >> 1);
|
|
} while(Upper > Pivot);
|
|
Result.First = Upper;
|
|
Result.Length = 0;
|
|
return Result;
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
void
|
|
SnipeChecksumAndCloseFile(file_buffer *File, void *FirstLandmark, int LandmarksInFile, buffer *Checksum, int *RunningLandmarkIndex)
|
|
{
|
|
for(int j = 0; j < LandmarksInFile; ++j, ++*RunningLandmarkIndex)
|
|
{
|
|
db_landmark Landmark = *(db_landmark *)(FirstLandmark + sizeof(DB.Landmark) * *RunningLandmarkIndex);
|
|
|
|
File->Buffer.Ptr = File->Buffer.Location + Landmark.Position;
|
|
CopyBufferSized(&File->Buffer, Checksum, Checksum->Ptr - Checksum->Location);
|
|
}
|
|
|
|
File->Handle = fopen(File->Path, "w");
|
|
fwrite(File->Buffer.Location, File->FileSize, 1, File->Handle);
|
|
fclose(File->Handle);
|
|
FreeBuffer(&File->Buffer);
|
|
}
|
|
|
|
void
|
|
SnipeChecksumIntoHTML(void *FirstLandmark, buffer *Checksum)
|
|
{
|
|
landmark_range SearchRange = BinarySearchForMetadataLandmark(FirstLandmark, PAGE_TYPE_SEARCH, DB.Asset.LandmarkCount);
|
|
int RunningLandmarkIndex = 0;
|
|
if(SearchRange.Length > 0)
|
|
{
|
|
file_buffer HTML;
|
|
ReadSearchPageIntoBuffer(&HTML);
|
|
SnipeChecksumAndCloseFile(&HTML, FirstLandmark, SearchRange.Length, Checksum, &RunningLandmarkIndex);
|
|
}
|
|
|
|
for(; RunningLandmarkIndex < DB.Asset.LandmarkCount;)
|
|
{
|
|
db_landmark Landmark = *(db_landmark *)(FirstLandmark + sizeof(Landmark) * RunningLandmarkIndex);
|
|
|
|
db_entry Entry = *(db_entry *)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * Landmark.EntryIndex);
|
|
int Length = GetIndexRangeLength(FirstLandmark, Landmark.EntryIndex, RunningLandmarkIndex, DB.Asset.LandmarkCount);
|
|
file_buffer HTML;
|
|
ReadPlayerPageIntoBuffer(&HTML, &Entry);
|
|
SnipeChecksumAndCloseFile(&HTML, FirstLandmark, Length, Checksum, &RunningLandmarkIndex);
|
|
}
|
|
}
|
|
|
|
void
|
|
PrintWatchHandles(void)
|
|
{
|
|
printf("\n"
|
|
"PrintWatchHandles()\n");
|
|
for(int i = 0; i < WatchHandles.Count; ++i)
|
|
{
|
|
printf(" %d • %s • %s\n",
|
|
WatchHandles.Handle[i].Descriptor,
|
|
WatchHandles.Handle[i].Type == WT_HMML ? "WT_HMML " : "WT_ASSET",
|
|
WatchHandles.Handle[i].Path);
|
|
}
|
|
}
|
|
|
|
bool
|
|
IsSymlink(char *Filepath)
|
|
{
|
|
int File = open(Filepath, O_RDONLY | O_NOFOLLOW);
|
|
bool Result = (errno == ELOOP);
|
|
close(File);
|
|
return Result;
|
|
}
|
|
|
|
void
|
|
FitWatchHandle(void)
|
|
{
|
|
int BlockSize = 8;
|
|
if(WatchHandles.Count == WatchHandles.Capacity)
|
|
{
|
|
WatchHandles.Capacity += BlockSize;
|
|
if(WatchHandles.Handle)
|
|
{
|
|
WatchHandles.Handle = realloc(WatchHandles.Handle, WatchHandles.Capacity * sizeof(*WatchHandles.Handle));
|
|
}
|
|
else
|
|
{
|
|
WatchHandles.Handle = calloc(WatchHandles.Capacity, sizeof(*WatchHandles.Handle));
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
PushHMMLWatchHandle(void)
|
|
{
|
|
FitWatchHandle();
|
|
CopyString(WatchHandles.Handle[WatchHandles.Count].Path, sizeof(WatchHandles.Handle[0].Path), Config.ProjectDir);
|
|
WatchHandles.Handle[WatchHandles.Count].Descriptor = inotify_add_watch(inotifyInstance, Config.ProjectDir, IN_CLOSE_WRITE | IN_DELETE);
|
|
WatchHandles.Handle[WatchHandles.Count].Type = WT_HMML;
|
|
++WatchHandles.Count;
|
|
}
|
|
|
|
void
|
|
PushAssetWatchHandle(file_buffer *AssetFile, uint32_t AssetIndex)
|
|
{
|
|
if(IsSymlink(AssetFile->Path))
|
|
{
|
|
char ResolvedSymlinkPath[4096] = {};
|
|
readlink(AssetFile->Path, ResolvedSymlinkPath, 4096);
|
|
Clear(AssetFile->Path, sizeof(AssetFile->Path));
|
|
CopyString(AssetFile->Path, sizeof(AssetFile->Path), ResolvedSymlinkPath);
|
|
}
|
|
|
|
ResolvePath(AssetFile->Path);
|
|
StripComponentFromPath(AssetFile->Path);
|
|
|
|
for(int i = 0; i < WatchHandles.Count; ++i)
|
|
{
|
|
if(!StringsDiffer(WatchHandles.Handle[i].Path, AssetFile->Path))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
FitWatchHandle();
|
|
WatchHandles.Handle[WatchHandles.Count].Type = WT_ASSET;
|
|
CopyString(WatchHandles.Handle[WatchHandles.Count].Path, sizeof(WatchHandles.Handle[0].Path), AssetFile->Path);
|
|
WatchHandles.Handle[WatchHandles.Count].Descriptor = inotify_add_watch(inotifyInstance, AssetFile->Path, IN_CLOSE_WRITE);
|
|
++WatchHandles.Count;
|
|
}
|
|
|
|
void
|
|
UpdateAssetInDB(int AssetIndex)
|
|
{
|
|
DB.Header = *(db_header *)DB.Metadata.Buffer.Location;
|
|
if(DB.Header.HexSignature != FOURCC("CNRA"))
|
|
{
|
|
printf("line %d: Malformed .metadata file. HexSignature not in expected location\n", __LINE__);
|
|
exit(1);
|
|
}
|
|
DB.Metadata.Buffer.Ptr = DB.Metadata.Buffer.Location + sizeof(DB.Header);
|
|
|
|
DB.EntriesHeader = *(db_header_entries *)DB.Metadata.Buffer.Ptr;
|
|
if(DB.EntriesHeader.BlockID != FOURCC("NTRY"))
|
|
{
|
|
printf("line %d: Malformed .metadata file. Entries BlockID not in expected location\n", __LINE__);
|
|
exit(1);
|
|
}
|
|
|
|
DB.Metadata.Buffer.Ptr += sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * DB.EntriesHeader.Count;
|
|
|
|
DB.AssetsHeader = *(db_header_assets *)DB.Metadata.Buffer.Ptr;
|
|
if(DB.AssetsHeader.BlockID != FOURCC("ASET"))
|
|
{
|
|
printf("line %d: Malformed .metadata file. Assets BlockID not in expected location\n", __LINE__);
|
|
exit(1);
|
|
}
|
|
int AssetsHeaderLocation = DB.Metadata.Buffer.Ptr - DB.Metadata.Buffer.Location;
|
|
DB.Metadata.Buffer.Ptr += sizeof(DB.AssetsHeader);
|
|
bool Found = FALSE;
|
|
for(int i = 0; i < DB.AssetsHeader.Count; ++i)
|
|
{
|
|
DB.Asset = *(db_asset *)DB.Metadata.Buffer.Ptr;
|
|
if(!StringsDiffer(DB.Asset.Filename, Assets.Asset[AssetIndex].Filename) && DB.Asset.Type == Assets.Asset[AssetIndex].Type)
|
|
{
|
|
Found = TRUE;
|
|
break;
|
|
}
|
|
DB.Metadata.Buffer.Ptr += sizeof(DB.Asset) + sizeof(DB.Landmark) * DB.Asset.LandmarkCount;
|
|
}
|
|
|
|
if(Found)
|
|
{
|
|
if(DB.Asset.Hash != Assets.Asset[AssetIndex].Hash)
|
|
{
|
|
DB.Asset.Hash = Assets.Asset[AssetIndex].Hash;
|
|
*(db_asset *)DB.Metadata.Buffer.Ptr = DB.Asset;
|
|
DB.Metadata.Buffer.Ptr += sizeof(DB.Asset);
|
|
|
|
buffer Checksum;
|
|
ClaimBuffer(&Checksum, "Checksum", 16);
|
|
CopyStringToBuffer(&Checksum, "%08x", DB.Asset.Hash);
|
|
|
|
file_buffer AssetFile;
|
|
ConstructAssetPath(&AssetFile, DB.Asset.Filename, DB.Asset.Type);
|
|
ResolvePath(AssetFile.Path);
|
|
|
|
char Message[256] = { };
|
|
CopyString(Message, sizeof(Message), "%sUpdating%s checksum %s of %s in HTML files", ColourStrings[CS_ONGOING], ColourStrings[CS_END], Checksum.Location, AssetFile.Path);
|
|
fprintf(stderr, Message);
|
|
|
|
SnipeChecksumIntoHTML(DB.Metadata.Buffer.Ptr, &Checksum);
|
|
|
|
ClearTerminalRow(StringLength(Message));
|
|
fprintf(stderr, "%sUpdated%s checksum %s of %s\n", ColourStrings[CS_REINSERTION], ColourStrings[CS_END], Checksum.Location, AssetFile.Path);
|
|
|
|
DeclaimBuffer(&Checksum);
|
|
|
|
DB.Metadata.Handle = fopen(DB.Metadata.Path, "w");
|
|
fwrite(DB.Metadata.Buffer.Location, DB.Metadata.FileSize, 1, DB.Metadata.Handle);
|
|
CycleFile(&DB.Metadata);
|
|
Assets.Asset[AssetIndex].DeferredUpdate = FALSE;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Append new asset, not bothering to insertion sort because there likely won't be many
|
|
++DB.AssetsHeader.Count;
|
|
|
|
DB.Metadata.Handle = fopen(DB.Metadata.Path, "w");
|
|
|
|
fwrite(DB.Metadata.Buffer.Location, AssetsHeaderLocation, 1, DB.Metadata.Handle);
|
|
fwrite(&DB.AssetsHeader, sizeof(DB.AssetsHeader), 1, DB.Metadata.Handle);
|
|
fwrite(DB.Metadata.Buffer.Location + AssetsHeaderLocation + sizeof(DB.AssetsHeader),
|
|
DB.Metadata.FileSize - AssetsHeaderLocation - sizeof(DB.AssetsHeader),
|
|
1,
|
|
DB.Metadata.Handle);
|
|
|
|
db_asset Asset = {};
|
|
Asset.Hash = Assets.Asset[AssetIndex].Hash;
|
|
Asset.Type = Assets.Asset[AssetIndex].Type;
|
|
ClearCopyStringNoFormat(Asset.Filename, sizeof(Asset.Filename), Assets.Asset[AssetIndex].Filename);
|
|
fwrite(&Asset, sizeof(Asset), 1, DB.Metadata.Handle);
|
|
printf("%sAppended%s %s asset: %s [%08x]\n", ColourStrings[CS_ADDITION], ColourStrings[CS_END], AssetTypeNames[Asset.Type], Asset.Filename, Asset.Hash);
|
|
|
|
CycleFile(&DB.Metadata);
|
|
}
|
|
if(!Assets.Asset[AssetIndex].Known)
|
|
{
|
|
Assets.Asset[AssetIndex].Known = TRUE;
|
|
}
|
|
}
|
|
|
|
void
|
|
FitAssetLandmark(enum8(builtin_assets + support_icons) AssetIndex, int PageType)
|
|
{
|
|
int BlockSize = 2;
|
|
if(PageType == PAGE_PLAYER)
|
|
{
|
|
if(Assets.Asset[AssetIndex].PlayerLandmarkCount == Assets.Asset[AssetIndex].PlayerLandmarkCapacity)
|
|
{
|
|
Assets.Asset[AssetIndex].PlayerLandmarkCapacity += BlockSize;
|
|
if(Assets.Asset[AssetIndex].PlayerLandmark)
|
|
{
|
|
Assets.Asset[AssetIndex].PlayerLandmark =
|
|
realloc(Assets.Asset[AssetIndex].PlayerLandmark,
|
|
Assets.Asset[AssetIndex].PlayerLandmarkCapacity * sizeof(*Assets.Asset[AssetIndex].PlayerLandmark));
|
|
}
|
|
else
|
|
{
|
|
Assets.Asset[AssetIndex].PlayerLandmark =
|
|
calloc(Assets.Asset[AssetIndex].PlayerLandmarkCapacity, sizeof(*Assets.Asset[AssetIndex].PlayerLandmark));
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if(Assets.Asset[AssetIndex].SearchLandmarkCount == Assets.Asset[AssetIndex].SearchLandmarkCapacity)
|
|
{
|
|
Assets.Asset[AssetIndex].SearchLandmarkCapacity += BlockSize;
|
|
if(Assets.Asset[AssetIndex].SearchLandmark)
|
|
{
|
|
Assets.Asset[AssetIndex].SearchLandmark =
|
|
realloc(Assets.Asset[AssetIndex].SearchLandmark,
|
|
Assets.Asset[AssetIndex].SearchLandmarkCapacity * sizeof(*Assets.Asset[AssetIndex].SearchLandmark));
|
|
}
|
|
else
|
|
{
|
|
Assets.Asset[AssetIndex].SearchLandmark =
|
|
calloc(Assets.Asset[AssetIndex].SearchLandmarkCapacity, sizeof(*Assets.Asset[AssetIndex].SearchLandmark));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
PushAssetLandmark(buffer *Dest, int AssetIndex, int PageType)
|
|
{
|
|
if(!(Config.Mode & MODE_NOREVVEDRESOURCE))
|
|
{
|
|
FitAssetLandmark(AssetIndex, PageType);
|
|
CopyStringToBuffer(Dest, "?%s=", Config.QueryString);
|
|
if(PageType == PAGE_PLAYER)
|
|
{
|
|
Assets.Asset[AssetIndex].PlayerLandmark[Assets.Asset[AssetIndex].PlayerLandmarkCount] = Dest->Ptr - Dest->Location;
|
|
++Assets.Asset[AssetIndex].PlayerLandmarkCount;
|
|
}
|
|
else
|
|
{
|
|
Assets.Asset[AssetIndex].SearchLandmark[Assets.Asset[AssetIndex].SearchLandmarkCount] = Dest->Ptr - Dest->Location;
|
|
++Assets.Asset[AssetIndex].SearchLandmarkCount;
|
|
}
|
|
CopyStringToBuffer(Dest, "%08x", Assets.Asset[AssetIndex].Hash);
|
|
}
|
|
}
|
|
|
|
void
|
|
ResetAssetLandmarks(void)
|
|
{
|
|
for(int AssetIndex = 0; AssetIndex < Assets.Count; ++AssetIndex)
|
|
{
|
|
for(int LandmarkIndex = 0; LandmarkIndex < Assets.Asset[AssetIndex].PlayerLandmarkCount; ++LandmarkIndex)
|
|
{
|
|
Assets.Asset[AssetIndex].PlayerLandmark[LandmarkIndex] = 0;
|
|
}
|
|
Assets.Asset[AssetIndex].PlayerLandmarkCount = 0;
|
|
|
|
for(int LandmarkIndex = 0; LandmarkIndex < Assets.Asset[AssetIndex].SearchLandmarkCount; ++LandmarkIndex)
|
|
{
|
|
Assets.Asset[AssetIndex].SearchLandmark[LandmarkIndex] = 0;
|
|
}
|
|
Assets.Asset[AssetIndex].SearchLandmarkCount = 0;
|
|
Assets.Asset[AssetIndex].OffsetLandmarks = FALSE;
|
|
}
|
|
}
|
|
|
|
void
|
|
FitAsset(void)
|
|
{
|
|
int BlockSize = 16;
|
|
if(Assets.Count == Assets.Capacity)
|
|
{
|
|
Assets.Capacity += BlockSize;
|
|
if(Assets.Asset)
|
|
{
|
|
Assets.Asset = realloc(Assets.Asset, Assets.Capacity * sizeof(*Assets.Asset));
|
|
}
|
|
else
|
|
{
|
|
Assets.Asset = calloc(Assets.Capacity, sizeof(*Assets.Asset));
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
UpdateAsset(uint32_t AssetIndex, bool Defer)
|
|
{
|
|
file_buffer File;
|
|
ConstructAssetPath(&File, Assets.Asset[AssetIndex].Filename, Assets.Asset[AssetIndex].Type);
|
|
if(ReadFileIntoBuffer(&File, 0) == RC_SUCCESS)
|
|
{
|
|
Assets.Asset[AssetIndex].Hash = StringToFletcher32(File.Buffer.Location, File.FileSize);
|
|
if(!Defer)
|
|
{
|
|
UpdateAssetInDB(AssetIndex);
|
|
}
|
|
else
|
|
{
|
|
Assets.Asset[AssetIndex].DeferredUpdate = TRUE;
|
|
}
|
|
FreeBuffer(&File.Buffer);
|
|
}
|
|
}
|
|
|
|
int
|
|
FinalPathComponentPosition(char *Path)
|
|
{
|
|
char *Ptr = Path + StringLength(Path) - 1;
|
|
while(Ptr > Path && *Ptr != '/')
|
|
{
|
|
--Ptr;
|
|
}
|
|
if(*Ptr == '/')
|
|
{
|
|
++Ptr;
|
|
}
|
|
return Ptr - Path;
|
|
}
|
|
|
|
int
|
|
PlaceAsset(char *Filename, int Type, int Position)
|
|
{
|
|
FitAsset();
|
|
Assets.Asset[Position].Type = Type;
|
|
CopyString(Assets.Asset[Position].Filename, sizeof(Assets.Asset[0].Filename), Filename);
|
|
Assets.Asset[Position].FilenameAt = FinalPathComponentPosition(Filename);
|
|
if(Position == Assets.Count && !Assets.Asset[Position].Known) { ++Assets.Count; }
|
|
|
|
file_buffer File;
|
|
ConstructAssetPath(&File, Filename, Type);
|
|
if(ReadFileIntoBuffer(&File, 0) == RC_SUCCESS)
|
|
{
|
|
Assets.Asset[Position].Hash = StringToFletcher32(File.Buffer.Location, File.FileSize);
|
|
FreeBuffer(&File.Buffer);
|
|
PushAssetWatchHandle(&File, Position);
|
|
return RC_SUCCESS;
|
|
}
|
|
else
|
|
{
|
|
ResolvePath(File.Path);
|
|
printf("%sNonexistent%s %s asset: %s\n", ColourStrings[CS_WARNING], ColourStrings[CS_END], AssetTypeNames[Type], File.Path);
|
|
return RC_ERROR_FILE;
|
|
}
|
|
}
|
|
|
|
int
|
|
PushAsset(char *Filename, int Type, uint32_t *AssetIndexPtr)
|
|
{
|
|
for(*AssetIndexPtr = 0; *AssetIndexPtr < Assets.Count; ++*AssetIndexPtr)
|
|
{
|
|
if(!StringsDiffer(Filename, Assets.Asset[*AssetIndexPtr].Filename) && Type == Assets.Asset[*AssetIndexPtr].Type)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
return PlaceAsset(Filename, Type, *AssetIndexPtr);
|
|
}
|
|
|
|
void
|
|
InitBuiltinAssets(void)
|
|
{
|
|
Assert(BUILTIN_ASSETS_COUNT == ArrayCount(BuiltinAssets));
|
|
CopyString(BuiltinAssets[ASSET_CSS_THEME].Filename, sizeof(BuiltinAssets[0].Filename), "cinera__%s.css", Config.Theme);
|
|
for(int AssetIndex = 0; AssetIndex < BUILTIN_ASSETS_COUNT; ++AssetIndex)
|
|
{
|
|
if(PlaceAsset(BuiltinAssets[AssetIndex].Filename, BuiltinAssets[AssetIndex].Type, AssetIndex) == RC_ERROR_FILE && AssetIndex == ASSET_CSS_TOPICS)
|
|
{
|
|
printf( " %s└───────┴────┴─── Don't worry about this one. We'll generate it if needed%s\n", ColourStrings[CS_COMMENT], ColourStrings[CS_END]);
|
|
}
|
|
}
|
|
Assets.Count = BUILTIN_ASSETS_COUNT;
|
|
|
|
Assert(SUPPORT_ICON_COUNT - BUILTIN_ASSETS_COUNT == ArrayCount(SupportIcons));
|
|
for(int SupportIconIndex = BUILTIN_ASSETS_COUNT; SupportIconIndex < SUPPORT_ICON_COUNT; ++SupportIconIndex)
|
|
{
|
|
PlaceAsset(SupportIcons[SupportIconIndex - BUILTIN_ASSETS_COUNT], ASSET_IMG, SupportIconIndex);
|
|
}
|
|
}
|
|
|
|
void
|
|
SkipEntriesBlock(void *EntriesHeaderLocation)
|
|
{
|
|
DB.EntriesHeader = *(db_header_entries *)EntriesHeaderLocation;
|
|
DB.Metadata.Buffer.Ptr = EntriesHeaderLocation + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * DB.EntriesHeader.Count;
|
|
}
|
|
|
|
void
|
|
LocateAssetsBlock(void)
|
|
{
|
|
DB.Header = *(db_header *)DB.Metadata.Buffer.Location;
|
|
DB.Metadata.Buffer.Ptr = DB.Metadata.Buffer.Location + sizeof(DB.Header);
|
|
for(int BlockIndex = 0; BlockIndex < DB.Header.BlockCount; ++BlockIndex)
|
|
{
|
|
uint32_t FirstInt = *(uint32_t *)DB.Metadata.Buffer.Ptr;
|
|
if(FirstInt == FOURCC("NTRY"))
|
|
{
|
|
SkipEntriesBlock(DB.Metadata.Buffer.Ptr);
|
|
}
|
|
else if(FirstInt == FOURCC("ASET"))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
void
|
|
InitAssets(void)
|
|
{
|
|
InitBuiltinAssets();
|
|
LocateAssetsBlock();
|
|
DB.AssetsHeader = *(db_header_assets *)DB.Metadata.Buffer.Ptr;
|
|
DB.Metadata.Buffer.Ptr += sizeof(DB.AssetsHeader);
|
|
for(int AssetIndex = 0; AssetIndex < DB.AssetsHeader.Count; ++AssetIndex)
|
|
{
|
|
DB.Asset = *(db_asset *)DB.Metadata.Buffer.Ptr;
|
|
uint32_t AI;
|
|
PushAsset(DB.Asset.Filename, DB.Asset.Type, &AI);
|
|
DB.Metadata.Buffer.Ptr += sizeof(DB.Asset) + sizeof(DB.Landmark) * DB.Asset.LandmarkCount;
|
|
}
|
|
}
|
|
|
|
void
|
|
ConstructResolvedAssetURL(buffer *Buffer, uint32_t AssetIndex, enum8(pages) PageType)
|
|
{
|
|
ClaimBuffer(Buffer, "URL", (MAX_ROOT_URL_LENGTH + 1 + MAX_RELATIVE_ASSET_LOCATION_LENGTH + 1) * 2);
|
|
ConstructURLPrefix(Buffer, Assets.Asset[AssetIndex].Type, PageType);
|
|
CopyStringToBufferHTMLPercentEncoded(Buffer, Assets.Asset[AssetIndex].Filename);
|
|
ResolvePath(Buffer->Location);
|
|
}
|
|
|
|
int
|
|
SearchCredentials(buffer *CreditsMenu, bool *HasCreditsMenu, char *Person, char *Role, speakers *Speakers)
|
|
{
|
|
bool Found = FALSE;
|
|
for(int CredentialIndex = 0; CredentialIndex < ArrayCount(Credentials); ++CredentialIndex)
|
|
{
|
|
if(!StringsDiffer(Person, Credentials[CredentialIndex].Username))
|
|
{
|
|
if(Speakers)
|
|
{
|
|
Speakers->Speaker[Speakers->Count].Credential = &Credentials[CredentialIndex];
|
|
++Speakers->Count;
|
|
}
|
|
Found = TRUE;
|
|
if(*HasCreditsMenu == FALSE)
|
|
{
|
|
CopyStringToBuffer(CreditsMenu,
|
|
" <div class=\"menu credits\">\n"
|
|
" <span>Credits</span>\n"
|
|
" <div class=\"credits_container\">\n");
|
|
|
|
*HasCreditsMenu = TRUE;
|
|
}
|
|
|
|
CopyStringToBuffer(CreditsMenu,
|
|
" <span class=\"credit\">\n");
|
|
if(Credentials[CredentialIndex].HomepageURL)
|
|
{
|
|
CopyStringToBuffer(CreditsMenu,
|
|
" <a class=\"person\" href=\"%s\" target=\"_blank\">\n"
|
|
" <div class=\"role\">%s</div>\n"
|
|
" <div class=\"name\">%s</div>\n"
|
|
" </a>\n",
|
|
Credentials[CredentialIndex].HomepageURL,
|
|
Role,
|
|
Credentials[CredentialIndex].CreditedName);
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(CreditsMenu,
|
|
" <div class=\"person\">\n"
|
|
" <div class=\"role\">%s</div>\n"
|
|
" <div class=\"name\">%s</div>\n"
|
|
" </div>\n",
|
|
Role,
|
|
Credentials[CredentialIndex].CreditedName);
|
|
}
|
|
|
|
if(Credentials[CredentialIndex].SupportURL)
|
|
{
|
|
buffer URL;
|
|
ConstructResolvedAssetURL(&URL, Credentials[CredentialIndex].SupportIconIndex, PAGE_PLAYER);
|
|
CopyStringToBuffer(CreditsMenu,
|
|
" <a class=\"support\" href=\"%s\" target=\"_blank\"><div class=\"support_icon\" data-sprite=\"%s",
|
|
Credentials[CredentialIndex].SupportURL,
|
|
URL.Location);
|
|
DeclaimBuffer(&URL);
|
|
|
|
PushAssetLandmark(CreditsMenu, Credentials[CredentialIndex].SupportIconIndex, PAGE_PLAYER);
|
|
|
|
CopyStringToBuffer(CreditsMenu, "\"></div></a>\n");
|
|
}
|
|
|
|
CopyStringToBuffer(CreditsMenu,
|
|
" </span>\n");
|
|
}
|
|
}
|
|
return Found ? RC_SUCCESS : CreditsError_NoCredentials;
|
|
}
|
|
|
|
void
|
|
ClearNullTerminatedString(char *String)
|
|
{
|
|
while(*String)
|
|
{
|
|
*String++ = '\0';
|
|
}
|
|
}
|
|
|
|
void
|
|
InitialString(char *Dest, char *Src)
|
|
{
|
|
ClearNullTerminatedString(Dest);
|
|
*Dest++ = *Src++;
|
|
while(*Src++)
|
|
{
|
|
if(*Src == ' ')
|
|
{
|
|
++Src;
|
|
if(*Src)
|
|
{
|
|
*Dest++ = *Src;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
GetFirstSubstring(char *Dest, char *Src)
|
|
{
|
|
ClearNullTerminatedString(Dest);
|
|
while(*Src && *Src != ' ')
|
|
{
|
|
*Dest++ = *Src++;
|
|
}
|
|
}
|
|
|
|
void
|
|
InitialAndGetFinalString(char *Dest, int DestSize, char *Src)
|
|
{
|
|
ClearNullTerminatedString(Dest);
|
|
int SrcLength = StringLength(Src);
|
|
char *SrcPtr = Src + SrcLength - 1;
|
|
while(SrcPtr > Src && *SrcPtr != ' ')
|
|
{
|
|
--SrcPtr;
|
|
}
|
|
if(*SrcPtr == ' ' && SrcPtr - Src < SrcLength - 1)
|
|
{
|
|
++SrcPtr;
|
|
}
|
|
|
|
if(Src < SrcPtr)
|
|
{
|
|
*Dest++ = *Src++;
|
|
*Dest++ = '.';
|
|
*Dest++ = ' ';
|
|
|
|
while(Src < SrcPtr - 1)
|
|
{
|
|
if(*Src == ' ')
|
|
{
|
|
++Src;
|
|
if(*Src)
|
|
{
|
|
*Dest++ = *Src;
|
|
*Dest++ = '.';
|
|
*Dest++ = ' ';
|
|
}
|
|
}
|
|
++Src;
|
|
}
|
|
}
|
|
|
|
CopyString(Dest, DestSize, "%s", SrcPtr);
|
|
}
|
|
|
|
bool
|
|
AbbreviationsClash(speakers *Speakers)
|
|
{
|
|
for(int i = 0; i < Speakers->Count; ++i)
|
|
{
|
|
for(int j = i + 1; j < Speakers->Count; ++j)
|
|
{
|
|
if(!StringsDiffer(Speakers->Speaker[i].Abbreviation, Speakers->Speaker[j].Abbreviation))
|
|
{
|
|
return TRUE;
|
|
}
|
|
}
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
void
|
|
SortAndAbbreviateSpeakers(speakers *Speakers)
|
|
{
|
|
for(int i = 0; i < Speakers->Count; ++i)
|
|
{
|
|
for(int j = i + 1; j < Speakers->Count; ++j)
|
|
{
|
|
if(StringsDiffer(Speakers->Speaker[i].Credential->Username, Speakers->Speaker[j].Credential->Username) > 0)
|
|
{
|
|
credential_info *Temp = Speakers->Speaker[j].Credential;
|
|
Speakers->Speaker[j].Credential = Speakers->Speaker[i].Credential;
|
|
Speakers->Speaker[i].Credential = Temp;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
for(int i = 0; i < Speakers->Count; ++i)
|
|
{
|
|
StringToColourHash(&Speakers->Speaker[i].Colour, Speakers->Speaker[i].Credential->Username);
|
|
InitialString(Speakers->Speaker[i].Abbreviation, Speakers->Speaker[i].Credential->CreditedName);
|
|
}
|
|
|
|
int Attempt = 0;
|
|
while(AbbreviationsClash(Speakers))
|
|
{
|
|
for(int i = 0; i < Speakers->Count; ++i)
|
|
{
|
|
switch(Attempt)
|
|
{
|
|
case 0: GetFirstSubstring(Speakers->Speaker[i].Abbreviation, Speakers->Speaker[i].Credential->CreditedName); break;
|
|
case 1: InitialAndGetFinalString(Speakers->Speaker[i].Abbreviation, sizeof(Speakers->Speaker[i].Abbreviation), Speakers->Speaker[i].Credential->CreditedName); break;
|
|
case 2: ClearCopyStringNoFormat(Speakers->Speaker[i].Abbreviation, sizeof(Speakers->Speaker[i].Abbreviation), Speakers->Speaker[i].Credential->Username); break;
|
|
}
|
|
}
|
|
++Attempt;
|
|
}
|
|
}
|
|
|
|
int
|
|
BuildCredits(buffer *CreditsMenu, bool *HasCreditsMenu, HMML_VideoMetaData *Metadata, speakers *Speakers)
|
|
// TODO(matt): Make this take the Credentials, once we are parsing them from a config
|
|
{
|
|
if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->member, "Host", Speakers) == CreditsError_NoCredentials)
|
|
{
|
|
printf("No credentials for member %s. Please contact miblodelcarpio@gmail.com with their:\n"
|
|
" Full name\n"
|
|
" Homepage URL (optional)\n"
|
|
" Financial support info, e.g. Patreon URL (optional)\n", Metadata->member);
|
|
return CreditsError_NoCredentials;
|
|
}
|
|
|
|
if(Metadata->co_host_count > 0)
|
|
{
|
|
for(int i = 0; i < Metadata->co_host_count; ++i)
|
|
{
|
|
if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->co_hosts[i], "Co-host", Speakers) == CreditsError_NoCredentials)
|
|
{
|
|
printf("No credentials for co-host %s. Please contact miblodelcarpio@gmail.com with their:\n"
|
|
" Full name\n"
|
|
" Homepage URL (optional)\n"
|
|
" Financial support info, e.g. Patreon URL (optional)\n", Metadata->co_hosts[i]);
|
|
return CreditsError_NoCredentials;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(Metadata->guest_count > 0)
|
|
{
|
|
for(int i = 0; i < Metadata->guest_count; ++i)
|
|
{
|
|
if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->guests[i], "Guest", Speakers) == CreditsError_NoCredentials)
|
|
{
|
|
printf("No credentials for guest %s. Please contact miblodelcarpio@gmail.com with their:\n"
|
|
" Full name\n"
|
|
" Homepage URL (optional)\n"
|
|
" Financial support info, e.g. Patreon URL (optional)\n", Metadata->guests[i]);
|
|
return CreditsError_NoCredentials;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(Speakers->Count > 1)
|
|
{
|
|
SortAndAbbreviateSpeakers(Speakers);
|
|
}
|
|
|
|
if(Metadata->annotator_count > 0)
|
|
{
|
|
for(int i = 0; i < Metadata->annotator_count; ++i)
|
|
{
|
|
if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->annotators[i], "Annotator", 0) == CreditsError_NoCredentials)
|
|
{
|
|
printf("No credentials for annotator %s. Please contact miblodelcarpio@gmail.com with their:\n"
|
|
" Full name\n"
|
|
" Homepage URL (optional)\n"
|
|
" Financial support info, e.g. Patreon URL (optional)\n", Metadata->annotators[i]);
|
|
return CreditsError_NoCredentials;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if(*HasCreditsMenu == TRUE)
|
|
{
|
|
CopyStringToBuffer(CreditsMenu,
|
|
" </div>\n"
|
|
" </div>\n");
|
|
}
|
|
fprintf(stderr, "Missing \"annotator\" in the [video] node\n");
|
|
return CreditsError_NoAnnotator;
|
|
}
|
|
|
|
if(*HasCreditsMenu == TRUE)
|
|
{
|
|
CopyStringToBuffer(CreditsMenu,
|
|
" </div>\n"
|
|
" </div>\n");
|
|
}
|
|
|
|
return RC_SUCCESS;
|
|
}
|
|
|
|
enum
|
|
{
|
|
REF_SITE = 1 << 0,
|
|
REF_PAGE = 1 << 1,
|
|
REF_URL = 1 << 2,
|
|
REF_TITLE = 1 << 3,
|
|
REF_ARTICLE = 1 << 4,
|
|
REF_AUTHOR = 1 << 5,
|
|
REF_EDITOR = 1 << 6,
|
|
REF_PUBLISHER = 1 << 7,
|
|
REF_ISBN = 1 << 8,
|
|
} reference_fields;
|
|
|
|
int
|
|
BuildReference(ref_info *ReferencesArray, int RefIdentifier, int UniqueRefs, HMML_Reference *Ref, HMML_Annotation *Anno)
|
|
{
|
|
if(Ref->isbn)
|
|
{
|
|
CopyString(ReferencesArray[UniqueRefs].ID, sizeof(ReferencesArray[UniqueRefs].ID), "%s", Ref->isbn);
|
|
if(!Ref->url) { CopyString(ReferencesArray[UniqueRefs].URL, sizeof(ReferencesArray[UniqueRefs].URL), "https://isbndb.com/book/%s", Ref->isbn); }
|
|
else { CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, sizeof(ReferencesArray[UniqueRefs].URL), Ref->url); }
|
|
}
|
|
else if(Ref->url)
|
|
{
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].ID, sizeof(ReferencesArray[UniqueRefs].ID), Ref->url);
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, sizeof(ReferencesArray[UniqueRefs].URL), Ref->url);
|
|
}
|
|
else { return RC_INVALID_REFERENCE; }
|
|
|
|
int Mask = 0;
|
|
if(Ref->site) { Mask |= REF_SITE; }
|
|
if(Ref->page) { Mask |= REF_PAGE; }
|
|
if(Ref->title) { Mask |= REF_TITLE; }
|
|
if(Ref->article) { Mask |= REF_ARTICLE; }
|
|
if(Ref->author) { Mask |= REF_AUTHOR; }
|
|
if(Ref->editor) { Mask |= REF_EDITOR; }
|
|
if(Ref->publisher) { Mask |= REF_PUBLISHER; }
|
|
|
|
// TODO(matt): Consider handling the various combinations more flexibly, unless we defer this stuff until we have the
|
|
// reference store, in which we could optionally customise the display of each reference entry
|
|
switch(Mask)
|
|
{
|
|
case (REF_TITLE | REF_AUTHOR | REF_PUBLISHER):
|
|
{
|
|
CopyString(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), "%s (%s)", Ref->author, Ref->publisher);
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Ref->title);
|
|
} break;
|
|
case (REF_AUTHOR | REF_SITE | REF_PAGE):
|
|
{
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), Ref->site);
|
|
CopyString(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), "%s: \"%s\"", Ref->author, Ref->page);
|
|
} break;
|
|
case (REF_PAGE | REF_TITLE):
|
|
{
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), Ref->title);
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Ref->page);
|
|
} break;
|
|
case (REF_SITE | REF_PAGE):
|
|
{
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), Ref->site);
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Ref->page);
|
|
} break;
|
|
case (REF_SITE | REF_TITLE):
|
|
{
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), Ref->site);
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Ref->title);
|
|
} break;
|
|
case (REF_TITLE | REF_AUTHOR):
|
|
{
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), Ref->author);
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Ref->title);
|
|
} break;
|
|
case (REF_ARTICLE | REF_AUTHOR):
|
|
{
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), Ref->author);
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Ref->article);
|
|
} break;
|
|
case (REF_TITLE | REF_PUBLISHER):
|
|
{
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), Ref->publisher);
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Ref->title);
|
|
} break;
|
|
case REF_TITLE:
|
|
{
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Ref->title);
|
|
} break;
|
|
case REF_SITE:
|
|
{
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Ref->site);
|
|
} break;
|
|
default: return RC_INVALID_REFERENCE; break;
|
|
}
|
|
|
|
CopyString(ReferencesArray[UniqueRefs].Identifier[ReferencesArray[UniqueRefs].IdentifierCount].Timecode, sizeof(ReferencesArray[UniqueRefs].Identifier[ReferencesArray[UniqueRefs].IdentifierCount].Timecode), "%s", Anno->time);
|
|
ReferencesArray[UniqueRefs].Identifier[ReferencesArray[UniqueRefs].IdentifierCount].Identifier = RefIdentifier;
|
|
return RC_SUCCESS;
|
|
}
|
|
|
|
void
|
|
InsertCategory(categories *GlobalTopics, categories *LocalTopics, categories *GlobalMedia, categories *LocalMedia, char *Marker)
|
|
{
|
|
bool IsMedium = FALSE;
|
|
|
|
int CategoryMediumIndex;
|
|
for(CategoryMediumIndex = 0; CategoryMediumIndex < ArrayCount(CategoryMedium); ++CategoryMediumIndex)
|
|
{
|
|
if(!StringsDiffer(CategoryMedium[CategoryMediumIndex].Medium, Marker))
|
|
{
|
|
IsMedium = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(IsMedium)
|
|
{
|
|
int MediumIndex;
|
|
for(MediumIndex = 0; MediumIndex < LocalMedia->Count; ++MediumIndex)
|
|
{
|
|
if(!StringsDiffer(CategoryMedium[CategoryMediumIndex].Medium, LocalMedia->Category[MediumIndex].Marker))
|
|
{
|
|
return;
|
|
}
|
|
if((StringsDiffer(CategoryMedium[CategoryMediumIndex].WrittenName, LocalMedia->Category[MediumIndex].WrittenText)) < 0)
|
|
{
|
|
int CategoryCount;
|
|
for(CategoryCount = LocalMedia->Count; CategoryCount > MediumIndex; --CategoryCount)
|
|
{
|
|
CopyString(LocalMedia->Category[CategoryCount].Marker, sizeof(LocalMedia->Category[CategoryCount].Marker), "%s", LocalMedia->Category[CategoryCount-1].Marker);
|
|
CopyString(LocalMedia->Category[CategoryCount].WrittenText, sizeof(LocalMedia->Category[CategoryCount].WrittenText), "%s", LocalMedia->Category[CategoryCount-1].WrittenText);
|
|
}
|
|
|
|
CopyString(LocalMedia->Category[CategoryCount].Marker, sizeof(LocalMedia->Category[CategoryCount].Marker), "%s", CategoryMedium[CategoryMediumIndex].Medium);
|
|
CopyString(LocalMedia->Category[CategoryCount].WrittenText, sizeof(LocalMedia->Category[CategoryCount].WrittenText), "%s", CategoryMedium[CategoryMediumIndex].WrittenName);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(MediumIndex == LocalMedia->Count)
|
|
{
|
|
CopyString(LocalMedia->Category[MediumIndex].Marker, sizeof(LocalMedia->Category[MediumIndex].Marker), "%s", CategoryMedium[CategoryMediumIndex].Medium);
|
|
CopyString(LocalMedia->Category[MediumIndex].WrittenText, sizeof(LocalMedia->Category[MediumIndex].WrittenText), "%s", CategoryMedium[CategoryMediumIndex].WrittenName);
|
|
}
|
|
|
|
++LocalMedia->Count;
|
|
|
|
for(MediumIndex = 0; MediumIndex < GlobalMedia->Count; ++MediumIndex)
|
|
{
|
|
if(!StringsDiffer(CategoryMedium[CategoryMediumIndex].Medium, GlobalMedia->Category[MediumIndex].Marker))
|
|
{
|
|
return;
|
|
}
|
|
if((StringsDiffer(CategoryMedium[CategoryMediumIndex].WrittenName, GlobalMedia->Category[MediumIndex].WrittenText)) < 0)
|
|
{
|
|
int CategoryCount;
|
|
for(CategoryCount = GlobalMedia->Count; CategoryCount > MediumIndex; --CategoryCount)
|
|
{
|
|
CopyString(GlobalMedia->Category[CategoryCount].Marker, sizeof(GlobalMedia->Category[CategoryCount].Marker), "%s", GlobalMedia->Category[CategoryCount-1].Marker);
|
|
CopyString(GlobalMedia->Category[CategoryCount].WrittenText, sizeof(GlobalMedia->Category[CategoryCount].WrittenText), "%s", GlobalMedia->Category[CategoryCount-1].WrittenText);
|
|
}
|
|
|
|
CopyString(GlobalMedia->Category[CategoryCount].Marker, sizeof(GlobalMedia->Category[CategoryCount].Marker), "%s", CategoryMedium[CategoryMediumIndex].Medium);
|
|
CopyString(GlobalMedia->Category[CategoryCount].WrittenText, sizeof(GlobalMedia->Category[CategoryCount].WrittenText), "%s", CategoryMedium[CategoryMediumIndex].WrittenName);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(MediumIndex == GlobalMedia->Count)
|
|
{
|
|
CopyString(GlobalMedia->Category[MediumIndex].Marker, sizeof(GlobalMedia->Category[MediumIndex].Marker), "%s", CategoryMedium[CategoryMediumIndex].Medium);
|
|
CopyString(GlobalMedia->Category[MediumIndex].WrittenText, sizeof(GlobalMedia->Category[MediumIndex].WrittenText), "%s", CategoryMedium[CategoryMediumIndex].WrittenName);
|
|
}
|
|
|
|
++GlobalMedia->Count;
|
|
}
|
|
else
|
|
{
|
|
int TopicIndex;
|
|
for(TopicIndex = 0; TopicIndex < LocalTopics->Count; ++TopicIndex)
|
|
{
|
|
if(!StringsDiffer(Marker, LocalTopics->Category[TopicIndex].Marker))
|
|
{
|
|
return;
|
|
}
|
|
if((StringsDiffer(Marker, LocalTopics->Category[TopicIndex].Marker)) < 0)
|
|
{
|
|
int CategoryCount;
|
|
for(CategoryCount = LocalTopics->Count; CategoryCount > TopicIndex; --CategoryCount)
|
|
{
|
|
CopyString(LocalTopics->Category[CategoryCount].Marker, sizeof(LocalTopics->Category[CategoryCount].Marker), "%s", LocalTopics->Category[CategoryCount-1].Marker);
|
|
}
|
|
|
|
CopyString(LocalTopics->Category[CategoryCount].Marker, sizeof(LocalTopics->Category[CategoryCount].Marker), "%s", Marker);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(TopicIndex == LocalTopics->Count)
|
|
{
|
|
CopyString(LocalTopics->Category[TopicIndex].Marker, sizeof(LocalTopics->Category[TopicIndex].Marker), "%s", Marker);
|
|
}
|
|
|
|
++LocalTopics->Count;
|
|
|
|
for(TopicIndex = 0; TopicIndex < GlobalTopics->Count; ++TopicIndex)
|
|
{
|
|
if(!StringsDiffer(Marker, GlobalTopics->Category[TopicIndex].Marker))
|
|
{
|
|
return;
|
|
}
|
|
// NOTE(matt): This successfully sorts "nullTopic" at the end, but maybe figure out a more general way to force the
|
|
// order of stuff, perhaps blocks of dudes that should sort to the start / end
|
|
if(((StringsDiffer(Marker, GlobalTopics->Category[TopicIndex].Marker)) < 0 || !StringsDiffer(GlobalTopics->Category[TopicIndex].Marker, "nullTopic")))
|
|
{
|
|
if(StringsDiffer(Marker, "nullTopic")) // NOTE(matt): This test (with the above || condition) forces nullTopic never to be inserted, only appended
|
|
{
|
|
int CategoryCount;
|
|
for(CategoryCount = GlobalTopics->Count; CategoryCount > TopicIndex; --CategoryCount)
|
|
{
|
|
CopyString(GlobalTopics->Category[CategoryCount].Marker, sizeof(GlobalTopics->Category[CategoryCount].Marker), "%s", GlobalTopics->Category[CategoryCount-1].Marker);
|
|
}
|
|
|
|
CopyString(GlobalTopics->Category[CategoryCount].Marker, sizeof(GlobalTopics->Category[CategoryCount].Marker), "%s", Marker);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(TopicIndex == GlobalTopics->Count)
|
|
{
|
|
CopyString(GlobalTopics->Category[TopicIndex].Marker, sizeof(GlobalTopics->Category[TopicIndex].Marker), "%s", Marker);
|
|
}
|
|
|
|
++GlobalTopics->Count;
|
|
}
|
|
}
|
|
|
|
void
|
|
BuildCategories(buffer *AnnotationClass, buffer *CategoryIcons, categories *LocalTopics, categories *LocalMedia, int *MarkerIndex, char *DefaultMedium)
|
|
{
|
|
bool CategoriesSpan = FALSE;
|
|
if(!(LocalTopics->Count == 1 && !StringsDiffer(LocalTopics->Category[0].Marker, "nullTopic")
|
|
&& LocalMedia->Count == 1 && !StringsDiffer(LocalMedia->Category[0].Marker, DefaultMedium)))
|
|
{
|
|
CategoriesSpan = TRUE;
|
|
CopyStringToBuffer(CategoryIcons, "<span class=\"cineraCategories\">");
|
|
}
|
|
|
|
if(LocalTopics->Count == 1 && !StringsDiffer(LocalTopics->Category[0].Marker, "nullTopic"))
|
|
{
|
|
char SanitisedMarker[StringLength(LocalTopics->Category[0].Marker) + 1];
|
|
CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%s", LocalTopics->Category[0].Marker);
|
|
SanitisePunctuation(SanitisedMarker);
|
|
CopyStringToBuffer(AnnotationClass, " cat_%s", SanitisedMarker);
|
|
}
|
|
else
|
|
{
|
|
for(int i = 0; i < LocalTopics->Count; ++i)
|
|
{
|
|
char SanitisedMarker[StringLength(LocalTopics->Category[i].Marker) + 1];
|
|
CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%s", LocalTopics->Category[i].Marker);
|
|
SanitisePunctuation(SanitisedMarker);
|
|
|
|
CopyStringToBuffer(CategoryIcons, "<div title=\"%s\" class=\"category %s\"></div>",
|
|
LocalTopics->Category[i].Marker,
|
|
SanitisedMarker);
|
|
|
|
CopyStringToBuffer(AnnotationClass, " cat_%s",
|
|
SanitisedMarker);
|
|
}
|
|
}
|
|
|
|
if(LocalMedia->Count == 1 && !StringsDiffer(LocalMedia->Category[0].Marker, DefaultMedium))
|
|
{
|
|
char SanitisedMarker[StringLength(LocalMedia->Category[0].Marker) + 1];
|
|
CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%s", LocalMedia->Category[0].Marker);
|
|
SanitisePunctuation(SanitisedMarker);
|
|
CopyStringToBuffer(AnnotationClass, " %s", SanitisedMarker);
|
|
}
|
|
else
|
|
{
|
|
for(int i = 0; i < LocalMedia->Count; ++i)
|
|
{
|
|
char SanitisedMarker[StringLength(LocalMedia->Category[i].Marker) + 1];
|
|
CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%s", LocalMedia->Category[i].Marker);
|
|
SanitisePunctuation(SanitisedMarker);
|
|
|
|
if(!StringsDiffer(LocalMedia->Category[i].Marker, "afk")) // TODO(matt): Initially hidden config
|
|
{
|
|
CopyStringToBuffer(AnnotationClass, " off_%s skip", SanitisedMarker); // TODO(matt): Bulletproof this?
|
|
}
|
|
else
|
|
{
|
|
for(int j = 0; j < ArrayCount(CategoryMedium); ++j)
|
|
{
|
|
if(!StringsDiffer(LocalMedia->Category[i].Marker, CategoryMedium[j].Medium))
|
|
{
|
|
CopyStringToBuffer(CategoryIcons, "<div title=\"%s\" class=\"categoryMedium %s\">%s</div>",
|
|
LocalMedia->Category[i].WrittenText,
|
|
LocalMedia->Category[i].Marker,
|
|
CategoryMedium[j].Icon);
|
|
CopyStringToBuffer(AnnotationClass, " %s", SanitisedMarker);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(CategoriesSpan)
|
|
{
|
|
CopyStringToBuffer(CategoryIcons, "</span>");
|
|
}
|
|
|
|
CopyStringToBuffer(AnnotationClass, "\"");
|
|
}
|
|
|
|
int
|
|
StringToInt(char *String)
|
|
{
|
|
int Result = 0;
|
|
while(*String)
|
|
{
|
|
Result = Result * 10 + (*String - '0');
|
|
++String;
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
size_t
|
|
CurlIntoBuffer(char *InPtr, size_t CharLength, size_t Chars, char **OutputPtr)
|
|
{
|
|
int Length = CharLength * Chars;
|
|
int i;
|
|
for(i = 0; InPtr[i] && i < Length; ++i)
|
|
{
|
|
*((*OutputPtr)++) = InPtr[i];
|
|
}
|
|
**OutputPtr = '\0';
|
|
return Length;
|
|
};
|
|
|
|
void
|
|
CurlQuotes(buffer *QuoteStaging, char *QuotesURL)
|
|
{
|
|
fprintf(stderr, "%sFetching%s quotes: %s\n", ColourStrings[CS_ONGOING], ColourStrings[CS_END], QuotesURL);
|
|
CURL *curl = curl_easy_init();
|
|
if(curl) {
|
|
CURLcode CurlReturnCode;
|
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &QuoteStaging->Ptr);
|
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlIntoBuffer);
|
|
curl_easy_setopt(curl, CURLOPT_URL, QuotesURL);
|
|
if((CurlReturnCode = curl_easy_perform(curl)))
|
|
{
|
|
fprintf(stderr, "%s\n", curl_easy_strerror(CurlReturnCode));
|
|
}
|
|
curl_easy_cleanup(curl);
|
|
}
|
|
}
|
|
|
|
int
|
|
SearchQuotes(buffer *QuoteStaging, int CacheSize, quote_info *Info, int ID)
|
|
{
|
|
QuoteStaging->Ptr = QuoteStaging->Location;
|
|
while(QuoteStaging->Ptr - QuoteStaging->Location < CacheSize)
|
|
{
|
|
char InID[8] = { 0 };
|
|
char InTime[16] = { 0 };
|
|
QuoteStaging->Ptr += CopyStringNoFormatT(InID, sizeof(InID), QuoteStaging->Ptr, ',') + 1; // Skip past the ','
|
|
|
|
if(StringToInt(InID) == ID)
|
|
{
|
|
QuoteStaging->Ptr += CopyStringNoFormatT(InTime, sizeof(InTime), QuoteStaging->Ptr, ',') + 1; // Skip past the ','
|
|
|
|
long int Time = StringToInt(InTime);
|
|
char DayString[3] = { 0 };
|
|
strftime(DayString, 3, "%d", gmtime(&Time));
|
|
int Day = StringToInt(DayString);
|
|
|
|
char DaySuffix[3]; if(DayString[1] == '1' && Day != 11) { CopyString(DaySuffix, sizeof(DaySuffix), "st"); }
|
|
else if(DayString[1] == '2' && Day != 12) { CopyString(DaySuffix, sizeof(DaySuffix), "nd"); }
|
|
else if(DayString[1] == '3' && Day != 13) { CopyString(DaySuffix, sizeof(DaySuffix), "rd"); }
|
|
else { CopyString(DaySuffix, sizeof(DaySuffix), "th"); }
|
|
|
|
char MonthYear[32];
|
|
strftime(MonthYear, 32, "%B, %Y", gmtime(&Time));
|
|
CopyString(Info->Date, sizeof(Info->Date), "%d%s %s", Day, DaySuffix, MonthYear);
|
|
|
|
CopyStringNoFormatT(Info->Text, sizeof(Info->Text), QuoteStaging->Ptr, '\n');
|
|
|
|
FreeBuffer(QuoteStaging);
|
|
return RC_FOUND;
|
|
}
|
|
else
|
|
{
|
|
while(*QuoteStaging->Ptr != '\n')
|
|
{
|
|
++QuoteStaging->Ptr;
|
|
}
|
|
++QuoteStaging->Ptr;
|
|
}
|
|
}
|
|
return RC_UNFOUND;
|
|
}
|
|
|
|
int
|
|
BuildQuote(quote_info *Info, char *Speaker, int ID, bool ShouldFetchQuotes)
|
|
{
|
|
char QuoteCacheDir[256];
|
|
CopyString(QuoteCacheDir, sizeof(QuoteCacheDir), "%s/quotes", Config.CacheDir);
|
|
char QuoteCachePath[256];
|
|
CopyString(QuoteCachePath, sizeof(QuoteCachePath), "%s/%s", QuoteCacheDir, Speaker);
|
|
|
|
FILE *QuoteCache;
|
|
char QuotesURL[256];
|
|
// TODO(matt): Make the URL configurable and also handle the case in which the .raw isn't available
|
|
CopyString(QuotesURL, sizeof(QuotesURL), "https://dev.abaines.me.uk/quotes/%s.raw", Speaker);
|
|
bool CacheAvailable = FALSE;
|
|
|
|
if(!(QuoteCache = fopen(QuoteCachePath, "a+")))
|
|
{
|
|
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));
|
|
}
|
|
else
|
|
{
|
|
CacheAvailable = TRUE;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CacheAvailable = TRUE;
|
|
}
|
|
|
|
buffer QuoteStaging;
|
|
QuoteStaging.ID = "QuoteStaging";
|
|
QuoteStaging.Size = Kilobytes(256);
|
|
if(!(QuoteStaging.Location = malloc(QuoteStaging.Size)))
|
|
{
|
|
fclose(QuoteCache);
|
|
return RC_ERROR_MEMORY;
|
|
}
|
|
|
|
#if DEBUG_MEM
|
|
FILE *MemLog = fopen("/home/matt/cinera_mem", "a+");
|
|
fprintf(MemLog, " Allocated QuoteStaging (%d)\n", QuoteStaging.Size);
|
|
fclose(MemLog);
|
|
printf(" Allocated QuoteStaging (%d)\n", QuoteStaging.Size);
|
|
#endif
|
|
|
|
QuoteStaging.Ptr = QuoteStaging.Location;
|
|
|
|
if(CacheAvailable)
|
|
{
|
|
fseek(QuoteCache, 0, SEEK_END);
|
|
int FileSize = ftell(QuoteCache);
|
|
fseek(QuoteCache, 0, SEEK_SET);
|
|
|
|
fread(QuoteStaging.Location, FileSize, 1, QuoteCache);
|
|
fclose(QuoteCache);
|
|
|
|
if(ShouldFetchQuotes || SearchQuotes(&QuoteStaging, FileSize, Info, ID) == RC_UNFOUND)
|
|
{
|
|
// TODO(matt): Error handling
|
|
CurlQuotes(&QuoteStaging, QuotesURL);
|
|
|
|
if(!(QuoteCache = fopen(QuoteCachePath, "w")))
|
|
{
|
|
perror(QuoteCachePath);
|
|
}
|
|
fwrite(QuoteStaging.Location, QuoteStaging.Ptr - QuoteStaging.Location, 1, QuoteCache);
|
|
fclose(QuoteCache);
|
|
|
|
int CacheSize = QuoteStaging.Ptr - QuoteStaging.Location;
|
|
QuoteStaging.Ptr = QuoteStaging.Location;
|
|
if(SearchQuotes(&QuoteStaging, CacheSize, Info, ID) == RC_UNFOUND)
|
|
{
|
|
FreeBuffer(&QuoteStaging);
|
|
return RC_UNFOUND;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CurlQuotes(&QuoteStaging, QuotesURL);
|
|
int CacheSize = QuoteStaging.Ptr - QuoteStaging.Location;
|
|
QuoteStaging.Ptr = QuoteStaging.Location;
|
|
if(SearchQuotes(&QuoteStaging, CacheSize, Info, ID) == RC_UNFOUND)
|
|
{
|
|
FreeBuffer(&QuoteStaging);
|
|
return RC_UNFOUND;
|
|
}
|
|
}
|
|
|
|
return RC_SUCCESS;
|
|
}
|
|
|
|
int
|
|
GenerateTopicColours(char *Topic)
|
|
{
|
|
char SanitisedTopic[StringLength(Topic) + 1];
|
|
CopyString(SanitisedTopic, sizeof(SanitisedTopic), "%s", Topic);
|
|
SanitisePunctuation(SanitisedTopic);
|
|
|
|
for(int i = 0; i < ArrayCount(CategoryMedium); ++i)
|
|
{
|
|
if(!StringsDiffer(Topic, CategoryMedium[i].Medium))
|
|
{
|
|
return RC_NOOP;
|
|
}
|
|
}
|
|
|
|
file_buffer Topics;
|
|
Topics.Buffer.ID = "Topics";
|
|
|
|
if(StringsDiffer(Config.CSSDir, ""))
|
|
{
|
|
CopyString(Topics.Path, sizeof(Topics.Path), "%s/%s/%s", Config.RootDir, Config.CSSDir, BuiltinAssets[ASSET_CSS_TOPICS].Filename);
|
|
}
|
|
else
|
|
{
|
|
CopyString(Topics.Path, sizeof(Topics.Path), "%s/%s", Config.RootDir, BuiltinAssets[ASSET_CSS_TOPICS].Filename);
|
|
}
|
|
|
|
char *Ptr = Topics.Path + StringLength(Topics.Path) - 1;
|
|
while(*Ptr != '/')
|
|
{
|
|
--Ptr;
|
|
}
|
|
*Ptr = '\0';
|
|
DIR *CSSDirHandle; // TODO(matt): open()
|
|
if(!(CSSDirHandle = opendir(Topics.Path)))
|
|
{
|
|
if(MakeDir(Topics.Path) == RC_ERROR_DIRECTORY)
|
|
{
|
|
LogError(LOG_ERROR, "Unable to create directory %s: %s", Topics.Path, strerror(errno));
|
|
fprintf(stderr, "Unable to create directory %s: %s\n", Topics.Path, strerror(errno));
|
|
return RC_ERROR_DIRECTORY;
|
|
};
|
|
}
|
|
closedir(CSSDirHandle);
|
|
*Ptr = '/';
|
|
|
|
if((Topics.Handle = fopen(Topics.Path, "a+")))
|
|
{
|
|
fseek(Topics.Handle, 0, SEEK_END);
|
|
Topics.FileSize = ftell(Topics.Handle);
|
|
Topics.Buffer.Size = Topics.FileSize;
|
|
fseek(Topics.Handle, 0, SEEK_SET);
|
|
|
|
if(!(Topics.Buffer.Location = malloc(Topics.Buffer.Size)))
|
|
{
|
|
return RC_ERROR_MEMORY;
|
|
}
|
|
|
|
#if DEBUG_MEM
|
|
FILE *MemLog = fopen("/home/matt/cinera_mem", "a+");
|
|
fprintf(MemLog, " Allocated Topics (%d)\n", Topics.Buffer.Size);
|
|
fclose(MemLog);
|
|
printf(" Allocated Topics (%d)\n", Topics.Buffer.Size);
|
|
#endif
|
|
|
|
Topics.Buffer.Ptr = Topics.Buffer.Location;
|
|
fread(Topics.Buffer.Location, Topics.Buffer.Size, 1, Topics.Handle);
|
|
|
|
while(Topics.Buffer.Ptr - Topics.Buffer.Location < Topics.Buffer.Size)
|
|
{
|
|
Topics.Buffer.Ptr += StringLength(".category.");
|
|
if(!StringsDifferT(SanitisedTopic, Topics.Buffer.Ptr, ' '))
|
|
{
|
|
FreeBuffer(&Topics.Buffer);
|
|
fclose(Topics.Handle);
|
|
return RC_NOOP;
|
|
}
|
|
while(Topics.Buffer.Ptr - Topics.Buffer.Location < Topics.Buffer.Size && *Topics.Buffer.Ptr != '\n')
|
|
{
|
|
++Topics.Buffer.Ptr;
|
|
}
|
|
++Topics.Buffer.Ptr;
|
|
}
|
|
|
|
if(!StringsDiffer(Topic, "nullTopic"))
|
|
{
|
|
fprintf(Topics.Handle, ".category.%s { border: 1px solid transparent; background: transparent; }\n",
|
|
SanitisedTopic);
|
|
}
|
|
else
|
|
{
|
|
hsl_colour Colour;
|
|
StringToColourHash(&Colour, Topic);
|
|
fprintf(Topics.Handle, ".category.%s { border: 1px solid hsl(%d, %d%%, %d%%); background: hsl(%d, %d%%, %d%%); }\n",
|
|
SanitisedTopic, Colour.Hue, Colour.Saturation, Colour.Lightness, Colour.Hue, Colour.Saturation, Colour.Lightness);
|
|
}
|
|
|
|
fclose(Topics.Handle);
|
|
FreeBuffer(&Topics.Buffer);
|
|
if(Assets.Asset[ASSET_CSS_TOPICS].Known)
|
|
{
|
|
UpdateAsset(ASSET_CSS_TOPICS, TRUE);
|
|
}
|
|
else
|
|
{
|
|
PlaceAsset(BuiltinAssets[ASSET_CSS_TOPICS].Filename, ASSET_CSS, ASSET_CSS_TOPICS);
|
|
}
|
|
return RC_SUCCESS;
|
|
}
|
|
else
|
|
{
|
|
// NOTE(matt): Maybe it shouldn't be possible to hit this case now that we MakeDir the actual dir...
|
|
perror(Topics.Path);
|
|
return RC_ERROR_FILE;
|
|
}
|
|
}
|
|
|
|
void
|
|
PrintUsage(char *BinaryLocation, config *DefaultConfig)
|
|
{
|
|
fprintf(stderr,
|
|
"Usage: %s [option(s)] filename(s)\n"
|
|
"\n"
|
|
"Options:\n"
|
|
" Paths: %s(advisedly universal, but may be set per-(sub)project as required)%s\n"
|
|
" -r <assets root directory>\n"
|
|
" Override default assets root directory (\"%s\")\n"
|
|
" -R <assets root URL>\n"
|
|
" Override default assets root URL (\"%s\")\n"
|
|
" %sIMPORTANT%s: -r and -R must correspond to the same location\n"
|
|
" %sUNSUPPORTED: If you move files from RootDir, the RootURL should\n"
|
|
" correspond to the resulting location%s\n"
|
|
"\n"
|
|
" -c <CSS directory path>\n"
|
|
" Override default CSS directory (\"%s\"), relative to root\n"
|
|
" -i <images directory path>\n"
|
|
" Override default images directory (\"%s\"), relative to root\n"
|
|
" -j <JS directory path>\n"
|
|
" Override default JS directory (\"%s\"), relative to root\n"
|
|
" -Q <revved resources query string>\n"
|
|
" Override default query string (\"%s\")\n"
|
|
" To disable revved resources, set an empty string with -Q \"\"\n"
|
|
"\n"
|
|
" Project Settings:\n"
|
|
" -p <project ID>\n"
|
|
" Set the project ID, triggering PROJECT EDITION\n"
|
|
" -m <default medium>\n"
|
|
" Override default default medium (\"%s\")\n"
|
|
" %sKnown project defaults:\n",
|
|
BinaryLocation,
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END], DefaultConfig->RootDir,
|
|
DefaultConfig->RootURL,
|
|
ColourStrings[CS_ERROR], ColourStrings[CS_END],
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END],
|
|
|
|
DefaultConfig->CSSDir,
|
|
DefaultConfig->ImagesDir,
|
|
DefaultConfig->JSDir,
|
|
DefaultConfig->QueryString,
|
|
|
|
DefaultConfig->DefaultMedium,
|
|
ColourStrings[CS_COMMENT]);
|
|
|
|
for(int ProjectIndex = 0; ProjectIndex < ArrayCount(ProjectInfo); ++ProjectIndex)
|
|
{
|
|
fprintf(stderr, " %s:",
|
|
ProjectInfo[ProjectIndex].ProjectID);
|
|
|
|
// NOTE(matt): This kind of thing really needs to loop over the dudes once to find the longest one
|
|
for(int i = 11; i > StringLength(ProjectInfo[ProjectIndex].ProjectID); i -= 4)
|
|
{
|
|
fprintf(stderr, "\t");
|
|
}
|
|
|
|
fprintf(stderr, "%s\n",
|
|
ProjectInfo[ProjectIndex].Medium);
|
|
}
|
|
|
|
fprintf(stderr,
|
|
"%s -s <style>\n"
|
|
" Set the style / theme, corresponding to a cinera__*.css file\n"
|
|
" This is equal to the \"project\" field in the HMML files by default\n"
|
|
"\n"
|
|
" Project Input Paths\n"
|
|
" -d <annotations directory>\n"
|
|
" Override default annotations directory (\"%s\")\n"
|
|
" -t <templates directory>\n"
|
|
" Override default templates directory (\"%s\")\n"
|
|
"\n"
|
|
" -x <search template location>\n"
|
|
" Set search template file path, either absolute or relative to\n"
|
|
" template directory, and enable integration\n"
|
|
" -y <player template location>\n"
|
|
" Set player template file path, either absolute or relative\n"
|
|
" to template directory, and enable integration\n"
|
|
"\n"
|
|
" Project Output Paths\n"
|
|
" -b <base output directory>\n"
|
|
" Override project's default base output directory (\"%s\")\n"
|
|
" -B <base URL>\n"
|
|
" Override default base URL (\"%s\")\n"
|
|
" NOTE: This must be set, if -n or -a are to be used\n"
|
|
"\n"
|
|
" -n <search location>\n"
|
|
" Override default search location (\"%s\"), relative to base\n"
|
|
" -a <player location>\n"
|
|
" Override default player location (\"%s\"), relative to base\n"
|
|
" NOTE: The PlayerURLPrefix is currently hardcoded in cinera.c but\n"
|
|
" will be configurable in the full configuration system\n"
|
|
"\n"
|
|
" Single Edition Output Path\n"
|
|
" -o <output location>\n"
|
|
" Override default output player location (\"%s\")\n"
|
|
"\n"
|
|
" -1\n"
|
|
" Open search result links in the same browser tab\n"
|
|
" NOTE: Ideal for a guide embedded in an iframe\n"
|
|
" -f\n"
|
|
" Force integration with an incomplete template\n"
|
|
" -g\n"
|
|
" Ignore video privacy status\n"
|
|
" NOTE: For use with projects whose videos are known to all be public,\n"
|
|
" to save us having to check their privacy status\n"
|
|
" -q\n"
|
|
" Quit after syncing with annotation files in project input directory\n"
|
|
" %sUNSUPPORTED: This is likely to be removed in the future%s\n"
|
|
" -w\n"
|
|
" Force quote cache rebuild %s(memory aid: \"wget\")%s\n"
|
|
"\n"
|
|
" -l <n>\n"
|
|
" Override default log level (%d), where n is from 0 (terse) to 7 (verbose)\n"
|
|
" -u <seconds>\n"
|
|
" Override default update interval (%d)\n"
|
|
//" -c config location\n"
|
|
"\n"
|
|
" -e\n"
|
|
" Display (examine) database and exit\n"
|
|
" -v\n"
|
|
" Display version and exit\n"
|
|
" -h\n"
|
|
" Display this help\n"
|
|
"\n"
|
|
"Template:\n"
|
|
" A valid Search Template shall contain exactly one each of the following tags:\n"
|
|
" <!-- __CINERA_INCLUDES__ -->\n"
|
|
" to put inside your own <head></head>\n"
|
|
" <!-- __CINERA_SEARCH__ -->\n"
|
|
"\n"
|
|
" A valid 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"
|
|
" <!-- __CINERA_PLAYER__ -->\n"
|
|
" <!-- __CINERA_SCRIPT__ -->\n"
|
|
" must come after <!-- __CINERA_MENUS__ --> and <!-- __CINERA_PLAYER__ -->\n"
|
|
"\n"
|
|
" Optional tags available for use in your Player Template:\n"
|
|
" <!-- __CINERA_TITLE__ -->\n"
|
|
" <!-- __CINERA_VIDEO_ID__ -->\n"
|
|
" <!-- __CINERA_VOD_PLATFORM__ -->\n"
|
|
"\n"
|
|
" Other tags available for use in either template:\n"
|
|
" Asset tags:\n"
|
|
" <!-- __CINERA_ASSET__ path.ext -->\n"
|
|
" General purpose tag that outputs the URL of the specified asset\n"
|
|
" relative to the Asset Root URL %s(-R)%s\n"
|
|
" <!-- __CINERA_IMAGE__ path.ext -->\n"
|
|
" General purpose tag that outputs the URL of the specified asset\n"
|
|
" relative to the Images Directory %s(-i)%s\n"
|
|
" <!-- __CINERA_CSS__ path.ext -->\n"
|
|
" Convenience tag that outputs a <link rel=\"stylesheet\"...> node\n"
|
|
" for the specified asset relative to the CSS Directory %s(-c)%s, for\n"
|
|
" use inside your <head> block\n"
|
|
" <!-- __CINERA_JS__ path.ext -->\n"
|
|
" Convenience tag that outputs a <script type=\"text/javascript\"...>\n"
|
|
" node for the specified asset relative to the JS Directory %s(-j)%s,\n"
|
|
" for use wherever a <script> node is valid\n"
|
|
" The path.ext in these tags supports parent directories to locate the\n"
|
|
" asset file relative to its specified type directory (generic, CSS, image\n"
|
|
" or JS), including the \"../\" directory, and paths containing spaces must\n"
|
|
" be surrounded with double-quotes (\\-escapable if the quoted path itself\n"
|
|
" contains double-quotes).\n"
|
|
"\n"
|
|
" All these asset tags additionally enable revving, appending a query\n"
|
|
" string %s(-Q)%s and the file's checksum to the URL. Changes to a file\n"
|
|
" trigger a rehash and edit of all HTML pages citing this asset.\n"
|
|
"\n"
|
|
" <!-- __CINERA_PROJECT__ -->\n"
|
|
" <!-- __CINERA_PROJECT_ID__ -->\n"
|
|
" <!-- __CINERA_SEARCH_URL__ -->\n"
|
|
" <!-- __CINERA_THEME__ -->\n"
|
|
" <!-- __CINERA_URL__ -->\n"
|
|
" Only really usable if BaseURL is set %s(-B)%s\n"
|
|
" <!-- __CINERA_CUSTOM0__ -->\n"
|
|
" <!-- __CINERA_CUSTOM1__ -->\n"
|
|
" <!-- __CINERA_CUSTOM2__ -->\n"
|
|
" ⋮\n"
|
|
" <!-- __CINERA_CUSTOM15__ -->\n"
|
|
" Freeform buffers for small snippets of localised information, e.g. a\n"
|
|
" single <a> element or perhaps a <!-- comment -->\n"
|
|
" They correspond to the custom0 to custom15 attributes in the [video]\n"
|
|
" node in your .hmml files\n"
|
|
" 0 to 11 may hold up to 255 characters\n"
|
|
" 12 to 15 may hold up to 1023 characters\n"
|
|
"\n"
|
|
"HMML Specification:\n"
|
|
" https://git.handmade.network/Annotation-Pushers/Annotation-System/wikis/hmmlspec\n",
|
|
|
|
ColourStrings[CS_END],
|
|
DefaultConfig->ProjectDir,
|
|
DefaultConfig->TemplatesDir,
|
|
|
|
DefaultConfig->BaseDir,
|
|
DefaultConfig->BaseURL,
|
|
DefaultConfig->SearchLocation,
|
|
DefaultConfig->PlayerLocation,
|
|
|
|
DefaultConfig->OutLocation,
|
|
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END],
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END],
|
|
|
|
DefaultConfig->LogLevel,
|
|
DefaultConfig->UpdateInterval,
|
|
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END],
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END],
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END],
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END],
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END],
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END]);
|
|
}
|
|
|
|
void
|
|
DepartComment(buffer *Template)
|
|
{
|
|
while(Template->Ptr - Template->Location < Template->Size)
|
|
{
|
|
if(!StringsDifferT("-->", Template->Ptr, 0))
|
|
{
|
|
Template->Ptr += StringLength("-->");
|
|
break;
|
|
}
|
|
++Template->Ptr;
|
|
}
|
|
}
|
|
|
|
char *
|
|
StripTrailingSlash(char *String) // NOTE(matt): For absolute paths
|
|
{
|
|
int Length = StringLength(String);
|
|
while(Length > 0 && String[Length - 1] == '/')
|
|
{
|
|
String[Length - 1] = '\0';
|
|
--Length;
|
|
}
|
|
return String;
|
|
}
|
|
|
|
char *
|
|
StripSurroundingSlashes(char *String) // NOTE(matt): For relative paths
|
|
{
|
|
char *Ptr = String;
|
|
int Length = StringLength(Ptr);
|
|
if(Length > 0)
|
|
{
|
|
while((Ptr[0]) == '/')
|
|
{
|
|
++Ptr;
|
|
--Length;
|
|
}
|
|
while(Ptr[Length - 1] == '/')
|
|
{
|
|
Ptr[Length - 1] = '\0';
|
|
--Length;
|
|
}
|
|
}
|
|
return Ptr;
|
|
}
|
|
|
|
void
|
|
ConsumeWhitespace(buffer *Buffer, char **Ptr)
|
|
{
|
|
while(*Ptr - Buffer->Location < Buffer->Size)
|
|
{
|
|
switch(**Ptr)
|
|
{
|
|
case ' ':
|
|
case '\t':
|
|
case '\n':
|
|
++*Ptr; break;
|
|
default: return;
|
|
}
|
|
}
|
|
}
|
|
|
|
int
|
|
ParseQuotedString(buffer *Dest, buffer *Src, char **Ptr, enum8(template_tag_codes) TagIndex)
|
|
{
|
|
char *End;
|
|
bool MissingPath = FALSE;
|
|
bool MissingClosingQuote = FALSE;
|
|
if(**Ptr == '\"')
|
|
{
|
|
++*Ptr;
|
|
End = *Ptr;
|
|
while(End - Src->Location < Src->Size && End - *Ptr < Dest->Size)
|
|
{
|
|
switch(*End)
|
|
{
|
|
case '-':
|
|
{
|
|
if((End + 2) - Src->Location < Src->Size && End[1] == '-' && End[2] == '>')
|
|
{
|
|
MissingClosingQuote = TRUE;
|
|
goto Finalise;
|
|
}
|
|
}
|
|
case '\"': goto Finalise;
|
|
case '\\': ++End;
|
|
default: *Dest->Ptr++ = *End++; break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
End = *Ptr;
|
|
if((End + 3) - Src->Location < Src->Size && End[0] == '-' && End[1] == '-' && End[2] == '>')
|
|
{
|
|
MissingPath = TRUE;
|
|
goto Finalise;
|
|
}
|
|
while(End - Src->Location < Src->Size && *End != ' ' && Dest->Ptr - Dest->Location < Dest->Size)
|
|
{
|
|
*Dest->Ptr++ = *End++;
|
|
}
|
|
}
|
|
*Dest->Ptr = '\0';
|
|
|
|
Finalise:
|
|
if(MissingClosingQuote)
|
|
{
|
|
printf("%sQuoted string%s seems to be missing its closing quote mark:\n"
|
|
" %.*s\n", ColourStrings[CS_ERROR], ColourStrings[CS_END], (int)(End - *Ptr), *Ptr);
|
|
return RC_ERROR_PARSING;
|
|
}
|
|
|
|
if(MissingPath || StringLength(Dest->Location) == 0)
|
|
{
|
|
printf("%s%s tag%s seems to be missing a file path\n"
|
|
" %.*s\n", ColourStrings[CS_ERROR], TemplateTags[TagIndex], ColourStrings[CS_END], (int)(End - *Ptr), *Ptr);
|
|
return RC_ERROR_PARSING;
|
|
}
|
|
|
|
if(End - *Ptr == Dest->Size)
|
|
{
|
|
printf("%sAsset file path%s is too long (we support paths up to %d characters):\n"
|
|
" %.*s...\n", ColourStrings[CS_ERROR], ColourStrings[CS_END], MAX_ASSET_FILENAME_LENGTH, (int)(End - *Ptr), *Ptr);
|
|
return RC_ERROR_PARSING;
|
|
}
|
|
return RC_SUCCESS;
|
|
}
|
|
|
|
void
|
|
StripPWDIndicators(char *Path)
|
|
{
|
|
buffer B;
|
|
ClaimBuffer(&B, "PWDStrippedPtr", (StringLength(Path) + 1) * 2);
|
|
CopyStringToBufferNoFormat(&B, Path);
|
|
|
|
B.Ptr = B.Location;
|
|
if(B.Size >= 2 && B.Ptr[0] == '.' && B.Ptr[1] == '/')
|
|
{
|
|
char *NextComponentHead = B.Ptr + 2;
|
|
int RemainingChars = StringLength(NextComponentHead);
|
|
CopyStringToBufferNoFormat(&B, NextComponentHead);
|
|
Clear(B.Ptr, B.Size - (B.Ptr - B.Location));
|
|
B.Ptr -= RemainingChars;
|
|
}
|
|
|
|
while(SeekBufferForString(&B, "/./", C_SEEK_FORWARDS, C_SEEK_END) == RC_SUCCESS)
|
|
{
|
|
char *NextComponentHead = B.Ptr;
|
|
int RemainingChars = StringLength(NextComponentHead);
|
|
--B.Ptr;
|
|
SeekBufferForString(&B, "/", C_SEEK_BACKWARDS, C_SEEK_START);
|
|
CopyStringToBufferNoFormat(&B, NextComponentHead);
|
|
Clear(B.Ptr, B.Size - (B.Ptr - B.Location));
|
|
B.Ptr -= RemainingChars;
|
|
}
|
|
|
|
CopyStringNoFormat(Path, StringLength(Path) + 1, B.Location);
|
|
DeclaimBuffer(&B);
|
|
}
|
|
|
|
int
|
|
ParseAssetString(template *Template, enum8(template_tag_codes) TagIndex, uint32_t *AssetIndexPtr)
|
|
{
|
|
char *Ptr = Template->File.Buffer.Ptr + StringLength(TemplateTags[TagIndex]);
|
|
ConsumeWhitespace(&Template->File.Buffer, &Ptr);
|
|
buffer AssetString;
|
|
ClaimBuffer(&AssetString, "AssetString", MAX_ASSET_FILENAME_LENGTH);
|
|
if(ParseQuotedString(&AssetString, &Template->File.Buffer, &Ptr, TagIndex) == RC_ERROR_PARSING)
|
|
{
|
|
DeclaimBuffer(&AssetString);
|
|
return RC_ERROR_PARSING;
|
|
}
|
|
int AssetType = 0;
|
|
switch(TagIndex)
|
|
{
|
|
case TAG_ASSET: AssetType = ASSET_GENERIC; break;
|
|
case TAG_CSS: AssetType = ASSET_CSS; break;
|
|
case TAG_IMAGE: AssetType = ASSET_IMG; break;
|
|
case TAG_JS: AssetType = ASSET_JS; break;
|
|
}
|
|
|
|
AssetString.Ptr = StripSurroundingSlashes(AssetString.Location);
|
|
StripPWDIndicators(AssetString.Ptr);
|
|
|
|
PushAsset(AssetString.Ptr, AssetType, AssetIndexPtr);
|
|
DeclaimBuffer(&AssetString);
|
|
return RC_SUCCESS;
|
|
}
|
|
|
|
int
|
|
PackTemplate(template *Template, char *Location, enum8(template_types) Type)
|
|
{
|
|
// TODO(matt): Record line numbers and contextual information:
|
|
// <? ?>
|
|
// <!-- -->
|
|
// < >
|
|
// <script </script>
|
|
|
|
InitTemplate(Template, Location, Type);
|
|
|
|
buffer Errors;
|
|
if(ClaimBuffer(&Errors, "Errors", Kilobytes(1)) == RC_ARENA_FULL) { FreeTemplate(Template); return RC_ARENA_FULL; };
|
|
|
|
bool HaveErrors = FALSE;
|
|
bool HaveAssetParsingErrors = FALSE;
|
|
|
|
bool FoundIncludes = FALSE;
|
|
bool FoundMenus = FALSE;
|
|
bool FoundPlayer = FALSE;
|
|
bool FoundScript = FALSE;
|
|
bool FoundSearch = FALSE;
|
|
|
|
char *Previous = Template->File.Buffer.Location;
|
|
|
|
while(Template->File.Buffer.Ptr - Template->File.Buffer.Location < Template->File.Buffer.Size)
|
|
{
|
|
NextTagSearch:
|
|
if(*Template->File.Buffer.Ptr == '!' && (Template->File.Buffer.Ptr > Template->File.Buffer.Location && !StringsDifferT("<!--", &Template->File.Buffer.Ptr[-1], 0)))
|
|
{
|
|
char *CommentStart = &Template->File.Buffer.Ptr[-1];
|
|
Template->File.Buffer.Ptr += StringLength("!--");
|
|
while(Template->File.Buffer.Ptr - Template->File.Buffer.Location < Template->File.Buffer.Size && StringsDifferT("-->", Template->File.Buffer.Ptr, 0))
|
|
{
|
|
for(int TagIndex = 0; TagIndex < TEMPLATE_TAG_COUNT; ++TagIndex)
|
|
{
|
|
if(!(StringsDifferT(TemplateTags[TagIndex], Template->File.Buffer.Ptr, 0)))
|
|
{
|
|
// TODO(matt): Pack up this data for BuffersToHTML() to use
|
|
/*
|
|
* Potential ways to compress these cases
|
|
*
|
|
* bool Found[TAG_COUNT]
|
|
* -Asaf
|
|
*
|
|
* int* flags[] = { [TAG_INCLUDES] = &FoundIncludes, [TAG_MENUS] = &FoundMenus } flags[Tags[i].Code] = true;
|
|
* -insofaras
|
|
*
|
|
*/
|
|
|
|
uint32_t AssetIndex = 0;
|
|
|
|
switch(TagIndex)
|
|
{
|
|
case TAG_SEARCH:
|
|
FoundSearch = TRUE;
|
|
goto RecordTag;
|
|
case TAG_INCLUDES:
|
|
if(!(Config.Mode & MODE_FORCEINTEGRATION) && FoundIncludes == TRUE)
|
|
{
|
|
CopyStringToBuffer(&Errors, "Template contains more than one <!-- %s --> tag\n", TemplateTags[TagIndex]);
|
|
HaveErrors = TRUE;
|
|
}
|
|
FoundIncludes = TRUE;
|
|
goto RecordTag;
|
|
case TAG_MENUS:
|
|
if(!(Config.Mode & MODE_FORCEINTEGRATION) && FoundMenus == TRUE)
|
|
{
|
|
CopyStringToBuffer(&Errors, "Template contains more than one <!-- %s --> tag\n", TemplateTags[TagIndex]);
|
|
HaveErrors = TRUE;
|
|
}
|
|
FoundMenus = TRUE;
|
|
goto RecordTag;
|
|
case TAG_PLAYER:
|
|
if(!(Config.Mode & MODE_FORCEINTEGRATION) && FoundPlayer == TRUE)
|
|
{
|
|
CopyStringToBuffer(&Errors, "Template contains more than one <!-- %s --> tag\n", TemplateTags[TagIndex]);
|
|
HaveErrors = TRUE;
|
|
}
|
|
FoundPlayer = TRUE;
|
|
goto RecordTag;
|
|
case TAG_SCRIPT:
|
|
if(!(Config.Mode & MODE_FORCEINTEGRATION) && (FoundMenus == FALSE || FoundPlayer == FALSE))
|
|
{
|
|
CopyStringToBuffer(&Errors, "<!-- %s --> must come after <!-- __CINERA_MENUS__ --> and <!-- __CINERA_PLAYER__ -->\n", TemplateTags[TagIndex]);
|
|
HaveErrors = TRUE;
|
|
}
|
|
if(!(Config.Mode & MODE_FORCEINTEGRATION) && FoundScript == TRUE)
|
|
{
|
|
CopyStringToBuffer(&Errors, "Template contains more than one <!-- %s --> tag\n", TemplateTags[TagIndex]);
|
|
HaveErrors = TRUE;
|
|
}
|
|
FoundScript = TRUE;
|
|
goto RecordTag;
|
|
case TAG_ASSET:
|
|
case TAG_CSS:
|
|
case TAG_IMAGE:
|
|
case TAG_JS:
|
|
{
|
|
if(ParseAssetString(Template, TagIndex, &AssetIndex) == RC_ERROR_PARSING)
|
|
{
|
|
HaveErrors = TRUE;
|
|
HaveAssetParsingErrors = TRUE;
|
|
}
|
|
} goto RecordTag;
|
|
default: // NOTE(matt): All freely usable tags should hit this case
|
|
RecordTag:
|
|
{
|
|
int Offset = CommentStart - Previous;
|
|
PushTemplateTag(Template, Offset, TagIndex, AssetIndex);
|
|
DepartComment(&Template->File.Buffer);
|
|
Previous = Template->File.Buffer.Ptr;
|
|
goto NextTagSearch;
|
|
}
|
|
};
|
|
}
|
|
}
|
|
++Template->File.Buffer.Ptr;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
++Template->File.Buffer.Ptr;
|
|
}
|
|
}
|
|
|
|
if(HaveAssetParsingErrors)
|
|
{
|
|
DeclaimBuffer(&Errors);
|
|
FreeTemplate(Template);
|
|
return RC_INVALID_TEMPLATE;
|
|
}
|
|
|
|
if(!HaveErrors && FoundIncludes && FoundSearch)
|
|
{
|
|
Template->Metadata.Validity |= PAGE_SEARCH;
|
|
}
|
|
|
|
if(!HaveErrors && FoundIncludes && FoundMenus && FoundPlayer && FoundScript)
|
|
{
|
|
Template->Metadata.Validity |= PAGE_PLAYER;
|
|
}
|
|
|
|
if(!(Config.Mode & MODE_FORCEINTEGRATION))
|
|
{
|
|
if(Type == TEMPLATE_SEARCH && !(Template->Metadata.Validity & PAGE_SEARCH))
|
|
{
|
|
if(!FoundIncludes) { CopyStringToBuffer(&Errors, "%sSearch template%s must include one <!-- __CINERA_INCLUDES__ --> tag\n", ColourStrings[CS_ERROR], ColourStrings[CS_END]); };
|
|
if(!FoundSearch) { CopyStringToBuffer(&Errors, "%sSearch template%s must include one <!-- __CINERA_SEARCH__ --> tag\n", ColourStrings[CS_ERROR], ColourStrings[CS_END]); };
|
|
fprintf(stderr, "%s", Errors.Location);
|
|
DeclaimBuffer(&Errors);
|
|
FreeTemplate(Template);
|
|
return RC_INVALID_TEMPLATE;
|
|
}
|
|
else if((Type == TEMPLATE_PLAYER || Type == TEMPLATE_BESPOKE) && !(Template->Metadata.Validity & PAGE_PLAYER))
|
|
{
|
|
if(!FoundIncludes){ CopyStringToBuffer(&Errors, "%s%slayer template%s must include one <!-- __CINERA_INCLUDES__ --> tag\n", ColourStrings[CS_ERROR], Type == TEMPLATE_BESPOKE ? "Bespoke p" : "P", ColourStrings[CS_END]); };
|
|
if(!FoundMenus){ CopyStringToBuffer(&Errors, "%s%slayer template%s must include one <!-- __CINERA_MENUS__ --> tag\n", ColourStrings[CS_ERROR], Type == TEMPLATE_BESPOKE ? "Bespoke p" : "P", ColourStrings[CS_END]); };
|
|
if(!FoundPlayer){ CopyStringToBuffer(&Errors, "%s%slayer template%s must include one <!-- __CINERA_PLAYER__ --> tag\n", ColourStrings[CS_ERROR], Type == TEMPLATE_BESPOKE ? "Bespoke p" : "P", ColourStrings[CS_END]); };
|
|
if(!FoundScript){ CopyStringToBuffer(&Errors, "%s%slayer template%s must include one <!-- __CINERA_SCRIPT__ --> tag\n", ColourStrings[CS_ERROR], Type == TEMPLATE_BESPOKE ? "Bespoke p" : "P", ColourStrings[CS_END]); };
|
|
fprintf(stderr, "%s", Errors.Location);
|
|
DeclaimBuffer(&Errors);
|
|
FreeTemplate(Template);
|
|
return RC_INVALID_TEMPLATE;
|
|
}
|
|
}
|
|
|
|
DeclaimBuffer(&Errors);
|
|
return RC_SUCCESS;
|
|
}
|
|
|
|
void
|
|
ConstructSearchURL(buffer *SearchURL)
|
|
{
|
|
RewindBuffer(SearchURL);
|
|
if(StringsDiffer(Config.BaseURL, ""))
|
|
{
|
|
CopyStringToBuffer(SearchURL, "%s/", Config.BaseURL);
|
|
if(StringsDiffer(Config.SearchLocation, ""))
|
|
{
|
|
CopyStringToBuffer(SearchURL, "%s/", Config.SearchLocation);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
ConstructPlayerURL(buffer *PlayerURL, char *BaseFilename)
|
|
{
|
|
RewindBuffer(PlayerURL);
|
|
if(StringsDiffer(Config.BaseURL, ""))
|
|
{
|
|
CopyStringToBuffer(PlayerURL, "%s/", Config.BaseURL);
|
|
if(StringsDiffer(Config.PlayerLocation, ""))
|
|
{
|
|
CopyStringToBuffer(PlayerURL, "%s/", Config.PlayerLocation);
|
|
}
|
|
}
|
|
if(StringsDiffer(BaseFilename, ""))
|
|
{
|
|
if(StringsDiffer(Config.PlayerURLPrefix, ""))
|
|
{
|
|
char *Ptr = BaseFilename + StringLength(Config.ProjectID);
|
|
CopyStringToBuffer(PlayerURL, "%s%s/", Config.PlayerURLPrefix, Ptr);
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(PlayerURL, "%s/", BaseFilename);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool
|
|
MediumExists(char *Medium)
|
|
{
|
|
for(int i = 0; i < ArrayCount(CategoryMedium); ++i)
|
|
{
|
|
if(!StringsDiffer(Medium, CategoryMedium[i].Medium))
|
|
{
|
|
return TRUE;
|
|
}
|
|
}
|
|
|
|
fprintf(stderr, "Specified default medium \"%s\" not available. Valid media are:\n", Medium);
|
|
for(int i = 0; i < ArrayCount(CategoryMedium); ++i)
|
|
{
|
|
fprintf(stderr, " %s\n", CategoryMedium[i].Medium);
|
|
}
|
|
fprintf(stderr, "To have \"%s\" added to the list, contact miblodelcarpio@gmail.com\n", Medium);
|
|
return FALSE;
|
|
}
|
|
|
|
typedef struct
|
|
{
|
|
char *BaseFilename;
|
|
char *Title;
|
|
} neighbour;
|
|
|
|
typedef struct
|
|
{
|
|
neighbour Prev;
|
|
neighbour Next;
|
|
} neighbours;
|
|
|
|
void
|
|
ExamineDB1(file_buffer File)
|
|
{
|
|
database1 LocalDB;
|
|
LocalDB.Header = *(db_header1 *)File.Buffer.Location;
|
|
printf("Current:\n"
|
|
"\tDBVersion: %d\n"
|
|
"\tAppVersion: %d.%d.%d\n"
|
|
"\tHMMLVersion: %d.%d.%d\n"
|
|
"\n"
|
|
"Entries: %d\n",
|
|
|
|
LocalDB.Header.DBVersion,
|
|
LocalDB.Header.AppVersion.Major, LocalDB.Header.AppVersion.Minor, LocalDB.Header.AppVersion.Patch,
|
|
LocalDB.Header.HMMLVersion.Major, LocalDB.Header.HMMLVersion.Minor, LocalDB.Header.HMMLVersion.Patch,
|
|
|
|
LocalDB.Header.EntryCount);
|
|
|
|
File.Buffer.Ptr = File.Buffer.Location + sizeof(LocalDB.Header);
|
|
for(int EntryIndex = 0; EntryIndex < LocalDB.Header.EntryCount; ++EntryIndex)
|
|
{
|
|
LocalDB.Entry = *(db_entry1 *)File.Buffer.Ptr;
|
|
printf(" %3d\t%s%sSize: %4d\n",
|
|
EntryIndex + 1, LocalDB.Entry.BaseFilename,
|
|
StringLength(LocalDB.Entry.BaseFilename) > 8 ? "\t" : "\t\t", // NOTE(matt): Janktasm
|
|
LocalDB.Entry.Size);
|
|
File.Buffer.Ptr += sizeof(LocalDB.Entry);
|
|
}
|
|
}
|
|
|
|
void
|
|
ExamineDB2(file_buffer File)
|
|
{
|
|
database2 LocalDB;
|
|
LocalDB.Header = *(db_header2 *)File.Buffer.Location;
|
|
printf("Current:\n"
|
|
"\tDBVersion: %d\n"
|
|
"\tAppVersion: %d.%d.%d\n"
|
|
"\tHMMLVersion: %d.%d.%d\n"
|
|
"\n"
|
|
"Relative Search Page Location: %s\n"
|
|
"Relative Player Page Location: %s\n"
|
|
"\n"
|
|
"Entries: %d\n",
|
|
|
|
LocalDB.Header.DBVersion,
|
|
LocalDB.Header.AppVersion.Major, LocalDB.Header.AppVersion.Minor, LocalDB.Header.AppVersion.Patch,
|
|
LocalDB.Header.HMMLVersion.Major, LocalDB.Header.HMMLVersion.Minor, LocalDB.Header.HMMLVersion.Patch,
|
|
|
|
LocalDB.Header.SearchLocation[0] != '\0' ? LocalDB.Header.SearchLocation : "(same as Base URL)",
|
|
LocalDB.Header.PlayerLocation[0] != '\0' ? LocalDB.Header.PlayerLocation : "(directly descended from Base URL)",
|
|
|
|
LocalDB.Header.EntryCount);
|
|
|
|
File.Buffer.Ptr = File.Buffer.Location + sizeof(LocalDB.Header);
|
|
for(int EntryIndex = 0; EntryIndex < LocalDB.Header.EntryCount; ++EntryIndex)
|
|
{
|
|
LocalDB.Entry = *(db_entry2 *)File.Buffer.Ptr;
|
|
printf(" %3d\t%s%sSize: %4d\n",
|
|
EntryIndex + 1, LocalDB.Entry.BaseFilename,
|
|
StringLength(LocalDB.Entry.BaseFilename) > 8 ? "\t" : "\t\t", // NOTE(matt): Janktasm
|
|
LocalDB.Entry.Size);
|
|
File.Buffer.Ptr += sizeof(LocalDB.Entry);
|
|
}
|
|
}
|
|
|
|
void
|
|
ExamineDB3(file_buffer File)
|
|
{
|
|
database3 LocalDB;
|
|
LocalDB.Header = *(db_header3 *)File.Buffer.Location;
|
|
printf("Current:\n"
|
|
"\tDBVersion: %d\n"
|
|
"\tAppVersion: %d.%d.%d\n"
|
|
"\tHMMLVersion: %d.%d.%d\n"
|
|
"\n"
|
|
"Initial:\n"
|
|
"\tDBVersion: %d\n"
|
|
"\tAppVersion: %d.%d.%d\n"
|
|
"\tHMMLVersion: %d.%d.%d\n"
|
|
"\n"
|
|
"Project ID: %s\n"
|
|
"Project Full Name: %s\n"
|
|
"\n"
|
|
"Base URL: %s\n"
|
|
"Relative Search Page Location: %s\n"
|
|
"Relative Player Page Location: %s\n"
|
|
"Player Page URL Prefix: %s\n"
|
|
"\n"
|
|
"Entries: %d\n",
|
|
|
|
LocalDB.Header.CurrentDBVersion,
|
|
LocalDB.Header.CurrentAppVersion.Major, LocalDB.Header.CurrentAppVersion.Minor, LocalDB.Header.CurrentAppVersion.Patch,
|
|
LocalDB.Header.CurrentHMMLVersion.Major, LocalDB.Header.CurrentHMMLVersion.Minor, LocalDB.Header.CurrentHMMLVersion.Patch,
|
|
|
|
LocalDB.Header.InitialDBVersion,
|
|
LocalDB.Header.InitialAppVersion.Major, LocalDB.Header.InitialAppVersion.Minor, LocalDB.Header.InitialAppVersion.Patch,
|
|
LocalDB.Header.InitialHMMLVersion.Major, LocalDB.Header.InitialHMMLVersion.Minor, LocalDB.Header.InitialHMMLVersion.Patch,
|
|
|
|
LocalDB.Header.ProjectID,
|
|
LocalDB.Header.ProjectName,
|
|
|
|
LocalDB.Header.BaseURL,
|
|
LocalDB.Header.SearchLocation[0] != '\0' ? LocalDB.Header.SearchLocation : "(same as Base URL)",
|
|
LocalDB.Header.PlayerLocation[0] != '\0' ? LocalDB.Header.PlayerLocation : "(directly descended from Base URL)",
|
|
LocalDB.Header.PlayerURLPrefix[0] != '\0' ? LocalDB.Header.PlayerURLPrefix : "(no special prefix, the player page URLs equal their entry's base filename)",
|
|
|
|
LocalDB.Header.EntryCount);
|
|
|
|
File.Buffer.Ptr = File.Buffer.Location + sizeof(LocalDB.Header);
|
|
for(int EntryIndex = 0; EntryIndex < LocalDB.Header.EntryCount; ++EntryIndex)
|
|
{
|
|
LocalDB.Entry = *(db_entry3 *)File.Buffer.Ptr;
|
|
printf(" %3d\t%s%sSize: %4d\t%d\t%d\t%d\t%d\n"
|
|
"\t %s\n",
|
|
EntryIndex + 1, LocalDB.Entry.BaseFilename,
|
|
StringLength(LocalDB.Entry.BaseFilename) > 8 ? "\t" : "\t\t", // NOTE(matt): Janktasm
|
|
LocalDB.Entry.Size,
|
|
LocalDB.Entry.LinkOffsets.PrevStart,
|
|
LocalDB.Entry.LinkOffsets.PrevEnd,
|
|
LocalDB.Entry.LinkOffsets.NextStart,
|
|
LocalDB.Entry.LinkOffsets.NextEnd,
|
|
LocalDB.Entry.Title);
|
|
File.Buffer.Ptr += sizeof(LocalDB.Entry);
|
|
}
|
|
}
|
|
|
|
void
|
|
ExamineDB4(file_buffer File)
|
|
{
|
|
database4 LocalDB;
|
|
LocalDB.Header = *(db_header4 *)File.Buffer.Location;
|
|
|
|
LocalDB.EntriesHeader = *(db_header_entries4 *)(File.Buffer.Location + sizeof(LocalDB.Header));
|
|
printf("Current:\n"
|
|
"\tDBVersion: %d\n"
|
|
"\tAppVersion: %d.%d.%d\n"
|
|
"\tHMMLVersion: %d.%d.%d\n"
|
|
"\n"
|
|
"Initial:\n"
|
|
"\tDBVersion: %d\n"
|
|
"\tAppVersion: %d.%d.%d\n"
|
|
"\tHMMLVersion: %d.%d.%d\n"
|
|
"\n"
|
|
"Database File Location: %s/%s.metadata\n"
|
|
"Search File Location: %s/%s.index\n"
|
|
"\n"
|
|
"Project ID: %s\n"
|
|
"Project Full Name: %s\n"
|
|
"\n"
|
|
"Base URL: %s\n"
|
|
"Relative Search Page Location: %s\n"
|
|
"Relative Player Page Location: %s\n"
|
|
"Player Page URL Prefix: %s\n"
|
|
"\n"
|
|
"Entries: %d\n",
|
|
|
|
LocalDB.Header.CurrentDBVersion,
|
|
LocalDB.Header.CurrentAppVersion.Major, LocalDB.Header.CurrentAppVersion.Minor, LocalDB.Header.CurrentAppVersion.Patch,
|
|
LocalDB.Header.CurrentHMMLVersion.Major, LocalDB.Header.CurrentHMMLVersion.Minor, LocalDB.Header.CurrentHMMLVersion.Patch,
|
|
|
|
LocalDB.Header.InitialDBVersion,
|
|
LocalDB.Header.InitialAppVersion.Major, LocalDB.Header.InitialAppVersion.Minor, LocalDB.Header.InitialAppVersion.Patch,
|
|
LocalDB.Header.InitialHMMLVersion.Major, LocalDB.Header.InitialHMMLVersion.Minor, LocalDB.Header.InitialHMMLVersion.Patch,
|
|
|
|
Config.BaseDir, Config.ProjectID,
|
|
Config.BaseDir, Config.ProjectID,
|
|
|
|
LocalDB.EntriesHeader.ProjectID,
|
|
LocalDB.EntriesHeader.ProjectName,
|
|
|
|
LocalDB.EntriesHeader.BaseURL,
|
|
LocalDB.EntriesHeader.SearchLocation[0] != '\0' ? LocalDB.EntriesHeader.SearchLocation : "(same as Base URL)",
|
|
LocalDB.EntriesHeader.PlayerLocation[0] != '\0' ? LocalDB.EntriesHeader.PlayerLocation : "(directly descended from Base URL)",
|
|
LocalDB.EntriesHeader.PlayerURLPrefix[0] != '\0' ? LocalDB.EntriesHeader.PlayerURLPrefix : "(no special prefix, the player page URLs equal their entry's base filename)",
|
|
|
|
LocalDB.EntriesHeader.Count);
|
|
|
|
File.Buffer.Ptr = File.Buffer.Location + sizeof(LocalDB.Header) + sizeof(LocalDB.EntriesHeader);
|
|
for(int EntryIndex = 0; EntryIndex < LocalDB.EntriesHeader.Count; ++EntryIndex)
|
|
{
|
|
LocalDB.Entry = *(db_entry4 *)File.Buffer.Ptr;
|
|
printf(" %3d\t%s%sSize: %4d\t%d\t%d\t%d\t%d\n"
|
|
"\t %s\n",
|
|
EntryIndex, LocalDB.Entry.BaseFilename,
|
|
StringLength(LocalDB.Entry.BaseFilename) > 8 ? "\t" : "\t\t", // NOTE(matt): Janktasm
|
|
LocalDB.Entry.Size,
|
|
LocalDB.Entry.LinkOffsets.PrevStart,
|
|
LocalDB.Entry.LinkOffsets.PrevEnd,
|
|
LocalDB.Entry.LinkOffsets.NextStart,
|
|
LocalDB.Entry.LinkOffsets.NextEnd,
|
|
LocalDB.Entry.Title);
|
|
File.Buffer.Ptr += sizeof(LocalDB.Entry);
|
|
}
|
|
|
|
LocalDB.AssetsHeader = *(db_header_assets4 *)File.Buffer.Ptr;
|
|
File.Buffer.Ptr += sizeof(LocalDB.AssetsHeader);
|
|
printf( "\n"
|
|
"Asset Root Directory: %s\n"
|
|
"Asset Root URL: %s\n"
|
|
" CSS Directory: %s\n"
|
|
" Images Directory: %s\n"
|
|
" JavaScript Directory: %s\n"
|
|
"Assets: %d\n"
|
|
"\n"
|
|
"%sLandmarks are displayed%s i•%sp%s %swhere i is the Entry Index and p is the Position\n"
|
|
"in bytes into the HTML file. Entry Index%s -1 %scorresponds to the Search Page%s\n",
|
|
LocalDB.AssetsHeader.RootDir,
|
|
LocalDB.AssetsHeader.RootURL,
|
|
StringsDiffer(LocalDB.AssetsHeader.CSSDir, "") ? LocalDB.AssetsHeader.CSSDir : "(same as root)",
|
|
StringsDiffer(LocalDB.AssetsHeader.ImagesDir, "") ? LocalDB.AssetsHeader.ImagesDir : "(same as root)",
|
|
StringsDiffer(LocalDB.AssetsHeader.JSDir, "") ? LocalDB.AssetsHeader.JSDir : "(same as root)",
|
|
LocalDB.AssetsHeader.Count,
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END],
|
|
ColourStrings[CS_PURPLE], ColourStrings[CS_END],
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END],
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END]);
|
|
|
|
for(int AssetIndex = 0; AssetIndex < LocalDB.AssetsHeader.Count; ++AssetIndex)
|
|
{
|
|
LocalDB.Asset = *(db_asset4*)File.Buffer.Ptr;
|
|
printf("\n"
|
|
"%s\n"
|
|
"Type: %s\n"
|
|
"Checksum: %08x\n"
|
|
"Landmarks: %d\n",
|
|
LocalDB.Asset.Filename,
|
|
AssetTypeNames[LocalDB.Asset.Type],
|
|
LocalDB.Asset.Hash,
|
|
LocalDB.Asset.LandmarkCount);
|
|
|
|
File.Buffer.Ptr += sizeof(LocalDB.Asset);
|
|
for(int LandmarkIndex = 0; LandmarkIndex < LocalDB.Asset.LandmarkCount; ++LandmarkIndex)
|
|
{
|
|
LocalDB.Landmark = *(db_landmark *)File.Buffer.Ptr;
|
|
File.Buffer.Ptr += sizeof(LocalDB.Landmark);
|
|
printf(" %d•%s%d%s", LocalDB.Landmark.EntryIndex, ColourStrings[CS_PURPLE], LocalDB.Landmark.Position, ColourStrings[CS_END]);
|
|
}
|
|
printf("\n");
|
|
}
|
|
}
|
|
|
|
int
|
|
ExamineDB(void)
|
|
{
|
|
file_buffer DBFile;
|
|
DBFile.Buffer.ID = "DBFile";
|
|
CopyString(DBFile.Path, sizeof(DBFile.Path), "%s/%s.metadata", Config.BaseDir, Config.ProjectID);
|
|
int DBFileReadCode = ReadFileIntoBuffer(&DBFile, 0);
|
|
|
|
switch(DBFileReadCode)
|
|
{
|
|
case RC_ERROR_MEMORY:
|
|
return RC_ERROR_MEMORY;
|
|
case RC_ERROR_FILE:
|
|
fprintf(stderr, "Unable to open metadata file %s: %s\n", DBFile.Path, strerror(errno));
|
|
return RC_ERROR_FILE;
|
|
case RC_SUCCESS:
|
|
break;
|
|
}
|
|
|
|
uint32_t FirstInt = *(uint32_t *)DBFile.Buffer.Location;
|
|
if(FirstInt != FOURCC("CNRA"))
|
|
{
|
|
switch(FirstInt)
|
|
{
|
|
case 1: ExamineDB1(DBFile); break;
|
|
case 2: ExamineDB2(DBFile); break;
|
|
case 3: ExamineDB3(DBFile); break;
|
|
default: printf("Invalid metadata file: %s\n", DBFile.Path); break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
uint32_t SecondInt = *(uint32_t *)(DBFile.Buffer.Location + sizeof(uint32_t));
|
|
switch(SecondInt)
|
|
{
|
|
case 4: ExamineDB4(DBFile); break;
|
|
default: printf("Invalid metadata file: %s\n", DBFile.Path); break;
|
|
}
|
|
}
|
|
FreeBuffer(&DBFile.Buffer);
|
|
return RC_SUCCESS;
|
|
}
|
|
|
|
#define HMMLCleanup() \
|
|
DeclaimBuffer(&CreditsMenu); \
|
|
DeclaimBuffer(&FilterMedia); \
|
|
DeclaimBuffer(&FilterTopics); \
|
|
DeclaimBuffer(&FilterMenu); \
|
|
DeclaimBuffer(&ReferenceMenu); \
|
|
DeclaimBuffer(&QuoteMenu); \
|
|
hmml_free(&HMML);
|
|
|
|
bool
|
|
VideoIsPrivate(char *VideoID)
|
|
{
|
|
// NOTE(matt): Currently only supports YouTube
|
|
char Message[128];
|
|
CopyString(Message, sizeof(Message), "%sChecking%s privacy status of: https://youtube.com/watch?v=%s", ColourStrings[CS_ONGOING], ColourStrings[CS_END], VideoID);
|
|
fprintf(stderr, "%s", Message);
|
|
int MessageLength = StringLength(Message);
|
|
buffer VideoAPIResponse;
|
|
ClaimBuffer(&VideoAPIResponse, "VideoAPIResponse", Kilobytes(1));
|
|
|
|
CURL *curl = curl_easy_init();
|
|
if(curl) {
|
|
LastPrivacyCheck = time(0);
|
|
#define APIKey "AIzaSyAdV2U8ivPk8PHMaPMId0gynksw_gdzr9k"
|
|
char URL[1024] = {0};
|
|
CopyString(URL, sizeof(URL), "https://www.googleapis.com/youtube/v3/videos?key=%s&part=status&id=%s", APIKey, VideoID);
|
|
CURLcode CurlReturnCode;
|
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &VideoAPIResponse.Ptr);
|
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlIntoBuffer);
|
|
curl_easy_setopt(curl, CURLOPT_URL, URL);
|
|
if((CurlReturnCode = curl_easy_perform(curl)))
|
|
{
|
|
fprintf(stderr, "%s\n", curl_easy_strerror(CurlReturnCode));
|
|
}
|
|
curl_easy_cleanup(curl);
|
|
|
|
VideoAPIResponse.Ptr = VideoAPIResponse.Location;
|
|
SeekBufferForString(&VideoAPIResponse, "{", C_SEEK_FORWARDS, C_SEEK_AFTER);
|
|
SeekBufferForString(&VideoAPIResponse, "\"totalResults\": ", C_SEEK_FORWARDS, C_SEEK_AFTER);
|
|
if(*VideoAPIResponse.Ptr == '0')
|
|
{
|
|
DeclaimBuffer(&VideoAPIResponse);
|
|
// printf("Private video: https://youtube.com/watch?v=%s\n", VideoID);
|
|
ClearTerminalRow(MessageLength);
|
|
return TRUE;
|
|
}
|
|
SeekBufferForString(&VideoAPIResponse, "{", C_SEEK_FORWARDS, C_SEEK_AFTER);
|
|
SeekBufferForString(&VideoAPIResponse, "\"privacyStatus\": \"", C_SEEK_FORWARDS, C_SEEK_AFTER);
|
|
char Status[16];
|
|
CopyStringNoFormatT(Status, sizeof(Status), VideoAPIResponse.Ptr, '\"');
|
|
if(!StringsDiffer(Status, "public"))
|
|
{
|
|
DeclaimBuffer(&VideoAPIResponse);
|
|
ClearTerminalRow(MessageLength);
|
|
return FALSE;
|
|
}
|
|
}
|
|
DeclaimBuffer(&VideoAPIResponse);
|
|
// printf("Unlisted video: https://youtube.com/watch?v=%s\n", VideoID);
|
|
ClearTerminalRow(MessageLength);
|
|
return TRUE;
|
|
}
|
|
|
|
typedef struct
|
|
{
|
|
db_entry Prev, This, Next;
|
|
uint32_t PreLinkPrevOffsetTotal, PreLinkThisOffsetTotal, PreLinkNextOffsetTotal;
|
|
uint32_t PrevOffsetModifier, ThisOffsetModifier, NextOffsetModifier;
|
|
bool FormerIsFirst, LatterIsFinal;
|
|
bool DeletedEntryWasFirst, DeletedEntryWasFinal;
|
|
short int PrevIndex, PreDeletionThisIndex, ThisIndex, NextIndex;
|
|
} neighbourhood;
|
|
|
|
int
|
|
LinearSearchForSpeaker(speakers Speakers, char *Username)
|
|
{
|
|
for(int i = 0; i < Speakers.Count; ++i)
|
|
{
|
|
if(!StringsDifferCaseInsensitive(Speakers.Speaker[i].Credential->Username, Username))
|
|
{
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
bool
|
|
IsCategorisedAFK(HMML_Annotation Anno)
|
|
{
|
|
for(int i = 0; i < Anno.marker_count; ++i)
|
|
{
|
|
if(!StringsDiffer(Anno.markers[i].marker, "afk"))
|
|
{
|
|
return TRUE;
|
|
}
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
// NOTE(matt): Perhaps these OffsetLandmarks* could be made redundant / generalised once we're on the LUT
|
|
void
|
|
OffsetLandmarks(buffer *Src, int AssetIndex, int PageType)
|
|
{
|
|
if(!(Config.Mode & MODE_NOREVVEDRESOURCE))
|
|
{
|
|
if(PageType == PAGE_PLAYER)
|
|
{
|
|
for(int LandmarkIndex = 0; LandmarkIndex < Assets.Asset[AssetIndex].PlayerLandmarkCount; ++LandmarkIndex)
|
|
{
|
|
Assets.Asset[AssetIndex].PlayerLandmark[LandmarkIndex] += Src->Ptr - Src->Location;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for(int LandmarkIndex = 0; LandmarkIndex < Assets.Asset[AssetIndex].SearchLandmarkCount; ++LandmarkIndex)
|
|
{
|
|
Assets.Asset[AssetIndex].SearchLandmark[LandmarkIndex] += Src->Ptr - Src->Location;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
OffsetLandmarksIncludes(buffer *Src, int PageType)
|
|
{
|
|
OffsetLandmarks(Src, ASSET_CSS_CINERA, PageType);
|
|
OffsetLandmarks(Src, ASSET_CSS_THEME, PageType);
|
|
OffsetLandmarks(Src, ASSET_CSS_TOPICS, PageType);
|
|
if(PageType == PAGE_PLAYER)
|
|
{
|
|
OffsetLandmarks(Src, ASSET_JS_PLAYER_PRE, PageType);
|
|
}
|
|
}
|
|
|
|
void
|
|
OffsetLandmarksCredits(buffer *Src)
|
|
{
|
|
OffsetLandmarks(Src, ICON_SENDOWL, PAGE_PLAYER);
|
|
OffsetLandmarks(Src, ICON_PATREON, PAGE_PLAYER);
|
|
}
|
|
|
|
void
|
|
OffsetLandmarksMenus(buffer *Src)
|
|
{
|
|
OffsetLandmarks(Src, ASSET_IMG_FILTER, PAGE_PLAYER);
|
|
OffsetLandmarksCredits(Src);
|
|
}
|
|
//
|
|
|
|
int
|
|
HMMLToBuffers(buffers *CollationBuffers, template *BespokeTemplate, char *Filename, neighbourhood *N)
|
|
{
|
|
RewindBuffer(&CollationBuffers->IncludesPlayer);
|
|
RewindBuffer(&CollationBuffers->Menus);
|
|
RewindBuffer(&CollationBuffers->Player);
|
|
RewindBuffer(&CollationBuffers->ScriptPlayer);
|
|
RewindBuffer(&CollationBuffers->SearchEntry);
|
|
*CollationBuffers->Custom0 = '\0';
|
|
*CollationBuffers->Custom1 = '\0';
|
|
*CollationBuffers->Custom2 = '\0';
|
|
*CollationBuffers->Custom3 = '\0';
|
|
*CollationBuffers->Custom4 = '\0';
|
|
*CollationBuffers->Custom5 = '\0';
|
|
*CollationBuffers->Custom6 = '\0';
|
|
*CollationBuffers->Custom7 = '\0';
|
|
*CollationBuffers->Custom8 = '\0';
|
|
*CollationBuffers->Custom9 = '\0';
|
|
*CollationBuffers->Custom10 = '\0';
|
|
*CollationBuffers->Custom11 = '\0';
|
|
*CollationBuffers->Custom12 = '\0';
|
|
*CollationBuffers->Custom13 = '\0';
|
|
*CollationBuffers->Custom14 = '\0';
|
|
*CollationBuffers->Custom15 = '\0';
|
|
*CollationBuffers->Title = '\0';
|
|
*CollationBuffers->URLPlayer = '\0';
|
|
*CollationBuffers->URLSearch = '\0';
|
|
*CollationBuffers->VODPlatform = '\0';
|
|
|
|
char Filepath[256];
|
|
if(Config.Edition == EDITION_PROJECT)
|
|
{
|
|
CopyString(Filepath, sizeof(Filepath), "%s/%s", Config.ProjectDir, Filename);
|
|
}
|
|
else
|
|
{
|
|
CopyString(Filepath, sizeof(Filepath), "%s", Filename);
|
|
}
|
|
|
|
FILE *InFile;
|
|
if(!(InFile = fopen(Filepath, "r")))
|
|
{
|
|
LogError(LOG_ERROR, "Unable to open (annotations file) %s: %s", Filename, strerror(errno));
|
|
fprintf(stderr, "Unable to open (annotations file) %s: %s\n", Filename, strerror(errno));
|
|
return RC_ERROR_FILE;
|
|
}
|
|
|
|
HMML_Output HMML = hmml_parse_file(InFile);
|
|
fclose(InFile);
|
|
|
|
char *BaseFilename = GetBaseFilename(Filename, ".hmml");
|
|
if(HMML.well_formed)
|
|
{
|
|
bool HaveErrors = FALSE;
|
|
|
|
if(StringLength(BaseFilename) > MAX_BASE_FILENAME_LENGTH)
|
|
{
|
|
fprintf(stderr, "%sBase filename \"%s\" is too long (%d/%d characters)%s\n", ColourStrings[CS_ERROR], BaseFilename, StringLength(BaseFilename), MAX_BASE_FILENAME_LENGTH, ColourStrings[CS_END]);
|
|
HaveErrors = TRUE;
|
|
}
|
|
|
|
if(!HMML.metadata.title)
|
|
{
|
|
fprintf(stderr, "Please set the title attribute in the [video] node of your .hmml file\n");
|
|
HaveErrors = TRUE;
|
|
}
|
|
else if(StringLength(HMML.metadata.title) > MAX_TITLE_LENGTH)
|
|
{
|
|
fprintf(stderr, "%sVideo title \"%s\" is too long (%d/%d characters)%s\n", ColourStrings[CS_ERROR], HMML.metadata.title, StringLength(HMML.metadata.title), MAX_TITLE_LENGTH, ColourStrings[CS_END]);
|
|
HaveErrors = TRUE;
|
|
}
|
|
else
|
|
{
|
|
CopyString(CollationBuffers->Title, sizeof(CollationBuffers->Title), "%s", HMML.metadata.title);
|
|
}
|
|
|
|
if(!HMML.metadata.member)
|
|
{
|
|
fprintf(stderr, "Please set the member attribute in the [video] node of your .hmml file\n");
|
|
HaveErrors = TRUE;
|
|
}
|
|
|
|
if(!StringsDiffer(CollationBuffers->Theme, ""))
|
|
{
|
|
if(HMML.metadata.project)
|
|
{
|
|
CopyStringNoFormat(CollationBuffers->Theme, sizeof(CollationBuffers->Theme), HMML.metadata.project);
|
|
}
|
|
else
|
|
{
|
|
fprintf(stderr, "Unable to determine which theme to apply to the HTML\n"
|
|
"Please set at least one of:\n"
|
|
"\t1. project attribute in the [video] node of your .hmml file\n"
|
|
"\t2. ProjectID on the command line with -p\n"
|
|
"\t3. Style on the command line with -s\n"
|
|
);
|
|
HaveErrors = TRUE;
|
|
}
|
|
}
|
|
|
|
if(!HMML.metadata.id)
|
|
{
|
|
fprintf(stderr, "Please set the id attribute in the [video] node of your .hmml file\n");
|
|
HaveErrors = TRUE;
|
|
}
|
|
else
|
|
{
|
|
CopyString(CollationBuffers->VideoID, sizeof(CollationBuffers->VideoID), "%s", HMML.metadata.id);
|
|
}
|
|
|
|
if(!HMML.metadata.vod_platform)
|
|
{
|
|
fprintf(stderr, "Please set the vod_platform attribute in the [video] node of your .hmml file\n");
|
|
HaveErrors = TRUE;
|
|
}
|
|
else
|
|
{
|
|
CopyString(CollationBuffers->VODPlatform, sizeof(CollationBuffers->VODPlatform), "%s", HMML.metadata.vod_platform);
|
|
}
|
|
|
|
buffer URLPlayer;
|
|
ClaimBuffer(&URLPlayer, "URLPlayer", MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH);
|
|
ConstructPlayerURL(&URLPlayer, BaseFilename);
|
|
CopyString(CollationBuffers->URLPlayer, sizeof(CollationBuffers->URLPlayer), "%s", URLPlayer.Location);
|
|
DeclaimBuffer(&URLPlayer);
|
|
|
|
if(HMML.metadata.project && (!StringsDiffer(CollationBuffers->ProjectID, "") || !StringsDiffer(CollationBuffers->ProjectName, "")))
|
|
{
|
|
for(int ProjectIndex = 0; ProjectIndex < ArrayCount(ProjectInfo); ++ProjectIndex)
|
|
{
|
|
if(!StringsDiffer(ProjectInfo[ProjectIndex].ProjectID,
|
|
HMML.metadata.project))
|
|
{
|
|
CopyStringNoFormat(CollationBuffers->ProjectID, sizeof(CollationBuffers->ProjectID), HMML.metadata.project);
|
|
CopyStringNoFormat(CollationBuffers->ProjectName, sizeof(CollationBuffers->ProjectName), ProjectInfo[ProjectIndex].FullName);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
char *DefaultMedium = Config.DefaultMedium;
|
|
if(HMML.metadata.medium)
|
|
{
|
|
if(MediumExists(HMML.metadata.medium))
|
|
{
|
|
DefaultMedium = HMML.metadata.medium;
|
|
}
|
|
else { HaveErrors = TRUE; }
|
|
}
|
|
|
|
// TODO(matt): Consider simply making these as buffers and claiming the necessary amount for them
|
|
// The nice thing about doing it this way, though, is that it encourages bespoke template use, which should
|
|
// usually be the more convenient way for people to write greater amounts of localised information
|
|
for(int CustomIndex = 0; CustomIndex < HMML_CUSTOM_ATTR_COUNT; ++CustomIndex)
|
|
{
|
|
if(HMML.metadata.custom[CustomIndex])
|
|
{
|
|
int LengthOfString = StringLength(HMML.metadata.custom[CustomIndex]);
|
|
if(LengthOfString > (CustomIndex < 12 ? MAX_CUSTOM_SNIPPET_SHORT_LENGTH : MAX_CUSTOM_SNIPPET_LONG_LENGTH))
|
|
{
|
|
fprintf(stderr, "%sCustom string %d \"%s%s%s\" is too long (%d/%d characters)%s\n",
|
|
ColourStrings[CS_ERROR], CustomIndex, ColourStrings[CS_END], HMML.metadata.custom[CustomIndex], ColourStrings[CS_ERROR],
|
|
LengthOfString, CustomIndex < 12 ? MAX_CUSTOM_SNIPPET_SHORT_LENGTH : MAX_CUSTOM_SNIPPET_LONG_LENGTH, ColourStrings[CS_END]);
|
|
if(LengthOfString < MAX_CUSTOM_SNIPPET_LONG_LENGTH)
|
|
{
|
|
fprintf(stderr, "Consider using custom12 to custom15, which can hold %d characters\n", MAX_CUSTOM_SNIPPET_LONG_LENGTH);
|
|
}
|
|
else
|
|
{
|
|
fprintf(stderr, "Consider using a bespoke template for longer amounts of localised information\n");
|
|
}
|
|
HaveErrors = TRUE;
|
|
}
|
|
else
|
|
{
|
|
switch(CustomIndex)
|
|
{
|
|
case 0: CopyStringNoFormat(CollationBuffers->Custom0, sizeof(CollationBuffers->Custom0), HMML.metadata.custom[CustomIndex]); break;
|
|
case 1: CopyStringNoFormat(CollationBuffers->Custom1, sizeof(CollationBuffers->Custom1), HMML.metadata.custom[CustomIndex]); break;
|
|
case 2: CopyStringNoFormat(CollationBuffers->Custom2, sizeof(CollationBuffers->Custom2), HMML.metadata.custom[CustomIndex]); break;
|
|
case 3: CopyStringNoFormat(CollationBuffers->Custom3, sizeof(CollationBuffers->Custom3), HMML.metadata.custom[CustomIndex]); break;
|
|
case 4: CopyStringNoFormat(CollationBuffers->Custom4, sizeof(CollationBuffers->Custom4), HMML.metadata.custom[CustomIndex]); break;
|
|
case 5: CopyStringNoFormat(CollationBuffers->Custom5, sizeof(CollationBuffers->Custom5), HMML.metadata.custom[CustomIndex]); break;
|
|
case 6: CopyStringNoFormat(CollationBuffers->Custom6, sizeof(CollationBuffers->Custom6), HMML.metadata.custom[CustomIndex]); break;
|
|
case 7: CopyStringNoFormat(CollationBuffers->Custom7, sizeof(CollationBuffers->Custom7), HMML.metadata.custom[CustomIndex]); break;
|
|
case 8: CopyStringNoFormat(CollationBuffers->Custom8, sizeof(CollationBuffers->Custom8), HMML.metadata.custom[CustomIndex]); break;
|
|
case 9: CopyStringNoFormat(CollationBuffers->Custom9, sizeof(CollationBuffers->Custom9), HMML.metadata.custom[CustomIndex]); break;
|
|
case 10: CopyStringNoFormat(CollationBuffers->Custom10, sizeof(CollationBuffers->Custom10), HMML.metadata.custom[CustomIndex]); break;
|
|
case 11: CopyStringNoFormat(CollationBuffers->Custom11, sizeof(CollationBuffers->Custom11), HMML.metadata.custom[CustomIndex]); break;
|
|
case 12: CopyStringNoFormat(CollationBuffers->Custom12, sizeof(CollationBuffers->Custom12), HMML.metadata.custom[CustomIndex]); break;
|
|
case 13: CopyStringNoFormat(CollationBuffers->Custom13, sizeof(CollationBuffers->Custom13), HMML.metadata.custom[CustomIndex]); break;
|
|
case 14: CopyStringNoFormat(CollationBuffers->Custom14, sizeof(CollationBuffers->Custom14), HMML.metadata.custom[CustomIndex]); break;
|
|
case 15: CopyStringNoFormat(CollationBuffers->Custom15, sizeof(CollationBuffers->Custom15), HMML.metadata.custom[CustomIndex]); break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(!HaveErrors)
|
|
{
|
|
if(HMML.metadata.template)
|
|
{
|
|
switch(PackTemplate(BespokeTemplate, HMML.metadata.template, TEMPLATE_BESPOKE))
|
|
{
|
|
case RC_ARENA_FULL:
|
|
case RC_INVALID_TEMPLATE: // Invalid template
|
|
case RC_ERROR_FILE: // Could not load template
|
|
case RC_ERROR_MEMORY: // Could not allocate memory for template
|
|
HaveErrors = TRUE;
|
|
case RC_SUCCESS:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(HaveErrors)
|
|
{
|
|
fprintf(stderr, "%sSkipping%s %s", ColourStrings[CS_ERROR], ColourStrings[CS_END], BaseFilename);
|
|
if(HMML.metadata.title) { fprintf(stderr, " - %s", HMML.metadata.title); }
|
|
fprintf(stderr, "\n");
|
|
hmml_free(&HMML);
|
|
return RC_ERROR_HMML;
|
|
}
|
|
|
|
if(N)
|
|
{
|
|
N->This.LinkOffsets.PrevStart = 0;
|
|
N->This.LinkOffsets.PrevEnd = 0;
|
|
N->This.LinkOffsets.NextStart = 0;
|
|
N->This.LinkOffsets.NextEnd = 0;
|
|
}
|
|
|
|
#if DEBUG
|
|
printf(
|
|
"================================================================================\n"
|
|
"%s\n"
|
|
"================================================================================\n",
|
|
Filename);
|
|
#endif
|
|
// NOTE(matt): Tree structure of "global" buffer dependencies
|
|
// Master
|
|
// IncludesPlayer
|
|
// Menus
|
|
// QuoteMenu
|
|
// ReferenceMenu
|
|
// FilterMenu
|
|
// FilterTopics
|
|
// FilterMedia
|
|
// CreditsMenu
|
|
// Player
|
|
// Script
|
|
|
|
buffer QuoteMenu;
|
|
buffer ReferenceMenu;
|
|
buffer FilterMenu;
|
|
buffer FilterTopics;
|
|
buffer FilterMedia;
|
|
buffer CreditsMenu;
|
|
|
|
buffer Annotation;
|
|
buffer AnnotationHeader;
|
|
buffer AnnotationClass;
|
|
buffer AnnotationData;
|
|
buffer Text;
|
|
buffer CategoryIcons;
|
|
|
|
if(ClaimBuffer(&QuoteMenu, "QuoteMenu", Kilobytes(32)) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; };
|
|
if(ClaimBuffer(&ReferenceMenu, "ReferenceMenu", Kilobytes(32)) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; };
|
|
if(ClaimBuffer(&FilterMenu, "FilterMenu", Kilobytes(16)) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; };
|
|
if(ClaimBuffer(&FilterTopics, "FilterTopics", Kilobytes(8)) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; };
|
|
if(ClaimBuffer(&FilterMedia, "FilterMedia", Kilobytes(8)) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; };
|
|
if(ClaimBuffer(&CreditsMenu, "CreditsMenu", Kilobytes(8)) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; };
|
|
|
|
ref_info ReferencesArray[200] = { };
|
|
categories Topics = { };
|
|
categories Media = { };
|
|
|
|
bool HasQuoteMenu = FALSE;
|
|
bool HasReferenceMenu = FALSE;
|
|
bool HasFilterMenu = FALSE;
|
|
bool HasCreditsMenu = FALSE;
|
|
|
|
int QuoteIdentifier = 0x3b1;
|
|
int RefIdentifier = 1;
|
|
int UniqueRefs = 0;
|
|
|
|
CopyStringToBuffer(&CollationBuffers->Menus,
|
|
"<div class=\"cineraMenus %s\">\n"
|
|
" <span class=\"episode_name\">", StringsDiffer(Config.Theme, "") ? Config.Theme : HMML.metadata.project);
|
|
CopyStringToBufferHTMLSafe(&CollationBuffers->Menus, HMML.metadata.title);
|
|
CopyStringToBuffer(&CollationBuffers->Menus, "</span>\n");
|
|
|
|
CopyStringToBuffer(&CollationBuffers->Player,
|
|
"<div class=\"cineraPlayerContainer\">\n"
|
|
" <div class=\"video_container\" data-videoId=\"%s\"></div>\n"
|
|
" <div class=\"markers_container %s\">\n", HMML.metadata.id, StringsDiffer(Config.Theme, "") ? Config.Theme : HMML.metadata.project);
|
|
|
|
if(N)
|
|
{
|
|
N->This.LinkOffsets.PrevStart = (CollationBuffers->Player.Ptr - CollationBuffers->Player.Location);
|
|
if(N->Prev.Size || N->Next.Size)
|
|
{
|
|
if(N->Prev.Size)
|
|
{
|
|
// TODO(matt): Once we have a more rigorous notion of "Day Numbers", perhaps also use them here
|
|
buffer PreviousPlayerURL;
|
|
ClaimBuffer(&PreviousPlayerURL, "PreviousPlayerURL", MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH);
|
|
ConstructPlayerURL(&PreviousPlayerURL, N->Prev.BaseFilename);
|
|
CopyStringToBuffer(&CollationBuffers->Player,
|
|
" <a class=\"episodeMarker prev\" href=\"%s\"><div>⏫</div><div>Previous: '%s'</div><div>⏫</div></a>\n",
|
|
PreviousPlayerURL.Location,
|
|
N->Prev.Title);
|
|
|
|
DeclaimBuffer(&PreviousPlayerURL);
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Player,
|
|
" <div class=\"episodeMarker first\"><div>•</div><div>Welcome to <cite>%s</cite></div><div>•</div></div>\n", CollationBuffers->ProjectName);
|
|
}
|
|
}
|
|
N->This.LinkOffsets.PrevEnd = (CollationBuffers->Player.Ptr - CollationBuffers->Player.Location - N->This.LinkOffsets.PrevStart);
|
|
}
|
|
|
|
CopyStringToBuffer(&CollationBuffers->Player,
|
|
" <div class=\"markers\">\n");
|
|
|
|
speakers Speakers = { };
|
|
switch(BuildCredits(&CreditsMenu, &HasCreditsMenu, &HMML.metadata, &Speakers))
|
|
{
|
|
case CreditsError_NoHost:
|
|
case CreditsError_NoAnnotator:
|
|
case CreditsError_NoCredentials:
|
|
fprintf(stderr, "%sSkipping%s %s - %s\n", ColourStrings[CS_ERROR], ColourStrings[CS_END], BaseFilename, HMML.metadata.title);
|
|
HMMLCleanup();
|
|
return RC_ERROR_HMML;
|
|
}
|
|
|
|
bool PrivateVideo = FALSE;
|
|
if(Config.Edition != EDITION_SINGLE && N->This.Size == 0 && !(Config.Mode & MODE_NOPRIVACY))
|
|
{
|
|
if(VideoIsPrivate(HMML.metadata.id))
|
|
{
|
|
// TODO(matt): Actually generate these guys, just putting them in a secret location
|
|
N->This.LinkOffsets.PrevStart = 0;
|
|
N->This.LinkOffsets.PrevEnd = 0;
|
|
PrivateVideo = TRUE;
|
|
HMMLCleanup();
|
|
return RC_PRIVATE_VIDEO;
|
|
}
|
|
}
|
|
|
|
if(Config.Edition != EDITION_SINGLE && !PrivateVideo)
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->SearchEntry, "name: \"");
|
|
if(StringsDiffer(Config.PlayerURLPrefix, ""))
|
|
{
|
|
char *Ptr = BaseFilename + StringLength(Config.ProjectID);
|
|
CopyStringToBuffer(&CollationBuffers->SearchEntry, "%s%s", Config.PlayerURLPrefix, Ptr);
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->SearchEntry, "%s", BaseFilename);
|
|
}
|
|
|
|
CopyStringToBuffer(&CollationBuffers->SearchEntry, "\"\n"
|
|
"title: \"");
|
|
CopyStringToBufferNoFormat(&CollationBuffers->SearchEntry, HMML.metadata.title);
|
|
CopyStringToBuffer(&CollationBuffers->SearchEntry, "\"\n"
|
|
"markers:\n");
|
|
}
|
|
|
|
#if DEBUG
|
|
printf("\n\n --- Entering Annotations Loop ---\n\n\n\n");
|
|
#endif
|
|
|
|
int PreviousTimecode = 0;
|
|
for(int AnnotationIndex = 0; AnnotationIndex < HMML.annotation_count; ++AnnotationIndex)
|
|
{
|
|
#if DEBUG
|
|
printf("%d\n", AnnotationIndex);
|
|
#endif
|
|
|
|
HMML_Annotation *Anno = HMML.annotations + AnnotationIndex;
|
|
|
|
if(TimecodeToSeconds(Anno->time) < PreviousTimecode)
|
|
{
|
|
fprintf(stderr, "%s:%d: Timecode %s is chronologically before previous timecode\n"
|
|
"%sSkipping%s %s - %s\n",
|
|
Filename, Anno->line, Anno->time,
|
|
ColourStrings[CS_ERROR], ColourStrings[CS_END], BaseFilename, HMML.metadata.title);
|
|
HMMLCleanup();
|
|
return RC_ERROR_HMML;
|
|
}
|
|
PreviousTimecode = TimecodeToSeconds(Anno->time);
|
|
|
|
categories LocalTopics = { };
|
|
categories LocalMedia = { };
|
|
bool HasQuote = FALSE;
|
|
bool HasReference = FALSE;
|
|
|
|
quote_info QuoteInfo = { };
|
|
|
|
// NOTE(matt): Tree structure of "annotation local" buffer dependencies
|
|
// Annotation
|
|
// AnnotationHeader
|
|
// AnnotationClass
|
|
// AnnotationData
|
|
// Text
|
|
// CategoryIcons
|
|
|
|
// TODO(matt): Shouldn't all of these guys DeclaimBuffer() before returning?
|
|
if(ClaimBuffer(&Annotation, "Annotation", Kilobytes(8)) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; };
|
|
if(ClaimBuffer(&AnnotationHeader, "AnnotationHeader", 512) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; };
|
|
if(ClaimBuffer(&AnnotationClass, "AnnotationClass", 256) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; };
|
|
if(ClaimBuffer(&AnnotationData, "AnnotationData", 512) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; };
|
|
if(ClaimBuffer(&Text, "Text", Kilobytes(4)) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; };
|
|
if(ClaimBuffer(&CategoryIcons, "CategoryIcons", Kilobytes(1)) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; };
|
|
|
|
CopyStringToBuffer(&AnnotationHeader,
|
|
" <div data-timestamp=\"%d\"",
|
|
TimecodeToSeconds(Anno->time));
|
|
|
|
CopyStringToBuffer(&AnnotationClass,
|
|
" class=\"marker");
|
|
|
|
if((Anno->author || Speakers.Count > 1) && !IsCategorisedAFK(*Anno))
|
|
{
|
|
int SpeakerIndex;
|
|
if(Anno->author && (SpeakerIndex = LinearSearchForSpeaker(Speakers, Anno->author)) == -1)
|
|
{
|
|
if(!HasFilterMenu)
|
|
{
|
|
HasFilterMenu = TRUE;
|
|
}
|
|
InsertCategory(&Topics, &LocalTopics, &Media, &LocalMedia, "authored");
|
|
hsl_colour AuthorColour;
|
|
StringToColourHash(&AuthorColour, Anno->author);
|
|
// TODO(matt): That EDITION_NETWORK site database API-polling stuff
|
|
CopyStringToBuffer(&Text,
|
|
"<span class=\"author\" data-hue=\"%d\" data-saturation=\"%d%%\">%s</span> ",
|
|
AuthorColour.Hue, AuthorColour.Saturation,
|
|
Anno->author);
|
|
}
|
|
else
|
|
{
|
|
if(!Anno->author)
|
|
{
|
|
SpeakerIndex = LinearSearchForSpeaker(Speakers, HMML.metadata.member);
|
|
}
|
|
|
|
CopyStringToBuffer(&Text,
|
|
"<span class=\"author\" data-hue=\"%d\" data-saturation=\"%d%%\">%s</span>: ",
|
|
Speakers.Speaker[SpeakerIndex].Colour.Hue,
|
|
Speakers.Speaker[SpeakerIndex].Colour.Saturation,
|
|
|
|
Speakers.Speaker[SpeakerIndex].Seen == FALSE ? Speakers.Speaker[SpeakerIndex].Credential->CreditedName : Speakers.Speaker[SpeakerIndex].Abbreviation);
|
|
|
|
Speakers.Speaker[SpeakerIndex].Seen = TRUE;
|
|
}
|
|
}
|
|
|
|
char *InPtr = Anno->text;
|
|
|
|
int MarkerIndex = 0, RefIndex = 0;
|
|
while(*InPtr || RefIndex < Anno->reference_count)
|
|
{
|
|
if(MarkerIndex < Anno->marker_count &&
|
|
InPtr - Anno->text == Anno->markers[MarkerIndex].offset)
|
|
{
|
|
char *Readable = Anno->markers[MarkerIndex].parameter
|
|
? Anno->markers[MarkerIndex].parameter
|
|
: Anno->markers[MarkerIndex].marker;
|
|
switch(Anno->markers[MarkerIndex].type)
|
|
{
|
|
case HMML_MEMBER:
|
|
case HMML_PROJECT:
|
|
{
|
|
// TODO(matt): That EDITION_NETWORK site database API-polling stuff
|
|
hsl_colour Colour;
|
|
StringToColourHash(&Colour, Anno->markers[MarkerIndex].marker);
|
|
CopyStringToBuffer(&Text,
|
|
"<span class=\"%s\" data-hue=\"%d\" data-saturation=\"%d%%\">%.*s</span>",
|
|
Anno->markers[MarkerIndex].type == HMML_MEMBER ? "member" : "project",
|
|
Colour.Hue, Colour.Saturation,
|
|
StringLength(Readable), InPtr);
|
|
|
|
} break;
|
|
case HMML_CATEGORY:
|
|
{
|
|
switch(GenerateTopicColours(Anno->markers[MarkerIndex].marker))
|
|
{
|
|
case RC_SUCCESS:
|
|
case RC_NOOP:
|
|
break;
|
|
case RC_ERROR_FILE:
|
|
case RC_ERROR_MEMORY:
|
|
hmml_free(&HMML);
|
|
return RC_ERROR_FATAL;
|
|
};
|
|
if(!HasFilterMenu)
|
|
{
|
|
HasFilterMenu = TRUE;
|
|
}
|
|
InsertCategory(&Topics, &LocalTopics, &Media, &LocalMedia, Anno->markers[MarkerIndex].marker);
|
|
CopyStringToBuffer(&Text, "%.*s", StringLength(Readable), InPtr);
|
|
} break;
|
|
case HMML_MARKER_COUNT: break;
|
|
}
|
|
InPtr += StringLength(Readable);
|
|
++MarkerIndex;
|
|
}
|
|
|
|
while(RefIndex < Anno->reference_count &&
|
|
InPtr - Anno->text == Anno->references[RefIndex].offset)
|
|
{
|
|
HMML_Reference *CurrentRef = Anno->references + RefIndex;
|
|
if(!HasReferenceMenu)
|
|
{
|
|
CopyStringToBuffer(&ReferenceMenu,
|
|
" <div class=\"menu references\">\n"
|
|
" <span>References ▼</span>\n"
|
|
" <div class=\"refs references_container\">\n");
|
|
|
|
if(BuildReference(ReferencesArray, RefIdentifier, UniqueRefs, CurrentRef, Anno) == RC_INVALID_REFERENCE)
|
|
{
|
|
LogError(LOG_ERROR, "Reference combination processing failed: %s:%d", Filename, Anno->line);
|
|
fprintf(stderr, "%s:%d: Cannot process new combination of reference info\n"
|
|
"\n"
|
|
"Either tweak your annotation, or contact miblodelcarpio@gmail.com\n"
|
|
"mentioning the ref node you want to write and how you want it to\n"
|
|
"appear in the references menu\n", Filename, Anno->line);
|
|
hmml_free(&HMML);
|
|
return RC_INVALID_REFERENCE;
|
|
}
|
|
++ReferencesArray[RefIdentifier - 1].IdentifierCount;
|
|
++UniqueRefs;
|
|
|
|
HasReferenceMenu = TRUE;
|
|
}
|
|
else
|
|
{
|
|
for(int i = 0; i < UniqueRefs; ++i)
|
|
{
|
|
if(ReferencesArray[i].IdentifierCount == MAX_REF_IDENTIFIER_COUNT)
|
|
{
|
|
LogError(LOG_EMERGENCY, "REF_MAX_IDENTIFIER (%d) reached. Contact miblodelcarpio@gmail.com", MAX_REF_IDENTIFIER_COUNT);
|
|
fprintf(stderr, "%s:%d: Too many timecodes associated with one reference (increase REF_MAX_IDENTIFIER)\n", Filename, Anno->line);
|
|
hmml_free(&HMML);
|
|
return RC_ERROR_MAX_REFS;
|
|
}
|
|
if(CurrentRef->isbn)
|
|
{
|
|
if(!StringsDiffer(CurrentRef->isbn, ReferencesArray[i].ID))
|
|
{
|
|
CopyString(ReferencesArray[i].Identifier[ReferencesArray[i].IdentifierCount].Timecode, sizeof(ReferencesArray[i].Identifier[ReferencesArray[i].IdentifierCount].Timecode), "%s", Anno->time);
|
|
ReferencesArray[i].Identifier[ReferencesArray[i].IdentifierCount].Identifier = RefIdentifier;
|
|
++ReferencesArray[i].IdentifierCount;
|
|
goto AppendedIdentifier;
|
|
}
|
|
}
|
|
else if(CurrentRef->url)
|
|
{
|
|
if(!StringsDiffer(CurrentRef->url, ReferencesArray[i].ID))
|
|
{
|
|
CopyString(ReferencesArray[i].Identifier[ReferencesArray[i].IdentifierCount].Timecode, sizeof(ReferencesArray[i].Identifier[ReferencesArray[i].IdentifierCount].Timecode), "%s", Anno->time);
|
|
ReferencesArray[i].Identifier[ReferencesArray[i].IdentifierCount].Identifier = RefIdentifier;
|
|
++ReferencesArray[i].IdentifierCount;
|
|
goto AppendedIdentifier;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
LogError(LOG_ERROR, "Reference missing ISBN or URL: %s:%d", Filename, Anno->line);
|
|
fprintf(stderr, "%s:%d: Reference must have an ISBN or URL\n", Filename, Anno->line);
|
|
hmml_free(&HMML);
|
|
return RC_INVALID_REFERENCE;
|
|
}
|
|
}
|
|
|
|
if(BuildReference(ReferencesArray, RefIdentifier, UniqueRefs, CurrentRef, Anno) == RC_INVALID_REFERENCE)
|
|
{
|
|
LogError(LOG_ERROR, "Reference combination processing failed: %s:%d", Filename, Anno->line);
|
|
fprintf(stderr, "%s:%d: Cannot process new combination of reference info\n"
|
|
"\n"
|
|
"Either tweak your annotation, or contact miblodelcarpio@gmail.com\n"
|
|
"mentioning the ref node you want to write and how you want it to\n"
|
|
"appear in the references menu\n", Filename, Anno->line);
|
|
hmml_free(&HMML);
|
|
return RC_INVALID_REFERENCE;
|
|
}
|
|
++ReferencesArray[UniqueRefs].IdentifierCount;
|
|
++UniqueRefs;
|
|
}
|
|
AppendedIdentifier:
|
|
if(!HasReference)
|
|
{
|
|
if(CurrentRef->isbn)
|
|
{
|
|
CopyStringToBuffer(&AnnotationData, " data-ref=\"%s", CurrentRef->isbn);
|
|
}
|
|
else if(CurrentRef->url)
|
|
{
|
|
CopyStringToBuffer(&AnnotationData, " data-ref=\"%s", CurrentRef->url);
|
|
}
|
|
else
|
|
{
|
|
LogError(LOG_ERROR, "Reference missing ISBN or URL: %s:%d", Filename, Anno->line);
|
|
fprintf(stderr, "%s:%d: Reference must have an ISBN or URL\n", Filename, Anno->line);
|
|
hmml_free(&HMML);
|
|
return RC_INVALID_REFERENCE;
|
|
}
|
|
|
|
HasReference = TRUE;
|
|
}
|
|
else
|
|
{
|
|
if(CurrentRef->isbn)
|
|
{
|
|
CopyStringToBuffer(&AnnotationData, ",%s", CurrentRef->isbn);
|
|
}
|
|
else if(CurrentRef->url)
|
|
{
|
|
CopyStringToBuffer(&AnnotationData, ",%s", CurrentRef->url);
|
|
}
|
|
else
|
|
{
|
|
LogError(LOG_ERROR, "Reference missing ISBN or URL: %s:%d", Filename, Anno->line);
|
|
fprintf(stderr, "%s:%d: Reference must have an ISBN or URL", Filename, Anno->line);
|
|
hmml_free(&HMML);
|
|
return RC_INVALID_REFERENCE;
|
|
}
|
|
}
|
|
|
|
CopyStringToBuffer(&Text, "<sup>%s%d</sup>",
|
|
Anno->references[RefIndex].offset == Anno->references[RefIndex-1].offset ? "," : "",
|
|
RefIdentifier);
|
|
|
|
++RefIndex;
|
|
++RefIdentifier;
|
|
}
|
|
|
|
if(*InPtr)
|
|
{
|
|
switch(*InPtr)
|
|
{
|
|
case '<':
|
|
CopyStringToBuffer(&Text, "<");
|
|
break;
|
|
case '>':
|
|
CopyStringToBuffer(&Text, ">");
|
|
break;
|
|
case '&':
|
|
CopyStringToBuffer(&Text, "&");
|
|
break;
|
|
case '\"':
|
|
CopyStringToBuffer(&Text, """);
|
|
break;
|
|
case '\'':
|
|
CopyStringToBuffer(&Text, "'");
|
|
break;
|
|
default:
|
|
*Text.Ptr++ = *InPtr;
|
|
*Text.Ptr = '\0';
|
|
break;
|
|
}
|
|
++InPtr;
|
|
}
|
|
}
|
|
|
|
if(Anno->is_quote)
|
|
{
|
|
if(!HasQuoteMenu)
|
|
{
|
|
CopyStringToBuffer(&QuoteMenu,
|
|
" <div class=\"menu quotes\">\n"
|
|
" <span>Quotes ▼</span>\n"
|
|
" <div class=\"refs quotes_container\">\n");
|
|
|
|
HasQuoteMenu = TRUE;
|
|
}
|
|
|
|
if(!HasReference)
|
|
{
|
|
CopyStringToBuffer(&AnnotationData, " data-ref=\"&#%d;", QuoteIdentifier);
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&AnnotationData, ",&#%d;", QuoteIdentifier);
|
|
}
|
|
|
|
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, 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';
|
|
|
|
fprintf(stderr, "Quote #%s %d not found\n"
|
|
"%sSkipping%s %s - %s\n",
|
|
Speaker, Anno->quote.id,
|
|
ColourStrings[CS_ERROR], ColourStrings[CS_END], BaseFilename, HMML.metadata.title);
|
|
hmml_free(&HMML);
|
|
return RC_ERROR_QUOTE;
|
|
}
|
|
|
|
CopyStringToBuffer(&QuoteMenu,
|
|
" <a target=\"_blank\" class=\"ref\" href=\"https://dev.abaines.me.uk/quotes/%s/%d\">\n"
|
|
" <span data-id=\"&#%d;\">\n"
|
|
" <span class=\"ref_content\">\n"
|
|
" <div class=\"source\">Quote %d</div>\n"
|
|
" <div class=\"ref_title\">",
|
|
Speaker,
|
|
Anno->quote.id,
|
|
QuoteIdentifier,
|
|
Anno->quote.id);
|
|
|
|
CopyStringToBufferHTMLSafe(&QuoteMenu, QuoteInfo.Text);
|
|
|
|
CopyStringToBuffer(&QuoteMenu, "</div>\n"
|
|
" <div class=\"quote_byline\">—%s, %s</div>\n"
|
|
" </span>\n"
|
|
" <div class=\"ref_indices\">\n"
|
|
" <span data-timestamp=\"%d\" class=\"timecode\"><span class=\"ref_index\">[&#%d;]</span><span class=\"time\">%s</span></span>\n"
|
|
" </div>\n"
|
|
" </span>\n"
|
|
" </a>\n",
|
|
Speaker,
|
|
QuoteInfo.Date,
|
|
TimecodeToSeconds(Anno->time),
|
|
QuoteIdentifier,
|
|
Anno->time);
|
|
if(!Anno->text[0])
|
|
{
|
|
CopyStringToBuffer(&Text, "“");
|
|
CopyStringToBufferHTMLSafe(&Text, QuoteInfo.Text);
|
|
CopyStringToBuffer(&Text, "”");
|
|
}
|
|
CopyStringToBuffer(&Text, "<sup>&#%d;</sup>", QuoteIdentifier);
|
|
++QuoteIdentifier;
|
|
}
|
|
|
|
if(Config.Edition != EDITION_SINGLE && !PrivateVideo)
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->SearchEntry, "\"%d\": \"", TimecodeToSeconds(Anno->time));
|
|
if(Anno->is_quote && !Anno->text[0])
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->SearchEntry, "\u201C");
|
|
CopyStringToBufferNoFormat(&CollationBuffers->SearchEntry, QuoteInfo.Text);
|
|
CopyStringToBuffer(&CollationBuffers->SearchEntry, "\u201D");
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBufferNoFormat(&CollationBuffers->SearchEntry, Anno->text);
|
|
}
|
|
CopyStringToBuffer(&CollationBuffers->SearchEntry, "\"\n");
|
|
}
|
|
|
|
while(MarkerIndex < Anno->marker_count)
|
|
{
|
|
switch(GenerateTopicColours(Anno->markers[MarkerIndex].marker))
|
|
{
|
|
case RC_SUCCESS:
|
|
case RC_NOOP:
|
|
break;
|
|
case RC_ERROR_FILE:
|
|
case RC_ERROR_MEMORY:
|
|
hmml_free(&HMML);
|
|
return RC_ERROR_FATAL;
|
|
}
|
|
if(!HasFilterMenu)
|
|
{
|
|
HasFilterMenu = TRUE;
|
|
}
|
|
InsertCategory(&Topics, &LocalTopics, &Media, &LocalMedia, Anno->markers[MarkerIndex].marker);
|
|
++MarkerIndex;
|
|
}
|
|
|
|
if(LocalTopics.Count == 0)
|
|
{
|
|
switch(GenerateTopicColours("nullTopic"))
|
|
{
|
|
case RC_SUCCESS:
|
|
case RC_NOOP:
|
|
break;
|
|
case RC_ERROR_FILE:
|
|
case RC_ERROR_MEMORY:
|
|
hmml_free(&HMML);
|
|
return RC_ERROR_FATAL;
|
|
};
|
|
InsertCategory(&Topics, &LocalTopics, &Media, &LocalMedia, "nullTopic");
|
|
}
|
|
|
|
if(LocalMedia.Count == 0)
|
|
{
|
|
InsertCategory(&Topics, &LocalTopics, &Media, &LocalMedia, DefaultMedium);
|
|
}
|
|
|
|
BuildCategories(&AnnotationClass, &CategoryIcons, &LocalTopics, &LocalMedia, &MarkerIndex, DefaultMedium);
|
|
CopyBuffer(&AnnotationHeader, &AnnotationClass);
|
|
|
|
if(HasQuote || HasReference)
|
|
{
|
|
CopyStringToBuffer(&AnnotationData, "\"");
|
|
CopyBuffer(&AnnotationHeader, &AnnotationData);
|
|
}
|
|
CopyStringToBuffer(&AnnotationHeader, ">\n");
|
|
|
|
CopyBuffer(&Annotation, &AnnotationHeader);
|
|
CopyStringToBuffer(&Annotation,
|
|
" <div class=\"cineraContent\"><span class=\"timecode\">%s</span>",
|
|
Anno->time);
|
|
|
|
CopyBuffer(&Annotation, &Text);
|
|
|
|
if(LocalTopics.Count > 0)
|
|
{
|
|
CopyBuffer(&Annotation, &CategoryIcons);
|
|
}
|
|
|
|
CopyStringToBuffer(&Annotation, "</div>\n"
|
|
" <div class=\"progress faded\">\n"
|
|
" <div class=\"cineraContent\"><span class=\"timecode\">%s</span>",
|
|
Anno->time);
|
|
|
|
CopyBuffer(&Annotation, &Text);
|
|
|
|
if(LocalTopics.Count > 0)
|
|
{
|
|
CopyBuffer(&Annotation, &CategoryIcons);
|
|
}
|
|
|
|
CopyStringToBuffer(&Annotation, "</div>\n"
|
|
" </div>\n"
|
|
" <div class=\"progress main\">\n"
|
|
" <div class=\"cineraContent\"><span class=\"timecode\">%s</span>",
|
|
Anno->time);
|
|
|
|
CopyBuffer(&Annotation, &Text);
|
|
|
|
if(LocalTopics.Count > 0)
|
|
{
|
|
CopyBuffer(&Annotation, &CategoryIcons);
|
|
}
|
|
|
|
CopyStringToBuffer(&Annotation, "</div>\n"
|
|
" </div>\n"
|
|
" </div>\n");
|
|
|
|
CopyBuffer(&CollationBuffers->Player, &Annotation);
|
|
|
|
// NOTE(matt): Tree structure of "annotation local" buffer dependencies
|
|
// CategoryIcons
|
|
// Text
|
|
// AnnotationData
|
|
// AnnotationClass
|
|
// AnnotationHeader
|
|
// Annotation
|
|
|
|
DeclaimBuffer(&CategoryIcons);
|
|
DeclaimBuffer(&Text);
|
|
DeclaimBuffer(&AnnotationData);
|
|
DeclaimBuffer(&AnnotationClass);
|
|
DeclaimBuffer(&AnnotationHeader);
|
|
DeclaimBuffer(&Annotation);
|
|
}
|
|
|
|
if(Config.Edition != EDITION_SINGLE && !PrivateVideo)
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->SearchEntry, "---\n");
|
|
N->This.Size = CollationBuffers->SearchEntry.Ptr - CollationBuffers->SearchEntry.Location;
|
|
}
|
|
|
|
#if DEBUG
|
|
printf("\n\n --- End of Annotations Loop ---\n\n\n\n");
|
|
#endif
|
|
if(HasQuoteMenu)
|
|
{
|
|
CopyStringToBuffer(&QuoteMenu,
|
|
" </div>\n"
|
|
" </div>\n");
|
|
CopyBuffer(&CollationBuffers->Menus, &QuoteMenu);
|
|
}
|
|
|
|
if(HasReferenceMenu)
|
|
{
|
|
for(int i = 0; i < UniqueRefs; ++i)
|
|
{
|
|
CopyStringToBuffer(&ReferenceMenu,
|
|
" <a data-id=\"%s\" href=\"%s\" target=\"_blank\" class=\"ref\">\n"
|
|
" <span class=\"ref_content\">\n",
|
|
ReferencesArray[i].ID,
|
|
ReferencesArray[i].URL);
|
|
|
|
if(*ReferencesArray[i].Source)
|
|
{
|
|
CopyStringToBuffer(&ReferenceMenu,
|
|
" <div class=\"source\">");
|
|
CopyStringToBufferHTMLSafeBreakingOnSlash(&ReferenceMenu, ReferencesArray[i].Source);
|
|
CopyStringToBuffer(&ReferenceMenu, "</div>\n"
|
|
" <div class=\"ref_title\">");
|
|
CopyStringToBufferHTMLSafeBreakingOnSlash(&ReferenceMenu, ReferencesArray[i].RefTitle);
|
|
CopyStringToBuffer(&ReferenceMenu, "</div>\n");
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&ReferenceMenu,
|
|
" <div class=\"ref_title\">");
|
|
CopyStringToBufferHTMLSafeBreakingOnSlash(&ReferenceMenu, ReferencesArray[i].RefTitle);
|
|
CopyStringToBuffer(&ReferenceMenu, "</div>\n");
|
|
}
|
|
CopyStringToBuffer(&ReferenceMenu,
|
|
" </span>\n");
|
|
|
|
for(int j = 0; j < ReferencesArray[i].IdentifierCount;)
|
|
{
|
|
CopyStringToBuffer(&ReferenceMenu,
|
|
" <div class=\"ref_indices\">\n ");
|
|
for(int k = 0; k < 3 && j < ReferencesArray[i].IdentifierCount; ++k, ++j)
|
|
{
|
|
CopyStringToBuffer(&ReferenceMenu,
|
|
"<span data-timestamp=\"%d\" class=\"timecode\"><span class=\"ref_index\">[%d]</span><span class=\"time\">%s</span></span>",
|
|
TimecodeToSeconds(ReferencesArray[i].Identifier[j].Timecode),
|
|
ReferencesArray[i].Identifier[j].Identifier,
|
|
ReferencesArray[i].Identifier[j].Timecode);
|
|
}
|
|
CopyStringToBuffer(&ReferenceMenu, "\n"
|
|
" </div>\n");
|
|
}
|
|
|
|
CopyStringToBuffer(&ReferenceMenu,
|
|
" </a>\n");
|
|
}
|
|
|
|
CopyStringToBuffer(&ReferenceMenu,
|
|
" </div>\n"
|
|
" </div>\n");
|
|
CopyBuffer(&CollationBuffers->Menus, &ReferenceMenu);
|
|
}
|
|
|
|
if(HasFilterMenu)
|
|
{
|
|
buffer URL;
|
|
ConstructResolvedAssetURL(&URL, ASSET_IMG_FILTER, PAGE_PLAYER);
|
|
CopyStringToBuffer(&FilterMenu,
|
|
" <div class=\"menu filter\">\n"
|
|
" <span><img src=\"%s",
|
|
URL.Location);
|
|
DeclaimBuffer(&URL);
|
|
|
|
PushAssetLandmark(&FilterMenu, ASSET_IMG_FILTER, PAGE_PLAYER);
|
|
|
|
CopyStringToBuffer(&FilterMenu,
|
|
"\"></span>\n"
|
|
" <div class=\"filter_container\">\n"
|
|
" <div class=\"filter_mode exclusive\">Filter mode: </div>\n"
|
|
" <div class=\"filters\">\n");
|
|
|
|
if(Topics.Count > 0)
|
|
{
|
|
CopyStringToBuffer(&FilterMenu,
|
|
" <div class=\"filter_topics\">\n"
|
|
" <div class=\"filter_title\">Topics</div>\n");
|
|
for(int i = 0; i < Topics.Count; ++i)
|
|
{
|
|
char SanitisedMarker[StringLength(Topics.Category[i].Marker) + 1];
|
|
CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%s", Topics.Category[i].Marker);
|
|
SanitisePunctuation(SanitisedMarker);
|
|
|
|
bool NullTopic = !StringsDiffer(Topics.Category[i].Marker, "nullTopic");
|
|
CopyStringToBuffer(&FilterTopics,
|
|
" <div %sclass=\"filter_content %s\">\n"
|
|
" <span class=\"icon category %s\"></span><span class=\"cineraText\">%s</span>\n"
|
|
" </div>\n",
|
|
|
|
NullTopic ? "title=\"Annotations that don't fit into the above topic(s) may be filtered using this pseudo-topic\" " : "",
|
|
SanitisedMarker,
|
|
SanitisedMarker,
|
|
NullTopic ? "(null topic)" : Topics.Category[i].Marker);
|
|
}
|
|
CopyStringToBuffer(&FilterTopics,
|
|
" </div>\n");
|
|
CopyBuffer(&FilterMenu, &FilterTopics);
|
|
}
|
|
|
|
if(Media.Count > 0)
|
|
{
|
|
CopyStringToBuffer(&FilterMedia,
|
|
" <div class=\"filter_media\">\n"
|
|
" <div class=\"filter_title\">Media</div>\n");
|
|
for(int i = 0; i < Media.Count; ++i)
|
|
{
|
|
char SanitisedMarker[StringLength(Media.Category[i].Marker) + 1];
|
|
CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%s", Media.Category[i].Marker);
|
|
SanitisePunctuation(SanitisedMarker);
|
|
|
|
int j;
|
|
for(j = 0; j < ArrayCount(CategoryMedium); ++j)
|
|
{
|
|
if(!StringsDiffer(Media.Category[i].Marker, CategoryMedium[j].Medium))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(!StringsDiffer(Media.Category[i].Marker, "afk")) // TODO(matt): Initially hidden config
|
|
// When we do this for real, we'll probably need to loop
|
|
// over the configured media to see who should be hidden
|
|
{
|
|
CopyStringToBuffer(&FilterMedia,
|
|
" <div class=\"filter_content %s off\">\n"
|
|
" <span class=\"icon\">%s</span><span class=\"cineraText\">%s</span>\n"
|
|
" </div>\n",
|
|
SanitisedMarker,
|
|
CategoryMedium[j].Icon,
|
|
CategoryMedium[j].WrittenName);
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&FilterMedia,
|
|
" <div class=\"filter_content %s\">\n"
|
|
" <span class=\"icon\">%s</span><span class=\"cineraText\">%s%s</span>\n"
|
|
" </div>\n",
|
|
SanitisedMarker,
|
|
CategoryMedium[j].Icon,
|
|
CategoryMedium[j].WrittenName,
|
|
!StringsDiffer(Media.Category[i].Marker, DefaultMedium) ? "</span><span class=\"cineraDefaultMediumIndicator\" title=\"Default medium\n"
|
|
"Annotations lacking a media icon are in this medium\">🟉" : "");
|
|
}
|
|
}
|
|
CopyStringToBuffer(&FilterMedia,
|
|
" </div>\n");
|
|
CopyBuffer(&FilterMenu, &FilterMedia);
|
|
}
|
|
|
|
CopyStringToBuffer(&FilterMenu,
|
|
" </div>\n"
|
|
" </div>\n"
|
|
" </div>\n");
|
|
|
|
OffsetLandmarks(&CollationBuffers->Menus, ASSET_IMG_FILTER, PAGE_PLAYER);
|
|
CopyBuffer(&CollationBuffers->Menus, &FilterMenu);
|
|
|
|
}
|
|
|
|
CopyStringToBuffer(&CollationBuffers->Menus,
|
|
" <div class=\"menu views\">\n"
|
|
" <div class=\"view\" data-id=\"theatre\" title=\"Theatre mode\">🎭</div>\n"
|
|
" <div class=\"views_container\">\n"
|
|
" <div class=\"view\" data-id=\"super\" title=\"SUPERtheatre mode\">🏟</div>\n"
|
|
" </div>\n"
|
|
" </div>\n"
|
|
" <div class=\"menu link\">\n"
|
|
" <span>🔗</span>\n"
|
|
" <div class=\"link_container\">\n"
|
|
" <div id=\"cineraLinkMode\">Link to current annotation</div>\n"
|
|
" <textarea title=\"Click to copy to clipboard\" id=\"cineraLink\" readonly spellcheck=\"false\"></textarea>\n"
|
|
" </div>\n"
|
|
" </div>\n");
|
|
|
|
if(HasCreditsMenu)
|
|
{
|
|
OffsetLandmarksCredits(&CollationBuffers->Menus);
|
|
CopyBuffer(&CollationBuffers->Menus, &CreditsMenu);
|
|
}
|
|
|
|
CopyStringToBuffer(&CollationBuffers->Menus,
|
|
" <div class=\"help\">\n"
|
|
" <span>?</span>\n"
|
|
" <div class=\"help_container\">\n"
|
|
" <span class=\"help_key\">?</span><h1>Keyboard Navigation</h1>\n"
|
|
"\n"
|
|
" <h2>Global Keys</h2>\n"
|
|
" <span class=\"help_key\">[</span>, <span class=\"help_key\"><</span> / <span class=\"help_key\">]</span>, <span class=\"help_key\">></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"
|
|
" <span class=\"help_key%s\">V</span> <span class=\"help_text%s\">Revert filter to original state</span> <span class=\"help_key\">Y</span> <span class=\"help_text\">Select link (requires manual Ctrl-c)</span>\n",
|
|
|
|
HasFilterMenu ? "" : " unavailable", HasFilterMenu ? "" : " unavailable");
|
|
|
|
CopyStringToBuffer(&CollationBuffers->Menus,
|
|
"\n"
|
|
" <h2>Menu toggling</h2>\n"
|
|
" <span class=\"help_key%s\">q</span> <span class=\"help_text%s\">Quotes</span>\n"
|
|
" <span class=\"help_key%s\">r</span> <span class=\"help_text%s\">References</span>\n"
|
|
" <span class=\"help_key%s\">f</span> <span class=\"help_text%s\">Filter</span>\n"
|
|
" <span class=\"help_key\">y</span> <span class=\"help_text\">Link</span>\n"
|
|
" <span class=\"help_key%s\">c</span> <span class=\"help_text%s\">Credits</span>\n",
|
|
|
|
HasQuoteMenu ? "" : " unavailable", HasQuoteMenu ? "" : " unavailable",
|
|
HasReferenceMenu ? "" : " unavailable", HasReferenceMenu ? "" : " unavailable",
|
|
HasFilterMenu ? "" : " unavailable", HasFilterMenu ? "" : " unavailable",
|
|
HasCreditsMenu ? "" : " unavailable", HasCreditsMenu ? "" : " unavailable");
|
|
|
|
CopyStringToBuffer(&CollationBuffers->Menus,
|
|
"\n"
|
|
" <h2>In-Menu Movement</h2>\n"
|
|
" <div class=\"help_paragraph\">\n"
|
|
" <div class=\"key_block\">\n"
|
|
" <div>\n"
|
|
" <span class=\"help_key\">a</span>\n"
|
|
" </div>\n"
|
|
" <div>\n"
|
|
" <span class=\"help_key\">w</span><br>\n"
|
|
" <span class=\"help_key\">s</span>\n"
|
|
" </div>\n"
|
|
" <div>\n"
|
|
" <span class=\"help_key\">d</span>\n"
|
|
" </div>\n"
|
|
" </div>\n"
|
|
" <div class=\"key_block\">\n"
|
|
" <span class=\"help_key\">h</span>\n"
|
|
" <span class=\"help_key\">j</span>\n"
|
|
" <span class=\"help_key\">k</span>\n"
|
|
" <span class=\"help_key\">l</span>\n"
|
|
" </div>\n"
|
|
" <div class=\"key_block\">\n"
|
|
" <div>\n"
|
|
" <span class=\"help_key\">←</span>\n"
|
|
" </div>\n"
|
|
" <div>\n"
|
|
" <span class=\"help_key\">↑</span><br>\n"
|
|
" <span class=\"help_key\">↓</span>\n"
|
|
" </div>\n"
|
|
" <div>\n"
|
|
" <span class=\"help_key\">→</span>\n"
|
|
" </div>\n"
|
|
" </div>\n"
|
|
" </div>\n"
|
|
" <br>\n");
|
|
|
|
CopyStringToBuffer(&CollationBuffers->Menus,
|
|
" <h2>%sQuotes %sand%s References%s Menus%s</h2>\n"
|
|
" <span class=\"help_key word%s\">Enter</span> <span class=\"help_text%s\">Jump to timecode</span><br>\n",
|
|
// Q R
|
|
//
|
|
// 0 0 <h2><span off>Quotes and References Menus</span></h2>
|
|
// 0 1 <h2><span off>Quotes and</span> References Menus</h2>
|
|
// 1 0 <h2>Quotes <span off>and References</span> Menus</h2>
|
|
// 1 1 <h2>Quotes and References Menus</h2>
|
|
|
|
HasQuoteMenu ? "" : "<span class=\"unavailable\">",
|
|
HasQuoteMenu && !HasReferenceMenu ? "<span class=\"unavailable\">" : "",
|
|
!HasQuoteMenu && HasReferenceMenu ? "</span>" : "",
|
|
HasQuoteMenu && !HasReferenceMenu ? "</span>" : "",
|
|
!HasQuoteMenu && !HasReferenceMenu ? "</span>" : "",
|
|
HasQuoteMenu || HasReferenceMenu ? "" : " unavailable", HasQuoteMenu || HasReferenceMenu ? "" : " unavailable");
|
|
|
|
CopyStringToBuffer(&CollationBuffers->Menus,
|
|
"\n"
|
|
" <h2>%sQuotes%s,%s References %sand%s Credits%s Menus%s</h2>"
|
|
" <span class=\"help_key%s\">o</span> <span class=\"help_text%s\">Open URL (in new tab)</span>\n",
|
|
// Q R C
|
|
//
|
|
// 0 0 0 <h2><span off>Quotes, References and Credits Menus</span></h2>
|
|
// 0 0 1 <h2><span off>Quotes, References and</span> Credits Menus</h2>
|
|
// 0 1 0 <h2><span off>Quotes,</span> References <span off>and Credits</span> Menus</h2>
|
|
// 0 1 1 <h2><span off>Quotes,</span> References and Credits Menus</h2>
|
|
// 1 0 0 <h2>Quotes<span off>, References and Credits</span> Menus</h2>
|
|
// 1 0 1 <h2>Quotes<span off>, References </span>and Credits Menus</h2>
|
|
// 1 1 0 <h2>Quotes, References <span off>and Credits</span> Menus</h2>
|
|
// 1 1 1 <h2>Quotes, References and Credits Menus</h2>
|
|
|
|
/* 0 */ HasQuoteMenu ? "" : "<span class=\"unavailable\">",
|
|
/* 1 */ HasQuoteMenu && !HasReferenceMenu ? "<span class=\"unavailable\">" : "",
|
|
/* 2 */ !HasQuoteMenu && HasReferenceMenu ? "</span>" : "",
|
|
/* 3 */ HasReferenceMenu && !HasCreditsMenu ? "<span class=\"unavailable\">" : HasQuoteMenu && !HasReferenceMenu && HasCreditsMenu ? "</span>" : "",
|
|
/* 4 */ !HasQuoteMenu && !HasReferenceMenu && HasCreditsMenu ? "</span>" : "",
|
|
/* 5 */ !HasCreditsMenu && (HasQuoteMenu || HasReferenceMenu) ? "</span>" : "",
|
|
/* 6 */ !HasQuoteMenu && !HasReferenceMenu && !HasCreditsMenu ? "</span>" : "",
|
|
|
|
HasQuoteMenu || HasReferenceMenu || HasCreditsMenu ? "" : " unavailable",
|
|
HasQuoteMenu || HasReferenceMenu || HasCreditsMenu ? "" : " unavailable");
|
|
|
|
CopyStringToBuffer(&CollationBuffers->Menus,
|
|
"\n"
|
|
" <h2>%sFilter Menu%s</h2>\n"
|
|
" <span class=\"help_key%s\">x</span>, <span class=\"help_key word%s\">Space</span> <span class=\"help_text%s\">Toggle category and focus next</span><br>\n"
|
|
" <span class=\"help_key%s\">X</span>, <span class=\"help_key word modifier%s\">Shift</span><span class=\"help_key word%s\">Space</span> <span class=\"help_text%s\">Toggle category and focus previous</span><br>\n"
|
|
" <span class=\"help_key%s\">v</span> <span class=\"help_text%s\">Invert topics / media as per focus</span>\n"
|
|
"\n"
|
|
" <h2>%sFilter and%s Link Menus</h2>\n"
|
|
" <span class=\"help_key\">z</span> <span class=\"help_text\">Toggle %sfilter /%s linking mode</span>\n",
|
|
|
|
HasFilterMenu ? "" : "<span class=\"unavailable\">", HasFilterMenu ? "" : "</span>",
|
|
HasFilterMenu ? "" : " unavailable", HasFilterMenu ? "" : " unavailable", HasFilterMenu ? "" : " unavailable",
|
|
HasFilterMenu ? "" : " unavailable", HasFilterMenu ? "" : " unavailable", HasFilterMenu ? "" : " unavailable", HasFilterMenu ? "" : " unavailable",
|
|
HasFilterMenu ? "" : " unavailable", HasFilterMenu ? "" : " unavailable",
|
|
HasFilterMenu ? "" : "<span class=\"unavailable\">", HasFilterMenu ? "" : "</span>",
|
|
HasFilterMenu ? "" : "<span class=\"help_text unavailable\">", HasFilterMenu ? "" : "</span>");
|
|
|
|
CopyStringToBuffer(&CollationBuffers->Menus,
|
|
"\n"
|
|
" <h2>%sCredits Menu%s</h2>\n"
|
|
" <span class=\"help_key word%s\">Enter</span> <span class=\"help_text%s\">Open URL (in new tab)</span><br>\n",
|
|
|
|
HasCreditsMenu ? "" : "<span class=\"unavailable\">", HasCreditsMenu ? "" : "</span>",
|
|
HasCreditsMenu ? "" : " unavailable", HasCreditsMenu ? "" : " unavailable");
|
|
|
|
CopyStringToBuffer(&CollationBuffers->Menus,
|
|
" </div>\n"
|
|
" </div>\n"
|
|
|
|
" </div>");
|
|
|
|
CopyStringToBuffer(&CollationBuffers->Player,
|
|
" </div>\n");
|
|
|
|
if(N)
|
|
{
|
|
N->This.LinkOffsets.NextStart = (CollationBuffers->Player.Ptr - CollationBuffers->Player.Location - (N->This.LinkOffsets.PrevStart + N->This.LinkOffsets.PrevEnd));
|
|
if(N->Prev.Size || N->Next.Size)
|
|
{
|
|
if(N->Next.Size > 0)
|
|
{
|
|
// TODO(matt): Once we have a more rigorous notion of "Day Numbers", perhaps also use them here
|
|
buffer NextPlayerURL;
|
|
ClaimBuffer(&NextPlayerURL, "NextPlayerURL", MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH);
|
|
ConstructPlayerURL(&NextPlayerURL, N->Next.BaseFilename);
|
|
CopyStringToBuffer(&CollationBuffers->Player,
|
|
" <a class=\"episodeMarker next\" href=\"%s\"><div>⏬</div><div>Next: '%s'</div><div>⏬</div></a>\n",
|
|
NextPlayerURL.Location,
|
|
N->Next.Title);
|
|
|
|
DeclaimBuffer(&NextPlayerURL);
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Player,
|
|
" <div class=\"episodeMarker last\"><div>•</div><div>You have arrived at the (current) end of <cite>%s</cite></div><div>•</div></div>\n", CollationBuffers->ProjectName);
|
|
}
|
|
}
|
|
N->This.LinkOffsets.NextEnd = (CollationBuffers->Player.Ptr - CollationBuffers->Player.Location - (N->This.LinkOffsets.PrevStart + N->This.LinkOffsets.PrevEnd + N->This.LinkOffsets.NextStart));
|
|
}
|
|
|
|
CopyStringToBuffer(&CollationBuffers->Player,
|
|
" </div>\n"
|
|
" </div>");
|
|
|
|
buffer URLSearch;
|
|
ClaimBuffer(&URLSearch, "URLSearch", MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1);
|
|
ConstructSearchURL(&URLSearch);
|
|
CopyString(CollationBuffers->URLSearch, sizeof(CollationBuffers->URLSearch), "%s", URLSearch.Location);
|
|
DeclaimBuffer(&URLSearch);
|
|
|
|
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
|
|
"<meta charset=\"UTF-8\">\n"
|
|
" <meta name=\"generator\" content=\"Cinera %d.%d.%d\">\n",
|
|
CINERA_APP_VERSION.Major,
|
|
CINERA_APP_VERSION.Minor,
|
|
CINERA_APP_VERSION.Patch);
|
|
|
|
if(Topics.Count || Media.Count)
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
|
|
" <meta name=\"keywords\" content=\"");
|
|
|
|
if(Topics.Count > 0)
|
|
{
|
|
for(int i = 0; i < Topics.Count; ++i)
|
|
{
|
|
if(StringsDiffer(Topics.Category[i].Marker, "nullTopic"))
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->IncludesPlayer, "%s, ", Topics.Category[i].Marker);
|
|
}
|
|
}
|
|
}
|
|
|
|
if(Media.Count > 0)
|
|
{
|
|
for(int i = 0; i < Media.Count; ++i)
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->IncludesPlayer, "%s, ", Media.Category[i].WrittenText);
|
|
}
|
|
}
|
|
|
|
CollationBuffers->IncludesPlayer.Ptr -= 2;
|
|
CopyStringToBuffer(&CollationBuffers->IncludesPlayer, "\">\n");
|
|
}
|
|
|
|
buffer URL;
|
|
|
|
ConstructResolvedAssetURL(&URL, ASSET_CSS_CINERA, PAGE_PLAYER);
|
|
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
|
|
"\n"
|
|
" <link rel=\"stylesheet\" type=\"text/css\" href=\"%s",
|
|
URL.Location);
|
|
DeclaimBuffer(&URL);
|
|
PushAssetLandmark(&CollationBuffers->IncludesPlayer, ASSET_CSS_CINERA, PAGE_PLAYER);
|
|
|
|
ConstructResolvedAssetURL(&URL, ASSET_CSS_THEME, PAGE_PLAYER);
|
|
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
|
|
"\">\n"
|
|
" <link rel=\"stylesheet\" type=\"text/css\" href=\"%s",
|
|
URL.Location);
|
|
DeclaimBuffer(&URL);
|
|
PushAssetLandmark(&CollationBuffers->IncludesPlayer, ASSET_CSS_THEME, PAGE_PLAYER);
|
|
|
|
ConstructResolvedAssetURL(&URL, ASSET_CSS_TOPICS, PAGE_PLAYER);
|
|
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
|
|
"\">\n"
|
|
" <link rel=\"stylesheet\" type=\"text/css\" href=\"%s",
|
|
URL.Location);
|
|
DeclaimBuffer(&URL);
|
|
PushAssetLandmark(&CollationBuffers->IncludesPlayer, ASSET_CSS_TOPICS, PAGE_PLAYER);
|
|
|
|
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
|
|
"\">\n");
|
|
|
|
ConstructResolvedAssetURL(&URL, ASSET_JS_PLAYER_PRE, PAGE_PLAYER);
|
|
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
|
|
" <script type=\"text/javascript\" src=\"%s",
|
|
URL.Location);
|
|
DeclaimBuffer(&URL);
|
|
PushAssetLandmark(&CollationBuffers->IncludesPlayer, ASSET_JS_PLAYER_PRE, PAGE_PLAYER);
|
|
|
|
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
|
|
"\"></script>");
|
|
|
|
ConstructResolvedAssetURL(&URL, ASSET_JS_PLAYER_POST, PAGE_PLAYER);
|
|
CopyStringToBuffer(&CollationBuffers->ScriptPlayer,
|
|
"<script type=\"text/javascript\" src=\"%s",
|
|
URL.Location);
|
|
DeclaimBuffer(&URL);
|
|
PushAssetLandmark(&CollationBuffers->ScriptPlayer, ASSET_JS_PLAYER_POST, PAGE_PLAYER);
|
|
|
|
CopyStringToBuffer(&CollationBuffers->ScriptPlayer,
|
|
"\"></script>");
|
|
|
|
// NOTE(matt): Tree structure of "global" buffer dependencies
|
|
// CreditsMenu
|
|
// FilterMedia
|
|
// FilterTopics
|
|
// FilterMenu
|
|
// ReferenceMenu
|
|
// QuoteMenu
|
|
|
|
DeclaimBuffer(&CreditsMenu);
|
|
DeclaimBuffer(&FilterMedia);
|
|
DeclaimBuffer(&FilterTopics);
|
|
DeclaimBuffer(&FilterMenu);
|
|
DeclaimBuffer(&ReferenceMenu);
|
|
DeclaimBuffer(&QuoteMenu);
|
|
}
|
|
else
|
|
{
|
|
LogError(LOG_ERROR, "%s:%d: %s", Filename, HMML.error.line, HMML.error.message);
|
|
fprintf(stderr, "%sSkipping%s %s:%d: %s\n", ColourStrings[CS_ERROR], ColourStrings[CS_END], Filename, HMML.error.line, HMML.error.message);
|
|
hmml_free(&HMML);
|
|
return RC_ERROR_HMML;
|
|
}
|
|
hmml_free(&HMML);
|
|
return RC_SUCCESS;
|
|
}
|
|
|
|
int
|
|
BuffersToHTML(buffers *CollationBuffers, template *Template, char *OutputPath, int PageType, unsigned int *PlayerOffset)
|
|
{
|
|
#if DEBUG
|
|
printf("\n\n --- Buffer Collation ---\n"
|
|
" %s\n\n\n", OutputPath ? OutputPath : Config.OutLocation);
|
|
#endif
|
|
|
|
#if DEBUG_MEM
|
|
FILE *MemLog = fopen("/home/matt/cinera_mem", "a+");
|
|
fprintf(MemLog, "\nEntered BuffersToHTML(%s)\n", OutputPath ? OutputPath : Config.OutLocation);
|
|
fclose(MemLog);
|
|
#endif
|
|
|
|
if(Template->File.Buffer.Location)
|
|
{
|
|
if((Template->Metadata.Validity & PageType) || Config.Mode & MODE_FORCEINTEGRATION)
|
|
{
|
|
buffer Output;
|
|
Output.Size = Template->File.Buffer.Size + (Kilobytes(512));
|
|
Output.ID = "Output";
|
|
if(!(Output.Location = malloc(Output.Size)))
|
|
{
|
|
LogError(LOG_ERROR, "BuffersToHTML(): %s",
|
|
strerror(errno));
|
|
return RC_ERROR_MEMORY;
|
|
}
|
|
|
|
#if DEBUG_MEM
|
|
MemLog = fopen("/home/matt/cinera_mem", "a+");
|
|
fprintf(MemLog, " Allocated Output (%d)\n", Output.Size);
|
|
fclose(MemLog);
|
|
printf(" Allocated Output (%d)\n", Output.Size);
|
|
#endif
|
|
|
|
Output.Ptr = Output.Location;
|
|
|
|
bool NeedPlayerOffset = PlayerOffset ? TRUE : FALSE;
|
|
Template->File.Buffer.Ptr = Template->File.Buffer.Location;
|
|
for(int i = 0; i < Template->Metadata.TagCount; ++i)
|
|
{
|
|
int j = 0;
|
|
while(Template->Metadata.Tags[i].Offset > j)
|
|
{
|
|
*Output.Ptr++ = *Template->File.Buffer.Ptr++;
|
|
++j;
|
|
}
|
|
|
|
// TODO(matt): Make this whole template stuff context-aware, so it can determine whether or not to HTML-encode
|
|
// or sanitise punctuation for CSS-safety
|
|
switch(Template->Metadata.Tags[i].TagCode)
|
|
{
|
|
case TAG_PROJECT_ID:
|
|
if(CollationBuffers->ProjectID[0] == '\0')
|
|
{
|
|
fprintf(stderr, "Template contains a <!-- __CINERA_PROJECT_ID__ --> tag\n"
|
|
"Skipping just this tag, because no project ID is set\n");
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBufferNoFormat(&Output, CollationBuffers->ProjectID); // NOTE(matt): Not HTML-safe
|
|
}
|
|
break;
|
|
case TAG_PROJECT:
|
|
if(CollationBuffers->ProjectName[0] == '\0')
|
|
{
|
|
fprintf(stderr, "Template contains a <!-- __CINERA_PROJECT__ --> tag\n"
|
|
"Skipping just this tag, because we do not know the project's full name\n");
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBufferHTMLSafe(&Output, CollationBuffers->ProjectName);
|
|
}
|
|
break;
|
|
case TAG_SEARCH_URL: CopyStringToBufferNoFormat(&Output, CollationBuffers->URLSearch); break; // NOTE(matt): Not HTML-safe
|
|
case TAG_THEME: CopyStringToBufferNoFormat(&Output, CollationBuffers->Theme); break; // NOTE(matt): Not HTML-safe
|
|
case TAG_TITLE: CopyStringToBufferHTMLSafe(&Output, CollationBuffers->Title); break;
|
|
case TAG_URL:
|
|
if(PageType == PAGE_PLAYER)
|
|
{
|
|
CopyStringToBufferNoFormat(&Output, CollationBuffers->URLPlayer);
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBufferNoFormat(&Output, CollationBuffers->URLSearch);
|
|
}
|
|
break;
|
|
case TAG_VIDEO_ID: CopyStringToBufferNoFormat(&Output, CollationBuffers->VideoID); break;
|
|
case TAG_VOD_PLATFORM: CopyStringToBufferNoFormat(&Output, CollationBuffers->VODPlatform); break;
|
|
case TAG_SEARCH:
|
|
if(Config.Edition == EDITION_SINGLE)
|
|
{
|
|
fprintf(stderr, "Template contains a <!-- __CINERA_SEARCH__ --> tag\n"
|
|
"Skipping just this tag, because a search cannot be generated for inclusion in a\n"
|
|
"bespoke template in Single Edition\n");
|
|
}
|
|
else
|
|
{
|
|
OffsetLandmarks(&Output, ASSET_JS_SEARCH, PAGE_SEARCH);
|
|
CopyBuffer(&Output, &CollationBuffers->Search);
|
|
}
|
|
break;
|
|
case TAG_INCLUDES:
|
|
if(PageType == PAGE_PLAYER)
|
|
{
|
|
OffsetLandmarksIncludes(&Output, PageType);
|
|
CopyBuffer(&Output, &CollationBuffers->IncludesPlayer);
|
|
}
|
|
else
|
|
{
|
|
OffsetLandmarksIncludes(&Output, PageType);
|
|
CopyBuffer(&Output, &CollationBuffers->IncludesSearch);
|
|
}
|
|
break;
|
|
case TAG_MENUS:
|
|
OffsetLandmarksMenus(&Output);
|
|
CopyBuffer(&Output, &CollationBuffers->Menus);
|
|
break;
|
|
case TAG_PLAYER:
|
|
if(NeedPlayerOffset) { *PlayerOffset += (Output.Ptr - Output.Location); NeedPlayerOffset = !NeedPlayerOffset; }
|
|
CopyBuffer(&Output, &CollationBuffers->Player);
|
|
break;
|
|
case TAG_SCRIPT:
|
|
OffsetLandmarks(&Output, ASSET_JS_PLAYER_POST, PAGE_PLAYER);
|
|
CopyBuffer(&Output, &CollationBuffers->ScriptPlayer);
|
|
break;
|
|
case TAG_ASSET:
|
|
{
|
|
buffer URL;
|
|
ConstructResolvedAssetURL(&URL, Template->Metadata.Tags[i].AssetIndex, PageType);
|
|
CopyStringToBuffer(&Output, "%s", URL.Location);
|
|
DeclaimBuffer(&URL);
|
|
PushAssetLandmark(&Output, Template->Metadata.Tags[i].AssetIndex, PageType);
|
|
} break;
|
|
case TAG_CSS:
|
|
{
|
|
buffer URL;
|
|
ConstructResolvedAssetURL(&URL, Template->Metadata.Tags[i].AssetIndex, PageType);
|
|
CopyStringToBuffer(&Output,
|
|
"<link rel=\"stylesheet\" type=\"text/css\" href=\"%s",
|
|
URL.Location);
|
|
DeclaimBuffer(&URL);
|
|
PushAssetLandmark(&Output, Template->Metadata.Tags[i].AssetIndex, PageType);
|
|
CopyStringToBuffer(&Output, "\">");
|
|
} break;
|
|
case TAG_IMAGE:
|
|
{
|
|
buffer URL;
|
|
ConstructResolvedAssetURL(&URL, Template->Metadata.Tags[i].AssetIndex, PageType);
|
|
CopyStringToBuffer(&Output, "%s", URL.Location);
|
|
DeclaimBuffer(&URL);
|
|
PushAssetLandmark(&Output, Template->Metadata.Tags[i].AssetIndex, PageType);
|
|
} break;
|
|
case TAG_JS:
|
|
{
|
|
buffer URL;
|
|
ConstructResolvedAssetURL(&URL, Template->Metadata.Tags[i].AssetIndex, PageType);
|
|
CopyStringToBuffer(&Output,
|
|
"<script type=\"text/javascript\" src=\"%s",
|
|
URL.Location);
|
|
DeclaimBuffer(&URL);
|
|
PushAssetLandmark(&Output, Template->Metadata.Tags[i].AssetIndex, PageType);
|
|
CopyStringToBuffer(&Output, "\"></script>");
|
|
} break;
|
|
case TAG_CUSTOM0: CopyStringToBufferNoFormat(&Output, CollationBuffers->Custom0); break;
|
|
case TAG_CUSTOM1: CopyStringToBufferNoFormat(&Output, CollationBuffers->Custom1); break;
|
|
case TAG_CUSTOM2: CopyStringToBufferNoFormat(&Output, CollationBuffers->Custom2); break;
|
|
case TAG_CUSTOM3: CopyStringToBufferNoFormat(&Output, CollationBuffers->Custom3); break;
|
|
case TAG_CUSTOM4: CopyStringToBufferNoFormat(&Output, CollationBuffers->Custom4); break;
|
|
case TAG_CUSTOM5: CopyStringToBufferNoFormat(&Output, CollationBuffers->Custom5); break;
|
|
case TAG_CUSTOM6: CopyStringToBufferNoFormat(&Output, CollationBuffers->Custom6); break;
|
|
case TAG_CUSTOM7: CopyStringToBufferNoFormat(&Output, CollationBuffers->Custom7); break;
|
|
case TAG_CUSTOM8: CopyStringToBufferNoFormat(&Output, CollationBuffers->Custom8); break;
|
|
case TAG_CUSTOM9: CopyStringToBufferNoFormat(&Output, CollationBuffers->Custom9); break;
|
|
case TAG_CUSTOM10: CopyStringToBufferNoFormat(&Output, CollationBuffers->Custom10); break;
|
|
case TAG_CUSTOM11: CopyStringToBufferNoFormat(&Output, CollationBuffers->Custom11); break;
|
|
case TAG_CUSTOM12: CopyStringToBufferNoFormat(&Output, CollationBuffers->Custom12); break;
|
|
case TAG_CUSTOM13: CopyStringToBufferNoFormat(&Output, CollationBuffers->Custom13); break;
|
|
case TAG_CUSTOM14: CopyStringToBufferNoFormat(&Output, CollationBuffers->Custom14); break;
|
|
case TAG_CUSTOM15: CopyStringToBufferNoFormat(&Output, CollationBuffers->Custom15); break;
|
|
}
|
|
|
|
DepartComment(&Template->File.Buffer);
|
|
}
|
|
while(Template->File.Buffer.Ptr - Template->File.Buffer.Location < Template->File.Buffer.Size)
|
|
{
|
|
*Output.Ptr++ = *Template->File.Buffer.Ptr++;
|
|
}
|
|
|
|
FILE *OutFile;
|
|
if(!(OutFile = fopen(Config.Edition == EDITION_PROJECT ? OutputPath : Config.OutIntegratedLocation, "w")))
|
|
{
|
|
LogError(LOG_ERROR, "Unable to open output file %s: %s", Config.Edition == EDITION_PROJECT ? OutputPath : Config.OutIntegratedLocation, strerror(errno));
|
|
FreeBuffer(&Output);
|
|
|
|
#if DEBUG_MEM
|
|
MemLog = fopen("/home/matt/cinera_mem", "a+");
|
|
fprintf(MemLog, " Freed Output\n");
|
|
fclose(MemLog);
|
|
printf(" Freed Output\n");
|
|
#endif
|
|
|
|
return RC_ERROR_FILE;
|
|
}
|
|
fwrite(Output.Location, Output.Ptr - Output.Location, 1, OutFile);
|
|
fclose(OutFile);
|
|
|
|
FreeBuffer(&Output);
|
|
|
|
#if DEBUG_MEM
|
|
MemLog = fopen("/home/matt/cinera_mem", "a+");
|
|
fprintf(MemLog, " Freed Output\n");
|
|
fclose(MemLog);
|
|
printf(" Freed Output\n");
|
|
#endif
|
|
|
|
return RC_SUCCESS;
|
|
}
|
|
else
|
|
{
|
|
return RC_INVALID_TEMPLATE;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
buffer Master;
|
|
if(ClaimBuffer(&Master, "Master", Kilobytes(512)) == RC_ARENA_FULL) { return RC_ARENA_FULL; };
|
|
|
|
CopyStringToBuffer(&Master,
|
|
"<html>\n"
|
|
" <head>\n"
|
|
" ");
|
|
|
|
OffsetLandmarksIncludes(&Master, PageType);
|
|
CopyBuffer(&Master, PageType == PAGE_PLAYER ? &CollationBuffers->IncludesPlayer : &CollationBuffers->IncludesSearch);
|
|
CopyStringToBuffer(&Master, "\n");
|
|
|
|
CopyStringToBuffer(&Master,
|
|
" </head>\n"
|
|
" <body>\n"
|
|
" ");
|
|
if(PageType == PAGE_PLAYER)
|
|
{
|
|
CopyStringToBuffer(&Master, "<div>\n"
|
|
" ");
|
|
|
|
OffsetLandmarksMenus(&Master);
|
|
CopyBuffer(&Master, &CollationBuffers->Menus);
|
|
CopyStringToBuffer(&Master, "\n"
|
|
" ");
|
|
|
|
if(PlayerOffset) { *PlayerOffset += Master.Ptr - Master.Location; }
|
|
|
|
CopyBuffer(&Master, &CollationBuffers->Player);
|
|
CopyStringToBuffer(&Master, "\n"
|
|
" ");
|
|
CopyStringToBuffer(&Master, "</div>\n"
|
|
" ");
|
|
|
|
OffsetLandmarks(&Master, ASSET_JS_PLAYER_POST, PAGE_PLAYER);
|
|
CopyBuffer(&Master, &CollationBuffers->ScriptPlayer);
|
|
CopyStringToBuffer(&Master, "\n");
|
|
}
|
|
else
|
|
{
|
|
OffsetLandmarks(&Master, ASSET_JS_SEARCH, PAGE_SEARCH);
|
|
CopyBuffer(&Master, &CollationBuffers->Search);
|
|
}
|
|
|
|
CopyStringToBuffer(&Master,
|
|
" </body>\n"
|
|
"</html>\n");
|
|
|
|
FILE *OutFile;
|
|
if(!(OutFile = fopen(Config.Edition == EDITION_PROJECT ? OutputPath : Config.OutLocation, "w")))
|
|
{
|
|
LogError(LOG_ERROR, "Unable to open output file %s: %s", Config.Edition == EDITION_PROJECT ? OutputPath : Config.OutLocation, strerror(errno));
|
|
DeclaimBuffer(&Master);
|
|
return RC_ERROR_FILE;
|
|
}
|
|
fwrite(Master.Location, Master.Ptr - Master.Location, 1, OutFile);
|
|
fclose(OutFile);
|
|
DeclaimBuffer(&Master);
|
|
return RC_SUCCESS;
|
|
}
|
|
}
|
|
|
|
int
|
|
BinarySearchForMetadataEntry(db_entry **Entry, char *SearchTerm)
|
|
{
|
|
DB.Metadata.Buffer.Ptr = DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader);
|
|
int Lower = 0;
|
|
db_entry *LowerEntry = (db_entry*)(DB.Metadata.Buffer.Ptr + sizeof(DB.Entry) * Lower);
|
|
if(StringsDiffer(SearchTerm, LowerEntry->BaseFilename) < 0 ) { return -1; }
|
|
|
|
int Upper = DB.EntriesHeader.Count - 1;
|
|
int Pivot = Upper - ((Upper - Lower) >> 1);
|
|
db_entry *UpperEntry;
|
|
db_entry *PivotEntry;
|
|
|
|
do {
|
|
LowerEntry = (db_entry*)(DB.Metadata.Buffer.Ptr + sizeof(DB.Entry) * Lower);
|
|
PivotEntry = (db_entry*)(DB.Metadata.Buffer.Ptr + sizeof(DB.Entry) * Pivot);
|
|
UpperEntry = (db_entry*)(DB.Metadata.Buffer.Ptr + sizeof(DB.Entry) * Upper);
|
|
|
|
if(!StringsDiffer(SearchTerm, LowerEntry->BaseFilename)) { *Entry = LowerEntry; return Lower; }
|
|
if(!StringsDiffer(SearchTerm, PivotEntry->BaseFilename)) { *Entry = PivotEntry; return Pivot; }
|
|
if(!StringsDiffer(SearchTerm, UpperEntry->BaseFilename)) { *Entry = UpperEntry; return Upper; }
|
|
|
|
if((StringsDiffer(SearchTerm, PivotEntry->BaseFilename) < 0)) { Upper = Pivot; }
|
|
else { Lower = Pivot; }
|
|
Pivot = Upper - ((Upper - Lower) >> 1);
|
|
} while(Upper > Pivot);
|
|
return Upper;
|
|
}
|
|
|
|
int
|
|
AccumulateDBEntryInsertionOffset(int EntryIndex)
|
|
{
|
|
int Result = 0;
|
|
db_entry *AccEntry = { 0 };
|
|
if(EntryIndex < DB.EntriesHeader.Count >> 1)
|
|
{
|
|
for(; EntryIndex > 0; --EntryIndex)
|
|
{
|
|
AccEntry = (db_entry*)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * (EntryIndex - 1));
|
|
Result += AccEntry->Size;
|
|
}
|
|
Result += StringLength("---\n");
|
|
}
|
|
else
|
|
{
|
|
for(; EntryIndex < DB.EntriesHeader.Count; ++EntryIndex)
|
|
{
|
|
AccEntry = (db_entry*)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * EntryIndex);
|
|
Result += AccEntry->Size;
|
|
}
|
|
Result = DB.File.FileSize - Result;
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
void
|
|
ClearEntry(db_entry *Entry)
|
|
{
|
|
Entry->LinkOffsets.PrevStart = 0;
|
|
Entry->LinkOffsets.NextStart = 0;
|
|
Entry->LinkOffsets.PrevEnd = 0;
|
|
Entry->LinkOffsets.NextEnd = 0;
|
|
Entry->Size = 0;
|
|
Clear(Entry->BaseFilename, sizeof(Entry->BaseFilename));
|
|
Clear(Entry->Title, sizeof(Entry->Title));
|
|
}
|
|
|
|
void
|
|
InitNeighbourhood(neighbourhood *N)
|
|
{
|
|
N->PrevIndex = -1;
|
|
N->PreDeletionThisIndex = N->ThisIndex = 0;
|
|
N->NextIndex = -1;
|
|
|
|
N->PreLinkPrevOffsetTotal = N->PreLinkThisOffsetTotal = N->PreLinkNextOffsetTotal = 0;
|
|
N->PrevOffsetModifier = N->ThisOffsetModifier = N->NextOffsetModifier = 0;
|
|
N->FormerIsFirst = N->LatterIsFinal = FALSE;
|
|
N->DeletedEntryWasFirst = N->DeletedEntryWasFinal = FALSE;
|
|
|
|
N->Prev.LinkOffsets.PrevStart = N->Prev.LinkOffsets.PrevEnd = N->Prev.LinkOffsets.NextStart = N->Prev.LinkOffsets.NextEnd = 0;
|
|
N->This.LinkOffsets.PrevStart = N->This.LinkOffsets.PrevEnd = N->This.LinkOffsets.NextStart = N->This.LinkOffsets.NextEnd = 0;
|
|
N->Next.LinkOffsets.PrevStart = N->Next.LinkOffsets.PrevEnd = N->Next.LinkOffsets.NextStart = N->Next.LinkOffsets.NextEnd = 0;
|
|
|
|
N->Prev.Size = N->This.Size = N->Next.Size = 0;
|
|
|
|
Clear(N->Prev.BaseFilename, sizeof(N->Prev.BaseFilename));
|
|
Clear(N->This.BaseFilename, sizeof(N->This.BaseFilename));
|
|
Clear(N->Next.BaseFilename, sizeof(N->Next.BaseFilename));
|
|
|
|
Clear(N->Prev.Title, sizeof(N->Prev.Title));
|
|
Clear(N->This.Title, sizeof(N->This.Title));
|
|
Clear(N->Next.Title, sizeof(N->Next.Title));
|
|
}
|
|
|
|
#define PrintNeighbourhood(N) PrintNeighbourhood_(N, __LINE__)
|
|
void
|
|
PrintNeighbourhood_(neighbourhood *N, int Line)
|
|
{
|
|
printf( "\n"
|
|
" Neighbourhood (line %d):\n", Line);
|
|
|
|
if(N->PrevIndex >= 0)
|
|
{
|
|
printf(
|
|
" Prev [%d]: %s: %6d %6d %6d %6d\n",
|
|
N->PrevIndex, N->Prev.BaseFilename,
|
|
N->Prev.LinkOffsets.PrevStart,
|
|
N->Prev.LinkOffsets.PrevEnd,
|
|
N->Prev.LinkOffsets.NextStart,
|
|
N->Prev.LinkOffsets.NextEnd);
|
|
}
|
|
|
|
if(N->ThisIndex >= 0)
|
|
{
|
|
printf(
|
|
" This [%d (pre-deletion %d)]: %s: %6d %6d %6d %6d\n",
|
|
N->ThisIndex, N->PreDeletionThisIndex, N->This.BaseFilename,
|
|
N->This.LinkOffsets.PrevStart,
|
|
N->This.LinkOffsets.PrevEnd,
|
|
N->This.LinkOffsets.NextStart,
|
|
N->This.LinkOffsets.NextEnd);
|
|
}
|
|
|
|
if(N->NextIndex >= 0)
|
|
{
|
|
printf(
|
|
" Next [%d]: %s: %6d %6d %6d %6d\n",
|
|
N->NextIndex, N->Next.BaseFilename,
|
|
N->Next.LinkOffsets.PrevStart,
|
|
N->Next.LinkOffsets.PrevEnd,
|
|
N->Next.LinkOffsets.NextStart,
|
|
N->Next.LinkOffsets.NextEnd);
|
|
}
|
|
|
|
printf(
|
|
" OffsetModifiers: Prev %6d • This %6d • Next %6d\n"
|
|
" PreLinkOffsetTotals: Prev %6d • This %6d • Next %6d\n"
|
|
" DeletedEntryWasFirst: %s\n"
|
|
" DeletedEntryWasFinal: %s\n"
|
|
" FormerIsFirst: %s\n"
|
|
" LatterIsFinal: %s\n"
|
|
"\n",
|
|
N->PrevOffsetModifier, N->ThisOffsetModifier, N->NextOffsetModifier,
|
|
N->PreLinkPrevOffsetTotal, N->PreLinkThisOffsetTotal, N->PreLinkNextOffsetTotal,
|
|
N->DeletedEntryWasFirst ? "yes" : "no", N->DeletedEntryWasFinal ? "yes" : "no",
|
|
N->FormerIsFirst ? "yes" : "no", N->LatterIsFinal ? "yes" : "no");
|
|
}
|
|
|
|
void
|
|
SetNeighbour(db_entry *Dest, db_entry *Src)
|
|
{
|
|
Dest->Size = Src->Size;
|
|
Dest->LinkOffsets.PrevStart = Src->LinkOffsets.PrevStart;
|
|
Dest->LinkOffsets.PrevEnd = Src->LinkOffsets.PrevEnd;
|
|
Dest->LinkOffsets.NextStart = Src->LinkOffsets.NextStart;
|
|
Dest->LinkOffsets.NextEnd = Src->LinkOffsets.NextEnd;
|
|
CopyString(Dest->BaseFilename, sizeof(Dest->BaseFilename), "%s", Src->BaseFilename);
|
|
CopyString(Dest->Title, sizeof(Dest->Title), "%s", Src->Title);
|
|
}
|
|
|
|
void
|
|
GetNeighbourhoodForAddition(neighbourhood *N, enum8(edit_types) EditType)
|
|
{
|
|
db_entry Entry = { };
|
|
|
|
int EntryIndex;
|
|
|
|
bool FoundPrev = FALSE;
|
|
bool FoundNext = FALSE;
|
|
|
|
N->FormerIsFirst = TRUE;
|
|
EntryIndex = N->ThisIndex - 1;
|
|
|
|
for(; EntryIndex >= 0; --EntryIndex)
|
|
{
|
|
Entry = *(db_entry*)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * EntryIndex);
|
|
if(Entry.Size > 0)
|
|
{
|
|
if(!FoundPrev)
|
|
{
|
|
N->PrevIndex = EntryIndex;
|
|
SetNeighbour(&N->Prev, &Entry);
|
|
FoundPrev = TRUE;
|
|
}
|
|
else
|
|
{
|
|
N->FormerIsFirst = FALSE;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
switch(EditType)
|
|
{
|
|
case EDIT_REINSERTION:
|
|
EntryIndex = N->ThisIndex + 1;
|
|
break;
|
|
case EDIT_ADDITION:
|
|
EntryIndex = N->ThisIndex;
|
|
break;
|
|
}
|
|
|
|
N->LatterIsFinal = TRUE;
|
|
for(; EntryIndex < DB.EntriesHeader.Count;
|
|
++EntryIndex)
|
|
{
|
|
Entry = *(db_entry*)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * EntryIndex);
|
|
if(Entry.Size > 0)
|
|
{
|
|
if(!FoundNext)
|
|
{
|
|
N->NextIndex = EntryIndex;
|
|
SetNeighbour(&N->Next, &Entry);
|
|
FoundNext = TRUE;
|
|
}
|
|
else
|
|
{
|
|
N->LatterIsFinal = FALSE;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(EditType == EDIT_ADDITION && FoundNext)
|
|
{
|
|
++N->NextIndex;
|
|
}
|
|
}
|
|
|
|
void
|
|
GetNeighbourhoodForDeletion(neighbourhood *N)
|
|
{
|
|
db_entry Entry = { };
|
|
Entry = *(db_entry *)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * N->ThisIndex);
|
|
N->PreDeletionThisIndex = N->ThisIndex;
|
|
SetNeighbour(&N->This, &Entry);
|
|
|
|
int EntryIndex;
|
|
|
|
N->DeletedEntryWasFirst = TRUE;
|
|
N->FormerIsFirst = TRUE;
|
|
bool FoundPrev = FALSE;
|
|
for(EntryIndex = N->PreDeletionThisIndex - 1; EntryIndex >= 0; --EntryIndex)
|
|
{
|
|
Entry = *(db_entry *)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * EntryIndex);
|
|
if(Entry.Size > 0)
|
|
{
|
|
if(!FoundPrev)
|
|
{
|
|
FoundPrev = TRUE;
|
|
N->DeletedEntryWasFirst = FALSE;
|
|
|
|
N->PrevIndex = EntryIndex;
|
|
SetNeighbour(&N->Prev, &Entry);
|
|
}
|
|
else
|
|
{
|
|
N->FormerIsFirst = FALSE;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
N->DeletedEntryWasFinal = TRUE;
|
|
N->LatterIsFinal = TRUE;
|
|
bool FoundNext = FALSE;
|
|
for(EntryIndex = N->PreDeletionThisIndex + 1; EntryIndex < DB.EntriesHeader.Count; ++EntryIndex)
|
|
{
|
|
Entry = *(db_entry *)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * EntryIndex);
|
|
if(Entry.Size > 0)
|
|
{
|
|
if(!FoundNext)
|
|
{
|
|
FoundNext = TRUE;
|
|
N->DeletedEntryWasFinal = FALSE;
|
|
|
|
N->NextIndex = EntryIndex - 1;
|
|
SetNeighbour(&N->Next, &Entry);
|
|
}
|
|
else
|
|
{
|
|
N->LatterIsFinal = FALSE;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
GetNeighbourhood(neighbourhood *N, enum8(edit_types) EditType)
|
|
{
|
|
if(EditType == EDIT_DELETION)
|
|
{
|
|
GetNeighbourhoodForDeletion(N);
|
|
}
|
|
else
|
|
{
|
|
GetNeighbourhoodForAddition(N, EditType);
|
|
}
|
|
}
|
|
|
|
void
|
|
SnipeEntryIntoMetadataBuffer(db_entry *Entry, int EntryIndex)
|
|
{
|
|
*(db_entry *)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * EntryIndex) = *Entry;
|
|
}
|
|
|
|
int
|
|
InsertIntoDB(neighbourhood *N, buffers *CollationBuffers, template *BespokeTemplate, char *BaseFilename, bool RecheckingPrivacy, bool *Reinserting)
|
|
{
|
|
enum8(edit_types) EditType = EDIT_APPEND;
|
|
int EntryInsertionStart = StringLength("---\n");
|
|
int EntryInsertionEnd;
|
|
|
|
if(DB.EntriesHeader.Count > 0)
|
|
{
|
|
db_entry *Entry = { 0 };
|
|
N->ThisIndex = BinarySearchForMetadataEntry(&Entry, BaseFilename);
|
|
if(Entry)
|
|
{
|
|
// Reinsert
|
|
*Reinserting = TRUE;
|
|
EntryInsertionStart = AccumulateDBEntryInsertionOffset(N->ThisIndex);
|
|
EntryInsertionEnd = EntryInsertionStart + Entry->Size;
|
|
EditType = EDIT_REINSERTION;
|
|
}
|
|
else
|
|
{
|
|
if(N->ThisIndex == -1) { ++N->ThisIndex; } // NOTE(matt): BinarySearchForMetadataEntry returns -1 if search term precedes the set
|
|
Entry = (db_entry*)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * N->ThisIndex);
|
|
if(StringsDiffer(BaseFilename, Entry->BaseFilename) < 0)
|
|
{
|
|
// Insert
|
|
EditType = EDIT_INSERTION;
|
|
}
|
|
else
|
|
{
|
|
// Append
|
|
++N->ThisIndex;
|
|
EditType = EDIT_APPEND;
|
|
}
|
|
EntryInsertionStart = AccumulateDBEntryInsertionOffset(N->ThisIndex);
|
|
}
|
|
GetNeighbourhood(N, *Reinserting ? EDIT_REINSERTION : EDIT_ADDITION);
|
|
}
|
|
|
|
char InputFile[StringLength(BaseFilename) + StringLength(".hmml") + 1];
|
|
CopyString(InputFile, sizeof(InputFile), "%s.hmml", BaseFilename);
|
|
bool VideoIsPrivate = FALSE;
|
|
|
|
switch(HMMLToBuffers(CollationBuffers, BespokeTemplate, InputFile, N))
|
|
{
|
|
// TODO(matt): Actually sort out the fatality of these cases
|
|
case RC_ERROR_FILE:
|
|
case RC_ERROR_FATAL:
|
|
return RC_ERROR_FATAL;
|
|
case RC_ERROR_HMML:
|
|
case RC_ERROR_MAX_REFS:
|
|
case RC_ERROR_QUOTE:
|
|
case RC_INVALID_REFERENCE:
|
|
return RC_ERROR_HMML;
|
|
case RC_PRIVATE_VIDEO:
|
|
VideoIsPrivate = TRUE;
|
|
case RC_SUCCESS:
|
|
break;
|
|
}
|
|
|
|
ClearCopyStringNoFormat(N->This.BaseFilename, sizeof(N->This.BaseFilename), BaseFilename);
|
|
if(!VideoIsPrivate) { ClearCopyStringNoFormat(N->This.Title, sizeof(N->This.Title), CollationBuffers->Title); }
|
|
|
|
if(EditType == EDIT_REINSERTION)
|
|
{
|
|
// NOTE(matt): To save opening the DB.Metadata file, we defer sniping N->This in until InsertNeighbourLink()
|
|
if(!VideoIsPrivate)
|
|
{
|
|
if(!(DB.File.Handle = fopen(DB.File.Path, "w"))) { return RC_ERROR_FILE; }
|
|
fwrite(DB.File.Buffer.Location, EntryInsertionStart, 1, DB.File.Handle);
|
|
fwrite(CollationBuffers->SearchEntry.Location, N->This.Size, 1, DB.File.Handle);
|
|
fwrite(DB.File.Buffer.Location + EntryInsertionEnd, DB.File.FileSize - EntryInsertionEnd, 1, DB.File.Handle);
|
|
fclose(DB.File.Handle);
|
|
|
|
if(N->This.Size == EntryInsertionEnd - EntryInsertionStart)
|
|
{
|
|
DB.File.Buffer.Ptr = DB.File.Buffer.Location + EntryInsertionStart;
|
|
CopyBufferSized(&DB.File.Buffer, &CollationBuffers->SearchEntry, N->This.Size);
|
|
}
|
|
else
|
|
{
|
|
FreeBuffer(&DB.File.Buffer);
|
|
ReadFileIntoBuffer(&DB.File, 0);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
int ExistingEntryCount = DB.EntriesHeader.Count;
|
|
++DB.EntriesHeader.Count;
|
|
|
|
if(!(DB.Metadata.Handle = fopen(DB.Metadata.Path, "w"))) { return RC_ERROR_FILE; }
|
|
fwrite(&DB.Header, sizeof(DB.Header), 1, DB.Metadata.Handle);
|
|
fwrite(&DB.EntriesHeader, sizeof(DB.EntriesHeader), 1, DB.Metadata.Handle);
|
|
|
|
DB.Metadata.Buffer.Ptr = DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader);
|
|
fwrite(DB.Metadata.Buffer.Ptr,
|
|
sizeof(DB.Entry),
|
|
N->ThisIndex,
|
|
DB.Metadata.Handle);
|
|
DB.Metadata.Buffer.Ptr += sizeof(DB.Entry) * N->ThisIndex;
|
|
|
|
fwrite(&N->This, sizeof(DB.Entry), 1, DB.Metadata.Handle);
|
|
|
|
fwrite(DB.Metadata.Buffer.Ptr,
|
|
sizeof(DB.Entry),
|
|
ExistingEntryCount - N->ThisIndex,
|
|
DB.Metadata.Handle);
|
|
DB.Metadata.Buffer.Ptr += sizeof(DB.Entry) * (ExistingEntryCount - N->ThisIndex);
|
|
|
|
fwrite(DB.Metadata.Buffer.Ptr,
|
|
DB.Metadata.FileSize - (DB.Metadata.Buffer.Ptr - DB.Metadata.Buffer.Location),
|
|
1,
|
|
DB.Metadata.Handle);
|
|
|
|
CycleFile(&DB.Metadata);
|
|
|
|
|
|
if(!(DB.File.Handle = fopen(DB.File.Path, "w"))) { return RC_ERROR_FILE; }
|
|
fwrite(DB.File.Buffer.Location,
|
|
DB.File.FileSize - (DB.File.FileSize - EntryInsertionStart),
|
|
1, DB.File.Handle);
|
|
fwrite(CollationBuffers->SearchEntry.Location, N->This.Size, 1, DB.File.Handle);
|
|
fwrite(DB.File.Buffer.Location + EntryInsertionStart, DB.File.FileSize - EntryInsertionStart, 1, DB.File.Handle);
|
|
|
|
CycleFile(&DB.File);
|
|
}
|
|
|
|
if(!VideoIsPrivate)
|
|
{
|
|
LogError(LOG_NOTICE, "%s %s - %s", EditTypes[EditType].Name, BaseFilename, CollationBuffers->Title);
|
|
fprintf(stderr, "%s%s%s %s - %s\n", ColourStrings[EditTypes[EditType].Colour], EditTypes[EditType].Name, ColourStrings[CS_END], BaseFilename, CollationBuffers->Title);
|
|
}
|
|
else if(!RecheckingPrivacy)
|
|
{
|
|
LogError(LOG_NOTICE, "Privately %s %s", EditTypes[EditType].Name, BaseFilename);
|
|
fprintf(stderr, "%sPrivately %s%s %s\n", ColourStrings[CS_PRIVATE], EditTypes[EditType].Name, ColourStrings[CS_END], BaseFilename);
|
|
}
|
|
|
|
// TODO(matt): Remove VideoIsPrivate in favour of generating a player page in a random location
|
|
return VideoIsPrivate ? RC_PRIVATE_VIDEO : RC_SUCCESS;
|
|
}
|
|
|
|
void
|
|
WritePastAssetsHeader(void)
|
|
{
|
|
DB.Metadata.Handle = fopen(DB.Metadata.Path, "w");
|
|
fwrite(DB.Metadata.Buffer.Location,
|
|
sizeof(DB.Header)
|
|
+ sizeof(DB.EntriesHeader)
|
|
+ sizeof(DB.Entry) * DB.EntriesHeader.Count
|
|
+ sizeof(DB.AssetsHeader),
|
|
1,
|
|
DB.Metadata.Handle);
|
|
|
|
DB.Metadata.Buffer.Ptr = DB.Metadata.Buffer.Location
|
|
+ sizeof(DB.Header)
|
|
+ sizeof(DB.EntriesHeader)
|
|
+ sizeof(DB.Entry) * DB.EntriesHeader.Count;
|
|
|
|
DB.AssetsHeader = *(db_header_assets *)DB.Metadata.Buffer.Ptr;
|
|
DB.Metadata.Buffer.Ptr += sizeof(DB.AssetsHeader);
|
|
}
|
|
|
|
void
|
|
PrintLandmarks(void *FirstLandmark, int LandmarkCount)
|
|
{
|
|
printf("PrintLandmarks()\n");
|
|
for(int i = 0; i < LandmarkCount; ++i)
|
|
{
|
|
db_landmark Landmark = *(db_landmark *)(FirstLandmark + sizeof(Landmark) * i);
|
|
printf(" %4d %d\n", Landmark.EntryIndex, Landmark.Position);
|
|
}
|
|
}
|
|
|
|
void
|
|
ProcessPrevLandmarks(neighbourhood *N, void *FirstLandmark, int ExistingLandmarkCount, landmark_range *CurrentTarget, int *RunningIndex)
|
|
{
|
|
if(N->PrevIndex >= 0)
|
|
{
|
|
landmark_range FormerTarget = BinarySearchForMetadataLandmark(FirstLandmark, N->PrevIndex, ExistingLandmarkCount);
|
|
fwrite(FirstLandmark, sizeof(db_landmark), FormerTarget.First, DB.Metadata.Handle);
|
|
*RunningIndex += FormerTarget.First;
|
|
|
|
for(int j = 0; j < FormerTarget.Length; ++j, ++*RunningIndex)
|
|
{
|
|
db_landmark Landmark = *(db_landmark *)(FirstLandmark + sizeof(Landmark) * *RunningIndex);
|
|
if(Landmark.Position >= N->PreLinkPrevOffsetTotal)
|
|
{
|
|
Landmark.Position += N->PrevOffsetModifier;
|
|
}
|
|
fwrite(&Landmark, sizeof(Landmark), 1, DB.Metadata.Handle);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
fwrite(FirstLandmark + sizeof(db_landmark) * *RunningIndex, sizeof(db_landmark), CurrentTarget->First, DB.Metadata.Handle);
|
|
*RunningIndex += CurrentTarget->First;
|
|
}
|
|
}
|
|
|
|
void
|
|
ProcessNextLandmarks(neighbourhood *N, void *FirstLandmark, int ExistingLandmarkCount, int *RunningIndex, enum8(edit_types) EditType)
|
|
{
|
|
if(N->NextIndex >= 0 && *RunningIndex < ExistingLandmarkCount)
|
|
{
|
|
db_landmark Landmark = *(db_landmark *)(FirstLandmark + sizeof(Landmark) * *RunningIndex);
|
|
landmark_range LatterTarget = BinarySearchForMetadataLandmark(FirstLandmark, Landmark.EntryIndex, ExistingLandmarkCount);
|
|
|
|
for(int j = 0; j < LatterTarget.Length; ++j, ++*RunningIndex)
|
|
{
|
|
Landmark = *(db_landmark *)(FirstLandmark + sizeof(Landmark) * *RunningIndex);
|
|
if(Landmark.Position >= N->PreLinkNextOffsetTotal)
|
|
{
|
|
Landmark.Position += N->NextOffsetModifier;
|
|
}
|
|
switch(EditType)
|
|
{
|
|
case EDIT_DELETION: --Landmark.EntryIndex; break;
|
|
case EDIT_ADDITION: ++Landmark.EntryIndex; break;
|
|
}
|
|
fwrite(&Landmark, sizeof(Landmark), 1, DB.Metadata.Handle);
|
|
}
|
|
|
|
for(; *RunningIndex < ExistingLandmarkCount; ++*RunningIndex)
|
|
{
|
|
Landmark = *(db_landmark *)(FirstLandmark + sizeof(Landmark) * *RunningIndex);
|
|
switch(EditType)
|
|
{
|
|
case EDIT_DELETION: --Landmark.EntryIndex; break;
|
|
case EDIT_ADDITION: ++Landmark.EntryIndex; break;
|
|
}
|
|
fwrite(&Landmark, sizeof(Landmark), 1, DB.Metadata.Handle);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
DeleteStaleAssets(void)
|
|
{
|
|
LocateAssetsBlock();
|
|
char *AssetsHeaderLocation = DB.Metadata.Buffer.Ptr;
|
|
DB.AssetsHeader = *(db_header_assets *)AssetsHeaderLocation;
|
|
int AssetDeletionCount = 0;
|
|
int AssetDeletionLocations[DB.AssetsHeader.Count];
|
|
DB.Metadata.Buffer.Ptr += sizeof(DB.AssetsHeader);
|
|
for(int AssetIndex = 0; AssetIndex < DB.AssetsHeader.Count; ++AssetIndex)
|
|
{
|
|
DB.Asset = *(db_asset*)DB.Metadata.Buffer.Ptr;
|
|
if(DB.Asset.LandmarkCount == 0)
|
|
{
|
|
AssetDeletionLocations[AssetDeletionCount] = DB.Metadata.Buffer.Ptr - DB.Metadata.Buffer.Location;
|
|
++AssetDeletionCount;
|
|
--DB.AssetsHeader.Count;
|
|
}
|
|
DB.Metadata.Buffer.Ptr += sizeof(DB.Asset) + sizeof(DB.Landmark) * DB.Asset.LandmarkCount;
|
|
}
|
|
|
|
if(AssetDeletionCount > 0)
|
|
{
|
|
DB.Metadata.Handle = fopen(DB.Metadata.Path, "w");
|
|
fwrite(DB.Metadata.Buffer.Location, AssetsHeaderLocation - DB.Metadata.Buffer.Location, 1, DB.Metadata.Handle);
|
|
fwrite(&DB.AssetsHeader, sizeof(DB.AssetsHeader), 1, DB.Metadata.Handle);
|
|
|
|
int WrittenBytes = (AssetsHeaderLocation - DB.Metadata.Buffer.Location) + sizeof(DB.AssetsHeader);
|
|
|
|
for(int DeletionIndex = 0; DeletionIndex < AssetDeletionCount; ++DeletionIndex)
|
|
{
|
|
DB.Metadata.Buffer.Ptr = DB.Metadata.Buffer.Location + AssetDeletionLocations[DeletionIndex];
|
|
fwrite(DB.Metadata.Buffer.Location + WrittenBytes,
|
|
(DB.Metadata.Buffer.Ptr - DB.Metadata.Buffer.Location) - WrittenBytes,
|
|
1,
|
|
DB.Metadata.Handle);
|
|
WrittenBytes += (DB.Metadata.Buffer.Ptr - DB.Metadata.Buffer.Location) - WrittenBytes + sizeof(DB.Asset);
|
|
}
|
|
fwrite(DB.Metadata.Buffer.Location + WrittenBytes, DB.Metadata.FileSize - WrittenBytes, 1, DB.Metadata.Handle);
|
|
CycleFile(&DB.Metadata);
|
|
}
|
|
}
|
|
|
|
void
|
|
DeleteStaleLandmarks(void)
|
|
{
|
|
WritePastAssetsHeader();
|
|
for(int AssetIndex = 0; AssetIndex < DB.AssetsHeader.Count; ++AssetIndex)
|
|
{
|
|
DB.Asset = *(db_asset *)DB.Metadata.Buffer.Ptr;
|
|
DB.Metadata.Buffer.Ptr += sizeof(DB.Asset) + sizeof(DB.Landmark) * DB.Asset.LandmarkCount;
|
|
DB.Asset.LandmarkCount = 0;
|
|
fwrite(&DB.Asset, sizeof(DB.Asset), 1, DB.Metadata.Handle);
|
|
}
|
|
CycleFile(&DB.Metadata);
|
|
}
|
|
|
|
void
|
|
DeleteLandmarks(neighbourhood *N)
|
|
{
|
|
for(int AssetIndex = 0; AssetIndex < DB.AssetsHeader.Count; ++AssetIndex)
|
|
{
|
|
DB.Asset = *(db_asset *)DB.Metadata.Buffer.Ptr;
|
|
DB.Metadata.Buffer.Ptr += sizeof(DB.Asset);
|
|
db_landmark *FirstLandmark = (db_landmark *)DB.Metadata.Buffer.Ptr;
|
|
int ExistingLandmarkCount = DB.Asset.LandmarkCount;
|
|
int RunningIndex = 0;
|
|
|
|
landmark_range DeletionTarget = BinarySearchForMetadataLandmark(FirstLandmark, N->PreDeletionThisIndex, ExistingLandmarkCount);
|
|
DB.Asset.LandmarkCount -= DeletionTarget.Length;
|
|
fwrite(&DB.Asset, sizeof(DB.Asset), 1, DB.Metadata.Handle);
|
|
|
|
ProcessPrevLandmarks(N, FirstLandmark, ExistingLandmarkCount, &DeletionTarget, &RunningIndex);
|
|
|
|
RunningIndex += DeletionTarget.Length;
|
|
|
|
ProcessNextLandmarks(N, FirstLandmark, ExistingLandmarkCount, &RunningIndex, EDIT_DELETION);
|
|
DB.Metadata.Buffer.Ptr += sizeof(db_landmark) * ExistingLandmarkCount;
|
|
}
|
|
CycleFile(&DB.Metadata);
|
|
}
|
|
|
|
void UpdateLandmarksForNeighbourhood(neighbourhood *N, enum8(edit_types) EditType);
|
|
|
|
void
|
|
AddLandmarks(neighbourhood *N, enum8(edit_types) EditType)
|
|
{
|
|
for(int i = 0; i < Assets.Count; ++i)
|
|
{
|
|
Assets.Asset[i].Known = FALSE;
|
|
}
|
|
|
|
for(int StoredAssetIndex = 0; StoredAssetIndex < DB.AssetsHeader.Count; ++StoredAssetIndex)
|
|
{
|
|
DB.Asset = *(db_asset *)DB.Metadata.Buffer.Ptr;
|
|
int ExistingLandmarkCount = DB.Asset.LandmarkCount;
|
|
DB.Metadata.Buffer.Ptr += sizeof(DB.Asset);
|
|
void *FirstLandmark;
|
|
if(ExistingLandmarkCount > 0)
|
|
{
|
|
FirstLandmark = DB.Metadata.Buffer.Ptr;
|
|
}
|
|
int RunningIndex = 0;
|
|
|
|
for(int i = 0; i < Assets.Count; ++i)
|
|
{
|
|
if(!StringsDiffer(DB.Asset.Filename, Assets.Asset[i].Filename) && DB.Asset.Type == Assets.Asset[i].Type)
|
|
{
|
|
Assets.Asset[i].Known = TRUE;
|
|
if(!Assets.Asset[i].OffsetLandmarks)
|
|
{
|
|
DB.Asset.LandmarkCount += Assets.Asset[i].PlayerLandmarkCount;
|
|
landmark_range ThisTarget;
|
|
if(ExistingLandmarkCount > 0)
|
|
{
|
|
ThisTarget = BinarySearchForMetadataLandmark(FirstLandmark, N->ThisIndex, ExistingLandmarkCount);
|
|
|
|
if(EditType == EDIT_REINSERTION) { DB.Asset.LandmarkCount -= ThisTarget.Length; }
|
|
}
|
|
|
|
fwrite(&DB.Asset, sizeof(DB.Asset), 1, DB.Metadata.Handle);
|
|
|
|
if(ExistingLandmarkCount > 0)
|
|
{
|
|
ProcessPrevLandmarks(N, FirstLandmark, ExistingLandmarkCount, &ThisTarget, &RunningIndex);
|
|
}
|
|
|
|
for(int j = 0; j < Assets.Asset[i].PlayerLandmarkCount; ++j)
|
|
{
|
|
db_landmark Landmark;
|
|
Landmark.EntryIndex = N->ThisIndex;
|
|
Landmark.Position = Assets.Asset[i].PlayerLandmark[j];
|
|
fwrite(&Landmark, sizeof(Landmark), 1, DB.Metadata.Handle);
|
|
}
|
|
|
|
if(ExistingLandmarkCount > 0)
|
|
{
|
|
if(EditType == EDIT_REINSERTION) { RunningIndex += ThisTarget.Length; }
|
|
|
|
ProcessNextLandmarks(N, FirstLandmark, ExistingLandmarkCount, &RunningIndex, EditType);
|
|
}
|
|
Assets.Asset[i].OffsetLandmarks = TRUE;
|
|
}
|
|
else
|
|
{
|
|
fwrite(&DB.Asset, sizeof(DB.Asset), 1, DB.Metadata.Handle);
|
|
fwrite(DB.Metadata.Buffer.Ptr, sizeof(db_landmark) * ExistingLandmarkCount, 1, DB.Metadata.Handle);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
DB.Metadata.Buffer.Ptr += sizeof(db_landmark) * ExistingLandmarkCount;
|
|
}
|
|
CycleFile(&DB.Metadata);
|
|
|
|
bool NewAsset = FALSE;
|
|
for(int i = 0; i < Assets.Count; ++i)
|
|
{
|
|
if(!Assets.Asset[i].Known && Assets.Asset[i].PlayerLandmarkCount > 0)
|
|
{
|
|
UpdateAssetInDB(i);
|
|
NewAsset = TRUE;
|
|
}
|
|
}
|
|
if(NewAsset) {
|
|
UpdateLandmarksForNeighbourhood(N, EDIT_REINSERTION); // NOTE(matt): EDIT_REINSERTION this time because all existing
|
|
// assets will have been updated in the first pass
|
|
}
|
|
}
|
|
|
|
void
|
|
UpdateLandmarksForNeighbourhood(neighbourhood *N, enum8(edit_types) EditType)
|
|
{
|
|
if(!(Config.Mode & MODE_NOREVVEDRESOURCE))
|
|
{
|
|
WritePastAssetsHeader();
|
|
switch(EditType)
|
|
{
|
|
case EDIT_DELETION: DeleteLandmarks(N); break;
|
|
case EDIT_ADDITION: case EDIT_REINSERTION: { AddLandmarks(N, EditType); break; }
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
DeleteLandmarksForSearch(void)
|
|
{
|
|
if(!(Config.Mode & MODE_NOREVVEDRESOURCE))
|
|
{
|
|
WritePastAssetsHeader();
|
|
for(int AssetIndex = 0; AssetIndex < DB.AssetsHeader.Count; ++AssetIndex)
|
|
{
|
|
DB.Asset = *(db_asset *)DB.Metadata.Buffer.Ptr;
|
|
DB.Metadata.Buffer.Ptr += sizeof(DB.Asset);
|
|
db_landmark *FirstLandmark = (db_landmark *)DB.Metadata.Buffer.Ptr;
|
|
int ExistingLandmarkCount = DB.Asset.LandmarkCount;
|
|
|
|
landmark_range DeletionTarget = BinarySearchForMetadataLandmark(FirstLandmark, PAGE_TYPE_SEARCH, ExistingLandmarkCount);
|
|
DB.Asset.LandmarkCount -= DeletionTarget.Length;
|
|
fwrite(&DB.Asset, sizeof(DB.Asset), 1, DB.Metadata.Handle);
|
|
|
|
DB.Metadata.Buffer.Ptr += sizeof(DB.Landmark) * DeletionTarget.Length;
|
|
fwrite(DB.Metadata.Buffer.Ptr, sizeof(DB.Landmark), ExistingLandmarkCount - DeletionTarget.Length, DB.Metadata.Handle);
|
|
|
|
DB.Metadata.Buffer.Ptr += sizeof(DB.Landmark) * ExistingLandmarkCount - DeletionTarget.Length;
|
|
}
|
|
CycleFile(&DB.Metadata);
|
|
}
|
|
}
|
|
|
|
void
|
|
UpdateLandmarksForSearch(void)
|
|
{
|
|
if(!(Config.Mode & MODE_NOREVVEDRESOURCE))
|
|
{
|
|
for(int i = 0; i < Assets.Count; ++i)
|
|
{
|
|
Assets.Asset[i].Known = FALSE;
|
|
}
|
|
|
|
WritePastAssetsHeader();
|
|
for(int AssetIndex = 0; AssetIndex < DB.AssetsHeader.Count; ++AssetIndex)
|
|
{
|
|
DB.Asset = *(db_asset *)DB.Metadata.Buffer.Ptr;
|
|
int ExistingLandmarkCount = DB.Asset.LandmarkCount;
|
|
DB.Metadata.Buffer.Ptr += sizeof(DB.Asset);
|
|
void *FirstLandmark = DB.Metadata.Buffer.Ptr;
|
|
|
|
for(int i = 0; i < Assets.Count; ++i)
|
|
{
|
|
if(!StringsDiffer(DB.Asset.Filename, Assets.Asset[i].Filename) && DB.Asset.Type == Assets.Asset[i].Type)
|
|
{
|
|
Assets.Asset[i].Known = TRUE;
|
|
|
|
landmark_range Target = BinarySearchForMetadataLandmark(FirstLandmark, PAGE_TYPE_SEARCH, DB.Asset.LandmarkCount);
|
|
|
|
DB.Asset.LandmarkCount += Assets.Asset[i].SearchLandmarkCount - Target.Length;
|
|
|
|
fwrite(&DB.Asset, sizeof(DB.Asset), 1, DB.Metadata.Handle);
|
|
|
|
for(int j = 0; j < Assets.Asset[i].SearchLandmarkCount; ++j)
|
|
{
|
|
DB.Landmark.EntryIndex = PAGE_TYPE_SEARCH;
|
|
DB.Landmark.Position = Assets.Asset[i].SearchLandmark[j];
|
|
fwrite(&DB.Landmark, sizeof(DB.Landmark), 1, DB.Metadata.Handle);
|
|
}
|
|
|
|
DB.Metadata.Buffer.Ptr += sizeof(DB.Landmark) * (Target.First + Target.Length);
|
|
fwrite(DB.Metadata.Buffer.Ptr, sizeof(DB.Landmark), ExistingLandmarkCount - (Target.First + Target.Length), DB.Metadata.Handle);
|
|
DB.Metadata.Buffer.Ptr += sizeof(DB.Landmark) * (ExistingLandmarkCount - (Target.First + Target.Length));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
CycleFile(&DB.Metadata);
|
|
|
|
bool NewAsset = FALSE;
|
|
for(int InternalAssetIndex = 0; InternalAssetIndex < Assets.Count; ++InternalAssetIndex)
|
|
{
|
|
if(!Assets.Asset[InternalAssetIndex].Known && Assets.Asset[InternalAssetIndex].SearchLandmarkCount > 0)
|
|
{
|
|
NewAsset = TRUE;
|
|
UpdateAssetInDB(InternalAssetIndex);
|
|
}
|
|
}
|
|
if(NewAsset) { UpdateLandmarksForSearch(); }
|
|
}
|
|
}
|
|
|
|
enum
|
|
{
|
|
LINK_INCLUDE,
|
|
LINK_EXCLUDE
|
|
} link_types;
|
|
|
|
enum
|
|
{
|
|
LINK_FORWARDS,
|
|
LINK_BACKWARDS
|
|
} link_directions;
|
|
|
|
int
|
|
InsertNeighbourLink(db_entry *From, int FromIndex, db_entry *To, enum8(link_directions) LinkDirection, bool FromHasOneNeighbour)
|
|
{
|
|
file_buffer HTML;
|
|
if(ReadPlayerPageIntoBuffer(&HTML, From) == RC_SUCCESS)
|
|
{
|
|
if(!(HTML.Handle = fopen(HTML.Path, "w"))) { FreeBuffer(&HTML.Buffer); return RC_ERROR_FILE; };
|
|
|
|
buffer Link;
|
|
ClaimBuffer(&Link, "Link", Kilobytes(4));
|
|
|
|
buffer ToPlayerURL;
|
|
if(To)
|
|
{
|
|
ClaimBuffer(&ToPlayerURL, "ToPlayerURL", MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH);
|
|
ConstructPlayerURL(&ToPlayerURL, To->BaseFilename);
|
|
}
|
|
|
|
int NewPrevEnd = 0;
|
|
int NewNextEnd = 0;
|
|
switch(LinkDirection)
|
|
{
|
|
case LINK_BACKWARDS:
|
|
{
|
|
fwrite(HTML.Buffer.Location, From->LinkOffsets.PrevStart, 1, HTML.Handle);
|
|
if(To)
|
|
{
|
|
CopyStringToBuffer(&Link,
|
|
" <a class=\"episodeMarker prev\" href=\"%s\"><div>⏫</div><div>Previous: '%s'</div><div>⏫</div></a>\n",
|
|
ToPlayerURL.Location,
|
|
To->Title);
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&Link,
|
|
" <div class=\"episodeMarker first\"><div>•</div><div>Welcome to <cite>%s</cite></div><div>•</div></div>\n", DB.EntriesHeader.ProjectName);
|
|
}
|
|
NewPrevEnd = Link.Ptr - Link.Location;
|
|
fwrite(Link.Location, (Link.Ptr - Link.Location), 1, HTML.Handle);
|
|
if(FromHasOneNeighbour)
|
|
{
|
|
fwrite(HTML.Buffer.Location + From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd, From->LinkOffsets.NextStart, 1, HTML.Handle);
|
|
RewindBuffer(&Link);
|
|
CopyStringToBuffer(&Link,
|
|
" <div class=\"episodeMarker last\"><div>•</div><div>You have arrived at the (current) end of <cite>%s</cite></div><div>•</div></div>\n", DB.EntriesHeader.ProjectName);
|
|
NewNextEnd = Link.Ptr - Link.Location;
|
|
fwrite(Link.Location, NewNextEnd, 1, HTML.Handle);
|
|
fwrite(HTML.Buffer.Location + From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd + From->LinkOffsets.NextStart + From->LinkOffsets.NextEnd,
|
|
HTML.FileSize - (From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd + From->LinkOffsets.NextStart + From->LinkOffsets.NextEnd),
|
|
1,
|
|
HTML.Handle);
|
|
}
|
|
else
|
|
{
|
|
fwrite(HTML.Buffer.Location + From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd,
|
|
HTML.FileSize - (From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd),
|
|
1,
|
|
HTML.Handle);
|
|
}
|
|
|
|
From->LinkOffsets.PrevEnd = NewPrevEnd;
|
|
if(FromHasOneNeighbour) { From->LinkOffsets.NextEnd = NewNextEnd; }
|
|
} break;
|
|
case LINK_FORWARDS:
|
|
{
|
|
if(FromHasOneNeighbour)
|
|
{
|
|
fwrite(HTML.Buffer.Location, From->LinkOffsets.PrevStart, 1, HTML.Handle);
|
|
CopyStringToBuffer(&Link,
|
|
" <div class=\"episodeMarker first\"><div>•</div><div>Welcome to <cite>%s</cite></div><div>•</div></div>\n", DB.EntriesHeader.ProjectName);
|
|
NewPrevEnd = Link.Ptr - Link.Location;
|
|
fwrite(Link.Location, NewPrevEnd, 1, HTML.Handle);
|
|
RewindBuffer(&Link);
|
|
fwrite(HTML.Buffer.Location + From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd,
|
|
From->LinkOffsets.NextStart, 1, HTML.Handle);
|
|
}
|
|
else
|
|
{
|
|
fwrite(HTML.Buffer.Location, From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd + From->LinkOffsets.NextStart, 1, HTML.Handle);
|
|
}
|
|
|
|
if(To)
|
|
{
|
|
CopyStringToBuffer(&Link,
|
|
" <a class=\"episodeMarker next\" href=\"%s\"><div>⏬</div><div>Next: '%s'</div><div>⏬</div></a>\n",
|
|
ToPlayerURL.Location,
|
|
To->Title);
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&Link,
|
|
" <div class=\"episodeMarker last\"><div>•</div><div>You have arrived at the (current) end of <cite>%s</cite></div><div>•</div></div>\n", DB.EntriesHeader.ProjectName);
|
|
}
|
|
NewNextEnd = Link.Ptr - Link.Location;
|
|
fwrite(Link.Location, (Link.Ptr - Link.Location), 1, HTML.Handle);
|
|
|
|
fwrite(HTML.Buffer.Location + From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd + From->LinkOffsets.NextStart + From->LinkOffsets.NextEnd,
|
|
HTML.FileSize - (From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd + From->LinkOffsets.NextStart + From->LinkOffsets.NextEnd),
|
|
1,
|
|
HTML.Handle);
|
|
|
|
if(FromHasOneNeighbour) { From->LinkOffsets.PrevEnd = NewPrevEnd; }
|
|
From->LinkOffsets.NextEnd = NewNextEnd;
|
|
} break;
|
|
}
|
|
|
|
if(To) { DeclaimBuffer(&ToPlayerURL); }
|
|
DeclaimBuffer(&Link);
|
|
fclose(HTML.Handle);
|
|
FreeBuffer(&HTML.Buffer);
|
|
SnipeEntryIntoMetadataBuffer(From, FromIndex);
|
|
return RC_SUCCESS;
|
|
}
|
|
else
|
|
{
|
|
return RC_ERROR_FILE;
|
|
}
|
|
}
|
|
|
|
int
|
|
DeleteNeighbourLinks(neighbourhood *N)
|
|
{
|
|
db_entry *Entry;
|
|
int EntryIndex;
|
|
if(N->PrevIndex >= 0)
|
|
{
|
|
Entry = &N->Prev;
|
|
EntryIndex = N->PrevIndex;
|
|
|
|
N->PreLinkPrevOffsetTotal = N->Prev.LinkOffsets.PrevEnd
|
|
+ N->Prev.LinkOffsets.NextStart
|
|
+ N->Prev.LinkOffsets.NextEnd;
|
|
}
|
|
else
|
|
{
|
|
Entry = &N->Next;
|
|
EntryIndex = N->NextIndex;
|
|
|
|
N->PreLinkNextOffsetTotal = N->Next.LinkOffsets.PrevEnd
|
|
+ N->Next.LinkOffsets.NextStart
|
|
+ N->Next.LinkOffsets.NextEnd;
|
|
}
|
|
|
|
file_buffer HTML;
|
|
if(ReadPlayerPageIntoBuffer(&HTML, Entry) == RC_SUCCESS)
|
|
{
|
|
if(!(HTML.Handle = fopen(HTML.Path, "w"))) { FreeBuffer(&HTML.Buffer); return RC_ERROR_FILE; };
|
|
|
|
fwrite(HTML.Buffer.Location, Entry->LinkOffsets.PrevStart, 1, HTML.Handle);
|
|
fwrite(HTML.Buffer.Location + Entry->LinkOffsets.PrevStart + Entry->LinkOffsets.PrevEnd, Entry->LinkOffsets.NextStart, 1, HTML.Handle);
|
|
fwrite(HTML.Buffer.Location + Entry->LinkOffsets.PrevStart + Entry->LinkOffsets.PrevEnd + Entry->LinkOffsets.NextStart + Entry->LinkOffsets.NextEnd,
|
|
HTML.FileSize - (Entry->LinkOffsets.PrevStart + Entry->LinkOffsets.PrevEnd + Entry->LinkOffsets.NextStart + Entry->LinkOffsets.NextEnd),
|
|
1,
|
|
HTML.Handle);
|
|
fclose(HTML.Handle);
|
|
Entry->LinkOffsets.PrevEnd = 0;
|
|
Entry->LinkOffsets.NextEnd = 0;
|
|
if(N->PrevIndex >= 0)
|
|
{
|
|
N->PrevOffsetModifier = N->Prev.LinkOffsets.PrevEnd
|
|
+ N->Prev.LinkOffsets.NextStart
|
|
+ N->Prev.LinkOffsets.NextEnd
|
|
- N->PreLinkPrevOffsetTotal;
|
|
}
|
|
else
|
|
{
|
|
N->NextOffsetModifier = N->Next.LinkOffsets.PrevEnd
|
|
+ N->Next.LinkOffsets.NextStart
|
|
+ N->Next.LinkOffsets.NextEnd
|
|
- N->PreLinkNextOffsetTotal;
|
|
}
|
|
FreeBuffer(&HTML.Buffer);
|
|
SnipeEntryIntoMetadataBuffer(Entry, EntryIndex);
|
|
}
|
|
|
|
return RC_SUCCESS;
|
|
}
|
|
|
|
void
|
|
LinkToNewEntry(neighbourhood *N)
|
|
{
|
|
N->PreLinkThisOffsetTotal = N->This.LinkOffsets.PrevEnd
|
|
+ N->This.LinkOffsets.NextStart
|
|
+ N->This.LinkOffsets.NextEnd;
|
|
|
|
if(N->PrevIndex >= 0)
|
|
{
|
|
N->PreLinkPrevOffsetTotal = N->Prev.LinkOffsets.PrevEnd
|
|
+ N->Prev.LinkOffsets.NextStart
|
|
+ N->Prev.LinkOffsets.NextEnd;
|
|
|
|
InsertNeighbourLink(&N->Prev, N->PrevIndex, &N->This, LINK_FORWARDS, N->FormerIsFirst);
|
|
|
|
N->PrevOffsetModifier = N->Prev.LinkOffsets.PrevEnd
|
|
+ N->Prev.LinkOffsets.NextStart
|
|
+ N->Prev.LinkOffsets.NextEnd
|
|
- N->PreLinkPrevOffsetTotal;
|
|
}
|
|
|
|
if(N->NextIndex >= 0)
|
|
{
|
|
N->PreLinkNextOffsetTotal = N->Next.LinkOffsets.PrevEnd
|
|
+ N->Next.LinkOffsets.NextStart
|
|
+ N->Next.LinkOffsets.NextEnd;
|
|
|
|
InsertNeighbourLink(&N->Next, N->NextIndex, &N->This, LINK_BACKWARDS, N->LatterIsFinal);
|
|
|
|
N->NextOffsetModifier = N->Next.LinkOffsets.PrevEnd
|
|
+ N->Next.LinkOffsets.NextStart
|
|
+ N->Next.LinkOffsets.NextEnd
|
|
- N->PreLinkNextOffsetTotal;
|
|
}
|
|
}
|
|
|
|
void
|
|
MarkNextAsFirst(neighbourhood *N)
|
|
{
|
|
file_buffer HTML;
|
|
ReadPlayerPageIntoBuffer(&HTML, &N->Next);
|
|
|
|
buffer Link;
|
|
ClaimBuffer(&Link, "Link", Kilobytes(4));
|
|
|
|
HTML.Handle = fopen(HTML.Path, "w");
|
|
|
|
fwrite(HTML.Buffer.Location, N->Next.LinkOffsets.PrevStart, 1, HTML.Handle);
|
|
|
|
CopyStringToBuffer(&Link,
|
|
" <div class=\"episodeMarker first\"><div>•</div><div>Welcome to <cite>%s</cite></div><div>•</div></div>\n", DB.EntriesHeader.ProjectName);
|
|
|
|
fwrite(Link.Location, (Link.Ptr - Link.Location), 1, HTML.Handle);
|
|
|
|
fwrite(HTML.Buffer.Location + N->Next.LinkOffsets.PrevStart + N->Next.LinkOffsets.PrevEnd,
|
|
HTML.FileSize - (N->Next.LinkOffsets.PrevStart + N->Next.LinkOffsets.PrevEnd),
|
|
1,
|
|
HTML.Handle);
|
|
|
|
N->Next.LinkOffsets.PrevEnd = Link.Ptr - Link.Location;
|
|
|
|
DeclaimBuffer(&Link);
|
|
fclose(HTML.Handle);
|
|
FreeBuffer(&HTML.Buffer);
|
|
|
|
SnipeEntryIntoMetadataBuffer(&N->Next, N->NextIndex);
|
|
}
|
|
|
|
void
|
|
MarkPrevAsFinal(neighbourhood *N)
|
|
{
|
|
file_buffer File;
|
|
ReadPlayerPageIntoBuffer(&File, &N->Prev);
|
|
|
|
File.Handle = fopen(File.Path, "w");
|
|
buffer Link;
|
|
ClaimBuffer(&Link, "Link", Kilobytes(4));
|
|
|
|
|
|
fwrite(File.Buffer.Location, N->Prev.LinkOffsets.PrevStart + N->Prev.LinkOffsets.PrevEnd + N->Prev.LinkOffsets.NextStart, 1, File.Handle);
|
|
|
|
CopyStringToBuffer(&Link,
|
|
" <div class=\"episodeMarker last\"><div>•</div><div>You have arrived at the (current) end of <cite>%s</cite></div><div>•</div></div>\n", DB.EntriesHeader.ProjectName);
|
|
|
|
fwrite(Link.Location, (Link.Ptr - Link.Location), 1, File.Handle);
|
|
|
|
fwrite(File.Buffer.Location + N->Prev.LinkOffsets.PrevStart + N->Prev.LinkOffsets.PrevEnd + N->Prev.LinkOffsets.NextStart + N->Prev.LinkOffsets.NextEnd,
|
|
File.FileSize - (N->Prev.LinkOffsets.PrevStart + N->Prev.LinkOffsets.PrevEnd + N->Prev.LinkOffsets.NextStart + N->Prev.LinkOffsets.NextEnd),
|
|
1,
|
|
File.Handle);
|
|
|
|
N->Prev.LinkOffsets.NextEnd = Link.Ptr - Link.Location;
|
|
|
|
DeclaimBuffer(&Link);
|
|
fclose(File.Handle);
|
|
FreeBuffer(&File.Buffer);
|
|
|
|
SnipeEntryIntoMetadataBuffer(&N->Prev, N->PrevIndex);
|
|
}
|
|
|
|
void
|
|
LinkOverDeletedEntry(neighbourhood *N)
|
|
{
|
|
if(DB.EntriesHeader.Count == 1)
|
|
{
|
|
DeleteNeighbourLinks(N);
|
|
}
|
|
else
|
|
{
|
|
if(N->DeletedEntryWasFirst)
|
|
{
|
|
N->PreLinkNextOffsetTotal = N->Next.LinkOffsets.PrevEnd
|
|
+ N->Next.LinkOffsets.NextStart
|
|
+ N->Next.LinkOffsets.NextEnd;
|
|
|
|
MarkNextAsFirst(N);
|
|
|
|
N->NextOffsetModifier = N->Next.LinkOffsets.PrevEnd
|
|
+ N->Next.LinkOffsets.NextStart
|
|
+ N->Next.LinkOffsets.NextEnd
|
|
- N->PreLinkNextOffsetTotal;
|
|
return;
|
|
}
|
|
else if(N->DeletedEntryWasFinal)
|
|
{
|
|
N->PreLinkPrevOffsetTotal = N->Prev.LinkOffsets.PrevEnd
|
|
+ N->Prev.LinkOffsets.NextStart
|
|
+ N->Prev.LinkOffsets.NextEnd;
|
|
|
|
MarkPrevAsFinal(N);
|
|
|
|
N->PrevOffsetModifier = N->Prev.LinkOffsets.PrevEnd
|
|
+ N->Prev.LinkOffsets.NextStart
|
|
+ N->Prev.LinkOffsets.NextEnd
|
|
- N->PreLinkPrevOffsetTotal;
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
// Assert(N->PrevIndex >= 0 && N->NextIndex >= 0)
|
|
N->PreLinkPrevOffsetTotal = N->Prev.LinkOffsets.PrevEnd
|
|
+ N->Prev.LinkOffsets.NextStart
|
|
+ N->Prev.LinkOffsets.NextEnd;
|
|
|
|
|
|
N->PreLinkNextOffsetTotal = N->Next.LinkOffsets.PrevEnd
|
|
+ N->Next.LinkOffsets.NextStart
|
|
+ N->Next.LinkOffsets.NextEnd;
|
|
}
|
|
|
|
InsertNeighbourLink(&N->Prev, N->PrevIndex, &N->Next, LINK_FORWARDS, N->FormerIsFirst);
|
|
InsertNeighbourLink(&N->Next, N->NextIndex, &N->Prev, LINK_BACKWARDS, N->LatterIsFinal);
|
|
|
|
N->PrevOffsetModifier = N->Prev.LinkOffsets.PrevEnd
|
|
+ N->Prev.LinkOffsets.NextStart
|
|
+ N->Prev.LinkOffsets.NextEnd
|
|
- N->PreLinkPrevOffsetTotal;
|
|
|
|
N->NextOffsetModifier = N->Next.LinkOffsets.PrevEnd
|
|
+ N->Next.LinkOffsets.NextStart
|
|
+ N->Next.LinkOffsets.NextEnd
|
|
- N->PreLinkNextOffsetTotal;
|
|
}
|
|
}
|
|
|
|
void
|
|
LinkNeighbours(neighbourhood *N, enum8(link_types) LinkType)
|
|
{
|
|
if(LinkType == LINK_INCLUDE)
|
|
{
|
|
LinkToNewEntry(N);
|
|
}
|
|
else
|
|
{
|
|
LinkOverDeletedEntry(N);
|
|
}
|
|
}
|
|
|
|
void
|
|
DeleteSearchPageFromFilesystem() // NOTE(matt): Do we need to handle relocating, like the PlayerPage function?
|
|
{
|
|
buffer SearchDirectory;
|
|
ClaimBuffer(&SearchDirectory, "SearchDirectory", MAX_BASE_DIR_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + 10);
|
|
ConstructDirectoryPath(&SearchDirectory, PAGE_SEARCH, Config.SearchLocation, "");
|
|
char SearchPagePath[1024];
|
|
CopyString(SearchPagePath, sizeof(SearchPagePath), "%s/index.html", SearchDirectory.Location);
|
|
remove(SearchPagePath);
|
|
remove(SearchDirectory.Location);
|
|
DeclaimBuffer(&SearchDirectory);
|
|
}
|
|
|
|
int
|
|
DeletePlayerPageFromFilesystem(char *BaseFilename, char *PlayerLocation, bool Relocating)
|
|
{
|
|
// NOTE(matt): Once we have the notion of an output filename format, we'll need to use that here
|
|
buffer OutputDirectoryPath;
|
|
ClaimBuffer(&OutputDirectoryPath, "OutputDirectoryPath", MAX_BASE_DIR_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH + 1 + 10);
|
|
ConstructDirectoryPath(&OutputDirectoryPath, PAGE_PLAYER, PlayerLocation, BaseFilename);
|
|
DIR *PlayerDir;
|
|
|
|
if((PlayerDir = opendir(OutputDirectoryPath.Location))) // There is a directory for the Player, which there probably should be if not for manual intervention
|
|
{
|
|
char PlayerPagePath[256];
|
|
CopyString(PlayerPagePath, sizeof(PlayerPagePath), "%s/index.html", OutputDirectoryPath.Location);
|
|
FILE *PlayerPage;
|
|
if((PlayerPage = fopen(PlayerPagePath, "r")))
|
|
{
|
|
fclose(PlayerPage);
|
|
remove(PlayerPagePath);
|
|
}
|
|
|
|
closedir(PlayerDir);
|
|
if((remove(OutputDirectoryPath.Location) == -1))
|
|
{
|
|
LogError(LOG_NOTICE, "Mostly deleted %s. Unable to remove directory %s: %s", BaseFilename, OutputDirectoryPath.Location, strerror(errno));
|
|
fprintf(stderr, "%sMostly deleted%s %s. %sUnable to remove directory%s %s: %s", ColourStrings[EditTypes[EDIT_DELETION].Colour], ColourStrings[CS_END], BaseFilename, ColourStrings[CS_ERROR], ColourStrings[CS_END], OutputDirectoryPath.Location, strerror(errno));
|
|
}
|
|
else
|
|
{
|
|
if(!Relocating)
|
|
{
|
|
LogError(LOG_INFORMATIONAL, "Deleted %s", BaseFilename);
|
|
fprintf(stderr, "%sDeleted%s %s\n", ColourStrings[EditTypes[EDIT_DELETION].Colour], ColourStrings[CS_END], BaseFilename);
|
|
}
|
|
|
|
}
|
|
}
|
|
DeclaimBuffer(&OutputDirectoryPath);
|
|
return RC_SUCCESS;
|
|
}
|
|
|
|
int
|
|
DeleteFromDB(neighbourhood *N, char *BaseFilename)
|
|
{
|
|
// TODO(matt): LogError()
|
|
DB.Header = *(db_header *)DB.Metadata.Buffer.Location;
|
|
DB.EntriesHeader = *(db_header_entries *)(DB.Metadata.Buffer.Location + sizeof(DB.Header));
|
|
|
|
db_entry *Entry = { 0 };
|
|
int EntryIndex = BinarySearchForMetadataEntry(&Entry, BaseFilename);
|
|
if(Entry)
|
|
{
|
|
int DeleteFileFrom = AccumulateDBEntryInsertionOffset(EntryIndex);
|
|
int DeleteFileTo = DeleteFileFrom + Entry->Size;
|
|
N->ThisIndex = EntryIndex;
|
|
GetNeighbourhood(N, EDIT_DELETION);
|
|
--DB.EntriesHeader.Count;
|
|
|
|
if(DB.EntriesHeader.Count == 0)
|
|
{
|
|
// TODO(matt): Handle this differently, allowing 0 entries but > 0 assets?
|
|
DeleteSearchPageFromFilesystem();
|
|
|
|
remove(DB.Metadata.Path);
|
|
DB.Metadata.FileSize = 0;
|
|
FreeBuffer(&DB.Metadata.Buffer);
|
|
|
|
remove(DB.File.Path);
|
|
DB.File.FileSize = 0;
|
|
FreeBuffer(&DB.File.Buffer);
|
|
}
|
|
else
|
|
{
|
|
if(!(DB.Metadata.Handle = fopen(DB.Metadata.Path, "w"))) { FreeBuffer(&DB.Metadata.Buffer); return RC_ERROR_FILE; }
|
|
if(!(DB.File.Handle = fopen(DB.File.Path, "w"))) { FreeBuffer(&DB.File.Buffer); return RC_ERROR_FILE; }
|
|
|
|
fwrite(&DB.Header, sizeof(DB.Header), 1, DB.Metadata.Handle);
|
|
fwrite(&DB.EntriesHeader, sizeof(DB.EntriesHeader), 1, DB.Metadata.Handle);
|
|
fwrite(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader), sizeof(DB.Entry) * EntryIndex, 1, DB.Metadata.Handle);
|
|
fwrite(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * (EntryIndex + 1),
|
|
DB.Metadata.FileSize - sizeof(DB.Header) - sizeof(DB.EntriesHeader) - sizeof(DB.Entry) * (EntryIndex + 1),
|
|
1, DB.Metadata.Handle);
|
|
CycleFile(&DB.Metadata);
|
|
|
|
fwrite(DB.File.Buffer.Location, DeleteFileFrom, 1, DB.File.Handle);
|
|
fwrite(DB.File.Buffer.Location + DeleteFileTo, DB.File.FileSize - DeleteFileTo, 1, DB.File.Handle);
|
|
CycleFile(&DB.File);
|
|
}
|
|
}
|
|
|
|
return Entry ? RC_SUCCESS : RC_NOOP;
|
|
}
|
|
|
|
int
|
|
SearchToBuffer(buffers *CollationBuffers) // NOTE(matt): This guy malloc's CollationBuffers->Search
|
|
{
|
|
if(DB.Metadata.Buffer.Location)
|
|
{
|
|
DB.Header = *(db_header *)DB.Metadata.Buffer.Location;
|
|
DB.EntriesHeader = *(db_header_entries *)(DB.Metadata.Buffer.Location + sizeof(DB.Header));
|
|
if(DB.EntriesHeader.Count > 0)
|
|
{
|
|
RewindBuffer(&CollationBuffers->IncludesSearch);
|
|
|
|
buffer URL;
|
|
ConstructResolvedAssetURL(&URL, ASSET_CSS_CINERA, PAGE_SEARCH);
|
|
CopyStringToBuffer(&CollationBuffers->IncludesSearch,
|
|
"<meta charset=\"UTF-8\">\n"
|
|
" <meta name=\"generator\" content=\"Cinera %d.%d.%d\">\n"
|
|
"\n"
|
|
" <link rel=\"stylesheet\" type=\"text/css\" href=\"%s",
|
|
CINERA_APP_VERSION.Major,
|
|
CINERA_APP_VERSION.Minor,
|
|
CINERA_APP_VERSION.Patch,
|
|
URL.Location);
|
|
DeclaimBuffer(&URL);
|
|
PushAssetLandmark(&CollationBuffers->IncludesSearch, ASSET_CSS_CINERA, PAGE_SEARCH);
|
|
|
|
ConstructResolvedAssetURL(&URL, ASSET_CSS_THEME, PAGE_SEARCH);
|
|
CopyStringToBuffer(&CollationBuffers->IncludesSearch,
|
|
"\">\n"
|
|
" <link rel=\"stylesheet\" type=\"text/css\" href=\"%s",
|
|
URL.Location);
|
|
DeclaimBuffer(&URL);
|
|
PushAssetLandmark(&CollationBuffers->IncludesSearch, ASSET_CSS_THEME, PAGE_SEARCH);
|
|
|
|
ConstructResolvedAssetURL(&URL, ASSET_CSS_TOPICS, PAGE_SEARCH);
|
|
CopyStringToBuffer(&CollationBuffers->IncludesSearch,
|
|
"\">\n"
|
|
" <link rel=\"stylesheet\" type=\"text/css\" href=\"%s",
|
|
URL.Location);
|
|
DeclaimBuffer(&URL);
|
|
PushAssetLandmark(&CollationBuffers->IncludesSearch, ASSET_CSS_TOPICS, PAGE_SEARCH);
|
|
|
|
CopyStringToBuffer(&CollationBuffers->IncludesSearch,
|
|
"\">\n");
|
|
|
|
int ProjectIndex;
|
|
for(ProjectIndex = 0; ProjectIndex < ArrayCount(ProjectInfo); ++ProjectIndex)
|
|
{
|
|
if(!StringsDiffer(ProjectInfo[ProjectIndex].ProjectID, Config.ProjectID))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
char *Theme = StringsDiffer(Config.Theme, "") ? Config.Theme : Config.ProjectID;
|
|
int Allowance = StringLength(Theme) * 2 + StringLength(Config.ProjectID) + StringLength(Config.BaseURL) + StringLength(Config.PlayerLocation);
|
|
char queryContainer[678 + Allowance]; // NOTE(matt): Update the size if changing the string
|
|
CopyString(queryContainer, sizeof(queryContainer),
|
|
"<div class=\"cineraQueryContainer %s\">\n"
|
|
" <label for=\"query\">Query:</label>\n"
|
|
" <div class=\"inputContainer\">\n"
|
|
" <input type=\"text\" autocomplete=\"off\" id=\"query\">\n"
|
|
" <div class=\"spinner\">\n"
|
|
" Downloading data...\n"
|
|
" </div>\n"
|
|
" </div>\n"
|
|
" </div>\n"
|
|
" <div id=\"cineraResultsSummary\">Found: 0 episodes, 0 markers, 0h 0m 0s total.</div>\n"
|
|
" <div id=\"cineraResults\" data-single=\"%d\"></div>\n"
|
|
"\n"
|
|
" <div id=\"cineraIndex\" class=\"%s\" data-project=\"%s\" data-baseURL=\"%s\" data-playerLocation=\"%s\">\n"
|
|
" <div id=\"cineraIndexSort\">Sort: Old to New ⏶</div>\n"
|
|
" <div id=\"cineraIndexEntries\">\n",
|
|
Theme,
|
|
Config.Mode & MODE_SINGLETAB ? 1 : 0,
|
|
Theme,
|
|
Config.ProjectID,
|
|
Config.BaseURL,
|
|
Config.PlayerLocation);
|
|
|
|
ConstructResolvedAssetURL(&URL, ASSET_JS_SEARCH, PAGE_SEARCH);
|
|
buffer Script;
|
|
ClaimBuffer(&Script, "Script", (117 + URL.Ptr - URL.Location) * 2); // NOTE(matt): Update the size if changing the string
|
|
CopyStringToBuffer(&Script,
|
|
" </div>\n"
|
|
" </div>\n"
|
|
" <script type=\"text/javascript\" src=\"%s", URL.Location);
|
|
// NOTE(matt): DeclaimBuffer(&URL) happens later, after Script is declaimed because Script was only claimed here
|
|
PushAssetLandmark(&Script, ASSET_JS_SEARCH, PAGE_SEARCH);
|
|
|
|
CopyStringToBuffer(&Script,
|
|
"\"></script>");
|
|
|
|
buffer PlayerURL;
|
|
ClaimBuffer(&PlayerURL, "PlayerURL", MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH);
|
|
ConstructPlayerURL(&PlayerURL, "");
|
|
char *ProjectUnit = 0;
|
|
if(StringsDiffer(ProjectInfo[ProjectIndex].Unit, ""))
|
|
{
|
|
ProjectUnit = ProjectInfo[ProjectIndex].Unit;
|
|
}
|
|
char Number[16];
|
|
db_entry *This;
|
|
char Text[(ProjectUnit ? StringLength(ProjectUnit) : 0) + sizeof(Number) + sizeof(This->Title) + 3];
|
|
|
|
int EntryLength = StringLength(PlayerURL.Location) + sizeof(Text) + 82;
|
|
CollationBuffers->Search.Size = StringLength(queryContainer) + (DB.EntriesHeader.Count * EntryLength) + Script.Ptr - Script.Location;
|
|
|
|
if(!(CollationBuffers->Search.Location = malloc(CollationBuffers->Search.Size))) { return RC_ERROR_MEMORY; }
|
|
|
|
CollationBuffers->Search.ID = "Search";
|
|
CollationBuffers->Search.Ptr = CollationBuffers->Search.Location;
|
|
|
|
CopyStringToBuffer(&CollationBuffers->Search, "%s", queryContainer);
|
|
|
|
int ProjectIDLength = StringLength(Config.ProjectID);
|
|
bool SearchRequired = FALSE;
|
|
|
|
DB.Metadata.Buffer.Ptr = DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader);
|
|
for(int EntryIndex = 0; EntryIndex < DB.EntriesHeader.Count; ++EntryIndex, DB.Metadata.Buffer.Ptr += sizeof(DB.Entry))
|
|
{
|
|
This = (db_entry *)DB.Metadata.Buffer.Ptr;
|
|
if(This->Size > 0)
|
|
{
|
|
SearchRequired = TRUE;
|
|
CopyString(Number, sizeof(Number), "%s", This->BaseFilename + ProjectIDLength);
|
|
if(ProjectInfo[ProjectIndex].NumberingScheme == NS_LINEAR)
|
|
{
|
|
for(int i = 0; Number[i]; ++i)
|
|
{
|
|
if(Number[i] == '_')
|
|
{
|
|
Number[i] = '.';
|
|
}
|
|
}
|
|
}
|
|
|
|
ConstructPlayerURL(&PlayerURL, This->BaseFilename);
|
|
|
|
if(ProjectUnit)
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Search,
|
|
" <div>\n"
|
|
" <a href=\"%s\">", PlayerURL.Location);
|
|
|
|
CopyString(Text, sizeof(Text), "%s %s: %s",
|
|
ProjectUnit, // TODO(matt): Do we need to special-case the various numbering schemes?
|
|
Number,
|
|
This->Title);
|
|
|
|
CopyStringToBufferHTMLSafe(&CollationBuffers->Search, Text);
|
|
|
|
CopyStringToBuffer(&CollationBuffers->Search,
|
|
"</a>\n"
|
|
" </div>\n");
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Search,
|
|
" <div>\n"
|
|
" <a href=\"%s\">", PlayerURL.Location);
|
|
|
|
CopyStringToBufferHTMLSafe(&CollationBuffers->Search, This->Title);
|
|
CopyStringToBuffer(&CollationBuffers->Search,
|
|
"</a>\n"
|
|
" </div>\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
OffsetLandmarks(&CollationBuffers->Search, ASSET_JS_SEARCH, PAGE_SEARCH);
|
|
CopyBuffer(&CollationBuffers->Search, &Script);
|
|
|
|
DeclaimBuffer(&PlayerURL);
|
|
DeclaimBuffer(&Script);
|
|
DeclaimBuffer(&URL);
|
|
|
|
if(!SearchRequired) { return RC_NOOP; }
|
|
else { return RC_SUCCESS; }
|
|
}
|
|
}
|
|
return RC_NOOP;
|
|
}
|
|
|
|
int
|
|
GeneratePlayerPage(neighbourhood *N, buffers *CollationBuffers, template *PlayerTemplate, char *BaseFilename, bool Reinserting)
|
|
{
|
|
buffer OutputDirectoryPath;
|
|
ClaimBuffer(&OutputDirectoryPath, "OutputDirectoryPath", MAX_BASE_DIR_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH + 1 + 10);
|
|
ConstructDirectoryPath(&OutputDirectoryPath, PAGE_PLAYER, Config.PlayerLocation, BaseFilename);
|
|
|
|
DIR *OutputDirectoryHandle;
|
|
if(!(OutputDirectoryHandle = opendir(OutputDirectoryPath.Location))) // TODO(matt): open()
|
|
{
|
|
if(MakeDir(OutputDirectoryPath.Location) == RC_ERROR_DIRECTORY)
|
|
{
|
|
LogError(LOG_ERROR, "Unable to create directory %s: %s", OutputDirectoryPath.Location, strerror(errno));
|
|
fprintf(stderr, "Unable to create directory %s: %s\n", OutputDirectoryPath.Location, strerror(errno));
|
|
return RC_ERROR_DIRECTORY;
|
|
};
|
|
}
|
|
closedir(OutputDirectoryHandle);
|
|
|
|
char PlayerPagePath[1024];
|
|
CopyString(PlayerPagePath, sizeof(PlayerPagePath), "%s/index.html", OutputDirectoryPath.Location);
|
|
DeclaimBuffer(&OutputDirectoryPath);
|
|
|
|
bool SearchInTemplate = FALSE;
|
|
for(int TagIndex = 0; TagIndex < PlayerTemplate->Metadata.TagCount; ++TagIndex)
|
|
{
|
|
if(PlayerTemplate->Metadata.Tags[TagIndex].TagCode == TAG_SEARCH)
|
|
{
|
|
SearchInTemplate = TRUE;
|
|
SearchToBuffer(CollationBuffers);
|
|
break;
|
|
}
|
|
}
|
|
|
|
BuffersToHTML(CollationBuffers, PlayerTemplate, PlayerPagePath, PAGE_PLAYER, &N->This.LinkOffsets.PrevStart);
|
|
// NOTE(matt): A previous InsertNeighbourLink() call will have done SnipeEntryIntoMetadataBuffer(), but we must do it here now
|
|
// that PrevStart has been adjusted by BuffersToHTML()
|
|
SnipeEntryIntoMetadataBuffer(&N->This, N->ThisIndex);
|
|
UpdateLandmarksForNeighbourhood(N, Reinserting ? EDIT_REINSERTION : EDIT_ADDITION);
|
|
|
|
if(SearchInTemplate)
|
|
{
|
|
FreeBuffer(&CollationBuffers->Search);
|
|
}
|
|
return RC_SUCCESS;
|
|
}
|
|
|
|
int
|
|
GenerateSearchPage(buffers *CollationBuffers, template *SearchTemplate)
|
|
{
|
|
buffer OutputDirectoryPath;
|
|
ClaimBuffer(&OutputDirectoryPath, "OutputDirectoryPath", MAX_BASE_DIR_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + 10);
|
|
ConstructDirectoryPath(&OutputDirectoryPath, PAGE_SEARCH, Config.SearchLocation, 0);
|
|
|
|
DIR *OutputDirectoryHandle;
|
|
if(!(OutputDirectoryHandle = opendir(OutputDirectoryPath.Location))) // TODO(matt): open()
|
|
{
|
|
if(MakeDir(OutputDirectoryPath.Location) == RC_ERROR_DIRECTORY)
|
|
{
|
|
LogError(LOG_ERROR, "Unable to create directory %s: %s", OutputDirectoryPath.Location, strerror(errno));
|
|
fprintf(stderr, "Unable to create directory %s: %s\n", OutputDirectoryPath.Location, strerror(errno));
|
|
return RC_ERROR_DIRECTORY;
|
|
};
|
|
}
|
|
closedir(OutputDirectoryHandle);
|
|
|
|
char SearchPagePath[1024];
|
|
CopyString(SearchPagePath, sizeof(SearchPagePath), "%s/index.html", OutputDirectoryPath.Location);
|
|
DeclaimBuffer(&OutputDirectoryPath);
|
|
switch(SearchToBuffer(CollationBuffers))
|
|
{
|
|
case RC_SUCCESS:
|
|
{
|
|
BuffersToHTML(CollationBuffers, SearchTemplate, SearchPagePath, PAGE_SEARCH, 0);
|
|
UpdateLandmarksForSearch();
|
|
break;
|
|
}
|
|
case RC_NOOP:
|
|
{
|
|
DeleteSearchPageFromFilesystem();
|
|
DeleteLandmarksForSearch();
|
|
break;
|
|
}
|
|
}
|
|
FreeBuffer(&CollationBuffers->Search);
|
|
return RC_SUCCESS;
|
|
}
|
|
|
|
int
|
|
DeleteEntry(neighbourhood *Neighbourhood, char *BaseFilename)
|
|
{
|
|
if(DeleteFromDB(Neighbourhood, BaseFilename) == RC_SUCCESS)
|
|
{
|
|
LinkNeighbours(Neighbourhood, LINK_EXCLUDE);
|
|
DeletePlayerPageFromFilesystem(BaseFilename, Config.PlayerLocation, FALSE);
|
|
UpdateLandmarksForNeighbourhood(Neighbourhood, EDIT_DELETION);
|
|
return RC_SUCCESS;
|
|
}
|
|
return RC_NOOP;
|
|
}
|
|
|
|
int
|
|
InsertEntry(neighbourhood *Neighbourhood, buffers *CollationBuffers, template *PlayerTemplate, template *BespokeTemplate, char *BaseFilename, bool RecheckingPrivacy)
|
|
{
|
|
bool Reinserting = FALSE;
|
|
if(InsertIntoDB(Neighbourhood, CollationBuffers, BespokeTemplate, BaseFilename, RecheckingPrivacy, &Reinserting) == RC_SUCCESS)
|
|
{
|
|
LinkNeighbours(Neighbourhood, LINK_INCLUDE);
|
|
if(BespokeTemplate->File.Buffer.Location)
|
|
{
|
|
GeneratePlayerPage(Neighbourhood, CollationBuffers, BespokeTemplate, BaseFilename, Reinserting);
|
|
FreeTemplate(BespokeTemplate);
|
|
}
|
|
else
|
|
{
|
|
GeneratePlayerPage(Neighbourhood, CollationBuffers, PlayerTemplate, BaseFilename, Reinserting);
|
|
}
|
|
return RC_SUCCESS;
|
|
}
|
|
return RC_NOOP;
|
|
}
|
|
|
|
int
|
|
RecheckPrivacy(buffers *CollationBuffers, template *SearchTemplate, template *PlayerTemplate, template *BespokeTemplate)
|
|
{
|
|
if(DB.Metadata.FileSize > 0)
|
|
{
|
|
DB.Header = *(db_header *)DB.Metadata.Buffer.Location;
|
|
DB.EntriesHeader = *(db_header_entries *)(DB.Metadata.Buffer.Location + sizeof(DB.Header));
|
|
db_entry Entry = { };
|
|
int PrivateEntryIndex = 0;
|
|
db_entry PrivateEntries[DB.EntriesHeader.Count];
|
|
bool Inserted = FALSE;
|
|
for(int IndexEntry = 0; IndexEntry < DB.EntriesHeader.Count; ++IndexEntry)
|
|
{
|
|
Entry = *(db_entry *)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * IndexEntry);
|
|
if(Entry.Size == 0)
|
|
{
|
|
PrivateEntries[PrivateEntryIndex] = Entry;
|
|
++PrivateEntryIndex;
|
|
}
|
|
}
|
|
|
|
for(int i = 0; i < PrivateEntryIndex; ++i)
|
|
{
|
|
neighbourhood Neighbourhood = { };
|
|
InitNeighbourhood(&Neighbourhood);
|
|
ResetAssetLandmarks();
|
|
Inserted = (InsertEntry(&Neighbourhood, CollationBuffers, PlayerTemplate, BespokeTemplate, PrivateEntries[i].BaseFilename, TRUE) == RC_SUCCESS);
|
|
}
|
|
|
|
if(Inserted)
|
|
{
|
|
GenerateSearchPage(CollationBuffers, SearchTemplate);
|
|
DeleteStaleAssets();
|
|
}
|
|
|
|
LastPrivacyCheck = time(0);
|
|
}
|
|
return RC_SUCCESS;
|
|
}
|
|
|
|
#define DEBUG_LANDMARKS 0
|
|
#if DEBUG_LANDMARKS
|
|
void
|
|
VerifyLandmarks(void)
|
|
{
|
|
printf("\n"
|
|
"VerifyLandmarks()\n");
|
|
DB.Metadata.Buffer.Ptr = DB.Metadata.Buffer.Location;
|
|
DB.Header = *(db_header *)DB.Metadata.Buffer.Ptr;
|
|
DB.Metadata.Buffer.Ptr += sizeof(DB.Header);
|
|
DB.EntriesHeader = *(db_header_entries *)DB.Metadata.Buffer.Ptr;
|
|
DB.Metadata.Buffer.Ptr += sizeof(DB.EntriesHeader);
|
|
char *FirstEntry = DB.Metadata.Buffer.Ptr;
|
|
|
|
DB.Metadata.Buffer.Ptr += sizeof(DB.Entry) * DB.EntriesHeader.Count;
|
|
DB.AssetsHeader = *(db_header_assets *)DB.Metadata.Buffer.Ptr;
|
|
DB.Metadata.Buffer.Ptr += sizeof(DB.AssetsHeader);
|
|
char *FirstAsset = DB.Metadata.Buffer.Ptr;
|
|
|
|
buffer OutputDirectoryPath;
|
|
ClaimBuffer(&OutputDirectoryPath, "OutputDirectoryPath", MAX_BASE_DIR_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH + 1 + 10);
|
|
|
|
bool UniversalSuccess = TRUE;
|
|
|
|
// Search
|
|
{
|
|
file_buffer HTML;
|
|
ReadSearchPageIntoBuffer(&HTML);
|
|
char *Cursor = FirstAsset;
|
|
for(int j = 0; j < DB.AssetsHeader.Count; ++j)
|
|
{
|
|
DB.Asset = *(db_asset *)Cursor;
|
|
Cursor += sizeof(DB.Asset);
|
|
landmark_range Target = BinarySearchForMetadataLandmark(Cursor, PAGE_TYPE_SEARCH, DB.Asset.LandmarkCount);
|
|
for(int k = 0; k < Target.Length; ++k)
|
|
{
|
|
DB.Landmark = *(db_landmark *)(Cursor + sizeof(db_landmark) * (Target.First + k));
|
|
bool Found = FALSE;
|
|
for(int l = 0; l < Assets.Count; ++l)
|
|
{
|
|
// TODO(matt): StringToInt (base16)
|
|
char HashString[9];
|
|
sprintf(HashString, "%08x", Assets.Asset[l].Hash);
|
|
|
|
if(HTML.Buffer.Size >= DB.Landmark.Position + 8 && !StringsDifferT(HashString, HTML.Buffer.Location + DB.Landmark.Position, 0))
|
|
{
|
|
Found = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
if(!Found)
|
|
{
|
|
printf("Failure!!!\n");
|
|
printf(" %s\n", HTML.Path);
|
|
printf(" %.*s [at byte %d] is not a known asset hash\n", 8, HTML.Buffer.Location + DB.Landmark.Position, DB.Landmark.Position);
|
|
UniversalSuccess = FALSE;
|
|
}
|
|
}
|
|
Cursor += sizeof(DB.Landmark) * DB.Asset.LandmarkCount;
|
|
}
|
|
FreeBuffer(&HTML.Buffer);
|
|
}
|
|
|
|
for(int i = 0; i < DB.EntriesHeader.Count; ++i)
|
|
{
|
|
DB.Entry = *(db_entry *)(FirstEntry + sizeof(DB.Entry) * i);
|
|
ConstructDirectoryPath(&OutputDirectoryPath, PAGE_PLAYER, Config.PlayerLocation, DB.Entry.BaseFilename);
|
|
file_buffer HTML;
|
|
CopyString(HTML.Path, sizeof(HTML.Path), "%s/index.html", OutputDirectoryPath.Location);
|
|
if(ReadFileIntoBuffer(&HTML, 0) == RC_SUCCESS)
|
|
{
|
|
char *Cursor = FirstAsset;
|
|
for(int j = 0; j < DB.AssetsHeader.Count; ++j)
|
|
{
|
|
DB.Asset = *(db_asset *)Cursor;
|
|
Cursor += sizeof(DB.Asset);
|
|
landmark_range Target = BinarySearchForMetadataLandmark(Cursor, i, DB.Asset.LandmarkCount);
|
|
for(int k = 0; k < Target.Length; ++k)
|
|
{
|
|
DB.Landmark = *(db_landmark *)(Cursor + sizeof(db_landmark) * (Target.First + k));
|
|
bool Found = FALSE;
|
|
for(int l = 0; l < Assets.Count; ++l)
|
|
{
|
|
// TODO(matt): StringToInt (base16)
|
|
char HashString[9];
|
|
sprintf(HashString, "%08x", Assets.Asset[l].Hash);
|
|
|
|
if((HTML.Buffer.Size >= DB.Landmark.Position + 8) && !StringsDifferT(HashString, HTML.Buffer.Location + DB.Landmark.Position, 0))
|
|
{
|
|
Found = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
if(!Found)
|
|
{
|
|
printf("%sFailure ↓%s\n", ColourStrings[CS_ERROR], ColourStrings[CS_END]);
|
|
printf(" %s\n", HTML.Path);
|
|
printf(" %.*s [at byte %d] is not a known asset hash\n", 8, HTML.Buffer.Location + DB.Landmark.Position, DB.Landmark.Position);
|
|
UniversalSuccess = FALSE;
|
|
}
|
|
}
|
|
Cursor += sizeof(DB.Landmark) * DB.Asset.LandmarkCount;
|
|
}
|
|
FreeBuffer(&HTML.Buffer);
|
|
}
|
|
else
|
|
{
|
|
printf("%sFailed to open%s %s\n", ColourStrings[CS_ERROR], ColourStrings[CS_END], HTML.Path);
|
|
}
|
|
}
|
|
Assert(UniversalSuccess);
|
|
printf("Success! All landmarks correspond to asset hashes\n");
|
|
DeclaimBuffer(&OutputDirectoryPath);
|
|
}
|
|
#endif
|
|
|
|
void
|
|
UpdateDeferredAssetChecksums(void)
|
|
{
|
|
for(int i = 0; i < Assets.Count; ++i)
|
|
{
|
|
if(Assets.Asset[i].DeferredUpdate)
|
|
{
|
|
UpdateAssetInDB(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
#define DEBUG_EVENTS 0
|
|
#if DEBUG_EVENTS
|
|
void
|
|
PrintEvent(struct inotify_event *Event, int EventIndex)
|
|
{
|
|
|
|
printf("\nEvent[%d]\n"
|
|
" wd: %d\n"
|
|
" mask: %d\n",
|
|
EventIndex,
|
|
Event->wd,
|
|
Event->mask);
|
|
|
|
if(Event->mask & IN_ACCESS) { printf(" IN_ACCESS\n"); }
|
|
if(Event->mask & IN_ATTRIB) { printf(" IN_ATTRIB\n"); }
|
|
if(Event->mask & IN_CLOSE_WRITE) { printf(" IN_CLOSE_WRITE\n"); }
|
|
if(Event->mask & IN_CLOSE_NOWRITE) { printf(" IN_CLOSE_NOWRITE\n"); }
|
|
if(Event->mask & IN_CREATE) { printf(" IN_CREATE\n"); }
|
|
if(Event->mask & IN_DELETE) { printf(" IN_DELETE\n"); }
|
|
if(Event->mask & IN_DELETE_SELF) { printf(" IN_DELETE_SELF\n"); }
|
|
if(Event->mask & IN_MODIFY) { printf(" IN_MODIFY\n"); }
|
|
if(Event->mask & IN_MOVE_SELF) { printf(" IN_MOVE_SELF\n"); }
|
|
if(Event->mask & IN_MOVED_FROM) { printf(" IN_MOVED_FROM\n"); }
|
|
if(Event->mask & IN_MOVED_TO) { printf(" IN_MOVED_TO\n"); }
|
|
if(Event->mask & IN_OPEN) { printf(" IN_OPEN\n"); }
|
|
if(Event->mask & IN_MOVE) { printf(" IN_MOVE\n"); }
|
|
if(Event->mask & IN_CLOSE) { printf(" IN_CLOSE\n"); }
|
|
if(Event->mask & IN_DONT_FOLLOW) { printf(" IN_DONT_FOLLOW\n"); }
|
|
if(Event->mask & IN_EXCL_UNLINK) { printf(" IN_EXCL_UNLINK\n"); }
|
|
if(Event->mask & IN_MASK_ADD) { printf(" IN_MASK_ADD\n"); }
|
|
if(Event->mask & IN_ONESHOT) { printf(" IN_ONESHOT\n"); }
|
|
if(Event->mask & IN_ONLYDIR) { printf(" IN_ONLYDIR\n"); }
|
|
if(Event->mask & IN_IGNORED) { printf(" IN_IGNORED\n"); }
|
|
if(Event->mask & IN_ISDIR) { printf(" IN_ISDIR\n"); }
|
|
if(Event->mask & IN_Q_OVERFLOW) { printf(" IN_Q_OVERFLOW\n"); }
|
|
if(Event->mask & IN_UNMOUNT) { printf(" IN_UNMOUNT\n"); }
|
|
|
|
printf( " cookie: %d\n"
|
|
" len: %d\n"
|
|
" name: %s\n",
|
|
Event->cookie,
|
|
Event->len,
|
|
Event->name);
|
|
}
|
|
#endif
|
|
|
|
int
|
|
MonitorFilesystem(buffers *CollationBuffers, template *SearchTemplate, template *PlayerTemplate, template *BespokeTemplate)
|
|
{
|
|
buffer Events;
|
|
if(ClaimBuffer(&Events, "inotify Events", Kilobytes(4)) == RC_ARENA_FULL) { return RC_ARENA_FULL; };
|
|
Clear(Events.Location, Events.Size);
|
|
|
|
struct inotify_event *Event;
|
|
int BytesRead = read(inotifyInstance, Events.Location, Events.Size); // TODO(matt): Handle error EINVAL
|
|
if(inotifyInstance < 0) { perror("MonitorFilesystem()"); }
|
|
|
|
// TODO(matt): Test this with longer update intervals, and combinations of events...
|
|
#if DEBUG_EVENTS
|
|
int i = 0;
|
|
#endif
|
|
bool Deleted = FALSE;
|
|
bool Inserted = FALSE;
|
|
bool UpdatedAsset = FALSE;
|
|
|
|
for(Events.Ptr = Events.Location;
|
|
Events.Ptr - Events.Location < BytesRead && Events.Ptr - Events.Location < Events.Size;
|
|
Events.Ptr += sizeof(struct inotify_event) + Event->len
|
|
#if DEBUG_EVENTS
|
|
, ++i
|
|
#endif
|
|
)
|
|
{
|
|
Event = (struct inotify_event *)Events.Ptr;
|
|
|
|
#if DEBUG_EVENTS
|
|
PrintEvent(Event, i);
|
|
#endif
|
|
//PrintWatchHandles();
|
|
|
|
for(int WatchHandleIndex = 0; WatchHandleIndex < WatchHandles.Count; ++WatchHandleIndex)
|
|
{
|
|
if(Event->wd == WatchHandles.Handle[WatchHandleIndex].Descriptor)
|
|
{
|
|
switch(WatchHandles.Handle[WatchHandleIndex].Type)
|
|
{
|
|
case WT_HMML:
|
|
{
|
|
char *Ptr = Event->name;
|
|
Ptr += (StringLength(Event->name) - StringLength(".hmml"));
|
|
if(!(StringsDiffer(Ptr, ".hmml")))
|
|
{
|
|
*Ptr = '\0';
|
|
|
|
neighbourhood Neighbourhood = { };
|
|
InitNeighbourhood(&Neighbourhood);
|
|
ResetAssetLandmarks();
|
|
|
|
if(Event->mask & IN_DELETE)
|
|
{
|
|
Deleted |= (DeleteEntry(&Neighbourhood, Event->name) == RC_SUCCESS);
|
|
}
|
|
else if(Event->mask & IN_CLOSE_WRITE)
|
|
{
|
|
Inserted |= (InsertEntry(&Neighbourhood, CollationBuffers, PlayerTemplate, BespokeTemplate, Event->name, 0) == RC_SUCCESS);
|
|
}
|
|
}
|
|
} break;
|
|
case WT_ASSET:
|
|
{
|
|
for(int i = 0; i < Assets.Count; ++i)
|
|
{
|
|
if(!StringsDiffer(Event->name, Assets.Asset[i].Filename + Assets.Asset[i].FilenameAt))
|
|
{
|
|
UpdateAsset(i, 0);
|
|
UpdatedAsset = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
} break;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(Deleted || Inserted)
|
|
{
|
|
UpdateDeferredAssetChecksums();
|
|
GenerateSearchPage(CollationBuffers, SearchTemplate);
|
|
DeleteStaleAssets();
|
|
#if DEBUG_LANDMARKS
|
|
VerifyLandmarks();
|
|
#endif
|
|
}
|
|
|
|
if(UpdatedAsset)
|
|
{
|
|
#if DEBUG_LANDMARKS
|
|
VerifyLandmarks();
|
|
#endif
|
|
}
|
|
|
|
DeclaimBuffer(&Events);
|
|
return RC_SUCCESS;
|
|
}
|
|
|
|
int
|
|
RemoveDirectory(char *Path)
|
|
{
|
|
if((remove(Path) == -1))
|
|
{
|
|
LogError(LOG_NOTICE, "Unable to remove directory %s: %s", Path, strerror(errno));
|
|
fprintf(stderr, "%sUnable to remove directory%s %s: %s\n", ColourStrings[CS_WARNING], ColourStrings[CS_END], Path, strerror(errno));
|
|
return RC_ERROR_DIRECTORY;
|
|
}
|
|
else
|
|
{
|
|
LogError(LOG_INFORMATIONAL, "Deleted %s", Path);
|
|
//fprintf(stderr, "%sDeleted%s %s\n", ColourStrings[CS_DELETION], ColourStrings[CS_END], Path);
|
|
return RC_SUCCESS;
|
|
}
|
|
}
|
|
|
|
int
|
|
RemoveDirectoryRecursively(char *Path)
|
|
{
|
|
if(RemoveDirectory(Path) == RC_ERROR_DIRECTORY) { return RC_ERROR_DIRECTORY; }
|
|
char *Ptr = Path + StringLength(Path) - 1;
|
|
while(Ptr > Path)
|
|
{
|
|
if(*Ptr == '/')
|
|
{
|
|
*Ptr = '\0';
|
|
if(RemoveDirectory(Path) == RC_ERROR_DIRECTORY) { return RC_ERROR_DIRECTORY; }
|
|
}
|
|
--Ptr;
|
|
}
|
|
return RC_SUCCESS;
|
|
}
|
|
|
|
int
|
|
UpgradeDB(int OriginalDBVersion)
|
|
{
|
|
// NOTE(matt): For each new DB version, we must declare and initialise one instance of each preceding version, only cast the
|
|
// incoming DB to the type of the OriginalDBVersion, and move into the final case all operations on that incoming DB
|
|
|
|
bool OnlyHeaderChanged = TRUE;
|
|
int OriginalHeaderSize = 0;
|
|
database1 DB1 = { };
|
|
database2 DB2 = { };
|
|
database3 DB3 = { };
|
|
|
|
switch(OriginalDBVersion)
|
|
{
|
|
case 1:
|
|
{
|
|
OriginalHeaderSize = sizeof(db_header1);
|
|
DB1.Header = *(db_header1 *)DB.Metadata.Buffer.Location;
|
|
|
|
DB2.Header.DBVersion = CINERA_DB_VERSION;
|
|
DB2.Header.AppVersion = CINERA_APP_VERSION;
|
|
DB2.Header.HMMLVersion.Major = hmml_version.Major;
|
|
DB2.Header.HMMLVersion.Minor = hmml_version.Minor;
|
|
DB2.Header.HMMLVersion.Patch = hmml_version.Patch;
|
|
DB2.Header.EntryCount = DB1.Header.EntryCount;
|
|
|
|
Clear(DB2.Header.SearchLocation, sizeof(DB2.Header.SearchLocation));
|
|
Clear(DB2.Header.PlayerLocation, sizeof(DB2.Header.PlayerLocation));
|
|
}
|
|
case 2:
|
|
{
|
|
if(OriginalDBVersion == 2)
|
|
{
|
|
OriginalHeaderSize = sizeof(db_header2);
|
|
DB2.Header = *(db_header2 *)DB.Metadata.Buffer.Location;
|
|
|
|
DB.Header.InitialDBVersion = DB2.Header.DBVersion;
|
|
DB.Header.InitialAppVersion = DB2.Header.AppVersion;
|
|
DB.Header.InitialHMMLVersion.Major = DB2.Header.HMMLVersion.Major;
|
|
DB.Header.InitialHMMLVersion.Minor = DB2.Header.HMMLVersion.Minor;
|
|
DB.Header.InitialHMMLVersion.Patch = DB2.Header.HMMLVersion.Patch;
|
|
|
|
}
|
|
|
|
DB.EntriesHeader.Count = DB2.Header.EntryCount;
|
|
|
|
ClearCopyStringNoFormat(DB.EntriesHeader.ProjectID, sizeof(DB.EntriesHeader.ProjectID), Config.ProjectID);
|
|
|
|
Clear(DB.EntriesHeader.ProjectName, sizeof(DB.EntriesHeader.ProjectName));
|
|
for(int ProjectIndex = 0; ProjectIndex < ArrayCount(ProjectInfo); ++ProjectIndex)
|
|
{
|
|
if(!StringsDiffer(ProjectInfo[ProjectIndex].ProjectID, Config.ProjectID))
|
|
{
|
|
CopyString(DB.EntriesHeader.ProjectName, sizeof(DB.EntriesHeader.ProjectName), "%s", ProjectInfo[ProjectIndex].FullName);
|
|
break;
|
|
}
|
|
}
|
|
|
|
ClearCopyStringNoFormat(DB.EntriesHeader.BaseURL, sizeof(DB.EntriesHeader.BaseURL), Config.BaseURL);
|
|
ClearCopyStringNoFormat(DB.EntriesHeader.SearchLocation, sizeof(DB.EntriesHeader.SearchLocation), DB2.Header.SearchLocation);
|
|
ClearCopyStringNoFormat(DB.EntriesHeader.PlayerLocation, sizeof(DB.EntriesHeader.PlayerLocation), DB2.Header.PlayerLocation);
|
|
ClearCopyStringNoFormat(DB.EntriesHeader.PlayerURLPrefix, sizeof(DB.EntriesHeader.PlayerURLPrefix), Config.PlayerURLPrefix);
|
|
|
|
OnlyHeaderChanged = FALSE;
|
|
|
|
if(!(DB.Metadata.Handle = fopen(DB.Metadata.Path, "w"))) { FreeBuffer(&DB.Metadata.Buffer); return RC_ERROR_FILE; }
|
|
fwrite(&DB.Header, sizeof(DB.Header), 1, DB.Metadata.Handle);
|
|
|
|
DB.Metadata.Buffer.Ptr = DB.Metadata.Buffer.Location + OriginalHeaderSize;
|
|
DB.File.Buffer.Ptr += StringLength("---\n");
|
|
|
|
for(int EntryIndex = 0; EntryIndex < DB.EntriesHeader.Count; ++EntryIndex)
|
|
{
|
|
// NOTE(matt): We can use either db_entry1 or 2 here because they are the same
|
|
db_entry2 This = *(db_entry2 *)DB.Metadata.Buffer.Ptr;
|
|
|
|
DB.Entry.LinkOffsets.PrevStart = 0;
|
|
DB.Entry.LinkOffsets.NextStart = 0;
|
|
DB.Entry.LinkOffsets.PrevEnd = 0;
|
|
DB.Entry.LinkOffsets.NextEnd = 0;
|
|
|
|
DB.Entry.Size = This.Size;
|
|
|
|
ClearCopyStringNoFormat(DB.Entry.BaseFilename, sizeof(DB.Entry.BaseFilename), This.BaseFilename);
|
|
|
|
char *EntryStart = DB.File.Buffer.Ptr;
|
|
SeekBufferForString(&DB.File.Buffer, "title: \"", C_SEEK_FORWARDS, C_SEEK_AFTER);
|
|
Clear(DB.Entry.Title, sizeof(DB.Entry.Title));
|
|
CopyStringNoFormatT(DB.Entry.Title, sizeof(DB.Entry.Title), DB.File.Buffer.Ptr, '\n');
|
|
DB.Entry.Title[StringLength(DB.Entry.Title) - 1] = '\0';
|
|
|
|
fwrite(&DB.Entry, sizeof(DB.Entry), 1, DB.Metadata.Handle);
|
|
|
|
DB.Metadata.Buffer.Ptr += sizeof(This);
|
|
EntryStart += This.Size;
|
|
DB.File.Buffer.Ptr = EntryStart;
|
|
}
|
|
|
|
CycleFile(&DB.Metadata);
|
|
}
|
|
case 3:
|
|
{
|
|
OriginalHeaderSize = sizeof(db_header3);
|
|
DB3.Header = *(db_header3 *)DB.Metadata.Buffer.Location;
|
|
DB.Header.HexSignature = FOURCC("CNRA");
|
|
|
|
DB.Header.CurrentDBVersion = CINERA_DB_VERSION;
|
|
DB.Header.CurrentAppVersion = CINERA_APP_VERSION;
|
|
DB.Header.CurrentHMMLVersion.Major = hmml_version.Major;
|
|
DB.Header.CurrentHMMLVersion.Minor = hmml_version.Minor;
|
|
DB.Header.CurrentHMMLVersion.Patch = hmml_version.Patch;
|
|
|
|
DB.Header.InitialDBVersion = DB3.Header.InitialDBVersion;
|
|
DB.Header.InitialAppVersion = DB3.Header.InitialAppVersion;
|
|
DB.Header.InitialHMMLVersion.Major = DB3.Header.InitialHMMLVersion.Major;
|
|
DB.Header.InitialHMMLVersion.Minor = DB3.Header.InitialHMMLVersion.Minor;
|
|
DB.Header.InitialHMMLVersion.Patch = DB3.Header.InitialHMMLVersion.Patch;
|
|
DB.Header.BlockCount = 0;
|
|
|
|
// TODO(matt): Consider allowing zero NTRY or ASET blocks in a future version
|
|
DB.EntriesHeader.BlockID = FOURCC("NTRY");
|
|
DB.EntriesHeader.Count = DB3.Header.EntryCount;
|
|
ClearCopyStringNoFormat(DB.EntriesHeader.ProjectID, sizeof(DB.EntriesHeader.ProjectID), DB3.Header.ProjectID);
|
|
ClearCopyStringNoFormat(DB.EntriesHeader.ProjectName, sizeof(DB.EntriesHeader.ProjectName), DB3.Header.ProjectName);
|
|
ClearCopyStringNoFormat(DB.EntriesHeader.BaseURL, sizeof(DB.EntriesHeader.BaseURL), DB3.Header.BaseURL);
|
|
ClearCopyStringNoFormat(DB.EntriesHeader.SearchLocation, sizeof(DB.EntriesHeader.SearchLocation), DB3.Header.SearchLocation);
|
|
ClearCopyStringNoFormat(DB.EntriesHeader.PlayerLocation, sizeof(DB.EntriesHeader.PlayerLocation), DB3.Header.PlayerLocation);
|
|
ClearCopyStringNoFormat(DB.EntriesHeader.PlayerURLPrefix, sizeof(DB.EntriesHeader.PlayerURLPrefix), DB3.Header.PlayerURLPrefix);
|
|
++DB.Header.BlockCount;
|
|
|
|
DB.AssetsHeader.BlockID = FOURCC("ASET");
|
|
DB.AssetsHeader.Count = 0;
|
|
ClearCopyStringNoFormat(DB.AssetsHeader.RootDir, sizeof(DB.AssetsHeader.RootDir), Config.RootDir);
|
|
ClearCopyStringNoFormat(DB.AssetsHeader.RootURL, sizeof(DB.AssetsHeader.RootURL), Config.RootURL);
|
|
ClearCopyStringNoFormat(DB.AssetsHeader.CSSDir, sizeof(DB.AssetsHeader.CSSDir), Config.CSSDir);
|
|
ClearCopyStringNoFormat(DB.AssetsHeader.ImagesDir, sizeof(DB.AssetsHeader.ImagesDir), Config.ImagesDir);
|
|
ClearCopyStringNoFormat(DB.AssetsHeader.JSDir, sizeof(DB.AssetsHeader.JSDir), Config.JSDir);
|
|
++DB.Header.BlockCount;
|
|
|
|
OnlyHeaderChanged = FALSE;
|
|
|
|
if(!(DB.Metadata.Handle = fopen(DB.Metadata.Path, "w"))) { FreeBuffer(&DB.Metadata.Buffer); return RC_ERROR_FILE; }
|
|
|
|
fwrite(&DB.Header, sizeof(DB.Header), 1, DB.Metadata.Handle);
|
|
fwrite(&DB.EntriesHeader, sizeof(DB.EntriesHeader), 1, DB.Metadata.Handle);
|
|
fwrite(DB.Metadata.Buffer.Location + OriginalHeaderSize,
|
|
sizeof(DB.Entry),
|
|
DB.EntriesHeader.Count,
|
|
DB.Metadata.Handle);
|
|
|
|
fwrite(&DB.AssetsHeader, sizeof(DB.AssetsHeader), 1, DB.Metadata.Handle);
|
|
|
|
CycleFile(&DB.Metadata);
|
|
}
|
|
// TODO(matt); DBVersion 4 is the first that uses HexSignatures
|
|
// We should try and deprecate earlier versions and just enforce a clean rebuild for invalid DB files
|
|
// Perhaps do this either the next time we have to bump the version, or when we scrap Single Edition
|
|
}
|
|
|
|
if(OnlyHeaderChanged)
|
|
{
|
|
if(!(DB.Metadata.Handle = fopen(DB.Metadata.Path, "w"))) { FreeBuffer(&DB.Metadata.Buffer); return RC_ERROR_FILE; }
|
|
fwrite(&DB.Header, sizeof(DB.Header), 1, DB.Metadata.Handle);
|
|
fwrite(DB.Metadata.Buffer.Location + OriginalHeaderSize, DB.Metadata.FileSize - OriginalHeaderSize, 1, DB.Metadata.Handle);
|
|
CycleFile(&DB.Metadata);
|
|
}
|
|
|
|
fprintf(stderr, "\n%sUpgraded Cinera DB from %d to %d!%s\n\n", ColourStrings[CS_SUCCESS], OriginalDBVersion, DB.Header.CurrentDBVersion, ColourStrings[CS_END]);
|
|
return RC_SUCCESS;
|
|
}
|
|
|
|
typedef struct
|
|
{
|
|
bool Present;
|
|
char ID[32];
|
|
} entry_presence_id; // Metadata, unless we actually want to bolster this?
|
|
|
|
void
|
|
RemoveChildDirectories(buffer FullPath, char *ParentDirectory)
|
|
{
|
|
char *Ptr = FullPath.Location + StringLength(ParentDirectory);
|
|
RemoveDirectory(FullPath.Location);
|
|
while(FullPath.Ptr > Ptr)
|
|
{
|
|
if(*FullPath.Ptr == '/')
|
|
{
|
|
*FullPath.Ptr = '\0';
|
|
RemoveDirectory(FullPath.Location);
|
|
}
|
|
--FullPath.Ptr;
|
|
}
|
|
}
|
|
|
|
int
|
|
DeleteDeadDBEntries(void)
|
|
{
|
|
bool NewPlayerLocation = FALSE;
|
|
bool NewSearchLocation = FALSE;
|
|
if(StringsDiffer(DB.EntriesHeader.PlayerLocation, Config.PlayerLocation))
|
|
{
|
|
buffer OldPlayerDirectory;
|
|
ClaimBuffer(&OldPlayerDirectory, "OldPlayerDirectory",
|
|
MAX_BASE_DIR_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH + 1);
|
|
ConstructDirectoryPath(&OldPlayerDirectory, PAGE_PLAYER, DB.EntriesHeader.PlayerLocation, 0);
|
|
buffer NewPlayerDirectory;
|
|
ClaimBuffer(&NewPlayerDirectory, "NewPlayerDirectory",
|
|
MAX_BASE_DIR_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH + 1);
|
|
ConstructDirectoryPath(&NewPlayerDirectory, PAGE_PLAYER, Config.PlayerLocation, 0);
|
|
printf("%sRelocating Player Page%s from %s to %s%s\n",
|
|
ColourStrings[CS_REINSERTION], DB.EntriesHeader.Count > 1 ? "s" : "",
|
|
OldPlayerDirectory.Location, NewPlayerDirectory.Location, ColourStrings[CS_END]);
|
|
DeclaimBuffer(&NewPlayerDirectory);
|
|
|
|
for(int EntryIndex = 0; EntryIndex < DB.EntriesHeader.Count; ++EntryIndex)
|
|
{
|
|
db_entry This = *(db_entry *)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * EntryIndex);
|
|
ConstructDirectoryPath(&OldPlayerDirectory, PAGE_PLAYER, DB.EntriesHeader.PlayerLocation, This.BaseFilename);
|
|
DeletePlayerPageFromFilesystem(This.BaseFilename, DB.EntriesHeader.PlayerLocation, TRUE);
|
|
}
|
|
|
|
ConstructDirectoryPath(&OldPlayerDirectory, PAGE_PLAYER, DB.EntriesHeader.PlayerLocation, 0);
|
|
|
|
if(StringLength(DB.EntriesHeader.PlayerLocation) > 0)
|
|
{
|
|
RemoveChildDirectories(OldPlayerDirectory, Config.BaseDir);
|
|
}
|
|
|
|
DeclaimBuffer(&OldPlayerDirectory);
|
|
|
|
ClearCopyStringNoFormat(DB.EntriesHeader.PlayerLocation, sizeof(DB.EntriesHeader.PlayerLocation), Config.PlayerLocation);
|
|
*(db_header *)DB.Metadata.Buffer.Location = DB.Header;
|
|
NewPlayerLocation = TRUE;
|
|
}
|
|
if(StringsDiffer(DB.EntriesHeader.SearchLocation, Config.SearchLocation))
|
|
{
|
|
buffer OldSearchDirectory;
|
|
ClaimBuffer(&OldSearchDirectory, "OldSearchDirectory", MAX_BASE_DIR_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1);
|
|
ConstructDirectoryPath(&OldSearchDirectory, PAGE_SEARCH, DB.EntriesHeader.SearchLocation, 0);
|
|
buffer NewSearchDirectory;
|
|
ClaimBuffer(&NewSearchDirectory, "NewSearchDirectory", MAX_BASE_DIR_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1);
|
|
ConstructDirectoryPath(&NewSearchDirectory, PAGE_SEARCH, Config.SearchLocation, 0);
|
|
printf("%sRelocating Search Page from %s to %s%s\n",
|
|
ColourStrings[CS_REINSERTION], OldSearchDirectory.Location, NewSearchDirectory.Location, ColourStrings[CS_END]);
|
|
DeclaimBuffer(&NewSearchDirectory);
|
|
|
|
char SearchPagePath[2048] = { 0 };
|
|
CopyString(SearchPagePath, sizeof(SearchPagePath), "%s/index.html", OldSearchDirectory.Location);
|
|
remove(SearchPagePath);
|
|
if(StringLength(DB.EntriesHeader.SearchLocation) > 0)
|
|
{
|
|
RemoveChildDirectories(OldSearchDirectory, Config.BaseDir);
|
|
}
|
|
DeclaimBuffer(&OldSearchDirectory);
|
|
|
|
ClearCopyStringNoFormat(DB.EntriesHeader.SearchLocation, sizeof(DB.EntriesHeader.SearchLocation), Config.SearchLocation);
|
|
*(db_header *)DB.Metadata.Buffer.Location = DB.Header;
|
|
NewSearchLocation = TRUE;
|
|
}
|
|
|
|
if(NewPlayerLocation || NewSearchLocation)
|
|
{
|
|
if(!(DB.Metadata.Handle = fopen(DB.Metadata.Path, "w"))) { FreeBuffer(&DB.Metadata.Buffer); return RC_ERROR_FILE; }
|
|
fwrite(DB.Metadata.Buffer.Location, DB.Metadata.FileSize, 1, DB.Metadata.Handle);
|
|
CycleFile(&DB.Metadata);
|
|
}
|
|
|
|
entry_presence_id Entries[DB.EntriesHeader.Count];
|
|
|
|
for(int EntryIndex = 0; EntryIndex < DB.EntriesHeader.Count; ++EntryIndex)
|
|
{
|
|
db_entry This = *(db_entry *)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * EntryIndex);
|
|
CopyStringNoFormat(Entries[EntryIndex].ID, sizeof(Entries[EntryIndex].ID), This.BaseFilename);
|
|
Entries[EntryIndex].Present = FALSE;
|
|
}
|
|
|
|
DIR *ProjectDirHandle;
|
|
if(!(ProjectDirHandle = opendir(Config.ProjectDir)))
|
|
{
|
|
LogError(LOG_ERROR, "Unable to scan project directory %s: %s", Config.ProjectDir, strerror(errno));
|
|
fprintf(stderr, "Unable to scan project directory %s: %s\n", Config.ProjectDir, strerror(errno));
|
|
return RC_ERROR_DIRECTORY;
|
|
}
|
|
|
|
struct dirent *ProjectFiles;
|
|
|
|
while((ProjectFiles = readdir(ProjectDirHandle)))
|
|
{
|
|
char *Ptr;
|
|
Ptr = ProjectFiles->d_name;
|
|
Ptr += (StringLength(ProjectFiles->d_name) - StringLength(".hmml"));
|
|
if(!(StringsDiffer(Ptr, ".hmml")))
|
|
{
|
|
*Ptr = '\0';
|
|
for(int i = 0; i < DB.EntriesHeader.Count; ++i)
|
|
{
|
|
if(!StringsDiffer(Entries[i].ID, ProjectFiles->d_name))
|
|
{
|
|
Entries[i].Present = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
closedir(ProjectDirHandle);
|
|
|
|
bool Deleted = FALSE;
|
|
for(int i = 0; i < DB.EntriesHeader.Count; ++i)
|
|
{
|
|
if(Entries[i].Present == FALSE)
|
|
{
|
|
Deleted = TRUE;
|
|
neighbourhood Neighbourhood = { };
|
|
InitNeighbourhood(&Neighbourhood);
|
|
ResetAssetLandmarks();
|
|
DeleteEntry(&Neighbourhood, Entries[i].ID);
|
|
}
|
|
}
|
|
|
|
return Deleted ? RC_SUCCESS : RC_NOOP;
|
|
}
|
|
|
|
int
|
|
SyncDBWithInput(buffers *CollationBuffers, template *SearchTemplate, template *PlayerTemplate, template *BespokeTemplate)
|
|
{
|
|
if(DB.Metadata.FileSize > 0 && Config.Mode & MODE_NOREVVEDRESOURCE)
|
|
{
|
|
DeleteStaleLandmarks();
|
|
}
|
|
|
|
bool Deleted = FALSE;
|
|
Deleted = (DB.Metadata.FileSize > 0 && DB.File.FileSize > 0 && DeleteDeadDBEntries() == RC_SUCCESS);
|
|
|
|
DIR *ProjectDirHandle;
|
|
if(!(ProjectDirHandle = opendir(Config.ProjectDir)))
|
|
{
|
|
LogError(LOG_ERROR, "Unable to scan project directory %s: %s", Config.ProjectDir, strerror(errno));
|
|
fprintf(stderr, "Unable to scan project directory %s: %s\n", Config.ProjectDir, strerror(errno));
|
|
return RC_ERROR_DIRECTORY;
|
|
}
|
|
|
|
struct dirent *ProjectFiles;
|
|
bool Inserted = FALSE;
|
|
|
|
while((ProjectFiles = readdir(ProjectDirHandle)))
|
|
{
|
|
char *Ptr = ProjectFiles->d_name;
|
|
Ptr += (StringLength(ProjectFiles->d_name) - StringLength(".hmml"));
|
|
if(!(StringsDiffer(Ptr, ".hmml")))
|
|
{
|
|
*Ptr = '\0';
|
|
neighbourhood Neighbourhood = { };
|
|
InitNeighbourhood(&Neighbourhood);
|
|
ResetAssetLandmarks();
|
|
Inserted |= (InsertEntry(&Neighbourhood, CollationBuffers, PlayerTemplate, BespokeTemplate, ProjectFiles->d_name, 0) == RC_SUCCESS);
|
|
}
|
|
}
|
|
closedir(ProjectDirHandle);
|
|
|
|
UpdateDeferredAssetChecksums();
|
|
if(Deleted || Inserted)
|
|
{
|
|
GenerateSearchPage(CollationBuffers, SearchTemplate);
|
|
DeleteStaleAssets();
|
|
#if DEBUG_LANDMARKS
|
|
VerifyLandmarks();
|
|
#endif
|
|
}
|
|
return RC_SUCCESS;
|
|
}
|
|
|
|
void
|
|
PrintVersions()
|
|
{
|
|
curl_version_info_data *CurlVersion = curl_version_info(CURLVERSION_NOW);
|
|
printf("Cinera: %d.%d.%d\n"
|
|
"Cinera DB: %d\n"
|
|
"hmmlib: %d.%d.%d\n"
|
|
"libcurl: %s\n",
|
|
CINERA_APP_VERSION.Major, CINERA_APP_VERSION.Minor, CINERA_APP_VERSION.Patch,
|
|
CINERA_DB_VERSION,
|
|
hmml_version.Major, hmml_version.Minor, hmml_version.Patch,
|
|
CurlVersion->version);
|
|
}
|
|
|
|
int
|
|
InitDB(void)
|
|
{
|
|
DB.Metadata.Buffer.ID = "DBMetadata";
|
|
CopyString(DB.Metadata.Path, sizeof(DB.Metadata.Path), "%s/%s.metadata", Config.BaseDir, Config.ProjectID);
|
|
ReadFileIntoBuffer(&DB.Metadata, 0); // NOTE(matt): Could we actually catch errors (permissions?) here and bail?
|
|
|
|
DB.File.Buffer.ID = "DBFile";
|
|
CopyString(DB.File.Path, sizeof(DB.File.Path), "%s/%s.index", Config.BaseDir, Config.ProjectID);
|
|
ReadFileIntoBuffer(&DB.File, 0); // NOTE(matt): Could we actually catch errors (permissions?) here and bail?
|
|
|
|
if(DB.Metadata.Buffer.Location)
|
|
{
|
|
// TODO(matt): Handle this gracefully (it'll be an invalid file)
|
|
Assert(DB.Metadata.FileSize >= sizeof(DB.Header));
|
|
uint32_t OriginalDBVersion = 0;
|
|
uint32_t FirstInt = *(uint32_t *)DB.Metadata.Buffer.Location;
|
|
if(FirstInt != FOURCC("CNRA"))
|
|
{
|
|
// TODO(matt): More checking, somehow? Ideally this should be able to report "Invalid .metadata file" rather than
|
|
// trying to upgrade a file that isn't even valid
|
|
// TODO(matt): We should try and deprecate < CINERA_DB_VERSION 4 and enforce a clean rebuild for invalid DB files
|
|
// Perhaps do this either the next time we have to bump the version, or when we scrap Single Edition
|
|
OriginalDBVersion = FirstInt;
|
|
}
|
|
else
|
|
{
|
|
OriginalDBVersion = *(uint32_t *)(DB.Metadata.Buffer.Location + sizeof(DB.Header.HexSignature));
|
|
}
|
|
|
|
if(OriginalDBVersion < CINERA_DB_VERSION)
|
|
{
|
|
if(CINERA_DB_VERSION == 5)
|
|
{
|
|
fprintf(stderr, "\n%sHandle conversion from CINERA_DB_VERSION %d to %d!%s\n\n", ColourStrings[CS_ERROR], OriginalDBVersion, CINERA_DB_VERSION, ColourStrings[CS_END]);
|
|
exit(RC_ERROR_FATAL);
|
|
}
|
|
if(UpgradeDB(OriginalDBVersion) == RC_ERROR_FILE) { return RC_NOOP; }
|
|
}
|
|
else if(OriginalDBVersion > CINERA_DB_VERSION)
|
|
{
|
|
fprintf(stderr, "%sUnsupported DB Version (%d). Please upgrade Cinera%s\n", ColourStrings[CS_ERROR], OriginalDBVersion, ColourStrings[CS_END]);
|
|
exit(RC_ERROR_FATAL);
|
|
}
|
|
|
|
DB.Header = *(db_header *)DB.Metadata.Buffer.Location;
|
|
DB.Header.CurrentAppVersion = CINERA_APP_VERSION;
|
|
DB.Header.CurrentHMMLVersion.Major = hmml_version.Major;
|
|
DB.Header.CurrentHMMLVersion.Minor = hmml_version.Minor;
|
|
DB.Header.CurrentHMMLVersion.Patch = hmml_version.Patch;
|
|
|
|
DB.Metadata.Handle = fopen(DB.Metadata.Path, "w");
|
|
fwrite(&DB.Header, sizeof(DB.Header), 1, DB.Metadata.Handle);
|
|
|
|
DB.Metadata.Buffer.Ptr = DB.Metadata.Buffer.Location + sizeof(DB.Header);
|
|
|
|
// NOTE(matt): In the future we may want to support multiple occurrences of a block type, at which point this code
|
|
// should continue to work
|
|
for(int BlockIndex = 0; BlockIndex < DB.Header.BlockCount; ++BlockIndex)
|
|
{
|
|
uint32_t BlockID = *(uint32_t *)DB.Metadata.Buffer.Ptr;
|
|
if(BlockID == FOURCC("NTRY"))
|
|
{
|
|
DB.EntriesHeader = *(db_header_entries *)DB.Metadata.Buffer.Ptr;
|
|
fwrite(DB.Metadata.Buffer.Ptr,
|
|
sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * DB.EntriesHeader.Count,
|
|
1,
|
|
DB.Metadata.Handle);
|
|
DB.Metadata.Buffer.Ptr += sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * DB.EntriesHeader.Count;
|
|
}
|
|
else if(BlockID == FOURCC("ASET"))
|
|
{
|
|
DB.AssetsHeader = *(db_header_assets *)DB.Metadata.Buffer.Ptr;
|
|
ClearCopyStringNoFormat(DB.AssetsHeader.RootDir, sizeof(DB.AssetsHeader.RootDir), Config.RootDir);
|
|
ClearCopyStringNoFormat(DB.AssetsHeader.RootURL, sizeof(DB.AssetsHeader.RootURL), Config.RootURL);
|
|
ClearCopyStringNoFormat(DB.AssetsHeader.CSSDir, sizeof(DB.AssetsHeader.CSSDir), Config.CSSDir);
|
|
ClearCopyStringNoFormat(DB.AssetsHeader.ImagesDir, sizeof(DB.AssetsHeader.ImagesDir), Config.ImagesDir);
|
|
ClearCopyStringNoFormat(DB.AssetsHeader.JSDir, sizeof(DB.AssetsHeader.JSDir), Config.JSDir);
|
|
fwrite(&DB.AssetsHeader, sizeof(DB.AssetsHeader), 1, DB.Metadata.Handle);
|
|
DB.Metadata.Buffer.Ptr += sizeof(DB.AssetsHeader);
|
|
for(int AssetIndex = 0; AssetIndex < DB.AssetsHeader.Count; ++AssetIndex)
|
|
{
|
|
DB.Asset = *(db_asset *)DB.Metadata.Buffer.Ptr;
|
|
fwrite(DB.Metadata.Buffer.Ptr,
|
|
sizeof(DB.Asset) + sizeof(DB.Landmark) * DB.Asset.LandmarkCount,
|
|
1,
|
|
DB.Metadata.Handle);
|
|
DB.Metadata.Buffer.Ptr += sizeof(DB.Asset) + sizeof(DB.Landmark) * DB.Asset.LandmarkCount;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
printf("%sMalformed metadata file%s: %s\n", ColourStrings[CS_ERROR], ColourStrings[CS_END], DB.Metadata.Path);
|
|
}
|
|
}
|
|
|
|
CycleFile(&DB.Metadata);
|
|
}
|
|
else
|
|
{
|
|
// NOTE(matt): Initialising new db_header
|
|
DB.Header.HexSignature = FOURCC("CNRA");
|
|
DB.Header.InitialDBVersion = DB.Header.CurrentDBVersion = CINERA_DB_VERSION;
|
|
DB.Header.InitialAppVersion = DB.Header.CurrentAppVersion = CINERA_APP_VERSION;
|
|
DB.Header.InitialHMMLVersion.Major = DB.Header.CurrentHMMLVersion.Major = hmml_version.Major;
|
|
DB.Header.InitialHMMLVersion.Minor = DB.Header.CurrentHMMLVersion.Minor = hmml_version.Minor;
|
|
DB.Header.InitialHMMLVersion.Patch = DB.Header.CurrentHMMLVersion.Patch = hmml_version.Patch;
|
|
DB.Header.BlockCount = 0;
|
|
|
|
CopyStringNoFormat(DB.EntriesHeader.ProjectID, sizeof(DB.EntriesHeader.ProjectID), Config.ProjectID);
|
|
|
|
for(int ProjectIndex = 0; ProjectIndex < ArrayCount(ProjectInfo); ++ProjectIndex)
|
|
{
|
|
if(!StringsDiffer(ProjectInfo[ProjectIndex].ProjectID, Config.ProjectID))
|
|
{
|
|
CopyStringNoFormat(DB.EntriesHeader.ProjectName, sizeof(DB.EntriesHeader.ProjectName), ProjectInfo[ProjectIndex].FullName);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// TODO(matt): Consider allowing zero NTRY or ASET blocks in a future version
|
|
DB.EntriesHeader.BlockID = FOURCC("NTRY");
|
|
DB.EntriesHeader.Count = 0;
|
|
CopyStringNoFormat(DB.EntriesHeader.BaseURL, sizeof(DB.EntriesHeader.BaseURL), Config.BaseURL);
|
|
CopyStringNoFormat(DB.EntriesHeader.SearchLocation, sizeof(DB.EntriesHeader.SearchLocation), Config.SearchLocation);
|
|
CopyStringNoFormat(DB.EntriesHeader.PlayerLocation, sizeof(DB.EntriesHeader.PlayerLocation), Config.PlayerLocation);
|
|
CopyStringNoFormat(DB.EntriesHeader.PlayerURLPrefix, sizeof(DB.EntriesHeader.PlayerURLPrefix), Config.PlayerURLPrefix);
|
|
++DB.Header.BlockCount;
|
|
|
|
DB.AssetsHeader.BlockID = FOURCC("ASET");
|
|
DB.AssetsHeader.Count = 0;
|
|
CopyStringNoFormat(DB.AssetsHeader.RootDir, sizeof(DB.AssetsHeader.RootDir), Config.RootDir);
|
|
CopyStringNoFormat(DB.AssetsHeader.RootURL, sizeof(DB.AssetsHeader.RootURL), Config.RootURL);
|
|
CopyStringNoFormat(DB.AssetsHeader.CSSDir, sizeof(DB.AssetsHeader.CSSDir), Config.CSSDir);
|
|
CopyStringNoFormat(DB.AssetsHeader.ImagesDir, sizeof(DB.AssetsHeader.ImagesDir), Config.ImagesDir);
|
|
CopyStringNoFormat(DB.AssetsHeader.JSDir, sizeof(DB.AssetsHeader.JSDir), Config.JSDir);
|
|
++DB.Header.BlockCount;
|
|
|
|
DIR *OutputDirectoryHandle;
|
|
if(!(OutputDirectoryHandle = opendir(Config.BaseDir)))
|
|
{
|
|
if(MakeDir(Config.BaseDir) == RC_ERROR_DIRECTORY)
|
|
{
|
|
LogError(LOG_ERROR, "Unable to create directory %s: %s", Config.BaseDir, strerror(errno));
|
|
fprintf(stderr, "Unable to create directory %s: %s\n", Config.BaseDir, strerror(errno));
|
|
return RC_ERROR_DIRECTORY;
|
|
};
|
|
}
|
|
closedir(OutputDirectoryHandle);
|
|
|
|
DB.Metadata.Handle = fopen(DB.Metadata.Path, "w");
|
|
fwrite(&DB.Header, sizeof(DB.Header), 1, DB.Metadata.Handle);
|
|
fwrite(&DB.EntriesHeader, sizeof(DB.EntriesHeader), 1, DB.Metadata.Handle);
|
|
fwrite(&DB.AssetsHeader, sizeof(DB.AssetsHeader), 1, DB.Metadata.Handle);
|
|
fclose(DB.Metadata.Handle);
|
|
ReadFileIntoBuffer(&DB.Metadata, 0);
|
|
|
|
DB.File.Handle = fopen(DB.File.Path, "w");
|
|
fprintf(DB.File.Handle, "---\n");
|
|
fclose(DB.File.Handle);
|
|
ReadFileIntoBuffer(&DB.File, 0);
|
|
}
|
|
return RC_SUCCESS;
|
|
}
|
|
|
|
void
|
|
SetCacheDirectory(config *DefaultConfig)
|
|
{
|
|
int Flags = WRDE_NOCMD | WRDE_UNDEF | WRDE_APPEND;
|
|
wordexp_t Expansions = {};
|
|
wordexp("$XDG_CACHE_HOME/cinera", &Expansions, Flags);
|
|
wordexp("$HOME/.cache/cinera", &Expansions, Flags);
|
|
if(Expansions.we_wordc > 0 ) { CopyString(DefaultConfig->CacheDir, sizeof(DefaultConfig->CacheDir), Expansions.we_wordv[0]); }
|
|
wordfree(&Expansions);
|
|
}
|
|
|
|
void
|
|
InitMemoryArena(arena *Arena, int Size)
|
|
{
|
|
Arena->Size = Size;
|
|
if(!(Arena->Location = calloc(1, Arena->Size)))
|
|
{
|
|
exit(RC_ERROR_MEMORY);
|
|
}
|
|
Arena->Ptr = Arena->Location;
|
|
}
|
|
|
|
int
|
|
main(int ArgC, char **Args)
|
|
{
|
|
// TODO(matt): Read all defaults from the config
|
|
config DefaultConfig = {
|
|
.RootDir = ".",
|
|
.RootURL = "",
|
|
.CSSDir = "",
|
|
.ImagesDir = "",
|
|
.JSDir = "",
|
|
.QueryString = "r",
|
|
.TemplatesDir = ".",
|
|
.TemplateSearchLocation = "",
|
|
.TemplatePlayerLocation = "",
|
|
.BaseDir = ".",
|
|
.BaseURL = "",
|
|
.SearchLocation = "",
|
|
.PlayerLocation = "", // Should default to the ProjectID
|
|
.Edition = EDITION_SINGLE,
|
|
.LogLevel = LOG_EMERGENCY,
|
|
.DefaultMedium = "programming",
|
|
.Mode = 0,
|
|
.OutLocation = "out.html",
|
|
.OutIntegratedLocation = "out_integrated.html",
|
|
.ProjectDir = ".",
|
|
.ProjectID = "",
|
|
.Theme = "",
|
|
.UpdateInterval = 4,
|
|
.PlayerURLPrefix = ""
|
|
};
|
|
|
|
SetCacheDirectory(&DefaultConfig);
|
|
|
|
Config = DefaultConfig;
|
|
|
|
if(ArgC < 2)
|
|
{
|
|
PrintUsage(Args[0], &DefaultConfig);
|
|
return RC_RIP;
|
|
}
|
|
|
|
char CommandLineArg;
|
|
while((CommandLineArg = getopt(ArgC, Args, "1a:b:B:c:d:efghi:j:l:m:n:o:p:qQ:r:R:s:t:u:vwx:y:")) != -1)
|
|
{
|
|
switch(CommandLineArg)
|
|
{
|
|
case '1':
|
|
Config.Mode |= MODE_SINGLETAB;
|
|
break;
|
|
case 'a':
|
|
Config.PlayerLocation = StripSurroundingSlashes(optarg);
|
|
break;
|
|
case 'b':
|
|
Config.BaseDir = StripTrailingSlash(optarg);
|
|
break;
|
|
case 'B':
|
|
Config.BaseURL = StripTrailingSlash(optarg);
|
|
break;
|
|
case 'c':
|
|
Config.CSSDir = StripSurroundingSlashes(optarg);
|
|
break;
|
|
case 'd':
|
|
Config.ProjectDir = StripTrailingSlash(optarg);
|
|
break;
|
|
case 'e':
|
|
Config.Mode |= MODE_EXAMINE;
|
|
break;
|
|
case 'f':
|
|
Config.Mode |= MODE_FORCEINTEGRATION;
|
|
break;
|
|
case 'g':
|
|
Config.Mode |= MODE_NOPRIVACY;
|
|
break;
|
|
case 'i':
|
|
Config.ImagesDir = StripSurroundingSlashes(optarg);
|
|
break;
|
|
case 'j':
|
|
Config.JSDir = StripSurroundingSlashes(optarg);
|
|
break;
|
|
case 'l':
|
|
// TODO(matt): Make this actually take a string, rather than requiring the LogLevel number
|
|
Config.LogLevel = StringToInt(optarg);
|
|
break;
|
|
case 'm':
|
|
Config.DefaultMedium = optarg;
|
|
break;
|
|
case 'n':
|
|
Config.SearchLocation = StripSurroundingSlashes(optarg);
|
|
break;
|
|
case 'o':
|
|
Config.OutLocation = optarg;
|
|
Config.OutIntegratedLocation = optarg;
|
|
break;
|
|
case 'p':
|
|
Config.ProjectID = optarg;
|
|
break;
|
|
case 'q':
|
|
Config.Mode |= MODE_ONESHOT;
|
|
break;
|
|
case 'Q':
|
|
Config.QueryString = optarg;
|
|
if(!StringsDiffer(Config.QueryString, ""))
|
|
{
|
|
Config.Mode |= MODE_NOREVVEDRESOURCE;
|
|
}
|
|
break;
|
|
case 'r':
|
|
Config.RootDir = StripTrailingSlash(optarg);
|
|
break;
|
|
case 'R':
|
|
Config.RootURL = StripTrailingSlash(optarg);
|
|
break;
|
|
case 's':
|
|
Config.Theme = optarg;
|
|
break;
|
|
case 't':
|
|
Config.TemplatesDir = StripTrailingSlash(optarg);
|
|
break;
|
|
case 'u':
|
|
Config.UpdateInterval = StringToInt(optarg);
|
|
break;
|
|
case 'v':
|
|
PrintVersions();
|
|
return RC_SUCCESS;
|
|
case 'w':
|
|
Config.Mode |= MODE_NOCACHE;
|
|
break;
|
|
case 'x':
|
|
Config.TemplateSearchLocation = optarg;
|
|
break;
|
|
case 'y':
|
|
Config.TemplatePlayerLocation = optarg;
|
|
break;
|
|
case 'h':
|
|
default:
|
|
PrintUsage(Args[0], &DefaultConfig);
|
|
return RC_SUCCESS;
|
|
}
|
|
}
|
|
|
|
if(Config.Mode & MODE_EXAMINE)
|
|
{
|
|
// TODO(matt): Allow optionally passing a .metadata file as an argument?
|
|
ExamineDB();
|
|
exit(RC_SUCCESS);
|
|
}
|
|
|
|
// NOTE(matt): Init MemoryArenas (they are global)
|
|
InitMemoryArena(&MemoryArena, Megabytes(4));
|
|
|
|
#if DEBUG_MEM
|
|
FILE *MemLog = fopen("/home/matt/cinera_mem", "a+");
|
|
fprintf(MemLog, " Allocated MemoryArena (%d)\n", MemoryArena.Size);
|
|
fclose(MemLog);
|
|
printf(" Allocated MemoryArena (%d)\n", MemoryArena.Size);
|
|
#endif
|
|
|
|
#if DEBUG
|
|
printf("Allocated MemoryArena: %d\n\n", MemoryArena.Size);
|
|
#endif
|
|
|
|
// NOTE(matt): Tree structure of buffer dependencies
|
|
// IncludesPlayer
|
|
// Menus
|
|
// Player
|
|
// ScriptPlayer
|
|
//
|
|
// IncludesSearch
|
|
// Search
|
|
|
|
buffers CollationBuffers = {};
|
|
if(ClaimBuffer(&CollationBuffers.IncludesPlayer, "IncludesPlayer", Kilobytes(1)) == RC_ARENA_FULL) { goto RIP; };
|
|
if(ClaimBuffer(&CollationBuffers.Menus, "Menus", Kilobytes(32)) == RC_ARENA_FULL) { goto RIP; };
|
|
if(ClaimBuffer(&CollationBuffers.Player, "Player", Kilobytes(256)) == RC_ARENA_FULL) { goto RIP; };
|
|
if(ClaimBuffer(&CollationBuffers.ScriptPlayer, "ScriptPlayer", Kilobytes(8)) == RC_ARENA_FULL) { goto RIP; };
|
|
|
|
if(ClaimBuffer(&CollationBuffers.IncludesSearch, "IncludesSearch", Kilobytes(1)) == RC_ARENA_FULL) { goto RIP; };
|
|
if(ClaimBuffer(&CollationBuffers.SearchEntry, "Search", Kilobytes(32)) == RC_ARENA_FULL) { goto RIP; };
|
|
|
|
bool HaveConfigErrors = FALSE;
|
|
if(StringsDiffer(Config.ProjectID, ""))
|
|
{
|
|
if(StringLength(Config.ProjectID) > MAX_PROJECT_ID_LENGTH)
|
|
{
|
|
fprintf(stderr, "%sProjectID \"%s\" is too long (%d/%d characters)%s\n", ColourStrings[CS_ERROR], Config.ProjectID, StringLength(Config.ProjectID), MAX_PROJECT_ID_LENGTH, ColourStrings[CS_END]);
|
|
HaveConfigErrors = TRUE;
|
|
}
|
|
|
|
Config.Edition = EDITION_PROJECT;
|
|
bool KnownProject = FALSE;
|
|
for(int ProjectInfoIndex = 0; ProjectInfoIndex < ArrayCount(ProjectInfo); ++ProjectInfoIndex)
|
|
{
|
|
if(!StringsDiffer(Config.ProjectID, ProjectInfo[ProjectInfoIndex].ProjectID))
|
|
{
|
|
KnownProject = TRUE;
|
|
CopyStringNoFormat(CollationBuffers.ProjectID, sizeof(CollationBuffers.ProjectID), Config.ProjectID);
|
|
if(!StringsDiffer(Config.Theme, ""))
|
|
{
|
|
Config.Theme = Config.ProjectID;
|
|
}
|
|
CopyStringNoFormat(CollationBuffers.Theme, sizeof(CollationBuffers.Theme), Config.Theme);
|
|
if(StringsDiffer(ProjectInfo[ProjectInfoIndex].FullName, ""))
|
|
{
|
|
CopyStringNoFormat(CollationBuffers.ProjectName, sizeof(CollationBuffers.ProjectName), ProjectInfo[ProjectInfoIndex].FullName);
|
|
}
|
|
if(StringsDiffer(ProjectInfo[ProjectInfoIndex].Medium, ""))
|
|
{
|
|
Config.DefaultMedium = ProjectInfo[ProjectInfoIndex].Medium;
|
|
}
|
|
|
|
if(StringsDiffer(ProjectInfo[ProjectInfoIndex].AltURLPrefix, ""))
|
|
{
|
|
if(StringLength(ProjectInfo[ProjectInfoIndex].AltURLPrefix) > MAX_PLAYER_URL_PREFIX_LENGTH)
|
|
{
|
|
fprintf(stderr, "%sPlayer URL Prefix \"%s\" is too long (%d/%d characters)%s\n", ColourStrings[CS_ERROR], ProjectInfo[ProjectInfoIndex].AltURLPrefix, StringLength(ProjectInfo[ProjectInfoIndex].AltURLPrefix), MAX_PLAYER_URL_PREFIX_LENGTH, ColourStrings[CS_END]);
|
|
HaveConfigErrors = TRUE;
|
|
}
|
|
else
|
|
{
|
|
Config.PlayerURLPrefix = ProjectInfo[ProjectInfoIndex].AltURLPrefix;
|
|
}
|
|
}
|
|
|
|
if(StringLength(ProjectInfo[ProjectInfoIndex].FullName) > MAX_PROJECT_NAME_LENGTH)
|
|
{
|
|
fprintf(stderr, "%sProject Name \"%s\" is too long (%d/%d characters)%s\n", ColourStrings[CS_ERROR], ProjectInfo[ProjectInfoIndex].FullName, StringLength(ProjectInfo[ProjectInfoIndex].FullName), MAX_PROJECT_NAME_LENGTH, ColourStrings[CS_END]);
|
|
HaveConfigErrors = TRUE;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
if(!KnownProject)
|
|
{
|
|
fprintf(stderr, "%sMissing Project Info for %s%s %s(-p)%s\n", ColourStrings[CS_ERROR], Config.ProjectID, ColourStrings[CS_END], ColourStrings[CS_COMMENT], ColourStrings[CS_END]);
|
|
HaveConfigErrors = TRUE;
|
|
}
|
|
}
|
|
|
|
if(StringsDiffer(Config.BaseURL, "") && StringLength(Config.BaseURL) > MAX_BASE_URL_LENGTH)
|
|
{
|
|
fprintf(stderr, "%sBase URL \"%s\" is too long (%d/%d characters)%s\n", ColourStrings[CS_ERROR], Config.BaseURL, StringLength(Config.BaseURL), MAX_BASE_URL_LENGTH, ColourStrings[CS_END]);
|
|
HaveConfigErrors = TRUE;
|
|
}
|
|
|
|
if(StringsDiffer(Config.SearchLocation, "") && StringLength(Config.SearchLocation) > MAX_RELATIVE_PAGE_LOCATION_LENGTH)
|
|
{
|
|
fprintf(stderr, "%sRelative Search Page Location \"%s\" is too long (%d/%d characters)%s\n", ColourStrings[CS_ERROR], Config.SearchLocation, StringLength(Config.SearchLocation), MAX_RELATIVE_PAGE_LOCATION_LENGTH, ColourStrings[CS_END]);
|
|
HaveConfigErrors = TRUE;
|
|
}
|
|
|
|
if(StringsDiffer(Config.PlayerLocation, "") && StringLength(Config.PlayerLocation) > MAX_RELATIVE_PAGE_LOCATION_LENGTH)
|
|
{
|
|
fprintf(stderr, "%sRelative Player Page Location \"%s\" is too long (%d/%d characters)%s\n", ColourStrings[CS_ERROR], Config.PlayerLocation, StringLength(Config.PlayerLocation), MAX_RELATIVE_PAGE_LOCATION_LENGTH, ColourStrings[CS_END]);
|
|
HaveConfigErrors = TRUE;
|
|
}
|
|
|
|
if(!MediumExists(Config.DefaultMedium))
|
|
{
|
|
// TODO(matt): We'll want to stick around when we have multiple projects configured
|
|
HaveConfigErrors = TRUE;
|
|
}
|
|
|
|
if(HaveConfigErrors) { exit(RC_RIP); }
|
|
|
|
// NOTE(matt): Templating
|
|
//
|
|
// Config will contain paths of multiple templates
|
|
// App is running all the time, and picking up changes to the config as we go
|
|
// If we find a new template, we first of all Init and Validate it
|
|
// In our case here, we just want to straight up Init our three possible templates
|
|
// and Validate the Search and Player templates if their locations are set
|
|
|
|
if(Config.Edition == EDITION_PROJECT)
|
|
{
|
|
#if DEBUG_MEM
|
|
FILE *MemLog = fopen("/home/matt/cinera_mem", "w+");
|
|
fprintf(MemLog, "Entered Project Edition\n");
|
|
fclose(MemLog);
|
|
#endif
|
|
|
|
// TODO(matt): Also log these startup messages?
|
|
PrintVersions();
|
|
printf( "\n"
|
|
"Universal\n"
|
|
" Cache Directory: %s(XDG_CACHE_HOME)%s\t%s\n"
|
|
" Update Interval: %s(-u)%s\t\t%d second%s\n"
|
|
"\n"
|
|
" Assets Root\n"
|
|
" Directory: %s(-r)%s\t\t\t%s\n"
|
|
" URL: %s(-R)%s\t\t\t%s\n"
|
|
" Paths relative to root\n"
|
|
" CSS: %s(-c)%s\t\t\t%s\n"
|
|
" Images: %s(-i)%s\t\t\t%s\n"
|
|
" JS: %s(-j)%s\t\t\t%s\n"
|
|
" Revved resources query string: %s(-Q)%s\t%s\n"
|
|
"\n"
|
|
"Project\n"
|
|
" ID: %s(-p)%s\t\t\t\t%s\n"
|
|
" Default Medium: %s(-m)%s\t\t%s\n"
|
|
" Style / Theme: %s(-s)%s\t\t\t%s\n"
|
|
"\n"
|
|
"Input Paths\n"
|
|
" Annotations Directory: %s(-d)%s\t\t%s\n"
|
|
" Templates Directory: %s(-t)%s\t\t%s\n"
|
|
" Search Template: %s(-x)%s\t\t%s\n"
|
|
" Player Template: %s(-y)%s\t\t%s\n"
|
|
"\n"
|
|
"Output Paths\n"
|
|
" Base\n"
|
|
" Directory: %s(-b)%s\t\t\t%s\n"
|
|
" URL: %s(-B)%s\t\t\t%s\n"
|
|
" Paths relative to base\n"
|
|
" Search Page: %s(-n)%s\t\t%s\n"
|
|
/* NOTE(matt): Here, I think, is where we'll split into sub-projects (...really?...) */
|
|
" Player Page(s): %s(-a)%s\t\t%s\n"
|
|
" Player Page Prefix: %s(hardcoded)%s\t%s\n"
|
|
"\n"
|
|
"Modes\n"
|
|
" Single browser tab: %s(-1)%s\t\t%s\n"
|
|
" Force template integration: %s(-f)%s\t%s\n"
|
|
" Ignore video privacy status: %s(-g)%s\t%s\n"
|
|
" Quit after sync: %s(-q)%s\t\t%s\n"
|
|
" Force quote cache rebuild: %s(-w)%s\t%s\n"
|
|
"\n",
|
|
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.CacheDir,
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.UpdateInterval,
|
|
Config.UpdateInterval == 1 ? "" : "s",
|
|
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.RootDir,
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END], StringsDiffer(Config.RootURL, "") ? Config.RootURL : "[empty]",
|
|
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END], StringsDiffer(Config.CSSDir, "") ? Config.CSSDir : "(same as root)",
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END], StringsDiffer(Config.ImagesDir, "") ? Config.ImagesDir : "(same as root)",
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END], StringsDiffer(Config.JSDir, "") ? Config.JSDir : "(same as root)",
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.Mode & MODE_NOREVVEDRESOURCE ? "[disabled]" : Config.QueryString,
|
|
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.ProjectID,
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.DefaultMedium,
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.Theme,
|
|
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.ProjectDir,
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.TemplatesDir,
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END], StringsDiffer(Config.TemplateSearchLocation, "") ? Config.TemplateSearchLocation : "[none set]",
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END], StringsDiffer(Config.TemplatePlayerLocation, "") ? Config.TemplatePlayerLocation : "[none set]",
|
|
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.BaseDir,
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END], StringsDiffer(Config.BaseURL, "") ? Config.BaseURL : "[empty]",
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END], StringsDiffer(Config.SearchLocation, "") ? Config.SearchLocation : "(same as base)",
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END], StringsDiffer(Config.PlayerLocation, "") ? Config.PlayerLocation : "(directly descended from base)",
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END], StringsDiffer(Config.PlayerURLPrefix, "") ? Config.PlayerURLPrefix : Config.ProjectID,
|
|
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.Mode & MODE_SINGLETAB ? "on" : "off",
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.Mode & MODE_FORCEINTEGRATION ? "on" : "off",
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.Mode & MODE_NOPRIVACY ? "on" : "off",
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.Mode & MODE_ONESHOT ? "on" : "off",
|
|
ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.Mode & MODE_NOCACHE ? "on" : "off");
|
|
|
|
if((StringsDiffer(Config.SearchLocation, "") || StringsDiffer(Config.PlayerLocation, ""))
|
|
&& StringLength(Config.BaseURL) == 0)
|
|
{
|
|
printf("%sPlease set a Project Base URL (-B) so we can output the Search / Player pages to\n"
|
|
"locations other than the defaults%s\n",
|
|
ColourStrings[CS_ERROR], ColourStrings[CS_END]);
|
|
return(RC_SUCCESS);
|
|
}
|
|
#if 0
|
|
for(int i = 0; i < Assets.Count; ++i)
|
|
{
|
|
printf("%08x - %s\n", Assets.Asset[i].Hash, Assets.Asset[i].Filename);
|
|
}
|
|
#endif
|
|
|
|
InitDB();
|
|
inotifyInstance = inotify_init1(IN_NONBLOCK);
|
|
|
|
printf("┌╼ Hashing assets ╾┐\n");
|
|
// NOTE(matt): This had to happen before PackTemplate() because those guys may need to do PushAsset() and we must
|
|
// ensure that the builtin assets get placed correctly
|
|
InitAssets();
|
|
}
|
|
|
|
printf("┌╼ Packing templates ╾┐\n");
|
|
template SearchTemplate;
|
|
template PlayerTemplate;
|
|
template BespokeTemplate;
|
|
|
|
if(StringsDiffer(Config.TemplatePlayerLocation, ""))
|
|
{
|
|
switch(PackTemplate(&PlayerTemplate, Config.TemplatePlayerLocation, TEMPLATE_PLAYER))
|
|
{
|
|
case RC_INVALID_TEMPLATE: // Invalid template
|
|
case RC_ERROR_FILE: // Could not load template
|
|
case RC_ERROR_MEMORY: // Could not allocate memory for template
|
|
goto RIP;
|
|
case RC_SUCCESS:
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(Config.Edition == EDITION_PROJECT && StringsDiffer(Config.TemplateSearchLocation, ""))
|
|
{
|
|
switch(PackTemplate(&SearchTemplate, Config.TemplateSearchLocation, TEMPLATE_SEARCH))
|
|
{
|
|
case RC_INVALID_TEMPLATE: // Invalid template
|
|
case RC_ERROR_MEMORY: // Could not allocate memory for template
|
|
case RC_ERROR_FILE: // Could not load template
|
|
goto RIP;
|
|
case RC_SUCCESS:
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(Config.Edition == EDITION_PROJECT)
|
|
{
|
|
printf("\n┌╼ Synchronising with Annotations Directory ╾┐\n");
|
|
SyncDBWithInput(&CollationBuffers, &SearchTemplate, &PlayerTemplate, &BespokeTemplate);
|
|
if(Config.Mode & MODE_ONESHOT)
|
|
{
|
|
goto RIP;
|
|
}
|
|
|
|
printf("\n┌╼ Monitoring Annotations Directory for %snew%s, %sedited%s and %sdeleted%s .hmml files ╾┐\n",
|
|
ColourStrings[EditTypes[EDIT_ADDITION].Colour], ColourStrings[CS_END],
|
|
ColourStrings[EditTypes[EDIT_REINSERTION].Colour], ColourStrings[CS_END],
|
|
ColourStrings[EditTypes[EDIT_DELETION].Colour], ColourStrings[CS_END]);
|
|
// NOTE(matt): Do we want to also watch IN_DELETE_SELF events?
|
|
PushHMMLWatchHandle();
|
|
|
|
while(MonitorFilesystem(&CollationBuffers, &SearchTemplate, &PlayerTemplate, &BespokeTemplate) != RC_ARENA_FULL)
|
|
{
|
|
// TODO(matt): Refetch the quotes and rebuild player pages if needed
|
|
//
|
|
// Every sixty mins, redownload the quotes and, I suppose, SyncDBWithInput(). 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.
|
|
//
|
|
if(!(Config.Mode & MODE_NOPRIVACY) && time(0) - LastPrivacyCheck > 60 * 60 * 4)
|
|
{
|
|
RecheckPrivacy(&CollationBuffers, &SearchTemplate, &PlayerTemplate, &BespokeTemplate);
|
|
}
|
|
sleep(Config.UpdateInterval);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// TODO(matt): Get rid of Single Edition once and for all, probably for v1.0.0
|
|
if(optind == ArgC)
|
|
{
|
|
fprintf(stderr, "%s: requires at least one input .hmml file\n", Args[0]);
|
|
PrintUsage(Args[0], &DefaultConfig);
|
|
goto RIP;
|
|
}
|
|
|
|
inotifyInstance = inotify_init1(IN_NONBLOCK);
|
|
|
|
printf("┌╼ Hashing assets ╾┐\n");
|
|
InitBuiltinAssets();
|
|
|
|
NextFile:
|
|
// TODO(matt): Just change the default output location so all these guys won't overwrite each other
|
|
for(int FileIndex = optind; FileIndex < ArgC; ++FileIndex)
|
|
{
|
|
bool HasBespokeTemplate = FALSE;
|
|
char *Ptr = Args[FileIndex];
|
|
Ptr += (StringLength(Args[FileIndex]) - StringLength(".hmml"));
|
|
if(!(StringsDiffer(Ptr, ".hmml")))
|
|
{
|
|
CopyString(Config.SingleHMMLFilePath, sizeof(Config.SingleHMMLFilePath), "%s", Args[FileIndex]);
|
|
switch(HMMLToBuffers(&CollationBuffers, &BespokeTemplate, Args[FileIndex], 0))
|
|
{
|
|
// TODO(matt): Actually sort out the fatality of these cases
|
|
case RC_ERROR_FILE:
|
|
case RC_ERROR_FATAL:
|
|
goto RIP;
|
|
case RC_ERROR_HMML:
|
|
case RC_ERROR_MAX_REFS:
|
|
case RC_ERROR_QUOTE:
|
|
case RC_INVALID_REFERENCE:
|
|
if(FileIndex < (ArgC - 1)) { goto NextFile; }
|
|
else { goto RIP; }
|
|
case RC_SUCCESS:
|
|
break;
|
|
};
|
|
|
|
HasBespokeTemplate = BespokeTemplate.File.Buffer.Location != NULL;
|
|
|
|
switch(BuffersToHTML(&CollationBuffers,
|
|
HasBespokeTemplate ? &BespokeTemplate : &PlayerTemplate,
|
|
0,
|
|
PAGE_PLAYER, 0))
|
|
{
|
|
// TODO(matt): Actually sort out the fatality of these cases
|
|
case RC_INVALID_TEMPLATE:
|
|
if(HasBespokeTemplate) { FreeTemplate(&BespokeTemplate); }
|
|
if(FileIndex < (ArgC - 1)) { goto NextFile; }
|
|
case RC_ERROR_MEMORY:
|
|
case RC_ERROR_FILE:
|
|
case RC_ARENA_FULL:
|
|
goto RIP;
|
|
case RC_SUCCESS:
|
|
#if 0
|
|
fprintf(stdout, "%sWritten%s %s\n", HasBespokeTemplate ? Config.OutIntegratedLocation : Config.OutLocation);
|
|
#endif
|
|
if(HasBespokeTemplate) { FreeTemplate(&BespokeTemplate); }
|
|
break;
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
if(PlayerTemplate.File.Buffer.Location)
|
|
{
|
|
FreeTemplate(&PlayerTemplate);
|
|
}
|
|
if(Config.Edition == EDITION_PROJECT && SearchTemplate.File.Buffer.Location)
|
|
{
|
|
FreeTemplate(&SearchTemplate);
|
|
}
|
|
|
|
DeclaimBuffer(&CollationBuffers.SearchEntry);
|
|
DeclaimBuffer(&CollationBuffers.IncludesSearch);
|
|
|
|
DeclaimBuffer(&CollationBuffers.ScriptPlayer);
|
|
DeclaimBuffer(&CollationBuffers.Player);
|
|
DeclaimBuffer(&CollationBuffers.Menus);
|
|
DeclaimBuffer(&CollationBuffers.IncludesPlayer);
|
|
RIP:
|
|
free(MemoryArena.Location);
|
|
|
|
#if DEBUG_MEM
|
|
MemLog = fopen("/home/matt/cinera_mem", "a+");
|
|
fprintf(MemLog, " Freed MemoryArena\n");
|
|
fclose(MemLog);
|
|
printf(" Freed MemoryArena\n");
|
|
#endif
|
|
|
|
}
|