Browse Source

progress

master
Rich Brown 2 months ago
parent
commit
be8362affb
16 changed files with 602 additions and 10 deletions
  1. +6
    -6
      .eslintrc.js
  2. +2
    -1
      .gitignore
  3. +31
    -0
      package-lock.json
  4. +3
    -0
      package.json
  5. +3
    -0
      public/favicon.svg
  6. +274
    -0
      public/shownotes.css
  7. +2
    -2
      src/helpers/isProd.js
  8. +2
    -0
      src/index.js
  9. +44
    -0
      src/shownotes/createShownotesMd.js
  10. +15
    -0
      src/shownotes/msToTime.js
  11. +11
    -0
      src/shownotes/rename.js
  12. +58
    -0
      src/shownotes/shownotesRouter.js
  13. +38
    -0
      src/shownotes/template.js
  14. +2
    -0
      src/uploader/uploadRouter.js
  15. +110
    -0
      src/views/shownotes-form.njk
  16. +1
    -1
      src/views/upload.njk

+ 6
- 6
.eslintrc.js View File

@@ -1,20 +1,20 @@
module.exports = {
env: {
es6: true,
node: true
node: true,
},
extends: ["airbnb-base"],
extends: ["airbnb-base", "eslint-config-prettier"],
globals: {
Atomics: "readonly",
SharedArrayBuffer: "readonly"
SharedArrayBuffer: "readonly",
},
parserOptions: {
ecmaVersion: 2018,
sourceType: "module"
sourceType: "module",
},
rules: {
"no-console": 0,
"max-len": ["error", { code: 120 }],
quotes: ["error", "double"]
}
quotes: ["error", "double"],
},
};

+ 2
- 1
.gitignore View File

@@ -111,4 +111,5 @@ dist

# RLB
uploads/
build/
build/
.DS_Store

+ 31
- 0
package-lock.json View File

@@ -2767,6 +2767,14 @@
"object.entries": "^1.1.0"
}
},
"eslint-config-prettier": {
"version": "6.10.1",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.10.1.tgz",
"integrity": "sha512-svTy6zh1ecQojvpbJSgH3aei/Rt7C6i090l5f2WQ4aB05lYHeZIR1qL4wZyyILTbtmnbHP5Yn8MrsOJMGa8RkQ==",
"requires": {
"get-stdin": "^6.0.0"
}
},
"eslint-import-resolver-node": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.3.tgz",
@@ -3147,6 +3155,14 @@
}
}
},
"express-formidable": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/express-formidable/-/express-formidable-1.2.0.tgz",
"integrity": "sha512-w1vXjF3gb50UKTNkFaW8/4rqY4dUrKfZ1sAZzwAF9YxCAgj/29QZsycf71di0GkskrZOAkubk9pvGYfxyAMYiw==",
"requires": {
"formidable": "^1.0.17"
}
},
"express-session": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.0.tgz",
@@ -3488,6 +3504,11 @@
"mime-types": "^2.1.12"
}
},
"formidable": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz",
"integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q=="
},
"forwarded": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
@@ -4085,6 +4106,16 @@
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz",
"integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg=="
},
"get-mp3-duration": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/get-mp3-duration/-/get-mp3-duration-1.0.0.tgz",
"integrity": "sha1-Rx78oPnkkw54ZgFvYHhYL0zpNMs="
},
"get-stdin": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz",
"integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g=="
},
"get-stream": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",


+ 3
- 0
package.json View File

@@ -19,9 +19,12 @@
"consolidate": "^0.15.1",
"cors": "^2.8.5",
"dotenv": "^8.2.0",
"eslint-config-prettier": "^6.10.1",
"express": "^4.17.1",
"express-formidable": "^1.2.0",
"express-session": "^1.17.0",
"form-data": "^3.0.0",
"get-mp3-duration": "^1.0.0",
"github-publish": "^3.0.0",
"google-spreadsheet": "^3.0.9",
"helmet": "^3.21.3",


