Updated design, added Video feature, working on documents feature.

This commit is contained in:
David Ball 2024-04-25 14:48:45 -04:00
parent 9947355d32
commit 9dbe87e166
17 changed files with 4355 additions and 189 deletions

View File

@ -8,6 +8,7 @@ const matter = require('gray-matter');
const ejs = require('ejs');
const helpers = require('../views/helpers/functions');
const search = require('../routes/search');
const fs = require('fs');
// const advancedSearch = require('../routes/advanced-search');
// Port number for HTTP server
@ -57,7 +58,7 @@ Allow: /
});
// Search endpoints
console.log("Setting route for /search");
console.log("Setting routes for /search");
app.use('/search', search.router);
// app.use('/advanced-search', advancedSearch.router);
@ -85,9 +86,32 @@ glob.globSync('pages/**/*.md', {
});
});
console.log("Scanning for documents to create routes");
glob.globSync('**/*{.pdf,.docx,.xlsx,.pptx,.doc,.xls,.ppt}', {
cwd: path.join(__dirname, '..', 'public'),
matchBase: true,
follow: true,
}).forEach((filePath) => {
const expressRoutePathFromFilePath = (filePath) => {
return filePath.substring(0, filePath.length - path.extname(filePath).length).replaceAll(path.sep, path.posix.sep);
};
const route = expressRoutePathFromFilePath(filePath);
const fullFilePath = path.join(__dirname, '..', 'public', filePath);
let paths = route.split(path.posix.sep);
paths[0] = 'public';
console.log(`Setting route for ${route}`);
app.get(route, async (req, res) => {
const fm = matter.read(fullFilePath);
const fmData = { fm: fm.data, excerpt: fm.excerpt };
const content = helpers.md.render(fm.content, fmData );
const renderData = { content, route, filePath, fullFilePath, req, paths, ...fmData };
res.render("page", { h: helpers, ...renderData });
});
});
// Endpoints for all the site's YouTube videos.
console.log("Scanning for archived videos to create routes");
glob.globSync('Russell_County/Board_of_Supervisors/YouTube_Archive/**/*.info.json', {
glob.globSync(['Russell_County/Board_of_Supervisors/YouTube_Archive/**/*.info.json', 'Virginia_Energy/YouTube_Archive/**/*.info.json'], {
cwd: path.join(__dirname, '..', 'public'),
matchBase: true,
follow: true,
@ -99,16 +123,17 @@ glob.globSync('Russell_County/Board_of_Supervisors/YouTube_Archive/**/*.info.jso
return filePath.substring(0, filePath.lastIndexOf(path.sep));
}
const directory = dirFromFilePath(filePath);
let videoURL = glob.globSync("*.{mpg,mpeg,mp4,mkv,webm}", {
let videoURL = ""+glob.globSync("*.{mpg,mpeg,mp4,mkv,webm}", {
cwd: path.join(__dirname, '..', 'public', directory),
matchBase: true,
follow: true,
}).pop();
let subtitleURL = glob.globSync("*.en.vtt", {
let subtitleURL = ""+glob.globSync("*.en.vtt", {
cwd: path.join(__dirname, '..', 'public', directory),
matchBase: true,
follow: true,
}).pop();
let subtitleFile = path.join(__dirname, '..', 'public', directory, subtitleURL);
const route = encodeURI(expressRoutePathFromFilePath(filePath));
let paths = filePath.substring(0, filePath.lastIndexOf(path.sep) > 0 ? filePath.lastIndexOf(path.sep) : filePath.length-1).split(path.sep);
paths = paths.map((name, idx, aPaths) => {
@ -122,13 +147,48 @@ glob.globSync('Russell_County/Board_of_Supervisors/YouTube_Archive/**/*.info.jso
console.log(`Setting route for ${route}`);
app.get(route, async (req, res) => {
let info = require(fullFilePath);
const renderData = { route, filePath, fullFilePath, req, paths, directory, videoURL, subtitleURL, info };
res.render("video-player", { h: helpers, ...renderData });
let subtitleVTT = fs.existsSync(subtitleFile)?fs.readFileSync(subtitleFile, 'utf8'):undefined;
const renderData = { route, filePath, fullFilePath, req, paths, directory, videoURL, subtitleURL, subtitleVTT, info };
res.render("video-player", { h: helpers, require, ...renderData });
});
});
//app.get('/OCR-Encoded-PDFs/Russell-County-Web-Site_2024-02-13_19_50_Modified-With-OCR-Encoding**', rewriter.rewrite('/Web_Site_Archives/Russell_County_Web_Site-2024-02-13_19_50_Modified_With_OCR_Encoding/$1'));
console.log(`Setting routes for /css/*.css`);;
app.get('/css/*.css', async (req, res) => {
await serve(req, res, {
public: path.join(__dirname, '..', 'static'),
symlinks: true,
trailingSlash: true,
cleanUrls: false,
renderSingle: false,
unlisted: [
".DS_Store",
".git",
"Thumbs.db",
// "README*",
],
});
});
console.log(`Setting routes for /svg/*.svg`);;
app.get('/svg/*.svg', async (req, res) => {
await serve(req, res, {
public: path.join(__dirname, '..', 'static'),
symlinks: true,
trailingSlash: true,
cleanUrls: false,
renderSingle: false,
unlisted: [
".DS_Store",
".git",
"Thumbs.db",
// "README*",
],
});
});
console.log(`Setting route for *`);
app.get('*', async (req, res) => {
await serve(req, res, {
@ -140,7 +200,8 @@ app.get('*', async (req, res) => {
unlisted: [
".DS_Store",
".git",
"README*"
"Thumbs.db",
// "README*",
],
redirects: [
{
@ -159,9 +220,9 @@ app.get('*', async (req, res) => {
source: "/OCR-Encoded-PDFs/Russell-County-Web-Site_2024-02-13_19_50_Modified-With-OCR-Encoding/:u(.*)",
destination: "/Web_Site_Archives/Russell_County_Web_Site-2024-02-13_19_50_Modified_With_OCR_Encoding:u"
},
{ source: '/YouTube Channel', destination: '/Russell_County_BOS/YouTube_Channel' },
{ source: '/YouTube Channel.zip', destination: '/Russell_County_BOS/YouTube_Channel.zip' },
{ source: '/YouTube Channel/:u?', destination: '/Russell_County_BOS/YouTube_Channel/:u' },
{ source: '/YouTube Channel', destination: '/Russell_County/Board_of_Supervisors/YouTube_Channel' },
// { source: '/YouTube Channel.zip', destination: '/Russell_County_BOS/YouTube_Channel.zip' },
// { source: '/YouTube Channel/:u?', destination: '/Russell_County_BOS/YouTube_Channel/:u' },
{ source: '/Project Reclaim [WI19KR9Ogwg].mkv', destination: '/YouTube_Archives/@VADMME/Project Reclaim [WI19KR9Ogwg].mkv' },
]
});

View File

@ -48,7 +48,10 @@ const errorTemplate = (vals) => {
const isDirectoryOrDirectorySymbolicLink = (path, max_recursion_depth = 10, cb) => {
lstat(path, {}, (err, sym_stats) => {
if (sym_stats.isSymbolicLink() && max_recursion_depth > 0) {
if (err) {
cb(err);
}
else if (sym_stats.isSymbolicLink() && max_recursion_depth > 0) {
readlink(path, {}, (err, path) => {
isDirectoryOrDirectorySymbolicLink(path, max_recursion_depth-1, cb);
});

View File

@ -13,6 +13,7 @@ const relPathToFiles = './public';
const baseUrl = 'https://no-moss-3-carbo-landfill-library.online'; // URL of the document to download and index
const tikaUrl = 'http://solr.services.cleveland.daball.me:9998'; // URL of the Tika instance
const solrUrl = 'http://solr.services.cleveland.daball.me:8983/solr/my_core'; // URL of your Solr instance
const solrVirginiaLawUrl = 'http://solr.services.cleveland.daball.me:8983/solr/va_code'; // URL of your Solr instance
// Task to clear out previous Solr data
gulp.task('index:clear', async () => {
@ -44,6 +45,19 @@ async function calculateSHA256Hash(filePath) {
});
}
// Function to retrieve metadata of a file from Solr
async function retrieveVirginiaLawMetadataFromSolr(url) {
// Retrieve metadata from Solr based on the file URL or unique identifier
// const response = await axios.get(`${solrUrl}/select?q=id:"${encodeURIComponent(url)}"&fl=${encodeURIComponent('sha256sum, content_length')}`, {
// responseType: 'json'
// });
const fl = encodeURIComponent("sha256sum, content_length");
const q = encodeURIComponent("id:")+"\""+encodeURIComponent(url)+"\"";//encodeURIComponent(`id:"${url}"`);
const uri = `${solrVirginiaLawUrl}/select?q=${q}&fl=${fl}`;
const response = await request({ uri: `${uri}`, json: true });
return response && response.response && response.response.docs && response.response.docs[0];
}
// Function to retrieve metadata of a file from Solr
async function retrieveMetadataFromSolr(url) {
// Retrieve metadata from Solr based on the file URL or unique identifier
@ -71,6 +85,20 @@ async function indexDocumentInSolr(document) {
}
}
async function indexLawDocumentInSolr(document) {
try {
// Send document to Solr using the Solr REST API or a Solr client library
// Example code to send document using Axios:
await axios.post(solrVirginiaLawUrl + '/update/json/docs', document, {
params: {
commit: true, // Commit changes immediately
},
});
} catch (error) {
throw new Error('Error indexing document in Solr: ' + error.message);
}
}
function extToMime(file_name) {
switch (path.extname(file_name)) {
case '.htm':
@ -80,6 +108,8 @@ function extToMime(file_name) {
return 'application/pdf';
case '.md':
case '.txt':
case '.mkv':
return 'video/x-matroska';
default:
return 'text/plain';
}
@ -87,17 +117,12 @@ function extToMime(file_name) {
// Task to index files into Solr
gulp.task('index:docs', async () => {
gulp.task('index:laws', async () => {
//let scanExts = ''; //set to empty string to scan all
let scanExts = '.{pdf,docx,pptx,xlsx,jpg,png,txt}';
let globs = [
'Potesta_&_Associates/**/*.{pdf, docx, jpg, png, txt}',
// 'Russell_County_BOS/Documents/**/*.{pdf, docx, jpg, png, txt}',
'Russell_County_BOS/Meetings/**/*.{pdf, docx, jpg, png, txt}',
'Russell_County_BOS/Ordinances/**/*.{pdf, docx, jpg, png, txt}',
'Russell_County_IDA/Meetings/**/*.{pdf, docx, jpg, png, txt}',
'Russell_County_Tourism/Agenda/**/*.{pdf, docx, jpg, png, txt}',
'Russell_County_Tourism/Minutes/**/*.{pdf, docx, jpg, png, txt}',
'United_Mine_Workers_of_America/**/*.{pdf, docx, jpg, png, txt}',
'Virginia_Energy/**/*.{pdf, docx, jpg, png, txt}',
`Russell_County/Ordinances/**/*${scanExts}`,
`Virginia_Law_Library/**/*${scanExts}`,
];
// Use glob to match files in the local directories
let files = [];
@ -121,7 +146,7 @@ gulp.task('index:docs', async () => {
console.log('URL: ' + url);
// Retrieve metadata of the file from Solr (if it exists)
const metadata = await retrieveMetadataFromSolr(url);
const metadata = await retrieveVirginiaLawMetadataFromSolr(url);
// Calculate file size
const stats = fs.statSync(fileFullPath);
@ -152,7 +177,7 @@ gulp.task('index:docs', async () => {
});
// Use the TikaClient's pipe method to extract text content
await client.pipe(f, writableStream, 'text/plain', path.basename(file));
await client.pipe(f, writableStream, 'text/plain', encodeURI(path.basename(file)));
console.log("Extracted Text:", extractedText);
// Create Solr document
@ -167,6 +192,129 @@ gulp.task('index:docs', async () => {
// Add additional fields as needed (e.g., title, author, etc.)
};
// Send document to Solr for indexing
// Index the file with its text content and metadata
console.log(`Indexing ${url}`);
await indexLawDocumentInSolr(solrDocument);
// Continue
console.log(`Done.`);
} else {
// Metadata matches, skip the file
console.log(`Skipping file '${file}' as metadata matches existing metadata in Solr index.`);
}
}
});
// Task to index files into Solr
gulp.task('index:docs', async () => {
//let scanExts = ''; //set to empty string to scan all
let scanExts = '.{pdf,docx,pptx,xlsx,jpg,png,txt,mkv}';
let globs = [
`Amys_Drop_Box/**/*${scanExts}`,
`CRS_Reports/**/*${scanExts}`,
`Mine_Safety_and_Health_Administration/**/*${scanExts}`,
`Potesta_&_Associates/**/*${scanExts}`,
`Russell_County/**/*${scanExts}`,
`Russell_County_Reclamation_LLC/**/*${scanExts}`,
`Tobacco_Region_Revitalization_Commission/**/*${scanExts}`,
`United_Mine_Workers_of_America/**/*${scanExts}`,
`Virginia_Energy/**/*${scanExts}`,
// I want to put Virginia Law in its own search category first.
// `Virginia_Law_Library/**/*${scanExts}`,
];
// Use glob to match files in the local directories
let files = [];
let cwd = path.resolve(__dirname, relPathToFiles.replaceAll('/', path.sep));
globs.forEach(async (globPattern) => {
files = files.concat(glob.globSync(globPattern, {
cwd,
matchBase: true,
follow: true,
}));
});
console.log(`Found ${files.length} files to index using ${globs.length} glob patterns.`);
// Loop through each file and process them
for (let f = 0; f < files.length; f++) {
const file = files[f];
console.log(`${f+1}/${files.length}: ${file}`);
const fileFullPath = path.join(cwd, file);
let url = `https://no-moss-3-carbo-landfill-library.online/${file.replaceAll(path.sep, '/')}`;
console.log('URL: ' + url);
// Retrieve metadata of the file from Solr (if it exists)
const metadata = await retrieveMetadataFromSolr(url);
// Calculate file size
const stats = fs.statSync(fileFullPath);
const fileSize = stats.size;
// Calculate SHA256 checksum
// const checksum = crypto.createHash('sha256').update(fileContents).digest('hex');
const checksum = await calculateSHA256Hash(fileFullPath);
// Compare metadata
if (!metadata || parseInt(metadata.content_length[0]) != fileSize || metadata.sha256sum[0] != checksum) {
// Metadata mismatch or file not found in Solr, proceed with indexing
console.log(`Processing text from file using Tika.`);
const client = new TikaClient({ host: tikaUrl });
const version = await client.getVersion();
console.info(`Tika Server Version: ${version}`);
let extractedText = '';
let subtitleExt = ".en.vtt";
if (url.endsWith(".webm") || url.endsWith(".mkv") || url.endsWith(".mpg") || url.endsWith(".mpeg") || url.endsWith(".mp4")) {
let subtitleFilePath = fileFullPath.substring(0, fileFullPath.lastIndexOf('.')) + subtitleExt;
if (fs.existsSync(subtitleFilePath)) {
console.log("Found VTT subtitle file at:", subtitleFilePath);
extractedText = fs.readFileSync(subtitleFilePath, 'utf8');
url = url.substring(0, url.lastIndexOf('/')+1);
}
else {
console.log("No subtitles found at: ", subtitleFilePath);
console.log("Skipping this video file. Not adding this to the index until subtitles are available.")
continue;
}
}
else {
// Create a Readable stream for the file contents
let f = fs.createReadStream(fileFullPath);
// Create a writable stream to capture the extracted text content into a string
const writableStream = new Writable({
write(chunk, encoding, callback) {
extractedText += chunk.toString(); // Append the chunk to the extracted text
callback();
}
});
// Use the TikaClient's pipe method to extract text content
await client.pipe(f, writableStream, 'text/plain', encodeURI(path.basename(file)));
}
if (!extractedText) {
console.log("Skipping document because no text was detected.");
continue;
}
else if (extractedText.length < 100) {
console.log("Extracted Text:", extractedText);
}
else {
console.log("Extracted Text (excerpt):", extractedText.substring(0, 99));
}
// Create Solr document
const solrDocument = {
id: url, // Replace with a unique identifier for the document
text: extractedText, // Add the extracted text content
sha256sum: checksum, // Add the checksum
//html: response.data,
url: url,
content_length: fileSize,
content_type: extToMime(url),
// Add additional fields as needed (e.g., title, author, etc.)
};
// Send document to Solr for indexing
// Index the file with its text content and metadata
console.log(`Indexing ${url}`);
@ -181,11 +329,11 @@ gulp.task('index:docs', async () => {
}
});
// Task to optionally run both clearing and indexing
gulp.task('index:reindex', gulp.series('index:clear', 'index:docs'));
// Default task to run indexing
gulp.task('index', gulp.series('index:docs'));
gulp.task('index', gulp.series('index:docs', 'index:laws'));
// Task to optionally run both clearing and indexing
gulp.task('index:reindex', gulp.series('index:clear', 'index'));
// Default task to run indexing
gulp.task('default', gulp.series('index'));

View File

@ -10,6 +10,7 @@
"index": "gulp index",
"index:clear": "gulp index:clear",
"index:docs": "gulp index:docs",
"index:laws": "gulp index:laws",
"index:reindex": "gulp index:reindex"
},
"author": "",

230
static/css/nm3clol.css Normal file
View File

@ -0,0 +1,230 @@
.result-highlight { background-color: #FBF719; font-weight: normal; }
.pt-1500 { padding-top: 100vh; }
:root {
--bs-body-font-size: 1.2rem !important;
}
body {
margin: 0;
background: #fff;
font-family: "Saira Extra Condensed", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
}
p, li {
font-family: "Noto Serif", "Times New Roman", Times, serif;
}
#files li {
font-family: inherit;
}
h1, h2, h3, h4, h5, h6 {
font-family: "Alegreya SC", system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-weight: 700;
}
.list-group-item {
background-color: transparent;
}
.list-group-item-action:focus,.list-group-item-action:hover {
background-color: rgba(244, 67, 54, 0.75);
color: #fff;
}
.list-group-item-action:focus a,.list-group-item-action:hover a:visited {
background-color: rgba(244, 67, 54, 0.75);
color: #fff;
}
.list-group-item-action:focus a:focus,.list-group-item-action:hover a:hover {
color: #fff !important;
text-decoration: underline;
}
.bg-primary {
background-color: #f44336 !important;
}
.nmc3clol-navbar-brand {
font-family: "Cinzel Decorative";
text-transform: capitalize !important;
}
.daball-navbar-brand {
font-family: "Saira Extra Condensed";
}
main {
max-width: 100vw;
margin-top: 50px;
}
header {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}
.nm3clol-navbar-brand, .navbar {
font-size: 1.2rem !important;
}
@media (min-width: 0px) and (max-width: 576px) {
.nm3clol-navbar-brand, .navbar {
font-size: 0.55rem !important;
}
}
@media (min-width: 576px) and (max-width: 768px) {
.nm3clol-navbar-brand, .navbar {
font-size: 0.75rem !important;
}
}
@media (min-width: 768px) and (max-width: 1200px) {
.nm3clol-navbar-brand, .navbar {
font-size: 1.0rem !important;
}
}
.btn-outline-search {
color: #fff;
border-color: #fff;
}
.btn-outline-search:hover {
color: #f44336;
border-color: #f44336;
background-color: rgba(255, 255, 255, 0.75);
}
h1 i {
font-style: normal;
}
ul#files {
margin: 0 0 0 -2px;
padding: 20px 0 0 0;
}
ul#files li {
list-style: none;
font-size: 14px;
display: flex;
justify-content: space-between;
}
a {
text-decoration: none;
}
ul#files a {
color: #000;
padding: 10px 5px;
margin: 0 -5px;
white-space: nowrap;
overflow: hidden;
display: block;
width: 100%;
text-overflow: ellipsis;
}
a {
color: #f44336;
display: inline-block;
line-height: 20px;
}
a:hover, a:active {
color: #0076FF;
display: inline-block;
line-height: 20px;
}
a .pretty, a .cool {
display: none;
}
a:hover .david, a:active .david, a:hover .cool, a:active .cool {
display: inline;
color: #0076FF;
}
a:hover .allen, a:active .allen {
display: inline;
color: #fff;
}
a:hover .ball, a:active .ball, a:hover .pretty, a:active .pretty {
display: inline;
color: #f44336;
}
svg {
height: 13px;
vertical-align: text-bottom;
}
ul#files a::before {
display: inline-block;
vertical-align: middle;
margin-right: 10px;
width: 24px;
text-align: center;
line-height: 12px;
}
/* file-icon svg inlined here, but it should also be possible to separate out. */
ul#files a.file::before {
content: url("data:image/svg+xml;utf8,<svg width='15' height='19' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M10 8C8.34 8 7 6.66 7 5V1H3c-1.1 0-2 .9-2 2v13c0 1.1.9 2 2 2h9c1.1 0 2-.9 2-2V8h-4zM8 5c0 1.1.9 2 2 2h3.59L8 1.41V5zM3 0h5l7 7v9c0 1.66-1.34 3-3 3H3c-1.66 0-3-1.34-3-3V3c0-1.66 1.34-3 3-3z' fill='black'/></svg>");
}
ul#files a.file:hover::before {
content: url("data:image/svg+xml;utf8,<svg width='15' height='19' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M10 8C8.34 8 7 6.66 7 5V1H3c-1.1 0-2 .9-2 2v13c0 1.1.9 2 2 2h9c1.1 0 2-.9 2-2V8h-4zM8 5c0 1.1.9 2 2 2h3.59L8 1.41V5zM3 0h5l7 7v9c0 1.66-1.34 3-3 3H3c-1.66 0-3-1.34-3-3V3c0-1.66 1.34-3 3-3z' fill='white'/></svg>");
text-decoration: underline;
}
/* folder-icon */
ul#files a.folder::before {
content: url("data:image/svg+xml;utf8,<svg width='20' height='16' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M18.784 3.87a1.565 1.565 0 0 0-.565-.356V2.426c0-.648-.523-1.171-1.15-1.171H8.996L7.908.25A.89.89 0 0 0 7.302 0H2.094C1.445 0 .944.523.944 1.171v2.3c-.21.085-.398.21-.565.356a1.348 1.348 0 0 0-.377 1.004l.398 9.83C.42 15.393 1.048 16 1.8 16h15.583c.753 0 1.36-.586 1.4-1.339l.398-9.83c.021-.313-.125-.69-.397-.962zM1.843 3.41V1.191c0-.146.104-.272.25-.272H7.26l1.234 1.088c.083.042.167.104.293.104h8.282c.125 0 .25.126.25.272V3.41H1.844zm15.54 11.712H1.78a.47.47 0 0 1-.481-.46l-.397-9.83c0-.147.041-.252.125-.356a.504.504 0 0 1 .377-.147H17.78c.125 0 .272.063.377.147.083.083.125.209.125.334l-.418 9.83c-.021.272-.23.482-.481.482z' fill='black'/></svg>");
}
ul#files a.folder:hover::before {
content: url("data:image/svg+xml;utf8,<svg width='20' height='16' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M18.784 3.87a1.565 1.565 0 0 0-.565-.356V2.426c0-.648-.523-1.171-1.15-1.171H8.996L7.908.25A.89.89 0 0 0 7.302 0H2.094C1.445 0 .944.523.944 1.171v2.3c-.21.085-.398.21-.565.356a1.348 1.348 0 0 0-.377 1.004l.398 9.83C.42 15.393 1.048 16 1.8 16h15.583c.753 0 1.36-.586 1.4-1.339l.398-9.83c.021-.313-.125-.69-.397-.962zM1.843 3.41V1.191c0-.146.104-.272.25-.272H7.26l1.234 1.088c.083.042.167.104.293.104h8.282c.125 0 .25.126.25.272V3.41H1.844zm15.54 11.712H1.78a.47.47 0 0 1-.481-.46l-.397-9.83c0-.147.041-.252.125-.356a.504.504 0 0 1 .377-.147H17.78c.125 0 .272.063.377.147.083.083.125.209.125.334l-.418 9.83c-.021.272-.23.482-.481.482z' fill='white'/></svg>");
}
/* image-icon */
ul#files a.file.gif::before,
ul#files a.file.jpg::before,
ul#files a.file.png::before,
ul#files a.file.svg::before {
content: url("data:image/svg+xml;utf8,<svg width='16' height='16' viewBox='0 0 80 80' xmlns='http://www.w3.org/2000/svg' fill='none' stroke='black' stroke-width='5' stroke-linecap='round' stroke-linejoin='round'><rect x='6' y='6' width='68' height='68' rx='5' ry='5'/><circle cx='24' cy='24' r='8'/><path d='M73 49L59 34 37 52m16 20L27 42 7 58'/></svg>");
}
::selection {
background-color: #f44336;
color: #fff;
}
::-moz-selection {
background-color: #f44336;
color: #fff;
}
header h1 {
font-size: 2em;
}
header h1 .separator {
font-size: 4.0rem;
line-height: 1rem;
vertical-align: -.75vh;
}
@media (max-width: 1200px) {
header h1 {
font-size: 1.5rem;
}
header h1 .separator {
font-size: 2em;
line-height: 1em;
vertical-align: -.75vh;
}
}
@media (min-width: 768px) {
ul#files {
display: flex;
flex-wrap: wrap;
}
}
@media (min-width: 992px) {
/* body {
padding: 45px;
} */
ul#files li {
font-size: 16pt;
box-sizing: border-box;
justify-content: flex-start;
}
}
img.no-trash-svg {
z-index: -10000;
position: fixed;
width: 1.5%;
opacity: 0.04;
top: 50%;
left: 50%;
margin-top: -0.5vh;
margin-left: -1.5vh;
transform: scale(35, 35);
}
table {
margin-bottom: 5pt;
}
tbody, td, tfoot, th, thead, tr {
font-family: 'Sometype Mono';
padding: 5pt;
}

3796
static/svg/no-trash.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 98 KiB

5
sync-youtube-videos.cmd Normal file
View File

@ -0,0 +1,5 @@
@echo off
S:\bin\yt-dlp.exe -U
S:\bin\yt-dlp.exe --live-from-start --yes-playlist -N 8 -R infinite -c --no-force-overwrites --mtime --write-description --write-info-json --write-playlist-metafiles --write-comments --no-cookies-from-browser --cookies S:\srv\www\no-moss-3-carbo-landfill-library.online\youtube-cookies.txt --write-thumbnail --write-all-thumbnails --write-url-link --write-webloc-link --write-desktop-link --progress --video-multistreams --audio-multistreams --write-subs --write-auto-subs --embed-subs --embed-thumbnail --embed-metadata --embed-chapters --embed-info-json -o "S:\srv\www\no-moss-3-carbo-landfill-library.online\YouTube\%%(uploader_id)s\%%(upload_date>%%Y-%%m-%%d)s-%%(title)s\%%(id)s.%%(ext)s" "https://www.youtube.com/@russellcountyvirginia8228"
S:\bin\yt-dlp.exe --live-from-start --yes-playlist -N 8 -R infinite -c --no-force-overwrites --mtime --write-description --write-info-json --write-playlist-metafiles --write-comments --no-cookies-from-browser --cookies S:\srv\www\no-moss-3-carbo-landfill-library.online\youtube-cookies.txt --write-thumbnail --write-all-thumbnails --write-url-link --write-webloc-link --write-desktop-link --progress --video-multistreams --audio-multistreams --write-subs --write-auto-subs --embed-subs --embed-thumbnail --embed-metadata --embed-chapters --embed-info-json -o "S:\srv\www\no-moss-3-carbo-landfill-library.online\YouTube\%%(uploader_id)s\%%(upload_date>%%Y-%%m-%%d)s-%%(title)s\%%(id)s.%%(ext)s" "https://www.youtube.com/@VADMME"
npm run-script index:docs

View File

@ -9,21 +9,23 @@
<%- include('./includes/top-navbar.ejs') %>
<%- include('./includes/no-trash-svg.ejs') %>
<main class="container">
<header>
<h1 class="mt-5">
<i>&nbsp;</i>
<h1 class="mt-5" style="font-family: 'Covered By Your Grace'">
<% paths.forEach(function(value, index) { %>
<% if (h.shouldShowDirectorySeparator({index})) { %>
<span class="separator">&rsaquo; </span>
<% } %>
<% if (h.shouldShowWelcomeBanner({paths})) { %>
Welcome to <%= h.getDirectoryTitle({directory}) %>
<i>&nbsp;</i>
Get Informed! Stay Informed!
<% } else if (h.shouldOmitLinkOnLastBreadcrumb({paths, index})) { %>
<%= h.trimSlashes({path: value.name}) %>
<%= h.trimSlashes({path: value.name}).replaceAll('_', ' ') %>
<% } else { %>
<a href="/<%= value.url %>">
<%= h.getDirectoryName({directory: value.name}) %>
<%= h.getDirectoryName({directory: value.name}).replaceAll('_', ' ') %>
</a>
<% } %>
<% }); %>
@ -32,7 +34,7 @@
<% if (h.directoryContainsReadme({directory})) {%>
<div class="row p-4 pb-0 pe-lg-0 pt-lg-5 align-items-center rounded-3 border shadow-lg">
<div class="col-lg-7 p-3 p-lg-5 pt-lg-3">
<div class="col-lg-12 p-3 p-lg-5 pt-lg-3">
<%- h.printReadme({directory}) %>
</div>
</div>

View File

@ -36,7 +36,8 @@ const getDirectoryTitle = ({directory}) => {
return (directory=="public") ? getSiteName() : `${title} Listing - ${getSiteName()}`;
};
const getWelcomeBanner = ({directory}) => {
return trimSlashes({path: directory.replace("public", `Welcome to ${getSiteName()}`)});
//return trimSlashes({path: directory.replace("public", `Welcome to ${getSiteName()}`)});
return "Get Informed! Stay Informed!";
};
const shouldShowDirectorySeparator = ({index}) => (index > 0);
const shouldShowWelcomeBanner = ({paths}) => (paths.length == 1);

View File

@ -1,6 +1,6 @@
<nav class="navbar navbar-dark bg-dark sticky-bottom pt-1500 mt-5">
<div class="container center">
<a class="navbar-brand" href="https://daball.me">A website by David A. Ball.</a>
<a class="navbar-brand daball-navbar-brand" href="https://daball.me">A <span class="pretty">pretty</span> <span class="cool">cool</span> website by <span class="david">David</span> <span class="allen">A.</span> <span class="ball">Ball</span>.</a>
</div>
<div class="container">
<p style="text-transform: none; font-weight: 300; justify-content: left; color: #eee; padding-top: 1em; padding-bottom: 1em;">

View File

@ -14,143 +14,15 @@
<!-- Bootstrap CSS -->
<link href="https://daball.me/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Saira+Extra+Condensed:100,200,300,400,500,600,700,800,900" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Cinzel+Decorative:100,200,300,400,500,600,700,800,900" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Alegreya+SC:100,200,300,400,500,600,700,800,900" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Covered+By+Your+Grace:100,200,300,400,500,600,700,800,900" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Sometype+Mono:100,200,300,400,500,600,700,800,900" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Noto+Serif:100,200,300,400,500,600,700,800,900" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,300i,400,400i,600,600i,700,700i,800,800i" rel="stylesheet">
<link href="https://daball.me/vendor/font-awesome/css/font-awesome.min.css" rel="stylesheet">
<link href="https://daball.me/vendor/devicons/css/devicons.min.css" rel="stylesheet">
<link href="https://daball.me/vendor/devicon/devicon.min.css" rel="stylesheet">
<link href="https://daball.me/vendor/simple-line-icons/css/simple-line-icons.css" rel="stylesheet">
<link href="https://daball.me/layouts/blog/css/blog.min.css" rel="stylesheet">
<style type="text/css">
.result-highlight { background-color: #FBF719; font-weight: normal; }
.pt-1500 { padding-top: 100vh; }
:root {
--bs-body-font-size: 1.2rem !important;
}
body {
margin: 0;
background: #fff;
font-family: "Saira Extra Condensed", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
}
main {
max-width: 100vw;
margin-top: 50px;
}
header {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}
h1 i {
font-style: normal;
}
ul#files {
margin: 0 0 0 -2px;
padding: 20px 0 0 0;
}
ul#files li {
list-style: none;
font-size: 14px;
display: flex;
justify-content: space-between;
}
a {
text-decoration: none;
}
ul#files a {
color: #000;
padding: 10px 5px;
margin: 0 -5px;
white-space: nowrap;
overflow: hidden;
display: block;
width: 100%;
text-overflow: ellipsis;
}
a {
color: #0076FF;
display: inline-block;
line-height: 20px;
}
a:hover, a:active {
color: #f44336;
display: inline-block;
line-height: 20px;
}
svg {
height: 13px;
vertical-align: text-bottom;
}
ul#files a::before {
display: inline-block;
vertical-align: middle;
margin-right: 10px;
width: 24px;
text-align: center;
line-height: 12px;
}
/* file-icon svg inlined here, but it should also be possible to separate out. */
ul#files a.file::before {
content: url("data:image/svg+xml;utf8,<svg width='15' height='19' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M10 8C8.34 8 7 6.66 7 5V1H3c-1.1 0-2 .9-2 2v13c0 1.1.9 2 2 2h9c1.1 0 2-.9 2-2V8h-4zM8 5c0 1.1.9 2 2 2h3.59L8 1.41V5zM3 0h5l7 7v9c0 1.66-1.34 3-3 3H3c-1.66 0-3-1.34-3-3V3c0-1.66 1.34-3 3-3z' fill='black'/></svg>");
}
ul#files a:hover {
text-decoration: underline;
}
/* folder-icon */
ul#files a.folder::before {
content: url("data:image/svg+xml;utf8,<svg width='20' height='16' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M18.784 3.87a1.565 1.565 0 0 0-.565-.356V2.426c0-.648-.523-1.171-1.15-1.171H8.996L7.908.25A.89.89 0 0 0 7.302 0H2.094C1.445 0 .944.523.944 1.171v2.3c-.21.085-.398.21-.565.356a1.348 1.348 0 0 0-.377 1.004l.398 9.83C.42 15.393 1.048 16 1.8 16h15.583c.753 0 1.36-.586 1.4-1.339l.398-9.83c.021-.313-.125-.69-.397-.962zM1.843 3.41V1.191c0-.146.104-.272.25-.272H7.26l1.234 1.088c.083.042.167.104.293.104h8.282c.125 0 .25.126.25.272V3.41H1.844zm15.54 11.712H1.78a.47.47 0 0 1-.481-.46l-.397-9.83c0-.147.041-.252.125-.356a.504.504 0 0 1 .377-.147H17.78c.125 0 .272.063.377.147.083.083.125.209.125.334l-.418 9.83c-.021.272-.23.482-.481.482z' fill='black'/></svg>");
}
ul#files a.lambda::before {
content: url("data:image/svg+xml; utf8,<svg width='15' height='19' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M3.5 14.4354H5.31622L7.30541 9.81311H7.43514L8.65315 13.0797C9.05676 14.1643 9.55405 14.5 10.7 14.5C11.0171 14.5 11.291 14.4677 11.5 14.4032V13.1572C11.3847 13.1766 11.2622 13.2024 11.1541 13.2024C10.6351 13.2024 10.3829 13.0281 10.1595 12.4664L8.02613 7.07586C7.21171 5.01646 6.54865 4.5 5.11441 4.5C4.83333 4.5 4.62432 4.53228 4.37207 4.59038V5.83635C4.56667 5.81052 4.66036 5.79761 4.77568 5.79761C5.64775 5.79761 5.9 6.0042 6.4045 7.19852L6.64234 7.77954L3.5 14.4354Z' fill='black'/><rect x='0.5' y='0.5' width='14' height='18' rx='2.5' stroke='black'/></svg>");
}
/* image-icon */
ul#files a.file.gif::before,
ul#files a.file.jpg::before,
ul#files a.file.png::before,
ul#files a.file.svg::before {
content: url("data:image/svg+xml;utf8,<svg width='16' height='16' viewBox='0 0 80 80' xmlns='http://www.w3.org/2000/svg' fill='none' stroke='black' stroke-width='5' stroke-linecap='round' stroke-linejoin='round'><rect x='6' y='6' width='68' height='68' rx='5' ry='5'/><circle cx='24' cy='24' r='8'/><path d='M73 49L59 34 37 52m16 20L27 42 7 58'/></svg>");
}
::selection {
background-color: #f44336;
color: #fff;
}
::-moz-selection {
background-color: #f44336;
color: #fff;
}
header h1 {
font-size: 2em;
}
header h1 .separator {
font-size: 4.0rem;
line-height: 1rem;
vertical-align: -.75vh;
}
@media (max-width: 1250px) {
header h1 {
font-size: 1.0rem;
}
header h1 .separator {
font-size: 2em;
line-height: 1em;
vertical-align: -.75vh;
}
}
@media (min-width: 768px) {
ul#files {
display: flex;
flex-wrap: wrap;
}
}
@media (min-width: 992px) {
/* body {
padding: 45px;
} */
ul#files li {
font-size: 16pt;
box-sizing: border-box;
justify-content: flex-start;
}
}
</style>
<link href="/css/nm3clol.css" rel="stylesheet">

View File

@ -0,0 +1,18 @@
<img id="no-trash-svg" alt="" src="/svg/no-trash.svg" class="no-trash-svg" onload="" />
<script>
function is_iOS() {
return [
'iPad Simulator',
'iPhone Simulator',
'iPod Simulator',
'iPad',
'iPhone',
'iPod'
].includes(navigator.platform)
// iPad on iOS 13 detection
|| (navigator.userAgent.includes("Mac") && "ontouchend" in document)
}
if (is_iOS() && navigator.userAgent.indexOf('AppleWebKit')) {
document.getElementById("no-trash-svg").style.visibility = "hidden";
}
</script>

View File

@ -1,16 +1,15 @@
<div class="navbar navbar-expand-lg fixed-top navbar-dark bg-primary">
<div class="container">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button>
<a class="navbar-brand nmc3clol-navbar-brand" href="/">No Moss 3 Carbo Landfill Online Library</a>
<div class="collapse navbar-collapse" id="navbarResponsive">
<a class="navbar-brand" href="/">No Moss 3 Carbo Landfill Online Library</a>
<!-- <ul class="navbar-nav">
<li class="nav-item"><a class="nav-link" href="/Russell_County_BOS/">Board of Supervisors</a></li>
<li class="nav-item"><a class="nav-link" href="/Russell_County_IDA/">IDA</a></li>
</ul> -->
<ul class="navbar-nav">
<li class="nav-item"><a class="nav-link" href="/">Home</a></li>
</ul>
<!-- Search form -->
<form class="d-flex ms-auto" method="get" action="/search">
<input class="form-control me-2" type="search" placeholder="Search" aria-label="Search" value="<%=(typeof query !='undefined')?query:''%>" name="q">
<button class="btn btn-outline-success" type="submit">Search</button>
<button class="btn btn-outline-search" type="submit">Search</button>
</form>
</div>
</div>

View File

@ -7,12 +7,11 @@
<body onload="initPage()">
<%- include('./includes/top-navbar.ejs') %>
<%- include('./includes/no-trash-svg.ejs') %>
<main class="container">
<header>
<h1 class="mt-5">
<i>&nbsp;</i>
<h1 class="mt-5" style="font-family: 'Covered By Your Grace'">
<% paths.forEach(function(value, index) { %>
<% if (h.shouldShowDirectorySeparator({index})) { %>
<span class="separator">&rsaquo; </span>

View File

@ -6,10 +6,10 @@
</head>
<body>
<%- include('./includes/top-navbar.ejs') %>
<%- include('./includes/no-trash-svg.ejs') %>
<main class="container">
<header>
<h1 class="mt-5">
<i>&nbsp;</i>
<h1 class="mt-5" style="font-family: 'Covered By Your Grace'">
<a href="/">No Moss 3 Carbo Landfill Online Library</a>
<span class="separator">&rsaquo; </span>
Search Error<% if ((typeof query != undefined) && query != '') { %> for <%- query %><% } %>

View File

@ -6,10 +6,10 @@
</head>
<body>
<%- include('./includes/top-navbar.ejs') %>
<%- include('./includes/no-trash-svg.ejs') %>
<main class="container">
<header>
<h1 class="mt-5">
<i>&nbsp;</i>
<h1 class="mt-5" style="font-family: 'Covered By Your Grace'">
<a href="/">No Moss 3 Carbo Landfill Online Library</a>
<span class="separator">&rsaquo; </span>
Search Results for <%- query %>
@ -47,7 +47,7 @@
</div>
<% } %>
<!-- Pagination controls -->
<% if (typeof totalPages !== "undefined") { %>
<% if (typeof totalPages !== "undefined" && totalPages) { %>
<nav aria-label="Search results pagination">
<ul class="pagination justify-content-center mt-4">
<% if (page > 1) { %>

View File

@ -8,11 +8,11 @@
<body onload="initPage()">
<%- include('./includes/top-navbar.ejs') %>
<%- include('./includes/no-trash-svg.ejs') %>
<main class="container">
<header>
<h1 class="mt-5">
<i>&nbsp;</i>
<h1 class="mt-5" style="font-family: 'Covered By Your Grace'">
<% paths.forEach(function(value, index) { %>
<% if (h.shouldShowDirectorySeparator({index})) { %>
<span class="separator">&rsaquo; </span>
@ -30,19 +30,50 @@
</h1>
</header>
<% if (typeof info !== 'undefined') {%>
<% if (typeof videoURL !== 'undefined') {%>
<div class="row p-4 pb-0 pe-lg-0 pt-lg-5 align-items-center rounded-3 border shadow-lg">
<div class="col-lg-12 p-3 p-lg-5 pt-lg-3">
<h1 class="title"><%= (typeof info.title !== 'undefined') ? info.title : "" %> </h1>
<h1 class="title"><%= (typeof info.title !== 'undefined') ? info.fulltitle : "" %></h1>
<video class="object-fit-fill ratio ratio-16x9" controls allowfullscreen>
<source src="<%-videoURL%>">
<source src="<%-encodeURI(videoURL)%>">
<track
label="English"
kind="subtitles"
srclang="en"
src="<%-subtitleURL%>"
src="<%-encodeURI(subtitleURL)%>"
default />
</video>
<% if (typeof info !== 'undefined') {%>
<p><%=info.description%></p>
<p>
<b>View Original:</b>
<a href="<%=info.webpage_url%>" target="_blank"><%=info.title%> (Video)</a>
| <a href="<%=info.channel_url%>" target="_blank"><%=info.channel%> (Channel)</a>
| <a href="<%=info.uploader_url%>" target="_blank"><%=info.uploader%> (Uploader)</a>
</p>
<p>
<b>Download/View:</b>
<a href="<%=encodeURI(videoURL)%>" target="_blank">Video (.<%=info.ext%>)</a>
| <a href="<%=encodeURI(subtitleURL)%>" target="_blank">Subtitles (.vtt)</a>
</p>
<!-- <pre><%=require('util').inspect(info)%></pre> -->
<%}%>
</div>
</div>
<% } %>
<% if (typeof subtitleVTT !== 'undefined') {%>
<div class="row p-4 pb-0 pe-lg-0 pt-lg-5 align-items-center rounded-3 border shadow-lg" style="max-height:65vh;overflow-y:scroll">
<div class="col-lg-12 p-3 p-lg-5 pt-lg-3">
<h2 class="title">Video Transcript (approximately)</h1>
<p>
This transcript may contain automatically generated subtitles as generated using YouTube's
automatic subtitles feature. They may not be correct. No effort has been made to correct
them (so far). These transcripts, whether or not generated, are also used in the site's
Search feature. Please review the <a href="/search-policy" target="_blank">Search Policy</a>
for details about the site features.
</p>
<pre style="white-space:pre-wrap;overflow-wrap:anywhere"><%- subtitleVTT %></pre>
</div>
</div>
<% } %>