├── .gitignore ├── deno.json ├── .github ├── ISSUE_TEMPLATE │ ├── other-issue.md │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── release.yml │ └── deno.yml ├── .vscode └── settings.json ├── config.json ├── LICENSE ├── email.ts ├── CHANGELOG.md ├── main.ts └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | deno.lock 2 | *.epub 3 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "fmt": { 3 | "lineWidth": 120 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/other-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Other Issue 3 | about: My issue is neither a bug nor a feature request 4 | title: '' 5 | labels: untriaged 6 | assignees: agrmohit 7 | --- 8 | 9 | **Describe your issue** 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true, 4 | "[typescript]": { 5 | "editor.defaultFormatter": "denoland.vscode-deno" 6 | }, 7 | "markdownlint.config": { 8 | "MD024": false, 9 | "MD033": false 10 | }, 11 | "markdownlint.ignore": [".github/**"] 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v[0-9]+.[0-9]+.[0-9]+" 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | 18 | - name: Create GitHub release 19 | uses: docker://ghcr.io/anton-yurchenko/git-release:v6 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG] Title" 5 | labels: bug, untriaged 6 | assignees: agrmohit 7 | --- 8 | 9 | **Describe the bug** 10 | 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | 15 | Steps to reproduce the behavior 16 | 17 | **Expected behavior** 18 | 19 | A clear and concise description of what you expected to happen. 20 | 21 | **Screenshots** 22 | 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Platform (please complete the following information):** 26 | 27 | - OS: 28 | - Version: 29 | 30 | **Additional context** 31 | 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FR] Title" 5 | labels: feature-request, untriaged 6 | assignees: agrmohit 7 | --- 8 | 9 | **Is your feature request related to a problem?** 10 | 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | 15 | A clear and concise description of what you want to happen. 16 | 17 | **Describe alternatives you've considered** 18 | 19 | A clear and concise description of any alternative solutions or features you've considered. 20 | 21 | **Additional context** 22 | 23 | Add any other context or screenshots about the feature request here. 24 | -------------------------------------------------------------------------------- /.github/workflows/deno.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | # This workflow will install Deno then run `deno lint` and `deno test`. 7 | # For more information see: https://github.com/denoland/setup-deno 8 | 9 | name: deno 10 | 11 | on: 12 | push: 13 | branches: 14 | - main 15 | pull_request: 16 | branches: 17 | - main 18 | 19 | permissions: 20 | contents: read 21 | 22 | jobs: 23 | deno: 24 | runs-on: ubuntu-latest 25 | 26 | steps: 27 | - name: Setup repo 28 | uses: actions/checkout@v4 29 | 30 | - name: Setup Deno 31 | uses: denoland/setup-deno@v1 32 | with: 33 | deno-version: v1.x 34 | 35 | - name: Verify formatting 36 | run: deno fmt --check 37 | 38 | - name: Run linter 39 | run: deno lint 40 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "token": "", 3 | "endpoint": "https://api-prod.omnivore.app/api/graphql", 4 | "title": "Omnivore Articles", 5 | "author": "Omnivore", 6 | "cover": "https://r2-public.agrmohit.com/omnivore-articles-bjd81a7x3k.jpg", 7 | "description": "Articles from Omnivore", 8 | "addLabelsInContent": true, 9 | "addArticleLinkInContent": true, 10 | "allowImages": true, 11 | "outputFileName": "output.epub", 12 | "maxArticleCount": 15, 13 | "searchQuery": "sort:saved-desc", 14 | "ignoredLabels": ["pdf"], 15 | "ignoredLinks": ["https://www.youtu", "https://youtu"], 16 | "emailSupport": false, 17 | "emailHost": "smtp.gmail.com", 18 | "emailPort": 587, 19 | "emailUser": "user@example.com", 20 | "emailPassword": "", 21 | "emailRecipient": "my-send-to-kindle-email@kindle.com", 22 | "emailFrom": "Omnivore EPUB Mailer", 23 | "emailAllowSTARTTLS": true, 24 | "emailSizeWarningSuppress": false, 25 | "emailSizeWarningMinSize": 25, 26 | "updateCheck": true, 27 | "showReleaseNotes": true 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Mohit Raj 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /email.ts: -------------------------------------------------------------------------------- 1 | import nodemailer from "npm:nodemailer"; 2 | import config from "./config.json" with { type: "json" }; 3 | 4 | export async function sendEmail() { 5 | const transporter = nodemailer.createTransport({ 6 | host: config.emailHost, 7 | port: config.emailPort, 8 | secure: !config.emailAllowSTARTTLS, 9 | auth: { 10 | user: config.emailUser, 11 | pass: config.emailPassword, 12 | }, 13 | }); 14 | 15 | const mailOptions = { 16 | from: `${config.emailFrom} <${config.emailUser}>`, 17 | to: config.emailRecipient, 18 | subject: config.title, 19 | text: `Sent by Omnivore EPUB (https://github.com/agrmohit/omnivore-epub)`, 20 | attachments: [ 21 | { 22 | path: config.outputFileName, 23 | }, 24 | ], 25 | }; 26 | 27 | if (await verifyEpub()) { 28 | try { 29 | console.log(`📧 Sending email from '${config.emailFrom} <${config.emailUser}>' to '${config.emailRecipient}'`); 30 | const info = await transporter.sendMail(mailOptions); 31 | console.log(`📨 Email sent: ${info.messageId}`); 32 | } catch (error) { 33 | console.error(`🚫 Error: ${error}`); 34 | Deno.exit(1); 35 | } 36 | } 37 | } 38 | 39 | async function verifyEpub(): Promise { 40 | try { 41 | const file = await Deno.stat(config.outputFileName); 42 | 43 | // Check if it is indeed a file 44 | if (!file.isFile) { 45 | console.error(`🚫 ${config.outputFileName} is not a file`); 46 | Deno.exit(1); 47 | } 48 | 49 | // Convert from bytes to MB (not MiB) rounded off to 2 digits after decimal 50 | const ebookSize = (file.size / 1_000_000).toFixed(2); 51 | 52 | // Show a warning if ebook is over a specified size 53 | if (!config.emailSizeWarningSuppress && Number(ebookSize) >= config.emailSizeWarningMinSize) { 54 | console.warn(`⚠️ ebook size is too large at ${ebookSize} MB (limit: ${config.emailSizeWarningMinSize} MB)`); 55 | console.warn("⚠️ Many email providers and eReader emailing services may reject this email"); 56 | console.warn("⚠️ To suppress this warning, set 'emailSuppressSizeWarning' to true in 'config.json'"); 57 | } 58 | } catch (_err) { 59 | console.error(`🚫 ebook file '${config.outputFileName}' is missing`); 60 | Deno.exit(1); 61 | } 62 | 63 | return true; 64 | } 65 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.6.2] - 2024-10-29 4 | 5 | ### Added 6 | 7 | - Note about Omnivore official hosted instance shutting down with deletion of user data on 15th November 2024 8 | 9 | ## [0.6.1] - 2024-04-03 10 | 11 | ### Added 12 | 13 | - Add control flows to stay under Omnivore's rate limits 14 | 15 | ## [0.6.0] - 2024-03-30 16 | 17 | ### Features 18 | 19 | - Switch to Omnivore API Client Library for Node.js 20 | - Add CHANGELOG.md 21 | 22 | ## [0.5.0] - 2024-03-14 23 | 24 | ### Features 25 | 26 | - Escape quotes in search query (by [**@tmr232**](https://github.com/tmr232) in 27 | [#6](https://github.com/agrmohit/omnivore-epub/issues/6)) 28 | - New config option to check for updates when run 29 | - New config option to show release notes when new update is available 30 | 31 | ### Fixes 32 | 33 | - Check for latest release instead of latest tag 34 | 35 | ### Documentation 36 | 37 | - How to use a custom search query along with the required escaping quotes in some cases 38 | - The ability to filter by label using search query 39 | 40 | ## [0.4.1] - 2024-03-02 41 | 42 | ### Fixes 43 | 44 | - Add a body to every email because some eReader mailing services may reject emails with an empty body 45 | 46 | ## [0.4.0] - 2024-03-02 47 | 48 | ### Features 49 | 50 | - Send ebook via email to eReaders like Kindle or Pocketbook (by [**@sascharucks**](https://github.com/sascharucks) in 51 | [#4](https://github.com/agrmohit/omnivore-epub/issues/4)) 52 | - Show Release notes in terminal when a new release is available 53 | 54 | ### Fixes 55 | 56 | - Properly handle error when internet is unavailable and communicate it to user 57 | 58 | ### Maintenance 59 | 60 | - Improve usage instructions 61 | 62 | ## [0.3.0] - 2024-01-10 63 | 64 | ### Features 65 | 66 | - Reduce default article count from 100 to 15 in order to reduce processing time and output file size (by 67 | [**@zsoltika**](https://github.com/zsoltika)) 68 | - Add new option for search-string specifier which can also be used to change the article fetch order to oldest article 69 | first from the current default of newest article first (by [**@zsoltika**](https://github.com/zsoltika)) 70 | 71 | [0.6.2]: https://github.com/agrmohit/omnivore-epub/releases/tag/v0.6.2 72 | [0.6.1]: https://github.com/agrmohit/omnivore-epub/releases/tag/v0.6.1 73 | [0.6.0]: https://github.com/agrmohit/omnivore-epub/releases/tag/v0.6.0 74 | [0.5.0]: https://github.com/agrmohit/omnivore-epub/releases/tag/v0.5.0 75 | [0.4.1]: https://github.com/agrmohit/omnivore-epub/releases/tag/v0.4.1 76 | [0.4.0]: https://github.com/agrmohit/omnivore-epub/releases/tag/v0.4.0 77 | [0.3.0]: https://github.com/agrmohit/omnivore-epub/releases/tag/v0.3.0 78 | -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | import { Omnivore } from "npm:@omnivore-app/api"; 2 | import epub, { Chapter } from "npm:epub-gen-memory"; 3 | import sanitizeHtml from "npm:sanitize-html"; 4 | import config from "./config.json" with { type: "json" }; 5 | import { sendEmail } from "./email.ts"; 6 | 7 | const currentVersion = "v0.6.2"; 8 | 9 | console.log(`ℹ Omnivore EPUB ${currentVersion}`); 10 | console.log("ℹ️ Homepage: https://github.com/agrmohit/omnivore-epub"); 11 | 12 | if (!config.token) { 13 | console.log("❌ Omnivore API token not set"); 14 | console.log( 15 | "❌ Get a token following instructions on: https://docs.omnivore.app/integrations/api.html#getting-an-api-token", 16 | ); 17 | console.log("❌ When you have a token, insert it as value for 'token' field in 'config.json' file"); 18 | Deno.exit(1); 19 | } 20 | 21 | async function checkForUpdates() { 22 | let response; 23 | try { 24 | response = await fetch("https://api.github.com/repos/agrmohit/omnivore-epub/releases/latest"); 25 | } catch (error) { 26 | console.error("🚫 Error: Unable to connect. Please check your internet connection"); 27 | console.error(`🚫 Error: ${error}`); 28 | Deno.exit(1); 29 | } 30 | const latestRelease = await response.json(); 31 | const latestReleaseTagName = latestRelease.tag_name; 32 | 33 | if (latestReleaseTagName !== currentVersion && latestReleaseTagName !== undefined) { 34 | console.log("ℹ New update available"); 35 | console.log(`ℹ ${currentVersion} --> ${latestReleaseTagName}`); 36 | 37 | if (config.showReleaseNotes) { 38 | console.log("ℹ Release Notes:"); 39 | console.log(latestRelease.body); 40 | } 41 | console.log("🌐 View on Web: https://github.com/agrmohit/omnivore-epub/releases/latest"); 42 | } 43 | } 44 | 45 | function sanitizeContent(content: string | null) { 46 | let allowedTags; 47 | if (config.allowImages) { 48 | allowedTags = sanitizeHtml.defaults.allowedTags.concat(["img"]); 49 | } else { 50 | allowedTags = sanitizeHtml.defaults.allowedTags.concat(); 51 | } 52 | 53 | const sanitizedContent = sanitizeHtml(content, { 54 | allowedTags: allowedTags, 55 | }); 56 | 57 | return sanitizedContent; 58 | } 59 | 60 | function sleep(milliseconds: number) { 61 | return new Promise((resolve) => { 62 | setTimeout(() => { 63 | resolve({}); 64 | }, milliseconds); 65 | }); 66 | } 67 | 68 | async function makeEbook() { 69 | const omnivore = new Omnivore({ 70 | apiKey: config.token, 71 | baseUrl: config.endpoint, 72 | }); 73 | 74 | const ignoredLabelsQuery = `-label:${config.ignoredLabels.join(",")}`; 75 | 76 | let endCursor = 0; 77 | const chapters: Chapter[] = []; 78 | const batchSize = 60; 79 | let totalProcessed = 0; 80 | let totalSkipped = 0; 81 | let libraryTotal = 0; 82 | 83 | while (endCursor < config.maxArticleCount) { 84 | if (endCursor !== 0) { 85 | console.log("💤 Sleeping for 1 minute"); 86 | await sleep(60_000); 87 | console.log("🌅 Woke up from sleep"); 88 | } 89 | 90 | const articlesToFetch = (config.maxArticleCount - endCursor > batchSize) 91 | ? batchSize 92 | : config.maxArticleCount - endCursor; 93 | 94 | console.log(`〰️Fetching ${articlesToFetch} articles`); 95 | const articles = await omnivore.items.search({ 96 | first: articlesToFetch, 97 | includeContent: true, 98 | format: "html", 99 | query: `${config.searchQuery} ${ignoredLabelsQuery}`, 100 | after: endCursor, 101 | }); 102 | console.log("🤖 done"); 103 | endCursor = Number(articles.pageInfo.endCursor); 104 | 105 | for (const edge of articles.edges) { 106 | const article = edge.node; 107 | console.log(`🌐 Processing ${article.title}`); 108 | let content = sanitizeContent(article.content); 109 | 110 | if ( 111 | config.ignoredLinks.some((link) => article.url.includes(link)) 112 | ) { 113 | console.log("⚠️ Article skipped: Matched ignored link"); 114 | totalSkipped += 1; 115 | continue; 116 | } 117 | 118 | if (article.labels?.length) { 119 | if (config.addLabelsInContent) { 120 | const labels = article.labels.map((label) => label.name); 121 | content = `Labels: ${labels.join(", ")}` + content; 122 | } 123 | } 124 | 125 | if (config.addArticleLinkInContent) { 126 | content = `Link to Article