+ 3
- 0
public/favicon.svg View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<text y=".9em" font-size="90">📝</text>
</svg>

+ 274
- 0
public/shownotes.css View File

@@ -0,0 +1,274 @@
/* https://github.com/hankchizljaw/modern-css-reset */

/* Box sizing rules */
*,
*::before,
*::after {
box-sizing: border-box;
}

/* Remove default padding */
ul[class],
ol[class] {
padding: 0;
}

/* Remove default margin */
body,
h1,
h2,
h3,
h4,
p,
ul[class],
ol[class],
li,
figure,
figcaption,
blockquote,
dl,
dd {
margin: 0;
}

/* Set core body defaults */
body {
min-height: 90vh;
scroll-behavior: smooth;
text-rendering: optimizeSpeed;
line-height: 1.5;
}

/* Remove list styles on ul, ol elements with a class attribute */
ul[class],
ol[class] {
list-style: none;
}

/* A elements that don't have a class get default styles */
a:not([class]) {
text-decoration-skip-ink: auto;
}

/* Make images easier to work with */
img {
max-width: 100%;
display: block;
}

/* Natural flow and rhythm in articles by default */
article > * + * {
margin-top: 1em;
}

/* Inherit fonts for inputs and buttons */
input,
button,
textarea,
select {
font: inherit;
}

/* Remove all animations and transitions for people that prefer not to see them */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}

/* end reset */

:root {
--font: 1rem Avenir, Helvetica, Arial, sans-serif;
--blue: rgb(215, 224, 233, 0.5);
}

body {
background:var(--blue);
padding: 1rem 0 4rem 0;
font: var(--font);
}

h1 {
text-align: center;
}

.form-wrap {
display: grid;
grid-auto-columns: 100px auto 100px;
}

.gutter-left {
grid-column: 1 / 2;
}

.gutter-right {
grid-column: 3 / 4;
}

#middle-col {
grid-column: 2 / 3;
padding: 1rem 0 0 4px;
background: linear-gradient(to top,var(--blue) 50%, white 100%);
}

.rest-of-post {
display: flex;
flex-flow: column nowrap;
}

.rest-of-post > label {
padding: 1rem 0 0 0;
}

#num-and-title-wrap {
display: grid;
grid-auto-columns: 4rem auto;
grid-auto-rows: auto auto;
width: 75%;
}
#epNumber-label {
grid-column: 1 / 2;
grid-row: 1 / 2;
}

#epNumber {
grid-column: 1 / 2;
grid-row: 2 / 3;
}

#title-label {
grid-column: 2 / 3;
grid-row: 1 / 2;
}

#title {
grid-column: 2 / 3;
grid-row: 2 / 3;
}
#subtitle, #keywords, #tags {
width: 75%;
}

#content, #description, #summary {
width: 90%;
}

textarea, input:not([type=submit]):not([type=file]) {
border: 3px solid white;
-webkit-box-shadow:
inset 0 0 8px rgba(0,0,0,0.1),
0 0 16px rgba(0,0,0,0.1);
-moz-box-shadow:
inset 0 0 8px rgba(0,0,0,0.1),
0 0 16px rgba(0,0,0,0.1);
box-shadow:
inset 0 0 8px rgba(0,0,0,0.1),
0 0 16px rgba(0,0,0,0.1);
padding: 5px;
background: rgba(255,255,255,0.5);
margin: 0 0 7px 0;
}


#double-file-input-wrap {
display: grid;
grid-auto-columns: 1fr 1fr;
}

.label-and-filename {
margin: 1rem 0 1rem 0;
}
/*
* File
*/

