├── import └── .gitkeep ├── .gitignore ├── .env.sample ├── Dockerfile ├── package.json ├── README.md ├── .devcontainer └── devcontainer.json ├── LICENSE └── index.js /import/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | import/*.xml 3 | .env -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /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"] -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------