├── .devcontainer └── devcontainer.json ├── .env.sample ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── import └── .gitkeep ├── index.js ├── package-lock.json └── package.json /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node 3 | { 4 | "name": "Node.js & TypeScript", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/typescript-node:1-20-bullseye", 7 | 8 | // Features to add to the dev container. More info: https://containers.dev/features. 9 | // "features": {}, 10 | 11 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 12 | // "forwardPorts": [], 13 | 14 | // Use 'postCreateCommand' to run commands after the container is created. 15 | "postCreateCommand": "npm install" 16 | 17 | // Configure tool-specific properties. 18 | // "customizations": {}, 19 | 20 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 21 | // "remoteUser": "root" 22 | } 23 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | OMNIVORE_API_KEY=12345678-1234-1234-1234-123456789012 2 | OMNIVORE_HOST=api-prod.omnivore.app 3 | OMNIVORE_PORT=443 4 | OMNIVORE_GRAPH_QL_PATH=/api/graphql -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | import/*.xml 3 | .env -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:bullseye-slim 2 | 3 | RUN mkdir -p /opt/app 4 | 5 | WORKDIR /opt/app 6 | 7 | COPY package.json package-lock.json . 8 | RUN npm install 9 | 10 | COPY .env . 11 | COPY index.js . 12 | COPY import/ ./import 13 | 14 | CMD ["npm", "run", "import"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Ed 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Omnivore OPML Import 2 | 3 | Omnivore OPML Import is a simple script to import OPML files into Omnivore using their GraphQL API. 4 | 5 | ## Features 6 | - Import multiple OPML (XML) files into Omnivore 7 | - Run in Docker or locally 8 | 9 | ## Desired Features 10 | - Label feeds based on OPML grouping 11 | 12 | ## Installation & Usage 13 | 1. Clone this repository 14 | 2. Copy OPML files into `./import` directory with an `.opml` or `.xml` file extension 15 | 3. Add [Omnivore API key](https://omnivore.app/settings/api) to `.env` file (copy example from `.env.sample`) 16 | 4. Build docker image: `docker build -t omnivore-opml-import .` 17 | 5. Run docker image: `docker run -it --rm omnivore-opml-import` 18 | 19 | ## Developing Locally 20 | 1. Clone this repository 21 | 2. Use `.devcontainer` to develop locally in VSCode as this will install all required dependencies 22 | 3. Execute `npm run import` 23 | -------------------------------------------------------------------------------- /import/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edleeman17/omnivore-opml-import/30bc73dc24bee972afe5ba8176a86c24f9552d60/import/.gitkeep -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import OpmlParser from 'opmlparser'; 2 | import { readdir, createReadStream } from 'fs'; 3 | import https from 'https'; 4 | 5 | var opmlparser = new OpmlParser(); 6 | 7 | readdir('./import', (err, files) => { 8 | 9 | if (err) throw err; 10 | 11 | files.forEach(file => { 12 | if (!file.endsWith('.xml') && !file.endsWith('.opml')) return; 13 | console.log(`Parsing ${file}`); 14 | createReadStream(`./import/${file}`).pipe(opmlparser); 15 | }); 16 | }); 17 | 18 | opmlparser.on('readable', function () { 19 | const stream = this; 20 | var feed; 21 | 22 | while (feed = stream.read()) { 23 | if (feed.xmlurl === undefined) continue; // Folder 24 | 25 | postToOmnivore(feed); 26 | } 27 | }); 28 | 29 | const postToOmnivore = (feed) => { 30 | const { folder, title, type, xmlurl } = feed; 31 | 32 | const validAuth = checkProvidedEnvironmentVariables(); 33 | 34 | if (!validAuth) throw new Error('Missing required environment variables'); 35 | 36 | var options = { 37 | hostname: process.env.OMNIVORE_HOST, 38 | port: process.env.OMNIVORE_PORT, 39 | path: process.env.OMNIVORE_GRAPH_QL_PATH, 40 | method: 'POST', 41 | headers: { 42 | 'Content-Type': 'application/json', 43 | 'Authorization': process.env.OMNIVORE_API_KEY 44 | } 45 | } 46 | var requestData = buildRequest(title, folder, xmlurl); 47 | 48 | const req = https.request(options, (res) => { 49 | res.on('data', (d) => { 50 | console.log(d.toString()); 51 | console.log(`:: Success :: Posted ${title} -> ${xmlurl} to Omnivore`); 52 | }) 53 | }) 54 | 55 | req.on('error', (error) => { 56 | console.error(`:: Failed :: Posting ${title} -> ${xmlurl} to Omnivore`) 57 | console.error(error) 58 | }) 59 | 60 | req.write(requestData) 61 | req.end() 62 | } 63 | 64 | const checkProvidedEnvironmentVariables = () => { 65 | if (!process.env.OMNIVORE_HOST) { 66 | console.error('OMNIVORE_HOST not provided'); 67 | return false; 68 | } 69 | if (!process.env.OMNIVORE_PORT) { 70 | console.error('OMNIVORE_PORT not provided'); 71 | return false; 72 | } 73 | if (!process.env.OMNIVORE_GRAPH_QL_PATH) { 74 | console.error('OMNIVORE_GRAPH_QL_PATH not provided'); 75 | return false; 76 | } 77 | if (!process.env.OMNIVORE_API_KEY) { 78 | console.error('OMNIVORE_API_KEY not provided'); 79 | return false; 80 | } 81 | return true; 82 | } 83 | 84 | const buildRequest = (title, folder, xmlurl) => { 85 | return ` 86 | { 87 | "query": "mutation Subscribe($input: SubscribeInput!) { subscribe(input: $input) {... on SubscribeSuccess {subscriptions {id}}... on SubscribeError {errorCodes}}}", 88 | "variables": { 89 | "input": { 90 | "url": "${xmlurl}", 91 | "subscriptionType": "RSS" 92 | } 93 | } 94 | } 95 | `; 96 | } 97 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "omnivore-opml-import", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "omnivore-opml-import", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "opmlparser": "^0.8.0" 13 | } 14 | }, 15 | "node_modules/core-util-is": { 16 | "version": "1.0.3", 17 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", 18 | "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" 19 | }, 20 | "node_modules/inherits": { 21 | "version": "2.0.4", 22 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 23 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 24 | }, 25 | "node_modules/isarray": { 26 | "version": "0.0.1", 27 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", 28 | "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" 29 | }, 30 | "node_modules/opmlparser": { 31 | "version": "0.8.0", 32 | "resolved": "https://registry.npmjs.org/opmlparser/-/opmlparser-0.8.0.tgz", 33 | "integrity": "sha512-kQqMYInALr4hDWOvsoXqJ04nNtaINWXFcX3CNwuAd6/8RnRGK+RNotFzN57h6Suq+JU/Tz/4BVWcovBhQUBP5Q==", 34 | "dependencies": { 35 | "readable-stream": "~1.1.10", 36 | "sax": "~0.6.0" 37 | }, 38 | "engines": { 39 | "node": ">= 0.8.0" 40 | } 41 | }, 42 | "node_modules/readable-stream": { 43 | "version": "1.1.14", 44 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", 45 | "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", 46 | "dependencies": { 47 | "core-util-is": "~1.0.0", 48 | "inherits": "~2.0.1", 49 | "isarray": "0.0.1", 50 | "string_decoder": "~0.10.x" 51 | } 52 | }, 53 | "node_modules/sax": { 54 | "version": "0.6.1", 55 | "resolved": "https://registry.npmjs.org/sax/-/sax-0.6.1.tgz", 56 | "integrity": "sha512-8ip+qnRh7m8OEyvoM1JoSBzlrepp3ajVR8nqgrfTig+TewfyvTijl0am8/anFqgbcdz62ofEUKE1hHNDCdbeSQ==" 57 | }, 58 | "node_modules/string_decoder": { 59 | "version": "0.10.31", 60 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", 61 | "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "omnivore-opml-import", 3 | "version": "1.0.1", 4 | "type": "module", 5 | "description": "OPML import helper for Omnivore", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "import": "node --env-file=.env index.js" 10 | }, 11 | "author": "Ed Leeman", 12 | "license": "MIT", 13 | "dependencies": { 14 | "opmlparser": "^0.8.0" 15 | } 16 | } 17 | --------------------------------------------------------------------------------