.file {
position: relative;
display: inline-block;
cursor: pointer;
height: 2.5rem;
}
.file input {
min-width: 14rem;
margin: 0;
filter: alpha(opacity=0);
opacity: 0;
}
.file-custom {
position: absolute;
top: 0;
right: 0;
left: 0;
z-index: 5;
height: 2.5rem;
padding: .5rem 1rem;
line-height: 1.5;
color: #555;
background-color: #fff;
border: .075rem solid #ddd;
border-radius: .25rem;
box-shadow: inset 0 .2rem .4rem rgba(0,0,0,.05);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.file-custom:after {
content: "Choose file...";
}
.file-custom:before {
position: absolute;
top: -.075rem;
right: -.075rem;
bottom: -.075rem;
z-index: 6;
display: block;
content: "Browse";
height: 2.5rem;
padding: .5rem 1rem;
line-height: 1.5;
color: #555;
background-color: #eee;
border: .075rem solid #ddd;
border-radius: 0 .25rem .25rem 0;
}

/* Focus */
.file input:focus ~ .file-custom {
box-shadow: 0 0 0 .075rem #fff, 0 0 0 .2rem #0074d9;
}

#filename-display, #image-filename-display {
height: 2.5rem;
padding: .5rem 1rem;
line-height: 1.5;
color: #555;
}

.submit-btn {
background: cornflowerblue;
font-family: inherit;
border: none;
height: 2.5rem;
padding: .5rem 1rem;
border-radius: .25rem;
line-height: 1.5;
color: #fff;
}

@media screen and (max-width: 768px) {
.form-wrap {
grid-auto-columns: 20px auto 20px;
}
#subtitle, #keywords, #tags, #content, #description, #summary, #num-and-title-wrap {
width: 100%;
}
}

.hidden {
display: none;
}

+ 2
- 2
src/helpers/isProd.js View File

@@ -1,3 +1,3 @@
import '../env';
import "../env";

export default process.env.NODE_ENV === 'production';
export default process.env.NODE_ENV === "production";

+ 2
- 0
src/index.js View File

@@ -23,6 +23,7 @@ import sayRouter from "./say/sayRouter";
import danielRouter from "./daniel/danielRouter";
import radioRouter from "./radio/radioRouter";
import micropubRouter from "./micropub/micropubRouter";
import shownotesRouter from "./shownotes/shownotesRouter";

/* app setup */
const { PORT } = process.env;
@@ -125,6 +126,7 @@ app.use("/episode/", episodeRouter); // GET returns a webpage, POST to / creates
app.use("/say/", sayRouter);
app.use("/daniel/", danielRouter); // POST a string (and code) to /daniel. puts MP3 in /podcasts/phrases
app.use("/radio/", radioRouter);
app.use("/shownotes/", shownotesRouter);
/*
* root router
*/


+ 44
- 0
src/shownotes/createShownotesMd.js View File

@@ -0,0 +1,44 @@
import template from "./template";

export default ({ body, name, fileSizeInBytes, duration }) => {
const {
title,
content,
description,
summary,
epNumber,
subtitle,
tags,
keywords,
image,
} = body;
const imageUrl =
image ||
"https://www.porknachos.com/files/podcasts/notes/greenindexcards.jpg";
const tagSplit = tags.split(",");
const twospaces = "&nbsp;&nbsp;";
const newline = "\n";
const tagsMd = tagSplit
.map((key, idx) => {
const useNewline = idx === tagSplit.length - 1 ? "" : newline;
return `${twospaces}- ${key}${useNewline}`;
})
.join("");
const now = Date.now();
const md = template
.replace(/DATE/g, new Date(now).toISOString())
.replace(/YEAR/g, new Date(now).getFullYear())
.replace(/TAGS/g, tagsMd)
.replace(/KEYWORDS/g, keywords)
.replace(/EPISODENUMBER/g, epNumber)
.replace(/SUBTITLE/g, subtitle) // BEFORE TITLE!
.replace(/TITLE/g, title)
.replace(/CONTENT/g, content)
.replace(/DESCRIPTION/g, description)
.replace(/SUMMARY/g, summary)
.replace(/LENGTH/g, fileSizeInBytes)
.replace(/DURATION/g, duration)
.replace(/FILENAME/g, name)
.replace(/IMAGE_URL/, imageUrl);
return md.replace(/\n/g, "<br />");
};

