2019-10-09 06:47:37 -04:00
const r2 = require ( 'r2' )
2019-12-10 07:45:09 -05:00
const path = require ( 'path' )
const fs = require ( 'fs' )
2020-05-04 09:32:55 -04:00
const { once , EventEmitter } = require ( 'events' )
const { inherits } = require ( 'util' )
2019-10-11 06:55:52 -04:00
const getPost = require ( 'mediumexporter' ) . getPost
2019-12-19 07:20:24 -05:00
const { createClient } = require ( 'webdav' )
2019-12-24 04:20:48 -05:00
const readline = require ( 'readline' )
2019-12-24 12:08:38 -05:00
const { markdown } = require ( 'markdown' )
2019-12-24 11:52:58 -05:00
const GhostAdminAPI = require ( '@tryghost/admin-api' )
2021-01-09 07:52:53 -05:00
const { Canvas , Image } = require ( 'canvas' )
2021-04-03 11:59:01 -04:00
const slugify = require ( 'underscore.string/slugify' )
2020-01-01 11:01:13 -05:00
const Rembrandt = require ( 'rembrandt' )
2021-04-02 11:29:22 -04:00
const nodepub = require ( 'nodepub' )
2021-04-03 11:59:01 -04:00
const cheerio = require ( 'cheerio' )
2019-10-09 06:47:37 -04:00
2020-04-24 11:50:41 -04:00
const config = require ( './config' )
2020-05-04 08:25:41 -04:00
class Seance {
constructor ( ... args ) {
this . MEDIUM _IMG _CDN = 'https://miro.medium.com/fit/c/'
try {
this . ghostAdmin = new GhostAdminAPI ( {
url : config . ghost . url ,
version : config . ghost . version ,
key : config . ghost . admin _key ,
} )
} catch ( err ) {
console . error ( 'Your Ghost isn\'t configured. Please run `seance setup` to fix this!' )
}
/ * *
* function [ fetchFromMedium ]
* @ returns [ string ] status
* /
}
2020-05-04 08:36:01 -04:00
async fetchFromMedium ( mediumUrl , options = {
json : null ,
} ) {
2020-05-04 09:32:55 -04:00
this . emit ( 'update' , {
status : 'starting' ,
message : ` Fetching: ${ mediumUrl } ` ,
loglevel : 'info'
} )
2020-05-04 08:25:41 -04:00
var output = path . join ( process . env . PWD , 'content' )
var json
2020-05-04 08:36:01 -04:00
if ( ! options . json ) {
json = await this . fetchMediumJSON ( mediumUrl )
} else {
json = options . json
}
2020-05-04 08:25:41 -04:00
// use mediumexporter's getPost function to fetch a Medium post
const post = await getPost ( mediumUrl , {
returnObject : true ,
output : output ,
postJSON : json
} ) . catch ( ( err ) => {
return {
error : err ,
}
} )
// set output folder path
// this is based on what mediumexporter chooses as the output folder
var outputFolder = path . join ( output , post . slug )
2020-05-04 09:32:55 -04:00
this . emit ( 'update' , {
status : 'normal' ,
message : ` Saving to: ${ outputFolder } ` ,
loglevel : 'info'
} )
2020-05-04 08:25:41 -04:00
if ( ! fs . existsSync ( path . join ( outputFolder , post . slug ) ) ) {
fs . mkdirSync ( outputFolder , { recursive : true } )
2019-12-10 07:45:09 -05:00
}
2020-05-04 08:25:41 -04:00
// mediumexporter writes a plain .md file if the post has no media
// if that is the case, we should create the subfolder manually
// and copy the data there.
if ( fs . existsSync ( path . join ( output , post . slug + '.md' ) ) ) {
fs . renameSync (
path . join ( output , post . slug + '.md' ) ,
path . join ( outputFolder , 'index.md' )
)
}
2019-12-10 07:45:09 -05:00
2020-05-04 08:25:41 -04:00
// generate metadata
const metadata = JSON . stringify ( {
title : post . title ,
subtitle : post . subtitle ,
author : post . author || "" ,
authors : post . authors || [ ] ,
date : new Date ( post . date ) ,
tags : post . tags ,
url : post . url ,
slug : post . slug ,
images : post . images ,
featuredImage : post . featuredImage ,
} )
2019-12-10 07:45:09 -05:00
2020-05-04 08:25:41 -04:00
// write metadata to output folder
2020-05-04 08:48:31 -04:00
await fs . promises . writeFile ( path . join ( outputFolder , 'metadata.json' ) , metadata )
2020-05-04 08:25:41 -04:00
return post
} ;
2019-12-10 07:45:09 -05:00
2020-05-04 08:25:41 -04:00
/ * *
* function [ pushToGhost ]
2021-03-30 08:11:13 -04:00
* @ description
* Pre - processes and uploads the given article to Ghost
*
* @ param { Boolean } options . noUpload Skip uploading of images
* @ param { Boolean } options . noPush Skip pushing to Ghost ; just generate the file
* @ param { Boolean } options . dryRun Combination of noUpload and noPush
2021-04-02 11:29:22 -04:00
* @ returns [ object ] object containing details of the uploaded Ghost post
2020-05-04 08:25:41 -04:00
* /
2021-03-30 08:11:13 -04:00
async pushToGhost ( postSlug , options = { } ) {
2020-05-04 09:32:55 -04:00
this . emit ( 'update' , {
status : 'starting' ,
message : 'Starting upload: ' + postSlug ,
loglevel : 'info'
} )
2019-12-24 04:20:48 -05:00
2021-03-30 08:11:13 -04:00
if ( ! ! options . dryRun ) {
options . noUpload = true
options . noPush = true
}
console . log ( 'noUpload' , options . noUpload )
2020-05-04 08:25:41 -04:00
// Decide working path
var postFolder = path . resolve ( 'content/' + postSlug )
2019-12-24 04:20:48 -05:00
2020-05-04 08:25:41 -04:00
// Verify file exists
if ( ! fs . existsSync ( postFolder ) ) {
2020-05-04 09:32:55 -04:00
this . emit ( 'error' , {
message : 'Could not find post folder! Is it fetched?' ,
} )
2020-05-04 08:25:41 -04:00
return false
}
// Decide file
const postContent = path . join ( postFolder , 'index.md' )
const postOutput = path . join ( postFolder , 'ghost.md' )
2019-12-24 04:20:48 -05:00
2020-05-04 08:25:41 -04:00
// Verify post exists
if ( ! fs . existsSync ( postContent ) ) {
2020-05-04 09:32:55 -04:00
this . emit ( 'error' , {
message : "Could not find 'index.md' in " + postSlug + "! Is it fetched?" ,
} )
2020-05-04 08:25:41 -04:00
return false
}
2019-12-24 04:20:48 -05:00
2020-05-04 08:25:41 -04:00
// Decide WebDAV upload path
var current _date = new Date ( )
2019-12-24 04:20:48 -05:00
2020-05-04 08:25:41 -04:00
const uploadPath = path . join (
current _date . getUTCFullYear ( ) . toString ( ) ,
( current _date . getUTCMonth ( ) + 1 ) . toString ( ) ,
postSlug
)
2019-12-24 04:20:48 -05:00
2020-05-04 08:25:41 -04:00
// Path where WebDAV files will be placed (eg. https://example.com:2078)
const davPath = path . join ( config . webdav . path _prefix , uploadPath )
2020-01-01 11:05:23 -05:00
2020-05-04 08:25:41 -04:00
// Public path to upload those files (eg. https://media.example.com/uploads)
// We'll do it directly since path.join mangles the protocol
const uploadedPath = config . webdav . uploaded _path _prefix + '/' + uploadPath
2020-01-01 11:05:23 -05:00
2020-05-04 08:25:41 -04:00
// load metadata file
2020-05-04 09:32:55 -04:00
this . emit ( 'update' , {
status : 'starting' ,
message : 'Loading metadata' ,
loglevel : 'debug'
} )
2019-12-24 04:20:48 -05:00
2020-05-04 08:25:41 -04:00
var postMetaFile = path . join ( postFolder , 'metadata.json' )
2020-05-04 08:48:31 -04:00
let postMeta = await JSON . parse ( await fs . promises . readFile ( postMetaFile ) )
2019-12-24 04:20:48 -05:00
2020-05-04 08:25:41 -04:00
// Process lines
const readInterface = readline . createInterface ( {
input : fs . createReadStream ( postContent ) ,
output : process . stdout ,
terminal : false
} )
2019-12-24 04:20:48 -05:00
2020-05-04 08:25:41 -04:00
const outStream = fs . createWriteStream ( postOutput , { encoding : 'utf-8' } )
2019-12-24 12:08:38 -05:00
2021-03-29 12:06:09 -04:00
// We'll calculate these later since Medium messes it up sometimes
let title = null
let subtitle = null
2020-05-04 08:25:41 -04:00
let reImage = new RegExp ( '^!\\[(.*)\\]\\((\\S+?)\\)(.*)' )
2021-03-29 12:06:09 -04:00
let reTitle = new RegExp ( '^#\ (.*)' )
let reSubtitle = new RegExp ( '^#+\ (.*)$' )
2020-05-04 08:25:41 -04:00
// Note down uploaded images
var uploadedImages = [ ]
2020-05-04 09:32:55 -04:00
this . emit ( 'update' , {
status : 'progress' ,
progress : null , // we don't know the percentage
message : 'Parsing post' ,
loglevel : 'info'
} )
2020-05-04 08:25:41 -04:00
for await ( const line of readInterface ) {
// Line to output
// Default is to make it same as input
var newLine = line
2021-03-29 12:06:09 -04:00
// Skip the header (and preceding blank lines)
if ( ! title ) {
// blanks
if ( ! line ) continue
// starting with a # (must be the title)
let match = await reTitle . exec ( line )
if ( match ) {
title = match [ 1 ]
continue // no need to add line; it'll come automatically
}
} else if ( ! subtitle ) {
// check if it's a repeat of the title (Medium does that)
if ( line . endsWith ( title ) ) continue
// otherwise set the subtitle if it doesn't exist
// or if it's a repeat of the title (Medium does that too)
if ( ! subtitle && postMeta . subtitle == postMeta . title ) {
let match = await reSubtitle . exec ( line )
if ( match ) {
subtitle = match [ 1 ]
postMeta . subtitle = match [ 1 ]
}
}
2020-05-04 08:25:41 -04:00
}
2019-12-24 12:08:38 -05:00
2020-05-04 08:25:41 -04:00
// check for images
var m = await reImage . exec ( line )
if ( m ) {
// Get image name
var imageAlt = m [ 1 ]
var imageName = m [ 2 ] . replace ( '*' , '' )
var imagePath = path . join ( postFolder , 'images' , imageName )
2019-12-24 04:20:48 -05:00
2020-05-04 08:25:41 -04:00
if ( ! fs . existsSync ( imagePath ) ) {
console . warn ( 'Skipping missing image: ' + imageName )
} else {
// check for separator image
var isScissors = await this . checkScissors ( imagePath )
if ( isScissors ) {
newLine = '\n---\n'
} else {
// upload pic to server
console . debug ( ` Adding to upload queue: ${ imageName } ` )
uploadedImages . push ( imageName )
2020-05-04 08:51:59 -04:00
// Let's wait for the upload, just to avoid conflicts
2021-03-30 08:11:13 -04:00
if ( ! options . noUpload ) {
await this . uploadDav ( davPath , imagePath )
}
2020-05-04 08:25:41 -04:00
newLine = '![' + imageAlt + '](' + uploadedPath + '/' + imageName + ')'
}
}
}
2019-12-24 04:20:48 -05:00
2020-05-04 08:25:41 -04:00
outStream . write ( newLine + '\n' )
2020-01-01 11:06:29 -05:00
}
2019-12-24 12:08:38 -05:00
2020-05-04 08:25:41 -04:00
// Upload feature_image, if required
var featuredImagePath
if ( ! ! postMeta . featuredImage ) {
var imageName = postMeta . featuredImage . replace ( '*' , '' )
2019-12-24 04:20:48 -05:00
2020-05-04 08:25:41 -04:00
// if the image is listed in postMeta.images, it would have
// already been uploaded
if ( uploadedImages . indexOf ( imageName ) != - 1 ) {
2020-05-04 09:32:55 -04:00
this . emit ( 'update' , {
status : 'progress' ,
progress : 95 , // we don't know the percentage
message : ` Skipping feature image ${ imageName } : already listed for upload ` ,
loglevel : 'info'
} )
2019-12-24 04:20:48 -05:00
} else {
2020-05-04 08:25:41 -04:00
var imagePath = path . join ( postFolder , 'images' , imageName )
2019-12-24 04:20:48 -05:00
2020-05-04 08:25:41 -04:00
// We can only upload if the file exists!
if ( ! fs . existsSync ( imagePath ) ) {
2020-05-04 09:32:55 -04:00
this . emit ( 'update' , {
status : 'progress' ,
progress : 95 , // we don't know the percentage
message : ` Skipping feature image " ${ imageName } ": file not found ` ,
loglevel : 'warning'
} )
2020-05-04 08:25:41 -04:00
} else {
2020-05-04 09:32:55 -04:00
this . emit ( 'update' , {
status : 'progress' ,
progress : 95 , // we don't know the percentage
message : ` Uploading feature image: ${ imageName } ` ,
loglevel : 'info'
} )
2021-03-30 08:11:13 -04:00
if ( ! options . noUpload ) {
this . uploadDav ( davPath , imagePath )
}
2020-05-04 08:25:41 -04:00
featuredImagePath = uploadedPath + '/' + imageName
2020-01-01 11:01:13 -05:00
}
2019-12-24 04:20:48 -05:00
}
}
2020-05-04 08:25:41 -04:00
// calculate users
let users = [ ]
postMeta . authors . forEach ( ( user ) => {
users . push ( { slug : user . username } )
} )
2019-12-24 12:08:38 -05:00
2020-05-04 08:25:41 -04:00
// This will happen once all the line reading is finished
// Uploads will continue in paralell though
2020-05-04 09:32:55 -04:00
this . emit ( 'update' , {
status : 'progress' ,
progress : 100 , // we don't know the percentage
message : 'Uploading to Ghost' ,
loglevel : 'info'
} )
2020-05-04 08:25:41 -04:00
2021-03-30 08:11:13 -04:00
if ( ! options . noPush ) {
let res = await this . ghostAdmin . posts . add ( {
title : postMeta . title ,
custom _excerpt : postMeta . subtitle || null ,
tags : postMeta . tags ,
authors : users ,
html : markdown . toHTML ( await fs . promises . readFile ( postOutput , 'utf-8' ) ) ,
feature _image : featuredImagePath
} , { source : 'html' } )
// Check if user was added
if ( res . primary _author . id == 1 ) {
this . emit ( 'notification' , {
message : ` WARNING: The admin editor, " ${ res . primary _author . name } ", is set as author for this post. If this is incorrect, there was some problem matching usernames. Please check and set it manually. ` ,
} )
}
2021-03-29 12:42:06 -04:00
2021-03-30 08:11:13 -04:00
this . emit ( 'update' , {
status : 'progress' ,
progress : 100 , // we don't know the percentage
message : 'Post conveyed successfully' ,
loglevel : 'info'
} )
2021-03-29 12:42:06 -04:00
2021-03-30 08:11:13 -04:00
return {
slug : res . slug ,
id : res . id ,
uuid : res . uuid ,
preview _url : res . url ,
primary _author : res . primary _author ,
title : res . title ,
subtitle : res . custom _excerpt ,
status : res . status ,
}
} else {
// just return without pushing to Ghost
return {
slug : postSlug ,
id : 0 ,
uuid : 0 ,
preview _url : null ,
primary _author : { } ,
title : postMeta . title ,
subtitle : postMeta . subtitle ,
status : 'none' ,
}
2021-03-29 12:42:06 -04:00
}
2021-03-30 08:11:13 -04:00
}
2019-12-24 04:20:48 -05:00
2019-12-24 12:08:38 -05:00
2020-05-04 08:25:41 -04:00
/ * *
* function [ mediumToGhost ]
* @ returns [ string ] status
* /
mediumToGhost ( mediumUrl ) {
console . info ( 'Copying: ' + mediumUrl ) ;
2020-01-01 11:01:13 -05:00
}
2019-12-24 12:08:38 -05:00
2020-05-04 08:25:41 -04:00
async fetchMediumJSON ( mediumUrl ) {
var json
var text
if ( mediumUrl . match ( /^http/i ) ) {
2020-05-08 06:11:07 -04:00
// remove the anchors at the end
2020-05-04 08:25:41 -04:00
mediumUrl = mediumUrl . replace ( /#.*$/ , '' )
2020-05-08 06:11:07 -04:00
// intelligently add ?json attribute
if ( mediumUrl . indexOf ( 'format=json' ) == - 1 ) {
2021-01-09 06:23:33 -05:00
if ( mediumUrl . indexOf ( '?' ) == - 1 ) {
2020-05-08 06:11:07 -04:00
mediumUrl = ` ${ mediumUrl } ?format=json `
} else {
mediumUrl = ` ${ mediumUrl } &format=json `
}
}
// let's get it!
2020-05-04 08:25:41 -04:00
const response = await fetch ( mediumUrl )
text = await response . text ( )
2020-05-04 08:36:01 -04:00
} else if ( fs . existsSync ( mediumUrl ) ) {
2020-05-04 08:48:31 -04:00
text = ( await fs . promises . readFile ( mediumUrl ) ) . toString ( )
2020-05-04 08:25:41 -04:00
} else {
2020-05-04 08:36:01 -04:00
throw { error : 'URL must be a Medium URL or existing JSON file' }
2020-01-01 11:01:13 -05:00
}
2019-10-09 06:47:37 -04:00
2020-05-04 08:48:31 -04:00
try {
json = await JSON . parse ( text . substr ( text . indexOf ( '{' ) ) )
} catch ( err ) {
throw { error : 'You JSON seems to be malformed' }
}
2020-05-04 08:25:41 -04:00
return json ;
}
/ * *
* function [ checkScissors ]
* @ returns [ boolean ] matchStatus
* /
async checkScissors ( imagePath ) {
// Decide "separator" image
// If set, images matching this will be ignored and replaced
// with a horizontal-rule ("---" in markdown) instead.
let scissors = config . scissors
// if scissors not set, return false
// (it's never a scissors since it never matches)
if ( ! scissors ) {
2020-05-04 09:32:55 -04:00
this . emit ( 'update' , {
status : 'normal' ,
message : '[scissors] No scissors set, so rejecting all images' ,
loglevel : 'warning'
} )
2020-01-01 11:01:13 -05:00
return false
2020-05-04 08:25:41 -04:00
} else {
2021-01-09 07:52:53 -05:00
/ * F i r s t , c h e c k t h a t t h e i m a g e h a s f i n i s h e d l o a d i n g
* ( we don ' t use this , because Rembrandt loads it again
* on its own , which is messy but what to do ¯ \ _ ( ツ ) _ / ¯
* /
try {
let ctx = new Canvas ( ) . getContext ( '2d' )
let img = new Image ( )
img . src = imagePath
ctx . drawImage ( img , 0 , 0 , img . width , img . height )
} catch ( err ) {
this . emit ( 'update' , {
status : 'normal' ,
message : ` [scissors] Skipping scissors check: ${ err . message } ` ,
loglevel : 'warning'
} )
return false
}
2020-05-04 08:25:41 -04:00
// Check if given image matches the scissors
try {
let isScissors = new Rembrandt ( {
imageA : scissors ,
imageB : imagePath ,
thresholdType : Rembrandt . THRESHOLD _PERCENT ,
maxThreshold : 0.1
} )
let result = await isScissors . compare ( )
return result . passed
} catch ( err ) {
2020-05-04 09:32:55 -04:00
this . emit ( 'update' , {
status : 'normal' ,
message : ` [scissors] Skipping scissors check: ${ err . message } ` ,
loglevel : 'warning'
} )
2020-05-04 08:25:41 -04:00
return false
}
2020-01-01 11:01:13 -05:00
}
}
2020-05-04 08:25:41 -04:00
/ * *
* function [ createUser ]
* @ returns [ object ] ghost data json
* /
async generateUserData ( mediumUsername , email ) {
2020-05-04 09:32:55 -04:00
this . emit ( 'update' , {
status : 'starting' ,
message : ` Creating: @ ${ mediumUsername } (email: ${ email } ) ` ,
loglevel : 'debug'
} )
2020-05-04 08:25:41 -04:00
const mediumUrl = ` https://medium.com/@ ${ mediumUsername } /?format=json ` ;
2020-05-08 06:09:44 -04:00
const json = await this . fetchMediumJSON ( mediumUrl ) ;
2020-05-04 08:25:41 -04:00
if ( ! json . success ) {
2020-05-04 09:32:55 -04:00
this . emit ( 'error' , {
message : ` Error: ${ json . error } ` ,
} )
2020-05-04 08:25:41 -04:00
return false
}
2019-10-09 06:47:37 -04:00
2020-05-04 09:32:55 -04:00
this . emit ( 'update' , {
status : 'normal' ,
message : ` Name: ${ json . payload . user . name } ` ,
loglevel : 'debug'
} )
this . emit ( 'update' , {
status : 'normal' ,
message : ` Bio: ${ json . payload . user . bio } ` ,
loglevel : 'debug'
} )
2019-10-09 06:47:37 -04:00
2020-05-04 08:25:41 -04:00
// Download and upload image
2019-12-24 10:00:01 -05:00
2020-05-04 08:25:41 -04:00
let imageId = json . payload . user . imageId
2020-05-04 09:32:55 -04:00
this . emit ( 'update' , {
status : 'normal' ,
message : ` Profile pic: ${ imageId } ` ,
loglevel : 'debug'
} )
2019-12-24 10:00:01 -05:00
2020-05-08 06:09:44 -04:00
let imagePath = this . MEDIUM _IMG _CDN + '256/256/' + imageId
2020-05-04 08:25:41 -04:00
let filetype = imageId . split ( '.' ) [ imageId . split ( '.' ) . length - 1 ]
let fileName = ` ${ mediumUsername } . ${ filetype } `
let filePath = path . join ( process . env . PWD , fileName )
2019-12-24 10:00:01 -05:00
2020-05-04 09:32:55 -04:00
this . emit ( 'update' , {
status : 'normal' ,
message : ` Fetching profile pic: ${ imagePath } ` ,
loglevel : 'info'
} )
2019-12-24 10:00:01 -05:00
2020-05-04 08:25:41 -04:00
const response = await ( await r2 . get ( imagePath ) . response ) . buffer ( )
2020-05-04 08:48:31 -04:00
await await fs . promises . writeFile ( filePath , response , 'base64' )
2019-12-24 10:00:01 -05:00
2020-05-04 09:32:55 -04:00
this . emit ( 'update' , {
status : 'normal' ,
message : ` Uploading profile pic: ${ imagePath } ` ,
loglevel : 'info'
} )
2019-12-24 10:00:01 -05:00
2020-05-04 08:25:41 -04:00
await this . uploadDav ( path . join ( config . webdav . path _prefix , 'avatars' ) ,
filePath )
2019-12-24 10:00:01 -05:00
2020-05-04 08:25:41 -04:00
// Generate Ghost JSON
2019-10-09 06:47:37 -04:00
2020-05-04 08:25:41 -04:00
const ghostData = {
data : {
users : [
{
id : 1 ,
slug : json . payload . user . username ,
bio : json . payload . user . bio ,
email : email ,
name : json . payload . user . name ,
profile _image : config . webdav . uploaded _path _prefix + '/avatars/' + fileName
}
]
} ,
meta : {
exported _on : new Date ,
version : '2.14.0'
}
2019-10-09 06:47:37 -04:00
}
2020-05-04 08:25:41 -04:00
return ( JSON . stringify ( ghostData ) )
} ;
2019-10-06 09:57:08 -04:00
2020-05-04 08:25:41 -04:00
async createDirIfNotExist ( client , folder ) {
// recursively create subfolders if they don't exist.
2019-12-19 07:20:24 -05:00
2020-05-04 08:25:41 -04:00
//safety: don't touch directories outside WEBDAV_PATH_PREFIX
if ( ! folder . startsWith ( config . webdav . path _prefix ) ) {
throw new Error ( ` Cannot create directories outside ${ config . webdav . path _prefix } ` )
}
2019-12-19 07:20:24 -05:00
2020-05-04 08:25:41 -04:00
// check the folder
await client . stat ( folder )
. catch ( async ( err ) => {
if ( ! err . response ) {
// no response! Maybe a network error or something :P
console . error ( ` [dav-upload:folder] Error creating folder " ${ folder } " ` )
console . error ( ` [dav-upload:folder] ${ err . toJSON ( ) . message } ` )
console . error ( '[dav-upload:folder] Please check your Internet connection and try again' )
return false
} else if ( err . response . status == 404 ) {
2019-12-19 07:20:24 -05:00
2020-05-04 08:25:41 -04:00
// it's a 404, so we'll create the directory
2020-05-04 09:32:55 -04:00
this . emit ( 'update' , {
status : 'normal' ,
message : ` Noting missing subdirectory: ${ folder } ` ,
loglevel : 'debug'
} )
2019-12-19 07:20:24 -05:00
2020-05-04 08:25:41 -04:00
// first, create the parent directory (if required)
if ( ! await this . createDirIfNotExist ( client , path . dirname ( folder ) ) ) {
// if not created, we fail too :-/
return false
}
2019-12-19 07:20:24 -05:00
2020-05-04 09:32:55 -04:00
this . emit ( 'update' , {
status : 'normal' ,
message : ` Creating missing subdirectory: ${ folder } ` ,
loglevel : 'debug'
} )
2020-05-04 08:25:41 -04:00
// then, create the current directory
await client . createDirectory ( folder )
. catch ( async ( err ) => {
if ( err . response . status == 405 ) { // Method Not Allowed
// Maybe the directory's already been created in the meantime?
await client . stat ( folder )
. catch ( ( err2 ) => {
// Bad guess. Panic (and raise the original error)
2020-05-04 09:32:55 -04:00
this . emit ( 'update' , {
status : 'error' ,
message : ` Error: ${ err . toJSON ( ) . message } \n We're not sure what went wrong. Help! ` ,
loglevel : 'error'
} )
2020-05-04 08:25:41 -04:00
throw err
} )
} else {
// what's this? Panic!
2020-05-04 09:32:55 -04:00
this . emit ( 'update' , {
status : 'error' ,
message : ` Error: ${ err . toJSON ( ) . message } \n We're not sure what went wrong. Help! ` ,
loglevel : 'error'
} )
2019-12-19 07:20:24 -05:00
throw err
2020-05-04 08:25:41 -04:00
}
} )
} else {
// it's not a 404; we don't know how to handle this. Panic!
2020-05-04 09:32:55 -04:00
this . emit ( 'update' , {
status : 'error' ,
message : 'An unknown error occured. Help!' ,
loglevel : 'error'
} )
2020-05-04 08:25:41 -04:00
console . error ( err . toJSON ( ) )
throw err
}
} )
return true
}
/ * *
* function [ uploadDav ]
* @ returns [ string ] status
* /
async uploadDav ( dirPath , filePath ) {
// connect to webdav
const client = createClient (
config . webdav . server _url ,
{
username : config . webdav . username ,
password : config . webdav . password ,
digest : config . webdav . use _digest
2019-12-19 07:20:24 -05:00
} )
2020-05-04 08:25:41 -04:00
// create directory if not exists
console . debug ( ` [dav-upload] Loading ${ dirPath } ` )
if ( ! await this . createDirIfNotExist ( client , dirPath ) ) {
console . error ( ` [dav-upload] Could not upload ${ path . basename ( filePath ) } :( ` )
return false
2019-12-19 07:20:24 -05:00
}
2020-05-04 08:25:41 -04:00
// upload a file
console . debug ( 'Uploading file' )
const outStream = client . createWriteStream (
path . join ( dirPath , path . basename ( filePath ) )
)
outStream . on ( 'finish' , ( ) => console . debug ( 'Uploaded successfully.' ) )
2019-12-19 07:20:24 -05:00
2020-05-04 08:25:41 -04:00
const inStream = fs . createReadStream ( filePath )
. pipe ( outStream )
2020-01-01 11:08:34 -05:00
2020-05-04 08:25:41 -04:00
return true
}
2019-12-19 07:20:24 -05:00
2021-04-02 11:29:22 -04:00
/ * *
* function [ fetchToEpub ]
* @ description fetches posts from Ghost and packs them into an epub
* @ options . id unique ID for the generated epub
* @ options . title title of the generated epub
* @ options . author author of the generated epub
* @ options . language language of the book
* @ genre genre of the book
* @ cover cover image to use
* @ returns [ string ] status
* /
async fetchToEpub ( postSlugs , options = { } ) {
if ( ! options . title ) options . title = 'Seance Collection'
if ( ! options . author ) options . author = 'Seance'
if ( ! options . language ) options . language = 'en'
if ( ! options . genre ) options . genre = 'Unknown'
if ( ! options . coverImage ) options . coverImage = 'random-cover.jpg'
if ( ! options . outputFolder ) options . outputFolder = '.'
console . log ( ` Fetching: ${ postSlugs } ` )
let allPosts = [ ]
// first, fetch all the posts
for ( let slug of postSlugs ) {
console . log ( ` Fetching: ${ slug } ` )
let post = await this . ghostAdmin . posts . read ( { slug : slug } , { formats : [ 'html' ] } )
allPosts . push ( post )
}
2021-04-03 12:32:21 -04:00
// prepare for image downloads, starting with the scissors!
let pics = [ path . join ( _ _dirname , 'scissors.png' ) ]
2021-04-03 11:59:01 -04:00
let picFolder = path . join ( options . outputFolder , 'seance-images' )
if ( ! fs . existsSync ( picFolder ) ) {
fs . mkdirSync ( picFolder , { recursive : true } )
}
// prepare array to collect processed posts
let processedPosts = [ ]
for ( let post of allPosts ) {
// decide a post slug, for future files
let postSlug = slugify ( post . title )
// get the cover pic
let featurePicTag
if ( ! ! post . feature _image ) {
let imgUrl = post . feature _image
if ( /^\/\//i . test ( imgUrl ) ) {
imgUrl = 'https:' + imgUrl
} else if ( ! /^https?:\/\//i . test ( imgUrl ) ) {
imgUrl = 'https://' + imgUrl
}
let response = await ( await r2 . get ( imgUrl ) . response ) . buffer ( )
let ext = post . feature _image . split ( '.' ) . pop ( )
await await fs . promises . writeFile ( path . join ( picFolder , ` ${ postSlug } . ${ ext } ` ) , response , 'base64' )
featurePicTag = ` <img src="../images/ ${ postSlug } . ${ ext } "/> `
pics . push ( ` ${ picFolder } / ${ postSlug } . ${ ext } ` )
}
2021-04-13 11:28:46 -04:00
let c = cheerio . load ( ` ${ featurePicTag } <h1> ${ post . title . toLowerCase ( ) } </h1> ${ post . html } ` )
2021-04-03 11:59:01 -04:00
// hunt for other pics
// TODO: make asynchronous
let picCounter = 0
c ( 'img' ) . each ( async function ( ) {
// skip if it's a local image
if ( c ( this ) . attr ( 'src' ) . indexOf ( '../images' ) == 0 ) {
return
}
// first, process the url
let imgUrl = c ( this ) . attr ( 'src' )
console . log ( 'Downloading:' , imgUrl )
if ( /^\/\//i . test ( imgUrl ) ) {
imgUrl = 'https:' + imgUrl
} else if ( ! /^https?:\/\//i . test ( imgUrl ) ) {
imgUrl = 'https://' + imgUrl
}
// now decide an output name
let ext = c ( this ) . attr ( 'src' ) . split ( '.' ) . pop ( )
let imageFile = path . join ( picFolder , ` ${ postSlug } -insert- ${ picCounter } . ${ ext } ` )
// note down our calculations
c ( this ) . attr ( 'src' , ` ../images/ ${ postSlug } -insert- ${ picCounter } . ${ ext } ` )
pics . push ( imageFile )
picCounter = picCounter + 1
// finally, download the images
let response = await ( await r2 . get ( imgUrl ) . response ) . buffer ( )
await fs . promises . writeFile ( imageFile , response , 'base64' )
console . log ( 'Downloaded to:' , imageFile )
} )
processedPosts . push ( {
title : post . title ,
body : c . html ( ) ,
} )
}
2021-04-02 11:29:22 -04:00
// decide metadata
let metadata = {
id : 'seance-test' , // FIXME
title : options . title ,
author : options . author ,
language : options . language ,
contents : 'Table of Contents' ,
genre : options . genre ,
cover : options . coverImage ,
2021-04-03 11:59:01 -04:00
images : pics ,
2021-04-02 11:29:22 -04:00
}
// create the ePub
let epub = nodepub . document ( metadata )
2021-04-03 11:59:01 -04:00
// add the documents
for ( let post of processedPosts ) {
epub . addSection ( post . title , post . body )
2021-04-02 11:29:22 -04:00
}
2021-04-03 12:32:21 -04:00
// add the styles
epub . addCSS ( `
img {
width : 100 % ;
height : auto ;
}
h1 {
font - family : "Abhaya Libre Extrabold" ;
text - transform : lowercase ;
text - align : center ;
font - size : 3.6 em ;
line - height : 1 em ;
margin - bottom : 0 ;
}
h1 + h2 {
font - family : "Open Sans Light" ;
font - variant : small - caps ;
text - align : center ;
font - size : 1.2 em ;
line - height : 1 em ;
margin - bottom : 2.4 em ;
}
p {
font - family : "Crimson Text Regular" ;
font - size : 1 em ;
line - height : 1.2 em ;
}
hr {
display : block ;
border : 0 px ;
height : 1 em ;
background - image : url ( '../images/scissors.png' ) ;
background - size : contain ;
background - repeat : no - repeat ;
background - position : 50 % ;
margin - top : 1.5 em ;
margin - bottom : 1.5 em ;
}
` )
2021-04-02 11:29:22 -04:00
// generate it!
2021-04-03 11:59:01 -04:00
await epub . writeEPUB ( options . outputFolder , options . title )
2021-04-02 11:29:22 -04:00
}
2019-12-19 07:20:24 -05:00
}
2020-05-04 09:32:55 -04:00
// Make Seance an EventEmitter
inherits ( Seance , EventEmitter )
2019-10-06 09:57:08 -04:00
module . exports = {
2020-05-04 08:25:41 -04:00
Seance
2019-10-06 09:57:08 -04:00
}