From 7e131a68c602be6aca7ea0b0b704cc4c3faafc6c Mon Sep 17 00:00:00 2001 From: pike Date: Mon, 25 Dec 2023 18:19:56 +0100 Subject: [PATCH] feat: Store FileInfo on folder and Post.. and add methods on Post to handle groups and use those everywhere --- src/models/Folder.ts | 129 ++++++++++++++------------ src/models/Platform.ts | 13 +-- src/models/Post.ts | 113 +++++++++++++++++----- src/platforms/Ayrshare/AsFacebook.ts | 13 ++- src/platforms/Ayrshare/AsInstagram.ts | 41 ++++---- src/platforms/Ayrshare/AsLinkedIn.ts | 25 +++-- src/platforms/Ayrshare/AsReddit.ts | 6 +- src/platforms/Ayrshare/AsTikTok.ts | 6 +- src/platforms/Ayrshare/AsTwitter.ts | 19 ++-- src/platforms/Ayrshare/AsYouTube.ts | 8 +- src/platforms/Ayrshare/Ayrshare.ts | 6 +- src/platforms/Facebook/Facebook.ts | 35 +++---- src/platforms/Instagram/Instagram.ts | 97 +++++++++---------- src/platforms/LinkedIn/LinkedIn.ts | 34 +++---- src/platforms/Reddit/Reddit.ts | 34 +++---- src/platforms/Twitter/Twitter.ts | 39 ++++---- 16 files changed, 340 insertions(+), 278 deletions(-) diff --git a/src/models/Folder.ts b/src/models/Folder.ts index 747b11f..d427c47 100644 --- a/src/models/Folder.ts +++ b/src/models/Folder.ts @@ -1,5 +1,6 @@ import * as fs from "fs"; import * as path from "path"; +import * as sharp from "sharp"; import Logger from "../services/Logger"; @@ -13,12 +14,7 @@ import Logger from "../services/Logger"; export default class Folder { id: string; path: string; - files?: { - text: string[]; - image: string[]; - video: string[]; - other: string[]; - }; + files?: FileInfo[]; constructor(path: string) { this.id = path; @@ -32,79 +28,78 @@ export default class Folder { report(): string { Logger.trace("Folder", "report"); - const files = this.getFiles(); let report = ""; report += "\nFolder: " + this.id; report += "\n - path: " + this.path; - report += - "\n - files: " + - Object.keys(files) - .map((k) => files[k].join()) - .join(); + report += "\n - files: " + this.getFileNames(); return report; } + /** + * Get the filenames in this folder + * @returns array of filenames relative to folder + */ + + getFileNames(): string[] { + Logger.trace("Folder", "getFileNames"); + if (this.files !== undefined) { + return this.files.map((file) => file.name); + } + const files = fs.readdirSync(this.path).filter((file) => { + const regex = /^[^._]/; + return fs.statSync(this.path + "/" + file).isFile() && file.match(regex); + }); + return files; + } + /** * Get the files in this folder * * reads info from disk once, then caches that - * @returns grouped filenames relative to folder + * @returns array of fileinfo for all files in this folder */ - getFiles(): { - text: string[]; - image: string[]; - video: string[]; - other: string[]; - } { + async getFiles(): Promise { Logger.trace("Folder", "getFiles"); - if (this.files != undefined) { - return { - text: [...this.files.text], - image: [...this.files.image], - video: [...this.files.video], - other: [...this.files.other], - }; + if (this.files !== undefined) { + return structuredClone(this.files); } - const files = fs.readdirSync(this.path).filter((file) => { - return ( - fs.statSync(this.path + "/" + file).isFile() && - !file.startsWith("_") && - !file.startsWith(".") - ); - }); - this.files = { - text: [], - image: [], - video: [], - other: [], - }; - this.files.text = files.filter((file) => - ["txt"].includes(file.split(".")?.pop() ?? ""), - ); - this.files.image = files.filter((file) => - ["jpg", "jpeg", "png"].includes(file.split(".")?.pop() ?? ""), - ); - this.files.video = files.filter((file) => - ["mp4"].includes(file.split(".")?.pop() ?? ""), - ); - this.files.other = files.filter( - (file) => - !this.files.text?.includes(file) && - !this.files.image?.includes(file) && - !this.files.video?.includes(file), - ); - return { - text: [...this.files.text], - image: [...this.files.image], - video: [...this.files.video], - other: [...this.files.other], - }; + const fileNames = this.getFileNames(); + this.files = []; + for (let index = 0; index < fileNames.length; index++) { + this.files.push(await this.getFile(fileNames[index], index)); + } + return structuredClone(this.files); + } + + public async getFile(name: string, order: number): Promise { + Logger.trace("Folder", "getFile", name); + const filepath = this.path + "/" + name; + const mime = Folder.guessMimeType(name); + const group = mime !== "application/unknown" ? mime.split("/")[0] : "other"; + const stats = fs.statSync(filepath); + const extension = path.extname(name); + const file = { + name: name, + basename: path.basename(name, extension || ""), + extension: extension.substring(1), + group: group, + mimetype: mime, + size: stats.size, + order: order, + } as FileInfo; + if (group === "image") { + const metadata = await sharp(filepath).metadata(); + file.width = metadata.width; + file.height = metadata.height; + } + return file; } public static guessMimeType(filename: string) { const extension = path.extname(filename); const mimeTypes = { + ".txt": "text/plain", ".png": "image/png", ".mov": "video/quicktime", ".mp4": "video/mp4", @@ -112,6 +107,18 @@ export default class Folder { ".jpeg": "image/jpeg", ".gif": "image/gif", }; - return mimeTypes[extension] ?? "application/unknown"; + return mimeTypes[extension.toLowerCase()] ?? "application/unknown"; } } + +export interface FileInfo { + name: string; + basename: string; + extension: string; + group: string; + size: number; + mimetype: string; + order: number; + width?: number; + height?: number; +} diff --git a/src/models/Platform.ts b/src/models/Platform.ts index 70601f0..a5d211a 100644 --- a/src/models/Platform.ts +++ b/src/models/Platform.ts @@ -139,24 +139,25 @@ export default class Platform { // some default logic. override this // in your own platform if you need. - post.files = folder.getFiles(); + post.files = await folder.getFiles(); + const textFiles = post.getFiles("text"); - if (post.files.text?.includes("body.txt")) { + if (post.hasFile("body.txt")) { post.body = fs.readFileSync(post.folder.path + "/body.txt", "utf8"); - } else if (post.files.text.length === 1) { - const bodyFile = post.files.text[0]; + } else if (textFiles.length === 1) { + const bodyFile = textFiles[0].name; post.body = fs.readFileSync(post.folder.path + "/" + bodyFile, "utf8"); } else { post.body = this.defaultBody; } - if (post.files.text?.includes("title.txt")) { + if (post.hasFile("title.txt")) { post.title = fs.readFileSync(post.folder.path + "/title.txt", "utf8"); } else { post.title = post.body.split("\n", 1)[0]; } - if (post.files.text?.includes("tags.txt")) { + if (post.hasFile("tags.txt")) { post.tags = fs.readFileSync(post.folder.path + "/tags.txt", "utf8"); } diff --git a/src/models/Post.ts b/src/models/Post.ts index 6f951d8..031a80c 100644 --- a/src/models/Post.ts +++ b/src/models/Post.ts @@ -1,6 +1,7 @@ import * as fs from "fs"; -import Folder from "./Folder"; +import Folder, { FileInfo } from "./Folder"; + import Logger from "../services/Logger"; import Platform from "./Platform"; @@ -24,12 +25,7 @@ export default class Post { title: string = ""; body?: string; tags?: string; - files?: { - text: string[]; - image: string[]; - video: string[]; - other: string[]; - }; + files?: FileInfo[]; link?: string; remoteId?: string; @@ -42,7 +38,7 @@ export default class Post { this.scheduled = this.scheduled ? new Date(this.scheduled) : undefined; this.published = this.published ? new Date(this.published) : undefined; } - const assetsPath = this.getFullPath(platform.assetsFolder()); + const assetsPath = this.getFilePath(platform.assetsFolder()); if (!fs.existsSync(assetsPath)) { fs.mkdirSync(assetsPath, { recursive: true }); } @@ -95,26 +91,101 @@ export default class Post { } /** - * @param filename relative path in this post.folder - * @returns the full path to that file + * @returns the files grouped by their group property + */ + getGroupedFiles(): { [group: string]: FileInfo[] } { + return this.files.reduce(function (collector, file) { + (collector[file["group"]] = collector[file["group"]] || []).push(file); + return collector; + }, {}); + } + + /** + * @param groups - names of groups to return files from + * @returns the files within those groups, sorted by order */ - getFullPath(filename: string): string { - return this.folder.path + "/" + filename; + getFiles(...groups: string[]): FileInfo[] { + if (!groups.length) { + return this.files.sort((a, b) => a.order - b.order); + } + return this.files + .filter((file) => groups.includes(file.group)) + .sort((a, b) => a.order - b.order); } /** - * Replace a file in the post with an alternative - * @param src relative path in this post.folder - * @param dst relative path in this post.folder + * @param groups - names of groups to require files from + * @returns boolean if files in post */ - useAlternativeFile(src: string, dst: string) { - for (const type in this.files) { - if (this.files[type].includes(src)) { - // be simple for now - this.files[type].push(dst); - this.files[type] = this.files[type].filter((file) => file !== src); + hasFiles(...groups: string[]): boolean { + if (!groups.length) { + return !!this.files.length; + } + return !!this.files.filter((file) => groups.includes(file.group)).length; + } + + /** + * @param group - the name of the group for which to remove the files + */ + removeFiles(group: string) { + this.files = this.files.filter((file) => file.group !== group); + } + + /** + * @param group - the name of the group for which to remove some files + * @param size - the number of files to leave in the group + */ + limitFiles(group: string, size: number) { + this.getFiles(group).forEach((file, index) => { + if (index > size) { + this.removeFile(file.name); } + }); + } + + /** + * @param name the name of the file + * @returns the files info + */ + hasFile(name: string): boolean { + return this.getFile(name) !== undefined; + } + + /** + * @param name the name of the file + * @returns the files info + */ + getFile(name: string): FileInfo | undefined { + return this.files.find((file) => file.name === name); + } + + /** + * @param name the name of the file to remove + */ + removeFile(name: string) { + this.files = this.files.filter((file) => file.name !== name); + } + + /** + * @param search - the name of the file to replace + * @param replace - the name of the file to replace it with + * @returns the info of the replaced file + */ + async replaceFile(search: string, replace: string): Promise { + const index = this.files.findIndex((file) => file.name === search); + if (index > -1) { + const oldFile = this.getFile(search); + this.files[index] = await this.folder.getFile(replace, oldFile.order); } + return this.files[index]; + } + + /** + * @param name relative path in this post.folder + * @returns the full path to that file + */ + getFilePath(name: string): string { + return this.folder.path + "/" + name; } /** diff --git a/src/platforms/Ayrshare/AsFacebook.ts b/src/platforms/Ayrshare/AsFacebook.ts index f9774cb..b4f18dd 100644 --- a/src/platforms/Ayrshare/AsFacebook.ts +++ b/src/platforms/Ayrshare/AsFacebook.ts @@ -1,4 +1,3 @@ -import * as fs from "fs"; import * as sharp from "sharp"; import Ayrshare from "./Ayrshare"; @@ -20,17 +19,17 @@ export default class AsFacebook extends Ayrshare { const post = await super.preparePost(folder); if (post) { // facebook : max 10mb images - for (const src of post.files.image) { + for (const file of post.getFiles("image")) { + const src = file.name; const dst = this.assetsFolder() + "/facebook-" + src; - const size = fs.statSync(post.getFullPath(src)).size / (1024 * 1024); - if (size >= 10) { + if (file.size / (1024 * 1024) >= 10) { console.log("Resizing " + src + " for facebook .."); - await sharp(post.getFullPath(src)) + await sharp(post.getFilePath(src)) .resize({ width: 1200, }) - .toFile(post.getFullPath(dst)); - post.useAlternativeFile(src, dst); + .toFile(post.getFilePath(dst)); + await post.replaceFile(src, dst); } } post.save(); diff --git a/src/platforms/Ayrshare/AsInstagram.ts b/src/platforms/Ayrshare/AsInstagram.ts index 26ce19a..d7ac93e 100644 --- a/src/platforms/Ayrshare/AsInstagram.ts +++ b/src/platforms/Ayrshare/AsInstagram.ts @@ -1,4 +1,3 @@ -import * as path from "path"; import * as sharp from "sharp"; import Ayrshare from "./Ayrshare"; @@ -20,38 +19,34 @@ export default class AsInstagram extends Ayrshare { async preparePost(folder: Folder): Promise { const post = await super.preparePost(folder); if (post && post.files) { - // instagram: 1 video for reel - if (post.files.video.length) { - if (post.files.video.length > 10) { - Logger.trace("Removing > 10 videos for instagram caroussel.."); - post.files.video.length = 10; - } - const remaining = 10 - post.files.video.length; - if (post.files.image.length > remaining) { - Logger.trace("Removing some images for instagram caroussel.."); - post.files.image.length = remaining; - } + if (post.getFiles("video").length > 10) { + Logger.trace("Removing > 10 videos for instagram caroussel.."); + post.limitFiles("video", 10); + } + const remaining = 10 - post.getFiles("video").length; + if (post.getFiles("image").length > remaining) { + Logger.trace("Removing some images for instagram caroussel.."); + post.limitFiles("image", remaining); } // instagram : scale images, jpeg only - for (const src of post.files.image) { - const metadata = await sharp(post.getFullPath(src)).metadata(); - if (metadata.width > 1440) { + for (const file of post.getFiles("image")) { + const src = file.name; + if (file.width > 1440) { Logger.trace("Resizing " + src + " for instagram .."); - const extension = src.split(".")?.pop(); - const basename = path.basename(src, extension ? "." + extension : ""); - const dst = this.assetsFolder() + "/instagram-" + basename + ".JPEG"; - await sharp(post.getFullPath(src)) + const dst = + this.assetsFolder() + "/instagram-" + file.basename + ".JPEG"; + await sharp(post.getFilePath(src)) .resize({ width: 1440, }) - .toFile(post.getFullPath(dst)); - post.useAlternativeFile(src, dst); + .toFile(post.getFilePath(dst)); + await post.replaceFile(src, dst); } } // instagram: require media - if (post.files.image.length + post.files.video.length === 0) { + if (!post.hasFiles("image", "video")) { post.valid = false; } post.save(); @@ -63,7 +58,7 @@ export default class AsInstagram extends Ayrshare { return super.publishAyrshare( post, { - isVideo: post.files.video.length !== 0, + isVideo: post.hasFiles("video"), instagramOptions: { // "autoResize": true -- only enterprise plans }, diff --git a/src/platforms/Ayrshare/AsLinkedIn.ts b/src/platforms/Ayrshare/AsLinkedIn.ts index 05c1138..4ff06bf 100644 --- a/src/platforms/Ayrshare/AsLinkedIn.ts +++ b/src/platforms/Ayrshare/AsLinkedIn.ts @@ -1,4 +1,3 @@ -import * as fs from "fs"; import * as sharp from "sharp"; import Ayrshare from "./Ayrshare"; @@ -21,27 +20,25 @@ export default class AsLinkedIn extends Ayrshare { const post = await super.preparePost(folder); if (post) { // linkedin: max 9 media - if (post.files.video.length > 9) { - post.files.video.length = 9; + if (post.getFiles("video").length > 9) { + post.limitFiles("video", 9); } - if (post.files.image.length + post.files.video.length > 9) { - post.files.image.length = Math.max( - 0, - post.files.image.length - post.files.video.length, - ); + const remaining = 9 - post.getFiles("video").length; + if (post.getFiles("image").length > remaining) { + post.limitFiles("image", remaining); } // linkedin: max 5mb images - for (const src of post.files.image) { + for (const file of post.getFiles("image")) { + const src = file.name; const dst = this.assetsFolder() + "/linkedin-" + src; - const size = fs.statSync(post.getFullPath(src)).size / (1024 * 1024); - if (size >= 5) { + if (file.size / (1024 * 1024) >= 5) { Logger.trace("Resizing " + src + " for linkedin .."); - await sharp(post.getFullPath(src)) + await sharp(post.getFilePath(src)) .resize({ width: 1200, }) - .toFile(post.getFullPath(dst)); - post.useAlternativeFile(src, dst); + .toFile(post.getFilePath(dst)); + await post.replaceFile(src, dst); } } post.save(); diff --git a/src/platforms/Ayrshare/AsReddit.ts b/src/platforms/Ayrshare/AsReddit.ts index e38e6a3..03139fa 100644 --- a/src/platforms/Ayrshare/AsReddit.ts +++ b/src/platforms/Ayrshare/AsReddit.ts @@ -20,10 +20,8 @@ export default class AsReddit extends Ayrshare { const post = await super.preparePost(folder); if (post) { // reddit: max 1 image, no video - post.files.video = []; - if (post.files.image.length > 1) { - post.files.image.length = 1; - } + post.removeFiles("video"); + post.limitFiles("image", 1); post.save(); } return post; diff --git a/src/platforms/Ayrshare/AsTikTok.ts b/src/platforms/Ayrshare/AsTikTok.ts index bb8d668..8527bf0 100644 --- a/src/platforms/Ayrshare/AsTikTok.ts +++ b/src/platforms/Ayrshare/AsTikTok.ts @@ -17,11 +17,11 @@ export default class AsTikTok extends Ayrshare { const post = await super.preparePost(folder); if (post) { // tiktok: one video - post.files.image = []; - if (!post.files.video.length) { + post.removeFiles("image"); + if (!post.hasFiles("video")) { post.valid = false; } else { - post.files.video.length = 1; + post.limitFiles("video", 1); } post.save(); } diff --git a/src/platforms/Ayrshare/AsTwitter.ts b/src/platforms/Ayrshare/AsTwitter.ts index d568017..e7d14bf 100644 --- a/src/platforms/Ayrshare/AsTwitter.ts +++ b/src/platforms/Ayrshare/AsTwitter.ts @@ -1,4 +1,3 @@ -import * as fs from "fs"; import * as sharp from "sharp"; import Ayrshare from "./Ayrshare"; @@ -21,23 +20,21 @@ export default class AsTwitter extends Ayrshare { const post = await super.preparePost(folder); if (post) { // twitter: no video - post.files.video = []; + post.removeFiles("video"); // twitter: max 4 images - if (post.files.image.length > 4) { - post.files.image.length = 4; - } + post.limitFiles("video", 4); // twitter: max 5mb images - for (const src of post.files.image) { + for (const file of post.getFiles("image")) { + const src = file.name; const dst = this.assetsFolder() + "/twitter-" + src; - const size = fs.statSync(post.getFullPath(src)).size / (1024 * 1024); - if (size >= 5) { + if (file.size / (1024 * 1024) >= 5) { Logger.trace("Resizing " + src + " for twitter .."); - await sharp(post.getFullPath(src)) + await sharp(post.getFilePath(src)) .resize({ width: 1200, }) - .toFile(post.getFullPath(dst)); - post.useAlternativeFile(src, dst); + .toFile(post.getFilePath(dst)); + await post.replaceFile(src, dst); } } post.save(); diff --git a/src/platforms/Ayrshare/AsYouTube.ts b/src/platforms/Ayrshare/AsYouTube.ts index 882837d..d4b16f2 100644 --- a/src/platforms/Ayrshare/AsYouTube.ts +++ b/src/platforms/Ayrshare/AsYouTube.ts @@ -17,11 +17,9 @@ export default class AsYouTube extends Ayrshare { const post = await super.preparePost(folder); if (post) { // youtube: only 1 video - post.files.image = []; - if (post.files.video.length > 1) { - post.files.video.length = 1; - } - if (!post.files.video.length) { + post.removeFiles("image"); + post.limitFiles("video", 1); + if (!post.hasFiles("video")) { post.valid = false; } post.save(); diff --git a/src/platforms/Ayrshare/Ayrshare.ts b/src/platforms/Ayrshare/Ayrshare.ts index a92d409..085b390 100644 --- a/src/platforms/Ayrshare/Ayrshare.ts +++ b/src/platforms/Ayrshare/Ayrshare.ts @@ -80,9 +80,9 @@ export default abstract class Ayrshare extends Platform { }[]; }; - const media = [...post.files.image, ...post.files.video].map( - (f) => post.folder.path + "/" + f, - ); + const media = post + .getFiles("image", "video") + .map((f) => post.getFilePath(f.name)); try { const uploads = media.length ? await this.uploadMedia(media) : []; diff --git a/src/platforms/Facebook/Facebook.ts b/src/platforms/Facebook/Facebook.ts index e47835d..d2e04b1 100644 --- a/src/platforms/Facebook/Facebook.ts +++ b/src/platforms/Facebook/Facebook.ts @@ -45,22 +45,23 @@ export default class Facebook extends Platform { const post = await super.preparePost(folder); if (post && post.files) { // facebook: video post can only contain 1 video - if (post.files.video.length) { - post.files.video.length = 1; - post.files.image = []; + if (post.hasFiles("video")) { + Logger.warn("have video"); + post.limitFiles("video", 1); + post.removeFiles("image"); } // facebook : max 4mb images - for (const src of post.files.image) { - const dst = this.assetsFolder() + "/facebook-" + src; - const size = fs.statSync(post.getFullPath(src)).size / (1024 * 1024); - if (size >= 4) { - Logger.trace("Resizing " + src + " for facebook .."); - await sharp(post.getFullPath(src)) + for (const file of post.getFiles("image")) { + if (file.size / (1024 * 1024) >= 4) { + Logger.trace("Resizing " + file.name + " for facebook .."); + const src = file.name; + const dst = this.assetsFolder() + "/facebook-" + file.name; + await sharp(post.getFilePath(src)) .resize({ width: 1200, }) - .toFile(post.getFullPath(dst)); - post.useAlternativeFile(src, dst); + .toFile(post.getFilePath(dst)); + await post.replaceFile(src, dst); } } post.save(); @@ -75,13 +76,13 @@ export default class Facebook extends Platform { let response = { id: "-99" } as { id: string }; let error = undefined; - if (post.files.video.length) { + if (post.hasFiles("video")) { try { response = await this.publishVideoPost(post, dryrun); } catch (e) { error = e; } - } else if (post.files.image.length) { + } else if (post.hasFiles("image")) { try { response = await this.publishImagesPost(post, dryrun); } catch (e) { @@ -130,7 +131,7 @@ export default class Facebook extends Platform { /** * POST images to the page/feed endpoint using json * @param post - the post - * @param dryrun - wether to really execure + * @param dryrun - wether to really execute * @returns object, incl. id of the created post */ private async publishImagesPost( @@ -138,9 +139,9 @@ export default class Facebook extends Platform { dryrun: boolean = false, ): Promise<{ id: string }> { const attachments = []; - for (const image of post.files.image) { + for (const image of post.getFiles("image")) { attachments.push({ - media_fbid: (await this.uploadImage(post.folder.path + "/" + image))[ + media_fbid: (await this.uploadImage(post.getFilePath(image.name)))[ "id" ], }); @@ -170,7 +171,7 @@ export default class Facebook extends Platform { post: Post, dryrun: boolean = false, ): Promise<{ id: string }> { - const file = post.folder.path + "/" + post.files.video[0]; + const file = post.getFilePath(post.getFiles("video")[0].name); const title = post.title; const description = post.body; diff --git a/src/platforms/Instagram/Instagram.ts b/src/platforms/Instagram/Instagram.ts index aa2fbf0..f262828 100644 --- a/src/platforms/Instagram/Instagram.ts +++ b/src/platforms/Instagram/Instagram.ts @@ -44,37 +44,37 @@ export default class Instagram extends Platform { const post = await super.preparePost(folder); if (post && post.files) { // instagram: 1 video for reel - if (post.files.video.length) { - if (post.files.video.length > 10) { + const numVideos = post.getFiles("video").length; + if (numVideos) { + if (numVideos > 10) { Logger.trace("Removing > 10 videos for instagram caroussel.."); - post.files.video.length = 10; + post.limitFiles("video", 10); } - const remaining = 10 - post.files.video.length; - if (post.files.image.length > remaining) { + const remaining = 10 - post.getFiles("video").length; + if (post.getFiles("image").length > remaining) { Logger.trace("Removing some images for instagram caroussel.."); - post.files.image.length = remaining; + post.limitFiles("images", remaining); } } // instagram : scale images, jpeg only - for (const src of post.files.image) { - const metadata = await sharp(post.getFullPath(src)).metadata(); - if (metadata.width > 1440) { + for (const file of post.getFiles("image")) { + if (file.width > 1440) { + const src = file.name; + const dst = + this.assetsFolder() + "/instagram-" + file.basename + ".JPEG"; Logger.trace("Resizing " + src + " for instagram .."); - const extension = src.split(".")?.pop(); - const basename = path.basename(src, extension ? "." + extension : ""); - const dst = this.assetsFolder() + "/instagram-" + basename + ".JPEG"; - await sharp(post.getFullPath(src)) + await sharp(post.getFilePath(src)) .resize({ width: 1440, }) - .toFile(post.getFullPath(dst)); - post.useAlternativeFile(src, dst); + .toFile(post.getFilePath(dst)); + await post.replaceFile(src, dst); } } // instagram: require media - if (post.files.image.length + post.files.video.length === 0) { + if (post.getFiles("image").length + post.getFiles("video").length === 0) { post.valid = false; } post.save(); @@ -89,13 +89,13 @@ export default class Instagram extends Platform { let response = { id: "-99" } as { id: string }; let error = undefined; - if (post.files.video.length === 1 && !post.files.image.length) { + if (post.getFiles("video").length === 1 && !post.hasFiles("image")) { try { response = await this.publishVideoPost(post, dryrun); } catch (e) { error = e; } - } else if (post.files.image.length === 1 && !post.files.video.length) { + } else if (post.getFiles("image").length === 1 && !post.hasFiles("video")) { try { response = await this.publishImagePost(post, dryrun); } catch (e) { @@ -131,7 +131,7 @@ export default class Instagram extends Platform { post: Post, dryrun: boolean = false, ): Promise<{ id: string }> { - const file = post.files.image[0]; + const file = post.getFilePath(post.getFiles("image")[0].name); const caption = post.body; const photoId = (await this.uploadImage(file))["id"]; const photoLink = await this.getImageLink(photoId); @@ -171,7 +171,7 @@ export default class Instagram extends Platform { post: Post, dryrun: boolean = false, ): Promise<{ id: string }> { - const file = post.files.video[0]; + const file = post.getFilePath(post.getFiles("video")[0].name); const caption = post.body; const videoId = (await this.uploadVideo(file))["id"]; const videoLink = await this.getVideoLink(videoId); @@ -213,34 +213,35 @@ export default class Instagram extends Platform { ): Promise<{ id: string }> { const uploadIds = [] as string[]; - for (const video of post.files.video) { - const videoId = (await this.uploadVideo(post.folder.path + "/" + video))[ - "id" - ]; - const videoLink = await this.getVideoLink(videoId); - uploadIds.push( - ( - await this.api.postJson("%USER%/media", { - is_carousel_item: true, - video_url: videoLink, - }) - )["id"], - ); - } - - for (const image of post.files.image) { - const photoId = (await this.uploadImage(post.folder.path + "/" + image))[ - "id" - ]; - const photoLink = await this.getImageLink(photoId); - uploadIds.push( - ( - await this.api.postJson("%USER%/media", { - is_carousel_item: true, - image_url: photoLink, - }) - )["id"], - ); + for (const file of post.getFiles("video", "image")) { + if (file.group === "video") { + const videoId = (await this.uploadVideo(post.getFilePath(file.name)))[ + "id" + ]; + const videoLink = await this.getVideoLink(videoId); + uploadIds.push( + ( + await this.api.postJson("%USER%/media", { + is_carousel_item: true, + video_url: videoLink, + }) + )["id"], + ); + } + if (file.group === "image") { + const photoId = (await this.uploadImage(post.getFilePath(file.name)))[ + "id" + ]; + const photoLink = await this.getImageLink(photoId); + uploadIds.push( + ( + await this.api.postJson("%USER%/media", { + is_carousel_item: true, + image_url: photoLink, + }) + )["id"], + ); + } } // create carousel diff --git a/src/platforms/LinkedIn/LinkedIn.ts b/src/platforms/LinkedIn/LinkedIn.ts index bfde400..36f17ad 100644 --- a/src/platforms/LinkedIn/LinkedIn.ts +++ b/src/platforms/LinkedIn/LinkedIn.ts @@ -58,23 +58,23 @@ export default class LinkedIn extends Platform { const post = await super.preparePost(folder); if (post) { // linkedin: prefer video, max 1 video - if (post.files.video.length) { - post.files.video.length = 1; - post.files.image = []; + if (post.hasFiles("video")) { + post.limitFiles("video", 1); + post.removeFiles("image"); } // linkedin: max 5mb images - for (const src of post.files.image) { + for (const file of post.getFiles("image")) { + const src = file.name; const dst = this.assetsFolder() + "/linkedin-" + src; - const size = fs.statSync(post.getFullPath(src)).size / (1024 * 1024); - if (size >= 5) { + if (file.size / (1024 * 1024) >= 5) { Logger.trace("Resizing " + src + " for linkedin .."); - await sharp(post.getFullPath(src)) + await sharp(post.getFilePath(src)) .resize({ width: 1200, }) - .toFile(post.getFullPath(dst)); - post.useAlternativeFile(src, dst); + .toFile(post.getFilePath(dst)); + await post.replaceFile(src, dst); } } post.save(); @@ -92,19 +92,19 @@ export default class LinkedIn extends Platform { }; let error = undefined; - if (post.files.video.length) { + if (post.hasFiles("video")) { try { response = await this.publishVideoPost(post, dryrun); } catch (e) { error = e; } - } else if (post.files.image.length > 1) { + } else if (post.getFiles("image").length > 1) { try { response = await this.publishImagesPost(post, dryrun); } catch (e) { error = e; } - } else if (post.files.image.length === 1) { + } else if (post.getFiles("image").length === 1) { try { response = await this.publishImagePost(post, dryrun); } catch (e) { @@ -183,7 +183,7 @@ export default class LinkedIn extends Platform { Logger.trace("LinkedIn.publishImagePost"); const title = post.title; const content = post.body; - const image = post.folder.path + "/" + post.files.image[0]; + const image = post.getFilePath(post.getFiles("image")[0].name); const leash = await this.getImageLeash(); await this.uploadImage(leash.value.uploadUrl, image); // TODO: save headers[etag] .. @@ -220,9 +220,9 @@ export default class LinkedIn extends Platform { private async publishImagesPost(post: Post, dryrun: boolean = false) { Logger.trace("LinkedIn.publishImagesPost"); const content = post.title + "\n\n" + post.body; - const images = post.files.image.map( - (image) => post.folder.path + "/" + image, - ); + const images = post + .getFiles("image") + .map((image) => post.getFilePath(image.name)); const imageIds = []; for (const image of images) { const leash = await this.getImageLeash(); @@ -269,7 +269,7 @@ export default class LinkedIn extends Platform { const title = post.title; const content = post.body; - const video = post.folder.path + "/" + post.files.video[0]; + const video = post.getFilePath(post.getFiles("video")[0].name); const leash = await this.getVideoLeash(video); await this.uploadVideo(leash.value.uploadInstructions[0].uploadUrl, video); diff --git a/src/platforms/Reddit/Reddit.ts b/src/platforms/Reddit/Reddit.ts index b370f60..2ec3d87 100644 --- a/src/platforms/Reddit/Reddit.ts +++ b/src/platforms/Reddit/Reddit.ts @@ -57,24 +57,24 @@ export default class Reddit extends Platform { if (post) { // reddit: max 1 image or video // TODO: extract video thumbnail - if (post.files.video.length >= 1) { // eslint-disable-line - post.files.video.length = 1; - post.files.image = []; - } else if (post.files.image.length > 1) { + if (post.hasFiles('video')) { // eslint-disable-line + post.limitFiles("video", 1); + post.removeFiles("image"); + } + if (post.hasFiles("image")) { + post.limitFiles("image", 1); // 20971520 - const src = post.files.image[0]; - const metadata = await sharp(post.getFullPath(src)).metadata(); - if (metadata.width > 3000) { + const file = post.getFiles("image")[0]; + const src = file.name; + if (file.width > 3000) { Logger.trace("Resizing " + src + " for reddit .."); - const extension = src.split(".")?.pop(); - const basename = path.basename(src, extension ? "." + extension : ""); - const dst = this.assetsFolder() + "/reddit-" + basename + ".jpg"; - await sharp(post.getFullPath(src)) + const dst = this.assetsFolder() + "/reddit-" + file.basename + ".jpg"; + await sharp(post.getFilePath(src)) .resize({ width: 3000, }) - .toFile(post.getFullPath(dst)); - post.files.image = [dst]; + .toFile(post.getFilePath(dst)); + await post.replaceFile(src, dst); } } post.save(); @@ -89,13 +89,13 @@ export default class Reddit extends Platform { let response = {}; let error = undefined; - if (post.files.video.length) { + if (post.hasFiles("video")) { try { response = await this.publishVideoPost(post, dryrun); } catch (e) { error = e; } - } else if (post.files.image.length) { + } else if (post.hasFiles("image")) { try { response = await this.publishImagePost(post, dryrun); } catch (e) { @@ -164,7 +164,7 @@ export default class Reddit extends Platform { private async publishImagePost(post: Post, dryrun = false): Promise { Logger.trace("Reddit.publishImagePost"); const title = post.title; - const file = post.folder.path + "/" + post.files.image[0]; + const file = post.getFilePath(post.getFiles("image")[0].name); const leash = await this.getUploadLeash(file); const imageUrl = await this.uploadFile(leash, file); if (!dryrun) { @@ -199,7 +199,7 @@ export default class Reddit extends Platform { private async publishVideoPost(post: Post, dryrun = false): Promise { Logger.trace("Reddit.publishVideoPost"); const title = post.title; - const file = post.folder.path + "/" + post.files.video[0]; + const file = post.getFilePath(post.getFiles("video")[0].name); const leash = await this.getUploadLeash(file); const videoUrl = await this.uploadFile(leash, file); if (!dryrun) { diff --git a/src/platforms/Twitter/Twitter.ts b/src/platforms/Twitter/Twitter.ts index 47d5cf5..d7578e5 100644 --- a/src/platforms/Twitter/Twitter.ts +++ b/src/platforms/Twitter/Twitter.ts @@ -1,4 +1,3 @@ -import * as fs from "fs"; import * as sharp from "sharp"; import Folder from "../../models/Folder"; @@ -56,23 +55,21 @@ export default class Twitter extends Platform { const post = await super.preparePost(folder); if (post) { // twitter: no video - post.files.video = []; + post.removeFiles("video"); // twitter: max 4 images - if (post.files.image.length > 4) { - post.files.image.length = 4; - } + post.limitFiles("image", 4); // twitter: max 5mb images - for (const src of post.files.image) { + for (const file of post.getFiles("image")) { + const src = file.name; const dst = this.assetsFolder() + "/twitter-" + src; - const size = fs.statSync(post.getFullPath(src)).size / (1024 * 1024); - if (size >= 5) { + if (file.size / (1024 * 1024) >= 5) { Logger.trace("Resizing " + src + " for twitter .."); - await sharp(post.getFullPath(src)) + await sharp(post.getFilePath(src)) .resize({ width: 1200, }) - .toFile(post.getFullPath(dst)); - post.useAlternativeFile(src, dst); + .toFile(post.getFilePath(dst)); + await post.replaceFile(src, dst); } } post.save(); @@ -91,7 +88,7 @@ export default class Twitter extends Platform { }; let error = undefined; - if (post.files.image.length) { + if (post.hasFiles("image")) { try { response = await this.publishImagesPost(post, dryrun); } catch (e) { @@ -176,17 +173,17 @@ export default class Twitter extends Platform { accessSecret: Storage.get("settings", "TWITTER_OA1_ACCESS_SECRET"), }); const mediaIds = []; - if (post.files.image.length) { - for (const image of post.files.image) { - const path = post.folder.path + "/" + image; - Logger.trace("Uploading " + path + "..."); - try { - mediaIds.push(await client1.v1.uploadMedia(path)); - } catch (e) { - throw Logger.error("Twitter.publishPost uploadMedia failed", e); - } + + for (const image of post.getFiles("image")) { + const path = post.getFilePath(image.name); + Logger.trace("Uploading " + path + "..."); + try { + mediaIds.push(await client1.v1.uploadMedia(path)); + } catch (e) { + throw Logger.error("Twitter.publishPost uploadMedia failed", e); } } + const client2 = new TwitterApi(Storage.get("auth", "TWITTER_ACCESS_TOKEN")); if (!dryrun) {