+ 15
- 0
src/shownotes/msToTime.js View File

@@ -0,0 +1,15 @@
const msToTime = (duration) => {
const milliseconds = parseInt(duration % 1000, 10);
let seconds = parseInt((duration / 1000) % 60, 10);
let minutes = parseInt((duration / (1000 * 60)) % 60, 10);
let hours = parseInt((duration / (1000 * 60 * 60)) % 24, 10);

hours = hours < 10 ? `0${hours}` : hours;
minutes = minutes < 10 ? `0${minutes}` : minutes;
if (milliseconds >= 500) seconds += 1;
seconds = seconds < 10 ? `0${seconds}` : seconds;

return `${hours}:${minutes}:${seconds}`;
};

export default msToTime;

+ 11
- 0
src/shownotes/rename.js View File

@@ -0,0 +1,11 @@
import fs from "fs";

const rename = ({ path, name }) => {
// rename from random UUID to name of orig upload
const pathArray = path.split("/");
const oldName = pathArray[pathArray.length - 1];
const oldPrePath = path.replace(oldName, "");
return fs.renameSync(path, `${oldPrePath}${name}`);
};

export default rename;

+ 58
- 0
src/shownotes/shownotesRouter.js View File

@@ -0,0 +1,58 @@
import fs from "fs";
import express from "express";
import formidableMiddleware from "express-formidable";
import getMP3Duration from "get-mp3-duration";
import isProd from "../helpers/isProd";
import createShownotesMd from "./createShownotesMd";
import msToTime from "./msToTime";
import rename from "./rename";

const shownotesRouter = express.Router();

const uploadDir = isProd
? "/var/www/fileserver/public/podcasts/notes"
: `${__dirname}/uploads`;

shownotesRouter.use(
formidableMiddleware({
encoding: "utf-8",
uploadDir,
multiples: false,
}),
);

shownotesRouter.get("/", (req, res) => res.render("shownotes-form.njk"));

shownotesRouter.post("/", (req, res) => {
console.log(req.files);
// deal with scope issue here.
let imageName;
let imagePath;

// audio file info
const { name, path } = req.files.file;
const fileSizeInBytes = fs.statSync(path).size;
const duration = msToTime(getMP3Duration(fs.readFileSync(path)));
rename({ name, path });

try {
// optional image file info
imageName = req.files.imagefile.name;
imagePath = req.files.imagefile.path;
rename({ name: imageName, path: imagePath });
} catch (e) {
console.log("no image file");
}

// create markdown
const markdown = createShownotesMd({
body: req.fields,
name,
fileSizeInBytes,
duration,
image: imageName || null,
});

return res.send(markdown);
});
export default shownotesRouter;

+ 38
- 0
src/shownotes/template.js View File

@@ -0,0 +1,38 @@
const template = `---
layout: episodelayout.njk
date: DATE
title: "TITLE"
description: "DESCRIPTION"
summary: "SUMMARY"
episode: "EPISODENUMBER"
tags:
TAGS
subtitle: "SUBTITLE"
keywords: "KEYWORDS"
filename: "FILENAME"
id3-duration: DURATION
id3-enclosure-length: LENGTH
id3-url: "https://www.porknachos.com/files/podcasts/notes/FILENAME"
mp3: "https://www.porknachos.com/files/podcasts/notes/FILENAME"
id3-title: "TITLE"
id3-artist: "Pineandvine"
id3-track: "EPISODENUMBER"
id3-album: "Just the Show Notes"
id3-year: "YEAR"
id3-genre: "Humor"
id3-image: "IMAGE_URL"
id3-copyright: "CC BY 4.0"
id3-explicit: "clean"
---
SUMMARY

## Some Notes

CONTENT

<audio controls>
<source src="https://www.porknachos.com/files/podcasts/notes/FILENAME">
</audio>
`;