` + content; 127 | } 128 | 129 | chapters.push({ 130 | title: article.title, 131 | author: article.author ?? "", 132 | content: content, 133 | filename: article.slug, 134 | }); 135 | 136 | console.log(`✅ done`); 137 | } 138 | 139 | totalProcessed += articles.edges.length; 140 | libraryTotal = Number(articles.pageInfo.totalCount); 141 | if (!articles.pageInfo.hasNextPage) break; 142 | } 143 | 144 | console.log(`🤖 Processed ${totalProcessed} articles out of ${libraryTotal} in your library`); 145 | console.log(`🤖 ${totalSkipped} skipped`); 146 | console.log(`📚 Creating ebook (${config.outputFileName})`); 147 | 148 | const fileBuffer = await epub.default( 149 | { 150 | title: config.title, 151 | author: config.author, 152 | cover: config.cover, 153 | description: config.description, 154 | ignoreFailedDownloads: true, 155 | }, 156 | chapters, 157 | ); 158 | 159 | await Deno.writeFile(config.outputFileName, fileBuffer); 160 | 161 | console.log("📔 Successfully created ebook"); 162 | } 163 | 164 | if (config.updateCheck) { 165 | await checkForUpdates(); 166 | } else { 167 | console.log("🌐 Update checks are disabled"); 168 | console.log("🌐 You can manually check for updates here: https://github.com/agrmohit/omnivore-epub/releases"); 169 | } 170 | 171 | await makeEbook(); 172 | 173 | if (config.emailSupport) { 174 | await sendEmail(); 175 | } 176 | 177 | Deno.exit(); 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Omnivore EPUB 2 | 3 | 4 | 5 | [![GitHub Repo stars](https://img.shields.io/github/stars/agrmohit/omnivore-epub)](https://github.com/agrmohit/omnivore-epub) 6 | [![Gitea Stars](https://img.shields.io/gitea/stars/agrmohit/omnivore-epub?gitea_url=https%3A%2F%2Fcodeberg.org&logo=codeberg)](https://codeberg.org/agrmohit/omnivore-epub) 7 | [![GitHub Release](https://img.shields.io/github/v/release/agrmohit/omnivore-epub)](https://github.com/agrmohit/omnivore-epub/releases) 8 | [![License](https://img.shields.io/badge/license-MIT-informational)](LICENSE) 9 | ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/agrmohit/omnivore-epub) 10 | [![Common Changelog](https://common-changelog.org/badge.svg)](https://common-changelog.org) 11 | 12 | 13 | 14 | > [!CAUTION] 15 | > Unfortunately, Omnivore is [shutting down](https://blog.omnivore.app/p/omnivore-is-joining-elevenlabs) and all data 16 | > will be deleted on 15th November 2024. Please export your data by then. Omnivore will remain open-source and can still 17 | > be self-hosted. This program should continue to work with self-hosted instances. 18 | > 19 | > For people looking to migrate, personally I would recommend [Readeck](https://readeck.org/en/) which is a great FOSS 20 | > alternative though it is missing a few features. [Readwise Reader](https://readwise.io/read) is another good one but 21 | > its paid and not open source. Both offer ways to import Omnivore data. 22 | > 23 | > I am not affiliated with the Omnivore project in any way. 24 | 25 | A program to generate epub file from articles saved in your [Omnivore](https://omnivore.app) library and optionally send 26 | it to your eReader using email. 27 | 28 | Omnivore is an open source read-it-later app similar to Pocket and Instapaper. 29 | 30 | Forked from [here](https://gist.github.com/kebot/90de9c41742cacf371368d85870c4a75) 31 | 32 | ## OS Support 33 | 34 | This program uses no OS specific code and should work on all platforms supported by `Deno`. 35 | 36 | ## Download 37 | 38 | - To download the latest version of the program, you can use git to clone the repository 39 | - If you are not familiar with git, you can always download the latest version by going to 40 | [the latest GitHub release](https://github.com/agrmohit/omnivore-epub/releases/latest) 41 | - Then, under `Assets` click on `Source code (zip)` which will download the latest release of the program in a zip file 42 | which you will need to unzip. 43 | - The program checks for updates when run and will notify you of new releases when available. This can be disabled in 44 | the config file. 45 | 46 | ## Usage 47 | 48 | - Install [Deno](https://deno.com/manual/getting_started/installation) 49 | - Get an API token for Omnivore following instructions 50 | [here](https://docs.omnivore.app/integrations/api.html#getting-an-api-token) 51 | - Put your token in the `token` field in [config file](config.json) 52 | - Modify the configuration file if necessary 53 | - In your terminal, go to the app folder and run the following command `deno run -A main.ts` 54 | - The ebook with extension `.epub` should be in the app directory after execution 55 | 56 | ### Send to eReader 57 | 58 | List of eReaders that support sending ebook using email: 59 | 60 | - [Kindle](https://www.amazon.com/sendtokindle/email) 61 | - [Pocketbook](https://www.youtube.com/watch?v=lFfWwzi8WEM) 62 | 63 | > [!TIP] 64 | > 65 | > Make sure the email address used is approved to send ebook to your eReader 66 | 67 | #### Configuring email 68 | 69 | - Ability to send ebook over email is disabled by default 70 | - To enable it, set `emailSupport` to `true` in the [config file](config.json) 71 | - Set `emailHost` to the [SMTP](https://www.cloudflare.com/en-in/learning/email-security/what-is-smtp/) address of your 72 | email provider 73 | - E.g. `smtp.gmail.com` for Gmail and `smtp-mail.outlook.com` for Outlook 74 | - Set `emailPort` to the SMTP Port. Usually it is 587, 465 or 25 75 | - Set `emailUser` which is usually your email address 76 | - Set `emailPassword` to your email account password. This is stored locally on your device and is never sent to us 77 | - Set `emailRecipient` to the email you want to receive ebook on (your eReader's email address). See the links in list 78 | of eReaders above to know more 79 | - You may need to set `emailAllowSTARTTLS` to false when using `465` as `emailPort`. Leave it to `true` when not sure 80 | 81 | > [!CAUTION] 82 | > 83 | > The email password is stored in plaintext on your device, unencrypted. Therefore, it is highly recommended to use app 84 | > password instead of your account password whenever your email provider supports it 85 | > 86 | > You may also need to turn on 2FA (Two Factor Authentication) 87 | > 88 | > Instructions for setting an app password for a few popular email providers: 89 | > 90 | > - [Gmail](https://support.google.com/accounts/answer/185833) 91 | > - [Outlook](https://support.microsoft.com/en-us/account-billing/5896ed9b-4263-e681-128a-a6f2979a7944) 92 | > - [Zoho Mail](https://help.zoho.com/portal/en/kb/bigin/channels/email/articles/generate-an-app-specific-password) 93 | 94 | ## Configuration 95 | 96 | Configuration options available in the [config file](config.json) 97 | 98 | | Option | Type | Description | 99 | | ------------------------ | -------- | -------------------------------------------------------- | 100 | | token | string | Omnivore API Token | 101 | | endpoint | string | Omnivore GraphQL API endpoint | 102 | | title | string | Title of the ebook | 103 | | author | string | Author of the ebook | 104 | | cover | string | URL for fetching cover image for the ebook | 105 | | description | string | Description of the ebook | 106 | | addLabelsInContent | boolean | Whether to add the labels for the article below title | 107 | | addArticleLinkInContent | boolean | Whether to add the link for the article below title | 108 | | allowImages | boolean | Whether to add images linked in article in the ebook | 109 | | outputFileName | string | ebook file name | 110 | | maxArticleCount | number | Number of articles to fetch | 111 | | searchQuery | string | Valid query for article search | 112 | | ignoredLabels | string[] | List of labels to exclude from the ebook | 113 | | ignoredLinks | string[] | List of urls to exclude from the ebook | 114 | | emailSupport | boolean | Whether to send the ebook via email (to your eReader) | 115 | | emailHost | string | SMTP Hostname of your email provider | 116 | | emailPort | number | Usually one of 587, 465 or 25. Prefer 587 when available | 117 | | emailUser | string | Username/Email address of your email account | 118 | | emailPassword | string | Password of your email account. Prefer app password | 119 | | emailRecipient | string | Email address that should receive your ebook | 120 | | emailFrom | string | Sender name that appears to the email receiver | 121 | | emailAllowSTARTTLS | boolean | Allow connecting to the SMTP server using STARTTLS | 122 | | emailSizeWarningSuppress | boolean | Show a warning if ebook is over emailSizeWarningMinSize | 123 | | emailSizeWarningMinSize | number | Min ebook size to show warning while sending email in MB | 124 | | updateCheck | boolean | Check for updates when run | 125 | | showReleaseNotes | boolean | Show release notes when an update is available | 126 | 127 | ### Custom searchQuery 128 | 129 | Setting `searchQuery` is the recommended way to filter and sort articles. You can learn more about how to use them on 130 | the official Omnivore documentation [Search | Omnivore Docs](https://docs.omnivore.app/using/search.html). 131 | 132 | The default value is `sort:saved-desc` which returns all unarchived articles sorted by saved date, recently added 133 | articles first. 134 | 135 | You can also use it to filter by labels. In case the label contain a space, you will need to escape the double-quotes 136 | using a forward slash `\`. Example: `sort:saved-desc label:\"Send to Kindle\"` 137 | 138 | ## Star History 139 | 140 | 141 | 142 | 143 | 144 | Star History Chart 145 | 146 | 147 | --------------------------------------------------------------------------------