Skip to content

Commit

Permalink
feat: import pdfs & images, improve clip from URL, direct to discord,…
Browse files Browse the repository at this point in the history
… new languages enabled (#1515)
  • Loading branch information
julianpoy authored Feb 8, 2025
2 parents 94bde5c + eeaf18b commit 6539b23
Show file tree
Hide file tree
Showing 39 changed files with 1,282 additions and 185 deletions.
4 changes: 2 additions & 2 deletions packages/backend/src/services/email/welcome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const sendWelcome = async (to: string[], ccTo: string[]) => {
<br /><br />
Please feel free to contact me if you have questions, concerns or comments at <a href="mailto:[email protected]?subject=RecipeSage%20Support">[email protected]</a>.
Please feel free to contact me if you have questions, concerns or comments via <a href="https://discord.gg/yCfzBft">Discord</a>.
<br />
Expand All @@ -30,7 +30,7 @@ export const sendWelcome = async (to: string[], ccTo: string[]) => {
You can access the RecipeSage user guide for more information about using the application: https://docs.recipesage.com
Please feel free to contact me if you have questions, concerns or comments at [email protected].
Please feel free to contact me if you have questions, concerns or comments via Discord https://discord.gg/yCfzBft.
${signaturePlain}
Expand Down
21 changes: 16 additions & 5 deletions packages/express/src/routes/import/job/cookmate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ import { JobMeta, prisma } from "@recipesage/prisma";
import * as Sentry from "@sentry/node";
import * as xmljs from "xml-js";
import { cleanLabelTitle, JOB_RESULT_CODES } from "@recipesage/util/shared";
import { deletePathsSilent } from "@recipesage/util/server/general";
import {
deletePathsSilent,
getImportJobResultCode,
} from "@recipesage/util/server/general";
import { z } from "zod";

const schema = {
Expand Down Expand Up @@ -161,6 +164,10 @@ export const cookmateHandler = defineHandler(
});
}

if (standardizedRecipeImportInput.length === 0) {
throw new Error("No recipes");
}

await prisma.job.update({
where: {
id: job.id,
Expand Down Expand Up @@ -213,19 +220,23 @@ export const cookmateHandler = defineHandler(
e instanceof BadRequestError &&
e.message === "Bad cookmate file format";

const isNoRecipesError =
e instanceof Error && e.message === "No recipes";

await prisma.job.update({
where: {
id: job.id,
},
data: {
status: JobStatus.FAIL,
resultCode: isBadFormatError
? JOB_RESULT_CODES.badFile
: JOB_RESULT_CODES.unknown,
resultCode: getImportJobResultCode({
isBadFormat: isBadFormatError,
isNoRecipes: isNoRecipesError,
}),
},
});

if (!isBadFormatError) {
if (!isBadFormatError && !isNoRecipesError) {
Sentry.captureException(e, {
extra: {
jobId: job.id,
Expand Down
21 changes: 16 additions & 5 deletions packages/express/src/routes/import/job/csv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ import {
cleanLabelTitle,
JOB_RESULT_CODES,
} from "@recipesage/util/shared";
import { deletePathsSilent } from "@recipesage/util/server/general";
import {
deletePathsSilent,
getImportJobResultCode,
} from "@recipesage/util/server/general";
import { z } from "zod";
import { parse } from "csv-parse";

Expand Down Expand Up @@ -194,6 +197,10 @@ export const csvHandler = defineHandler(

await done;

if (standardizedRecipeImportInput.length === 0) {
throw new Error("No recipes");
}

await prisma.job.update({
where: {
id: job.id,
Expand Down Expand Up @@ -246,19 +253,23 @@ export const csvHandler = defineHandler(
e instanceof Error &&
e.message === "end of central directory record signature not found";

const isNoRecipesError =
e instanceof Error && e.message === "No recipes";

await prisma.job.update({
where: {
id: job.id,
},
data: {
status: JobStatus.FAIL,
resultCode: isBadFormatError
? JOB_RESULT_CODES.badFile
: JOB_RESULT_CODES.unknown,
resultCode: getImportJobResultCode({
isBadFormat: isBadFormatError,
isNoRecipes: isNoRecipesError,
}),
},
});

if (!isBadFormatError) {
if (!isBadFormatError && !isNoRecipesError) {
Sentry.captureException(e, {
extra: {
jobId: job.id,
Expand Down
11 changes: 7 additions & 4 deletions packages/express/src/routes/import/job/fdxz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import * as Sentry from "@sentry/node";
import { userHasCapability } from "@recipesage/util/server/capabilities";
import { z } from "zod";
import { spawn } from "child_process";
import { deletePathsSilent } from "@recipesage/util/server/general";
import {
deletePathsSilent,
getImportJobResultCode,
} from "@recipesage/util/server/general";
import {
Capabilities,
cleanLabelTitle,
Expand Down Expand Up @@ -155,9 +158,9 @@ export const fdxzHandler = defineHandler(
},
data: {
status: JobStatus.FAIL,
resultCode: isBadFormatError
? JOB_RESULT_CODES.badFile
: JOB_RESULT_CODES.unknown,
resultCode: getImportJobResultCode({
isBadFormat: isBadFormatError,
}),
},
});

Expand Down
200 changes: 200 additions & 0 deletions packages/express/src/routes/import/job/images.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import { BadRequestError } from "../../../errors";
import {
AuthenticationEnforcement,
defineHandler,
} from "../../../defineHandler";
import * as multer from "multer";
import * as fs from "fs/promises";
import * as extract from "extract-zip";
import * as path from "path";
import { indexRecipes } from "@recipesage/util/server/search";
import { JobStatus, JobType } from "@prisma/client";
import {
importStandardizedRecipes,
StandardizedRecipeImportEntry,
} from "@recipesage/util/server/db";
import { ocrImagesToRecipe } from "@recipesage/util/server/ml";
import { JobMeta, prisma } from "@recipesage/prisma";
import * as Sentry from "@sentry/node";
import {
deletePathsSilent,
getImportJobResultCode,
} from "@recipesage/util/server/general";
import { cleanLabelTitle, JOB_RESULT_CODES } from "@recipesage/util/shared";
import { z } from "zod";

const schema = {
query: z.object({
labels: z.string().optional(),
}),
};

export const imagesHandler = defineHandler(
{
schema,
authentication: AuthenticationEnforcement.Required,
beforeHandlers: [
multer({
dest: "/tmp/import/",
}).single("file"),
],
},
async (req, res) => {
const userLabels =
req.query.labels?.split(",").map((label) => cleanLabelTitle(label)) || [];

const cleanupPaths: string[] = [];

const file = req.file;
if (!file) {
throw new BadRequestError(
"Request must include multipart file under the 'file' field",
);
}

const job = await prisma.job.create({
data: {
userId: res.locals.session.userId,
type: JobType.IMPORT,
status: JobStatus.RUN,
progress: 1,
meta: {
importType: "images",
importLabels: userLabels,
} satisfies JobMeta,
},
});

// We complete this work outside of the scope of the request
const start = async () => {
const zipPath = file.path;
cleanupPaths.push(zipPath);
const extractPath = zipPath + "-extract";
cleanupPaths.push(extractPath);

await extract(zipPath, { dir: extractPath });

const fileNames = await fs.readdir(extractPath);

const standardizedRecipeImportInput: StandardizedRecipeImportEntry[] = [];
for (const fileName of fileNames) {
const filePath = path.join(extractPath, fileName);

if (
!filePath.endsWith(".jpg") &&
!filePath.endsWith(".jpeg") &&
!filePath.endsWith(".png")
) {
continue;
}

const recipeImageBuffer = await fs.readFile(filePath);
const recipeImageBase64 = await fs.readFile(filePath, "base64");
const images = [];
images.push(recipeImageBase64);

const recipe = await ocrImagesToRecipe([recipeImageBuffer]);
if (!recipe) {
continue;
}

standardizedRecipeImportInput.push({
...recipe,
images,
labels: userLabels,
});
}

if (standardizedRecipeImportInput.length === 0) {
throw new Error("No recipes");
}

await prisma.job.update({
where: {
id: job.id,
},
data: {
progress: 50,
},
});

const createdRecipeIds = await importStandardizedRecipes(
res.locals.session.userId,
standardizedRecipeImportInput,
);

const recipesToIndex = await prisma.recipe.findMany({
where: {
id: {
in: createdRecipeIds,
},
userId: res.locals.session.userId,
},
});

await prisma.job.update({
where: {
id: job.id,
},
data: {
progress: 75,
},
});

await indexRecipes(recipesToIndex);

await prisma.job.update({
where: {
id: job.id,
},
data: {
status: JobStatus.SUCCESS,
resultCode: JOB_RESULT_CODES.success,
progress: 100,
},
});
};

start()
.catch(async (e) => {
const isBadZipError =
e instanceof Error &&
e.message === "end of central directory record signature not found";

const isNoRecipesError =
e instanceof Error && e.message === "No recipes";

await prisma.job.update({
where: {
id: job.id,
},
data: {
status: JobStatus.FAIL,
resultCode: getImportJobResultCode({
isBadFormat: isBadZipError,
isNoRecipes: isNoRecipesError,
}),
},
});

if (!isBadZipError && !isNoRecipesError) {
Sentry.captureException(e, {
extra: {
jobId: job.id,
},
});
console.error(e);
}
})
.finally(async () => {
await deletePathsSilent(cleanupPaths);
});

return {
statusCode: 201,
data: {
jobId: job.id,
},
};
},
);
4 changes: 4 additions & 0 deletions packages/express/src/routes/import/job/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { pepperplateHandler } from "./pepperplate";
import { recipekeeperHandler } from "./recipekeeper";
import { urlsHandler } from "./urls";
import { csvHandler } from "./csv";
import { pdfsHandler } from "./pdfs";
import { imagesHandler } from "./images";

const router = express.Router();

Expand All @@ -22,5 +24,7 @@ router.post("/recipekeeper", ...recipekeeperHandler);
router.post("/textfiles", ...textfilesHandler);
router.post("/urls", ...urlsHandler);
router.post("/csv", ...csvHandler);
router.post("/pdfs", ...pdfsHandler);
router.post("/images", ...imagesHandler);

export { router as importJobRouter };
Loading

0 comments on commit 6539b23

Please sign in to comment.