export default template;

+ 2
- 0
src/uploader/uploadRouter.js View File

@@ -57,6 +57,8 @@ uploadRouter.post("/phrase", (req, res) =>
uploadRouter.post("/image", (req, res) => {
/* TODO: right now this is both an image uploader AND a verry basic quasi-CMS.
* It would make sense to make this JUST an uploader, and make a full(er) CMS elsewhere

or honestly neither.
*/
uploadImage(req, res, async (error) => {
const fqdnFilename = req.file.path.replace(


+ 110
- 0
src/views/shownotes-form.njk View File

@@ -0,0 +1,110 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>make a shownotes episode</title>
{# FIXME: next line must be /node/public/shownotes in prod #}
<link rel="stylesheet" type="text/css" href="/public/shownotes.css" />
</head>
<body>
<h1>Make a shownotes episode: </h1>
<div class="form-wrap">
<div class="gutter-left"></div>
{# FIXME: next line must be /node/shownotes in prod #}
<form id="middle-col" action="/shownotes/" method="post" enctype="multipart/form-data">
<div class="rest-of-post">
<div id="num-and-title-wrap">
<label id="epNumber-label" for="epNumber">#</label>
<input type="number" id="epNumber" name="epNumber"></input>

<label for="title">Title</label>
<input type="text" id="title" name="title" placeholder="ep title"></input>
</div>

<label for="subtitle">Subtitle</label>
<input type="text" id="subtitle" name="subtitle" placeholder="ep subtitle"></input>

<label for="content">Content (markdown)</label>
<textarea id="content" name="content" rows="5" cols="33"></textarea>

<label for="description">Description</label>
<input type="text" id="description" name="description" placeholder="ep description"></input>

<label for="summary">Summary</label>
<input type="text" id="summary" name="summary" placeholder="ep summary"></input>

<label for="keywords">Keywords</label>
<input type="text" id="keywords" name="keywords" placeholder="a single string of keywords"></input>


<label for="tags">tags</label>
<input type="text" id="tags" name="tags" placeholder="comma, separated, tags"></input>

</div>
<div id="double-file-input-wrap">
{# audio file #}
<div class="label-and-filename">
<p>Audio (.mp3)</p><br />
<label class="file" for="file">
<input
id="file"
type="file"
name="file"
aria-label="mp3 uploader"
accept="audio/mp3"
onchange="handleAudioFile(this.files)"
/>
<span class="file-custom"></span>
</label>
<span id="filename-display"></span>
</div>

{# optional image file #}
<div class="label-and-filename">
<p>Optional show image (.jpg)</p><br />
<label class="file" for="imagefile">
<input
id="imagefile"
type="file"
name="imagefile"
aria-label="image uploader"
accept="image/jpg"
onchange="handleImageFile(this.files)"
/>
<span class="file-custom"></span>
</label>
<span id="image-filename-display"></span>
</div>

</div>
<input class="submit-btn" type="submit" />
</form>
<div class="gutter-right"></div>
</div>
<script>
const filename = document.getElementById("filename-display");
const imageFilename = document.getElementById("image-filename-display");
const sub = document.getElementsByClassName("submit-btn")[0];
if (filename.innerText == "") {
sub.classList.add("hidden");
}
function handleAudioFile(f) {
try {
filename.innerText = f[0].name;
sub.classList.remove("hidden");
} catch(e) {
filename.innerText = e;
}
}
function handleImageFile(f) {
try {
imageFilename.innerText = f[0].name;
sub.classList.remove("hidden");
} catch(e) {
imageFilename.innerText = e;
}
}
</script>
</body>
</html>

+ 1
- 1
src/views/upload.njk View File

@@ -75,7 +75,7 @@
sub.classList.remove("hidden");
}
function handleAudioFile(f) {
audioFilename.innerText = f[0].name | "none";
audioFilename.innerText = f[0].name | "none"; // typo?
}
</script>
</body>

Loading…
Cancel
Save