├── .envrc ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── test.yml ├── .gitignore ├── .pylintrc ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── Makefile ├── README.html ├── README.md ├── docs ├── Screen-Shot-2013-03-18-at-7.06.38-PM.png ├── Screen-Shot-2013-03-18-at-7.07.33-PM.png ├── Screen-Shot-2013-03-18-at-7.14.02-PM.png ├── Screen-Shot-2013-03-18-at-7.14.49-PM.png ├── Screen-Shot-2013-03-18-at-7.33.22-PM.png └── index.html ├── githooks ├── README.md └── pre-commit ├── ir ├── __init__.py ├── _version.py ├── about.py ├── data │ └── colors.u8 ├── gui.py ├── importer │ ├── __init__.py │ ├── base_importer.py │ ├── concrete_importers.py │ ├── epub.py │ ├── exceptions.py │ ├── html_cleaner.py │ ├── importer.py │ ├── local_file.py │ ├── models.py │ ├── pocket.py │ └── web.py ├── lib │ ├── __init__.py │ ├── cgi.py │ └── feedparser.py ├── main.py ├── manifest.json ├── schedule.py ├── settings.py ├── text.py ├── util.py ├── view.py └── web │ ├── model.css │ ├── scroll.js │ ├── table_of_contents.js │ └── text.js ├── poetry.lock ├── poetry.toml ├── pyproject.toml ├── screenshots ├── extraction-and-highlighting.png ├── highlighting-tab.png └── quick-keys-tab.png └── tests ├── __init__.py ├── conftest.py ├── test_html_cleaner.py ├── test_scheduler.py └── test_settings.py /.envrc: -------------------------------------------------------------------------------- 1 | # Setting for direnv 2 | export VIRTUAL_ENV=".venv" 3 | layout python 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Specs (please complete the following information):** 24 | - OS: [e.g. Windows, iOS, Ubuntu] 25 | - Anki Version [e.g. v2.0.52, v2.1.5] 26 | - Incremental Reading Version [e.g. v0.5.3-beta] 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main # Or your main branch name (e.g., master) 7 | push: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | 19 | - name: Install poetry 20 | run: pipx install poetry 21 | 22 | - name: Set up Python 23 | uses: actions/setup-python@v5 24 | with: 25 | python-version: '3.13' 26 | cache: 'poetry' 27 | 28 | - name: Install Dependencies 29 | run: | 30 | make install-deps 31 | 32 | - name: Run make test 33 | run: | 34 | make test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Backup files 2 | *~ 3 | 4 | # Local Python version 5 | .python-version 6 | 7 | # Python bytecode cache 8 | __pycache__ 9 | 10 | # MyPy type checking cache 11 | .mypy_cache/ 12 | 13 | # Code coverage reports 14 | .coverage 15 | 16 | # JetBrains IDE settings 17 | .idea 18 | 19 | # Visual Studio Code settings 20 | .vscode 21 | 22 | # virtual environment 23 | .venv/* 24 | 25 | # Release artefacts 26 | release 27 | incremental-reading-v*.zip 28 | 29 | # Anki addon metadata 30 | meta.json -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | disable= 3 | C0103, # invalid-name 4 | C0114, # missing-module-docstring 5 | C0115, # missing-class-docstring 6 | C0116, # missing-function-docstring 7 | R0903, # too-few-public-methods 8 | W0107, # unnecessary-pass 9 | W0212, # protected-access, because we hack Anki quite a bit 10 | E0611, # no-name-in-module because of aqt.qt 11 | C0415, # import-outside-toplevel, because of tests 12 | 13 | 14 | ignore=lib 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - '3.6' 5 | 6 | install: 7 | - pip install nose 8 | 9 | script: nosetests ./tests 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [4.13.0] - 2025-04-23 9 | 10 | ### Added 11 | - Table of contents (#67) 12 | 13 | ### Changed 14 | - Remove nav tags during import (#65) 15 | 16 | 17 | ## [4.12.1] - 2025-01-13 18 | 19 | ### Fixed 20 | - Fix LinkedIn import issue where some images don't have a src attribute (#56). 21 | - Major refactoring of the import process (#43). 22 | - (Try to) fix an issue with epub import on Windows where tempfile returns a relative temp path (#37). 23 | 24 | ## [4.12.0] - 2024-12-30 25 | 26 | ### Fixed 27 | - Don't switch to deck browser after import (#38). 28 | - Properly handle importing a page when title is missing (#34). 29 | - Missing About menu on OSX (#47). 30 | 31 | ### Changed 32 | - Various improvements to development process (#43). 33 | 34 | ### Removed 35 | - Remove `maxWidth` configuration as this should be part of the card's CSS. New 36 | users will have the correct CSS, but existing users might need to manually 37 | update their Card template's CSS to 38 | https://github.com/tvhong/incremental-reading/blob/main/ir/web/model.css 39 | 40 | 41 | ## [4.11.9] - 2023-03-05 42 | 43 | ### Added 44 | - Add ability to import Epub files (lujun9972@). 45 | 46 | ### Fixed 47 | - Fix compatibility with 23.10 (lujun9972@, khonkhortisan@, tvhong@). 48 | 49 | ## [4.11.8] 50 | 51 | ### Fixed 52 | - Use resolved title from Pocket when import from Pocket (contribution by lujun9972@). 53 | 54 | ## [4.11.7] 55 | 56 | ### Fixed 57 | Fix bug for first time user creating IR cards. 58 | 59 | ## [4.11.6] 60 | 61 | ### Fixed 62 | - Fix bug in enabling priority queue. 63 | 64 | ## [4.11.5] 65 | 66 | ### Fixed 67 | - Fix images and named anchors importing. 68 | 69 | ## [4.11.4] 70 | 71 | ### Fixed 72 | - Fix HTTPS imports in MacOS. 73 | 74 | ## [4.11.3] 75 | 76 | ### Removed 77 | - Remove deprecated imports from Anki 2.1.54. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | The project is structured as follow: 3 | * `ir/` contains the source code (Python + javascript) 4 | * `tests/` contains the test code 5 | * `docs/` contains documentation 6 | 7 | # Dev setup 8 | We use `poetry` to manage project dependency. 9 | 10 | First, install poetry following the instructions from https://python-poetry.org/docs/#installation . 11 | Then, install poetry-bumpversion plugin 12 | ``` 13 | poetry self add poetry-bumpversion 14 | ``` 15 | 16 | Then, install dependencies: 17 | ```shell 18 | make install-deps 19 | ``` 20 | 21 | Poetry will automatically create a virtual environment and install the dependencies. 22 | To use the virtual environment, see https://python-poetry.org/docs/basic-usage/#using-your-virtual-environment 23 | 24 | # Manual Test 25 | 26 | ## Add local repo as add-on 27 | To iterate quickly on your changes, you should add the local repo to Anki as an add-on. 28 | The manual testing cycle then becomes: make code changes, restart Anki, and test your change. 29 | 30 | Steps: 31 | 1. Find where Anki stores its add-on: Open Anki > Tools > Add-ons > View Files. 32 | 2. Then create a symlink from Anki's add-on directory to your "ir" directory. 33 | For example: 34 | * My Anki add-on directory is `$HOME/.local/share/Anki2/addons21`. 35 | * My local incremental reading workspace is `$HOME/workplace/incremental-reading`. 36 | * Then to add my local workspace as an Anki add-on, I'd run 37 | ```shell 38 | ln -s $HOME/workplace/incremental-reading/ir $HOME/.local/share/Anki2/addons21/ir 39 | ``` 40 | 3. Restart Anki. 41 | 42 | ## Run Anki 43 | 44 | 1. Create a "Test" profile to test your changes, for your own safety. 45 | 2. Then run Anki from terminal. This will show stdout, which is useful for debugging. 46 | ```shell 47 | # On Ubuntu 48 | /usr/local/bin/anki -p Test 49 | 50 | # On Mac 51 | /Applications/Anki.app/Contents/MacOS/anki -p Test 52 | ``` 53 | 54 | # Unit test 55 | 56 | ```shell 57 | make test 58 | ``` 59 | 60 | # Publishing 61 | 62 | 1. Update the version 63 | ``` 64 | poetry version patch|minor|major 65 | ``` 66 | 2. Build the `release/incremental-reading-v{version}.zip` file: 67 | ```shell 68 | make release 69 | ``` 70 | 3. Test the zip file in Anki 71 | * Disable current IR add-on: Open Anki > Tools > Add-ons > Select current IR add-on > Toggle Enabled. 72 | * Add the zipped add-on: In Add-ons page > Install from file... > Pick the zip file from earlier. 73 | * Restart Anki to test. 74 | * After finish testing, disable the local add-on and re-enable the public add-on. 75 | 4. Upload to https://ankiweb.net/shared/addons/ . 76 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright 2013 Tiago Barroso 4 | Copyright 2013 Frank Kmiec 5 | Copyright 2013-2016 Aleksej 6 | Copyright 2017 Christian Weiß 7 | Copyright 2018 Timothée Chauvin 8 | Copyright 2017-2022 Joseph Lorimer 9 | Copyright 2022-2024 Vy Hong 10 | 11 | Permission to use, copy, modify, and/or distribute this software for any 12 | purpose with or without fee is hereby granted, provided that the above 13 | copyright notice and this permission notice appear in all copies. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 16 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 17 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 18 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 19 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 20 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 21 | PERFORMANCE OF THIS SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2017-2019 Joseph Lorimer 2 | # 3 | # Permission to use, copy, modify, and distribute this software for any purpose 4 | # with or without fee is hereby granted, provided that the above copyright 5 | # notice and this permission notice appear in all copies. 6 | # 7 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | # REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 9 | # AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 11 | # LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 12 | # OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 13 | # PERFORMANCE OF THIS SOFTWARE. 14 | 15 | export PYTHONPATH=. 16 | VERSION=$(shell poetry version -s) 17 | PROJECT_SHORT=ir 18 | PROJECT_LONG=incremental-reading 19 | 20 | RELEASE_DIR=$(CURDIR)/release 21 | RELEASE_FILE=$(RELEASE_DIR)/$(PROJECT_LONG)-v$(VERSION).zip 22 | 23 | .PHONY: install-deps lint format test clean release 24 | 25 | all: install-deps test 26 | 27 | install-deps: 28 | @echo "Installing dependencies..." 29 | poetry install --sync --no-root --with dev 30 | 31 | lint: 32 | @echo "Linting code..." 33 | poetry run pylint "$(PROJECT_SHORT)" tests 34 | 35 | format: 36 | @echo "Formatting code..." 37 | poetry run black "$(PROJECT_SHORT)" tests 38 | poetry run isort "$(PROJECT_SHORT)" tests 39 | 40 | test: 41 | @echo "Running tests..." 42 | poetry run pytest -W ignore::DeprecationWarning tests -v 43 | 44 | check: lint test 45 | 46 | clean: 47 | @echo "Cleaning up..." 48 | rm -rf "$(RELEASE_DIR)" 49 | find . -name '*.pyc' -type f -delete 50 | find . -name '*~' -type f -delete 51 | find . -name .mypy_cache -type d -exec rm -rf {} + 52 | find . -name .ropeproject -type d -exec rm -rf {} + 53 | find . -name __pycache__ -type d -exec rm -rf {} + 54 | 55 | release: test clean 56 | @echo "Creating release file: $(RELEASE_FILE)" 57 | mkdir -p "$(RELEASE_DIR)" 58 | cd "$(PROJECT_SHORT)" && zip -r "$(RELEASE_FILE)" * 59 | zip $(RELEASE_FILE) LICENSE.md -------------------------------------------------------------------------------- /README.html: -------------------------------------------------------------------------------- 1 | 2021 Project Status: Active development will resume on Saturday the 27th of February. 2 | 3 | Build Status 4 | 5 | 6 | Note: Version 4 of the add-on is only available for Anki 2.1+. Some features will be missing from the earlier versions. 7 | 8 | Introduction 9 | 10 | This is a rewrite of the Incremental Reading add-on, which aims to provide features that support incremental reading in Anki. The idea of working with long-form content within a spaced-repetition program appears to have originated with SuperMemo, which offers an elaborate implementation of the technique (see their help article for more information). This add-on for Anki is comparatively bare-bones, providing a minimal set of tools for iterating over long texts and creating new flashcards from existing ones. For an overview of these features, see below. 11 | 12 | 13 | Main Features 14 | 15 |
  • Import content from web feeds (RSS/Atom), webpages, or Pocket (v4 only)
  • Extract selected text into a new card by pressing x
  • Highlight selected text by pressing h
  • Remove selected text by pressing z
  • Undo changes to the text by pressing u
  • Apply rich text formatting while reading
  • Create custom shortcuts to quickly add cards
  • Maintain scroll position and zoom on a per-card basis
  • Rearrange cards in the built-in organiser
  • Control the scheduling of incremental reading cards
  • Limit the width of cards (useful on large screens) (v4 only)
16 | New to Version 4 17 | 18 |
  • Compatible with Anki 2.1
  • Import single webpages (Alt+3)
  • Import web feeds (Alt+4)
  • Import Pocket articles (Alt+5)
  • Apply bold, italics, underline or strikethrough (Ctrl+B, I, U, or S)
  • Toggle formatting on and off (Ctrl+Shift+O)
  • Choose maximum width of cards (see options: Alt+1)
  • Control initial scheduling of extracts (see options: Alt+1)
19 | New to Version 3 20 | 21 |
  • Remove unwanted text with a single key-press (z)
  • Multi-level undo, for reverting text changes (u)
  • New options to control how text is extracted:
    • Open the full note editor for each extraction (slow), or simply a title entry box (fast)
    • Extract selected text as HTML (retain color and formatting) or plain text (remove all formatting)
    • Choose a destination deck for extracts
  • New options for several aspects of zoom and scroll functionality:
    • Zoom Step (the amount that magnification changes when zooming in or out)
    • General Zoom (the zoom level for the deck browser and overview screens)
    • Line Step (the amount the page moves up or down when the Up or Down direction keys are used)
    • Page Step (same as above, but with the Page Up and Page Down keys)
  • Highlighting:
    • Both the background color and text color used for highlighting can be customized
    • A drop-down list of available colors is provided
    • A preview is now displayed when selecting highlight colors
    • The colors applied to text extracted with x can now be set independently
  • Quick Keys
    • A list of all existing Quick Keys is now shown, to allow easy modification
    • Unwanted Quick Keys can be easily deleted
    • A plain text extraction option has also been added
  • All options have been consolidated into a single tabbed dialog
22 | Screenshots 23 | 24 | Note: These are fairly outdated. 25 | 26 | Screenshot #1Screenshot #2Screenshot #3 27 | 28 | Installation 29 | 30 | You will first need to have Anki installed. Download the relevant installer here. 31 | 32 | To install through Anki, navigate to Tools → Add-ons → Get Add-ons..., and enter the code 935264945. To install manually, download the GitHub repository (here) and place the ir folder into your add-ons folder. 33 | 34 | Usage 35 | 36 | Experimentation should lead to a pretty quick understanding of how the add-on works. If in doubt, start with the following: 37 | 38 |
  1. Create a new IR note with an article you want to study (the easiest way to do this is to import a webpage, by pressing Alt+3 while on the deck overview screen)
  2. Set up a shortcut for creating regular Anki cards from IR cards (press Alt+1, or go to the menu, then go to the Quick Keys tab)
  3. Review the IR card that was created, and extract any text you find interesting (by selecting the text and pressing x)
  4. Choose Soon or Later when you want to move to the next card (which will be a portion of text you extracted)
  5. Whenever you want to create a regular Anki note, simply select the desired text and use the shortcut you created earlier
39 | Outdated instructions can be found here. They were written for v2, but the basic behaviour of the add-on is still similar. 40 | 41 | Support 42 | 43 | If any issues are encountered, please post details to the Anki add-ons forum. It’s best if you post in the existing thread (here) so I receive an email notification. Otherwise, note an issue or make a pull request on GitHub. 44 | 45 | Please include the following information in your post: 46 | 47 |
  • The version of Anki you are using (e.g., v2.1.0-beta5; can be found in Help → About...)
  • The version of IR you are using (this can be found in Read → About...)
  • The operating system you are using
  • Details of the problem
  • Steps needed to reproduce the problem
48 | License 49 | 50 | Multiple people have contributed to this add-on, and it’s somewhat unclear who to credit for which changes and which licenses to apply. 51 | 52 | Tiago Barroso appears to have initiated the project, and he has stated that he releases all of his add-ons under the ISC license. Frank Kmiec later vastly expanded the add-on, but it’s unclear which license his changes were released under. Presuming he didn’t specify one, the AnkiWeb terms and conditions suggest they were automatically released under the AGPL v3. Aleksej’s changes to Frank’s version are multi-licensed under the GPL and ISC licenses. 53 | 54 | For the sake of simplicity, my changes are also released under the ISC license. For each author, I have placed a copyright lines where appropriate, with what I believe are correct dates. If I have made a mistake in this respect, please let me know. 55 | 56 | Frank Raiser released an Anki 1 add-on under a similar name, but it doesn’t appear to share any code with the current project and functions quite differently. For more information, see Anki Incremental Reading. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **_2021 Project Status: Active development will resume on Saturday the 27th of February._** 2 | 3 | # Incremental Reading for Anki 4 | 5 | [![Build Status](https://travis-ci.org/luoliyan/incremental-reading.svg?branch=master)](https://travis-ci.org/luoliyan/incremental-reading) 6 | 7 | **Note:** Version 4 of the add-on is only available for Anki 2.1+. Some features will be missing from the earlier versions. 8 | 9 | ## Introduction 10 | 11 | This is a rewrite of the [Incremental Reading add-on](https://github.com/aleksejrs/anki-2.0-vsa-and-ire), which aims to provide features that support incremental reading in Anki. The idea of working with long-form content within a spaced-repetition program appears to have originated with SuperMemo, which offers an elaborate implementation of the technique (see their [help article](https://www.supermemo.com/help/read.htm) for more information). This add-on for Anki is comparatively bare-bones, providing a minimal set of tools for iterating over long texts and creating new flashcards from existing ones. For an overview of these features, see below. 12 | 13 | - Version 4: [GitHub](https://github.com/luoliyan/incremental-reading), [issue tracker](https://github.com/luoliyan/incremental-reading/issues), [discussion board](https://anki.tenderapp.com/discussions/add-ons/9054-incremental-reading-add-on-discussion-support) 14 | - Version 3: [GitHub](https://github.com/luoliyan/incremental-reading/tree/legacy), [discussion board](https://anki.tenderapp.com/discussions/add-ons/9054-incremental-reading-add-on-discussion-support) 15 | - Version 2: [AnkiWeb](https://ankiweb.net/shared/info/355348508), [GitHub](https://github.com/aleksejrs/anki-2.0-vsa-and-ire), [manual](https://luoliyan.github.io/incremental-reading) 16 | 17 | ## Main Features 18 | 19 | - Import content from web feeds (RSS/Atom), webpages, or Pocket (**v4 only**) 20 | - Extract selected text into a new card by pressing x 21 | - Highlight selected text by pressing h 22 | - Remove selected text by pressing z 23 | - Undo changes to the text by pressing u 24 | - Apply rich text formatting while reading 25 | - Create custom shortcuts to quickly add cards 26 | - Maintain scroll position and zoom on a per-card basis 27 | - Rearrange cards in the built-in organiser 28 | - Control the scheduling of incremental reading cards 29 | - Limit the width of cards (useful on large screens) (**v4 only**) 30 | 31 | ### New to Version 4 32 | 33 | - Compatible with Anki 2.1 34 | - Import single webpages (Alt+3) 35 | - Import web feeds (Alt+4) 36 | - Import Pocket articles (Alt+5) 37 | - Apply bold, italics, underline or strikethrough (Ctrl+B, I, U, or S) 38 | - Toggle formatting on and off (Ctrl+Shift+O) 39 | - Choose maximum width of cards (see options: Alt+1) 40 | - Control initial scheduling of extracts (see options: Alt+1) 41 | 42 | ### New to Version 3 43 | 44 | - Remove unwanted text with a single key-press (z) 45 | - Multi-level undo, for reverting text changes (u) 46 | - New options to control how text is extracted: 47 | - Open the full note editor for each extraction (slow), or simply a title entry box (fast) 48 | - Extract selected text as HTML (retain color and formatting) or plain text (remove all formatting) 49 | - Choose a destination deck for extracts 50 | - New options for several aspects of zoom and scroll functionality: 51 | - _Zoom Step_ (the amount that magnification changes when zooming in or out) 52 | - _General Zoom_ (the zoom level for the deck browser and overview screens) 53 | - _Line Step_ (the amount the page moves up or down when the Up or Down direction keys are used) 54 | - _Page Step_ (same as above, but with the Page Up and Page Down keys) 55 | - Highlighting: 56 | - Both the background color and text color used for highlighting can be customized 57 | - A drop-down list of available colors is provided 58 | - A preview is now displayed when selecting highlight colors 59 | - The colors applied to text extracted with x can now be set independently 60 | - Quick Keys 61 | - A list of all existing Quick Keys is now shown, to allow easy modification 62 | - Unwanted Quick Keys can be easily deleted 63 | - A plain text extraction option has also been added 64 | - All options have been consolidated into a single tabbed dialog 65 | 66 | ## Screenshots 67 | 68 | **Note:** These are fairly outdated. 69 | 70 | ![Screenshot #1](https://raw.githubusercontent.com/luoliyan/incremental-reading/master/screenshots/extraction-and-highlighting.png) 71 | ![Screenshot #2](https://raw.githubusercontent.com/luoliyan/incremental-reading/master/screenshots/highlighting-tab.png) 72 | ![Screenshot #3](https://raw.githubusercontent.com/luoliyan/incremental-reading/master/screenshots/quick-keys-tab.png) 73 | 74 | ## Installation 75 | 76 | You will first need to have Anki installed. Download the relevant installer [here](http://ankisrs.net). 77 | 78 | To install through Anki, navigate to Tools → Add-ons → Get Add-ons..., and enter the code `935264945`. To install manually, download the GitHub repository ([here](https://github.com/luoliyan/incremental-reading-for-anki/archive/master.zip)) and place the `ir` folder into your add-ons folder. 79 | 80 | ## Usage 81 | 82 | Experimentation should lead to a pretty quick understanding of how the add-on works. If in doubt, start with the following: 83 | 84 | 1. Create a new IR note with an article you want to study (the easiest way to do this is to import a webpage, by pressing Alt+3 while on the deck overview screen) 85 | 2. Set up a shortcut for creating regular Anki cards from IR cards (press Alt+1, or go to the menu, then go to the Quick Keys tab) 86 | 3. Review the IR card that was created, and extract any text you find interesting (by selecting the text and pressing x) 87 | 4. Choose _Soon_ or _Later_ when you want to move to the next card (which will be a portion of text you extracted) 88 | 5. Whenever you want to create a regular Anki note, simply select the desired text and use the shortcut you created earlier 89 | 90 | Outdated instructions can be found [here](https://luoliyan.github.io/incremental-reading). They were written for v2, but the basic behaviour of the add-on is still similar. 91 | 92 | ## Support 93 | 94 | If any issues are encountered, please post details to the [Anki add-ons forum](https://anki.tenderapp.com/discussions/add-ons). It’s best if you post in the existing thread ([here](https://anki.tenderapp.com/discussions/add-ons/9054-incremental-reading-add-on-discussion-support)) so I receive an email notification. Otherwise, [note an issue](https://github.com/luoliyan/incremental-reading-for-anki/issues) or make a pull request on GitHub. 95 | 96 | Please include the following information in your post: 97 | 98 | - The version of Anki you are using (e.g., v2.1.0-beta5; can be found in Help → About...) 99 | - The version of IR you are using (this can be found in Read → About...) 100 | - The operating system you are using 101 | - Details of the problem 102 | - Steps needed to reproduce the problem 103 | 104 | ## License 105 | 106 | Multiple people have contributed to this add-on, and it’s somewhat unclear who to credit for which changes and which licenses to apply. 107 | 108 | Tiago Barroso appears to have initiated the project, and he has [stated](https://groups.google.com/d/msg/anki-addons/xibqDVFqQwQ/-qpxKvxurPMJ) that he releases all of his add-ons under the ISC license. Frank Kmiec later vastly expanded the add-on, but it’s unclear which license his changes were released under. Presuming he didn’t specify one, the [AnkiWeb terms and conditions](https://ankiweb.net/account/terms) suggest they were automatically released under the AGPL v3. Aleksej’s changes to Frank’s version are [multi-licensed under the GPL and ISC licenses](https://github.com/aleksejrs/anki-2.0-vsa-and-ire). 109 | 110 | For the sake of simplicity, my changes are also released under the ISC license. For each author, I have placed a copyright lines where appropriate, with what I believe are correct dates. If I have made a mistake in this respect, please let me know. 111 | 112 | Frank Raiser released an Anki 1 add-on under a similar name, but it doesn’t appear to share any code with the current project and functions quite differently. For more information, see [Anki Incremental Reading](http://frankraiser.de/drupal/AnkiIR). 113 | -------------------------------------------------------------------------------- /docs/Screen-Shot-2013-03-18-at-7.06.38-PM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tvhong/incremental-reading/ac70d1af3f2148d19b682deba2e2fe64d6b719b6/docs/Screen-Shot-2013-03-18-at-7.06.38-PM.png -------------------------------------------------------------------------------- /docs/Screen-Shot-2013-03-18-at-7.07.33-PM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tvhong/incremental-reading/ac70d1af3f2148d19b682deba2e2fe64d6b719b6/docs/Screen-Shot-2013-03-18-at-7.07.33-PM.png -------------------------------------------------------------------------------- /docs/Screen-Shot-2013-03-18-at-7.14.02-PM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tvhong/incremental-reading/ac70d1af3f2148d19b682deba2e2fe64d6b719b6/docs/Screen-Shot-2013-03-18-at-7.14.02-PM.png -------------------------------------------------------------------------------- /docs/Screen-Shot-2013-03-18-at-7.14.49-PM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tvhong/incremental-reading/ac70d1af3f2148d19b682deba2e2fe64d6b719b6/docs/Screen-Shot-2013-03-18-at-7.14.49-PM.png -------------------------------------------------------------------------------- /docs/Screen-Shot-2013-03-18-at-7.33.22-PM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tvhong/incremental-reading/ac70d1af3f2148d19b682deba2e2fe64d6b719b6/docs/Screen-Shot-2013-03-18-at-7.33.22-PM.png -------------------------------------------------------------------------------- /githooks/README.md: -------------------------------------------------------------------------------- 1 | To reuse these githooks, run the following command after cloning the repository: 2 | ``` 3 | git config core.hooksPath githooks 4 | ``` -------------------------------------------------------------------------------- /githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Get list of Python files that are staged for commit 4 | files=$(git diff --cached --name-only --diff-filter=d | grep '\.py$' | grep -v '^ir/lib/') 5 | 6 | if [ -z "$files" ]; then 7 | # No Python files to format 8 | exit 0 9 | fi 10 | 11 | echo "Running code formatting on staged files..." 12 | 13 | # Create a temporary file to store the file list 14 | temp_file=$(mktemp) 15 | echo "$files" > "$temp_file" 16 | 17 | # Format only the staged files 18 | poetry run black --quiet $(cat "$temp_file") 19 | poetry run isort --quiet $(cat "$temp_file") 20 | 21 | # Clean up 22 | rm "$temp_file" 23 | 24 | # Add the formatted files back to staging 25 | if ! git diff --quiet; then 26 | echo "Code formatting created changes. Adding them to the commit..." 27 | git add $files 28 | fi 29 | 30 | exit 0 31 | -------------------------------------------------------------------------------- /ir/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017-2019 Joseph Lorimer 2 | # 3 | # Permission to use, copy, modify, and distribute this software for any purpose 4 | # with or without fee is hereby granted, provided that the above copyright 5 | # notice and this permission notice appear in all copies. 6 | # 7 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | # REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 9 | # AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 11 | # LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 12 | # OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 13 | # PERFORMANCE OF THIS SOFTWARE. 14 | 15 | import os 16 | 17 | if not os.environ.get("IR_TESTING"): 18 | from aqt import mw 19 | 20 | from .main import ReadingManager 21 | 22 | mw.readingManager = ReadingManager() 23 | -------------------------------------------------------------------------------- /ir/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = "4.13.0" 2 | -------------------------------------------------------------------------------- /ir/about.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017-2019 Joseph Lorimer 2 | # 3 | # Permission to use, copy, modify, and distribute this software for any purpose 4 | # with or without fee is hereby granted, provided that the above copyright 5 | # notice and this permission notice appear in all copies. 6 | # 7 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | # REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 9 | # AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 11 | # LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 12 | # OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 13 | # PERFORMANCE OF THIS SOFTWARE. 14 | 15 | from aqt import mw 16 | from aqt.qt import QDialog, QDialogButtonBox, QLabel, QVBoxLayout 17 | 18 | from ._version import __version__ 19 | 20 | IR_GITHUB_URL = "https://github.com/tvhong/incremental-reading" 21 | 22 | 23 | def showAbout(): 24 | dialog = QDialog(mw) 25 | 26 | label = QLabel() 27 | label.setStyleSheet("QLabel { font-size: 14px; }") 28 | contributors = [ 29 | "Joseph Lorimer Timothée Chauvin", 30 | "Christian Weiß", 31 | "Aleksej", 32 | "Frank Kmiec", 33 | "Tiago Barroso", 34 | ] 35 | text = f""" 36 |
Incremental Reading v{__version__}
37 |
Vy Hong <contact@vyhong.me>
38 |
Contributors: {", ".join(contributors)}
39 | 40 | """ 41 | label.setText(text) 42 | 43 | buttonBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok) 44 | buttonBox.accepted.connect(dialog.accept) 45 | 46 | layout = QVBoxLayout() 47 | layout.addWidget(label) 48 | layout.addWidget(buttonBox) 49 | 50 | dialog.setLayout(layout) 51 | dialog.setWindowTitle("About") 52 | dialog.exec() 53 | -------------------------------------------------------------------------------- /ir/data/colors.u8: -------------------------------------------------------------------------------- 1 | AliceBlue 2 | AntiqueWhite 3 | Aqua 4 | Aquamarine 5 | Azure 6 | Beige 7 | Bisque 8 | Black 9 | BlanchedAlmond 10 | Blue 11 | BlueViolet 12 | Brown 13 | BurlyWood 14 | CadetBlue 15 | Chartreuse 16 | Chocolate 17 | Coral 18 | CornflowerBlue 19 | Cornsilk 20 | Crimson 21 | Cyan 22 | DarkBlue 23 | DarkCyan 24 | DarkGoldenRod 25 | DarkGray 26 | DarkGrey 27 | DarkGreen 28 | DarkKhaki 29 | DarkMagenta 30 | DarkOliveGreen 31 | DarkOrange 32 | DarkOrchid 33 | DarkRed 34 | DarkSalmon 35 | DarkSeaGreen 36 | DarkSlateBlue 37 | DarkSlateGray 38 | DarkSlateGrey 39 | DarkTurquoise 40 | DarkViolet 41 | DeepPink 42 | DeepSkyBlue 43 | DimGray 44 | DimGrey 45 | DodgerBlue 46 | FireBrick 47 | FloralWhite 48 | ForestGreen 49 | Fuchsia 50 | Gainsboro 51 | GhostWhite 52 | Gold 53 | GoldenRod 54 | Gray 55 | Grey 56 | Green 57 | GreenYellow 58 | HoneyDew 59 | HotPink 60 | IndianRed 61 | Indigo 62 | Ivory 63 | Khaki 64 | Lavender 65 | LavenderBlush 66 | LawnGreen 67 | LemonChiffon 68 | LightBlue 69 | LightCoral 70 | LightCyan 71 | LightGoldenRodYellow 72 | LightGray 73 | LightGrey 74 | LightGreen 75 | LightPink 76 | LightSalmon 77 | LightSeaGreen 78 | LightSkyBlue 79 | LightSlateGray 80 | LightSlateGrey 81 | LightSteelBlue 82 | LightYellow 83 | Lime 84 | LimeGreen 85 | Linen 86 | Magenta 87 | Maroon 88 | MediumAquaMarine 89 | MediumBlue 90 | MediumOrchid 91 | MediumPurple 92 | MediumSeaGreen 93 | MediumSlateBlue 94 | MediumSpringGreen 95 | MediumTurquoise 96 | MediumVioletRed 97 | MidnightBlue 98 | MintCream 99 | MistyRose 100 | Moccasin 101 | NavajoWhite 102 | Navy 103 | OldLace 104 | Olive 105 | OliveDrab 106 | Orange 107 | OrangeRed 108 | Orchid 109 | PaleGoldenRod 110 | PaleGreen 111 | PaleTurquoise 112 | PaleVioletRed 113 | PapayaWhip 114 | PeachPuff 115 | Peru 116 | Pink 117 | Plum 118 | PowderBlue 119 | Purple 120 | RebeccaPurple 121 | Red 122 | RosyBrown 123 | RoyalBlue 124 | SaddleBrown 125 | Salmon 126 | SandyBrown 127 | SeaGreen 128 | SeaShell 129 | Sienna 130 | Silver 131 | SkyBlue 132 | SlateBlue 133 | SlateGray 134 | SlateGrey 135 | Snow 136 | SpringGreen 137 | SteelBlue 138 | Tan 139 | Teal 140 | Thistle 141 | Tomato 142 | Turquoise 143 | Violet 144 | Wheat 145 | White 146 | WhiteSmoke 147 | Yellow 148 | YellowGreen -------------------------------------------------------------------------------- /ir/importer/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Timothée Chauvin 2 | # Copyright 2017-2019 Joseph Lorimer 3 | # 4 | # Permission to use, copy, modify, and distribute this software for any purpose 5 | # with or without fee is hereby granted, provided that the above copyright 6 | # notice and this permission notice appear in all copies. 7 | # 8 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 9 | # REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 10 | # AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 11 | # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 12 | # LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 13 | # OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 14 | # PERFORMANCE OF THIS SOFTWARE. 15 | -------------------------------------------------------------------------------- /ir/importer/base_importer.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from datetime import date 3 | from typing import List, Optional 4 | 5 | from anki.notes import Note 6 | from aqt import mw 7 | from aqt.utils import chooseList, showCritical, showWarning, tooltip 8 | 9 | from ..settings import SettingsManager 10 | from ..util import Article, setField 11 | from .exceptions import ErrorLevel, ImporterError 12 | from .models import NoteModel 13 | 14 | 15 | class BaseImporter(ABC): 16 | def __init__(self, settings: SettingsManager): 17 | self.settings = settings 18 | 19 | def importContent(self) -> None: 20 | """Template method that defines the import algorithm""" 21 | try: 22 | articles = self._getArticles() 23 | selected = self._selectArticles(articles) 24 | if not selected: 25 | return 26 | 27 | priority = self._getPriority() if self.settings["prioEnabled"] else None 28 | mw.progress.start( 29 | label=self._getProgressLabel(), max=len(selected), immediate=True 30 | ) 31 | 32 | deckName = None 33 | for i, article in enumerate(selected, start=1): 34 | noteModel = self._processArticle(article, priority) 35 | deckName = self._createNote(noteModel) 36 | self._postProcessArticle(article) 37 | mw.progress.update(value=i) 38 | 39 | mw.progress.finish() 40 | 41 | tooltip(f"Added {len(selected)} item(s) to deck: {deckName}") 42 | 43 | return 44 | 45 | except ImporterError as e: 46 | self._handleError(e) 47 | return 48 | 49 | @abstractmethod 50 | def _getArticles(self) -> List[Article]: 51 | """Get the articles to be imported""" 52 | pass 53 | 54 | @abstractmethod 55 | def _selectArticles(self, articles: List[Article]) -> List[Article]: 56 | """Select which articles to import. Can be overridden by subclasses.""" 57 | pass 58 | 59 | @abstractmethod 60 | def _processArticle(self, article: Article, priority: Optional[str]) -> NoteModel: 61 | """Process a single article""" 62 | pass 63 | 64 | def _postProcessArticle(self, article: Article) -> None: 65 | """Post-process a single article""" 66 | pass 67 | 68 | @abstractmethod 69 | def _getProgressLabel(self) -> str: 70 | """Get the progress label for the import operation""" 71 | pass 72 | 73 | def _getPriority(self) -> str: 74 | prompt = "Select priority for import" 75 | return self.settings["priorities"][ 76 | chooseList(prompt, self.settings["priorities"]) 77 | ] 78 | 79 | def _createNote(self, noteModel: NoteModel) -> str: 80 | """Create a note from a NoteModel""" 81 | if self.settings["importDeck"]: 82 | deck = mw.col.decks.by_name(self.settings["importDeck"]) 83 | if not deck: 84 | showWarning( 85 | f'Destination deck "{deck}" no longer exists. Please update your settings.' 86 | ) 87 | return "" 88 | deckId = deck["id"] 89 | else: 90 | deckId = mw.col.conf["curDeck"] 91 | 92 | model = mw.col.models.by_name(self.settings["modelName"]) 93 | note = Note(mw.col, model) 94 | setField(note, self.settings["titleField"], noteModel.title) 95 | setField(note, self.settings["textField"], noteModel.content) 96 | 97 | source = self.settings["sourceFormat"].format( 98 | date=date.today(), url=f'{noteModel.url}' 99 | ) 100 | setField(note, self.settings["sourceField"], source) 101 | if noteModel.priority: 102 | setField(note, self.settings["prioField"], noteModel.priority) 103 | 104 | note.note_type()["did"] = deckId 105 | mw.col.addNote(note) 106 | 107 | return mw.col.decks.get(deckId)["name"] 108 | 109 | def _handleError(self, error: ImporterError) -> None: 110 | """Handle import errors""" 111 | if error.errorLevel == ErrorLevel.CRITICAL: 112 | showCritical(error.message) 113 | elif error.errorLevel == ErrorLevel.WARNING: 114 | showWarning(error.message) 115 | -------------------------------------------------------------------------------- /ir/importer/concrete_importers.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | from urllib.parse import urlsplit 3 | 4 | from aqt.utils import getFile, getText 5 | 6 | from ..lib.feedparser import parse 7 | from ..settings import SettingsManager 8 | from ..util import Article, selectArticles 9 | from .base_importer import BaseImporter 10 | from .epub import getEpubToc 11 | from .exceptions import ErrorLevel, ImporterError 12 | from .local_file import LocalFile 13 | from .models import NoteModel 14 | from .pocket import Pocket 15 | from .web import Web 16 | 17 | 18 | class WebpageImporter(BaseImporter): 19 | def __init__(self, settings: SettingsManager, web: Web) -> None: 20 | super().__init__(settings) 21 | self.web = web 22 | 23 | def _getArticles(self) -> List[Article]: 24 | url, accepted = getText("Enter URL:", title="Import Webpage") 25 | if not url or not accepted: 26 | return [] 27 | 28 | if not urlsplit(url).scheme: 29 | url = "http://" + url 30 | 31 | return [Article(title=url, data=url)] 32 | 33 | def _selectArticles(self, articles: List[Article]) -> List[Article]: 34 | return articles 35 | 36 | def _processArticle(self, article: Article, priority: Optional[str]) -> NoteModel: 37 | webpage = self.web.download(article.data) 38 | return NoteModel(webpage.title, webpage.body, webpage.url, priority) 39 | 40 | def _getProgressLabel(self) -> str: 41 | return "Importing webpage..." 42 | 43 | 44 | class FeedImporter(BaseImporter): 45 | def __init__(self, settings: SettingsManager, web: Web) -> None: 46 | super().__init__(settings) 47 | self.web = web 48 | self.log = settings["feedLog"] 49 | 50 | def _getArticles(self) -> List[Article]: 51 | feedUrl, accepted = getText("Enter RSS URL:", title="Import Feed") 52 | if not feedUrl or not accepted: 53 | return [] 54 | 55 | if not urlsplit(feedUrl).scheme: 56 | feedUrl = "http://" + feedUrl 57 | 58 | try: 59 | feed = parse( 60 | feedUrl, 61 | agent=self.settings["userAgent"], 62 | etag=self.log[feedUrl]["etag"], 63 | modified=self.log[feedUrl]["modified"], 64 | ) 65 | except KeyError: 66 | self.log[feedUrl] = {"downloaded": []} 67 | feed = parse(feedUrl, agent=self.settings["userAgent"]) 68 | 69 | if feed["status"] not in [200, 301, 302]: 70 | raise ImporterError( 71 | ErrorLevel.WARNING, 72 | f"The remote server returned an unexpected status: {feed['status']}", 73 | ) 74 | 75 | articles = [ 76 | Article( 77 | title=e["title"], 78 | data={"feedUrl": feedUrl, "feed": feed, "url": e["link"]}, 79 | ) 80 | for e in feed["entries"] 81 | if e["link"] not in self.log[feedUrl]["downloaded"] 82 | ] 83 | 84 | if not articles: 85 | raise ImporterError( 86 | ErrorLevel.WARNING, "There are no new entries in the feed." 87 | ) 88 | 89 | return articles 90 | 91 | def _selectArticles(self, articles: List[Article]) -> List[Article]: 92 | return selectArticles(articles) 93 | 94 | def _processArticle(self, article: Article, priority: Optional[str]) -> NoteModel: 95 | url = article.data["url"] 96 | webpage = self.web.download(url) 97 | return NoteModel(webpage.title, webpage.body, webpage.url, priority) 98 | 99 | def _postProcessArticle(self, article: Article) -> None: 100 | feedUrl = article.data["feedUrl"] 101 | 102 | url = article.data["url"] 103 | self.log[feedUrl]["downloaded"].append(url) 104 | 105 | feed = article.data["feed"] 106 | self.log[feedUrl]["etag"] = feed.etag if hasattr(feed, "etag") else "" 107 | self.log[feedUrl]["modified"] = ( 108 | feed.modified if hasattr(feed, "modified") else "" 109 | ) 110 | 111 | def _getProgressLabel(self) -> str: 112 | return "Importing feed..." 113 | 114 | 115 | class EpubImporter(BaseImporter): 116 | def __init__(self, settings: SettingsManager, localFile: LocalFile) -> None: 117 | super().__init__(settings) 118 | self.localFile = localFile 119 | 120 | def _getArticles(self) -> List[Article]: 121 | epubFilePath = getFile(None, "Enter epub file path", None, filter="*.epub") 122 | 123 | if not epubFilePath: 124 | return [] 125 | 126 | articles = getEpubToc(epubFilePath) 127 | if not articles: 128 | raise ImporterError( 129 | ErrorLevel.WARNING, f"No articles found in {epubFilePath}." 130 | ) 131 | 132 | return articles 133 | 134 | def _selectArticles(self, articles: List[Article]) -> List[Article]: 135 | return selectArticles(articles) 136 | 137 | def _processArticle(self, article: Article, priority: Optional[str]) -> NoteModel: 138 | url = article.data["url"] 139 | parsedFile = self.localFile.process(url) 140 | return NoteModel(article.title, parsedFile.body, url, priority) 141 | 142 | def _getProgressLabel(self) -> str: 143 | return "Importing epub..." 144 | 145 | 146 | class PocketImporter(BaseImporter): 147 | def __init__(self, settings: SettingsManager, pocket: Pocket, web: Web) -> None: 148 | super().__init__(settings) 149 | self.pocket = pocket 150 | self.web = web 151 | 152 | def _getArticles(self) -> List[Article]: 153 | articles = self.pocket.getArticles() 154 | if not articles: 155 | raise ImporterError( 156 | ErrorLevel.WARNING, "There are no new articles in Pocket." 157 | ) 158 | 159 | return articles 160 | 161 | def _selectArticles(self, articles: List[Article]) -> List[Article]: 162 | return selectArticles(articles) 163 | 164 | def _processArticle(self, article: Article, priority: Optional[str]) -> NoteModel: 165 | url = article.data["given_url"] 166 | webpage = self.web.download(url) 167 | return NoteModel( 168 | article.data.get("resolved_title") or webpage.title, 169 | webpage.body, 170 | webpage.url, 171 | priority, 172 | ) 173 | 174 | def _postProcessArticle(self, article: Article) -> None: 175 | if self.settings["pocketArchive"]: 176 | self.pocket.archive(article) 177 | 178 | def _getProgressLabel(self) -> str: 179 | return "Importing Pocket articles..." 180 | -------------------------------------------------------------------------------- /ir/importer/epub.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 DarkSun 2 | # 3 | # Permission to use, copy, modify, and distribute this software for any purpose 4 | # with or without fee is hereby granted, provided that the above copyright 5 | # notice and this permission notice appear in all copies. 6 | # 7 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | # REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 9 | # AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 11 | # LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 12 | # OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 13 | # PERFORMANCE OF THIS SOFTWARE. 14 | 15 | import os 16 | import tempfile 17 | import xml.etree.ElementTree as ET 18 | import zipfile 19 | from pathlib import Path 20 | from typing import List 21 | from urllib.parse import urlsplit, urlunsplit 22 | 23 | from ..util import Article 24 | 25 | 26 | def nov_container_content_filename(filename: str) -> str: 27 | """Return the content filename for CONTENT.""" 28 | query = "{*}rootfiles/{*}rootfile[@media-type='application/oebps-package+xml']" 29 | doc = ET.parse(filename) 30 | root = doc.getroot() 31 | node = root.find(query) 32 | if node is not None: 33 | return node.get("full-path") 34 | return "OEBPS/Content.opf" 35 | 36 | 37 | def nov_content_version(root) -> float: 38 | """Return the EPUB version for ROOT.""" 39 | version = root.get("version") if root is not None else None 40 | if not version: 41 | raise ValueError("Version not specified") 42 | return float(version) 43 | 44 | 45 | def nov_content_manifest(directory, root): 46 | """Extract an alist of manifest files for CONTENT in DIRECTORY. 47 | Each alist item consists of the identifier and full path.""" 48 | query = "{*}manifest/{*}item" 49 | nodes = root.findall(query) 50 | return {node.get("id"): os.path.join(directory, node.get("href")) for node in nodes} 51 | 52 | 53 | def nov_content_epub2_toc_file(root, manifest): 54 | """Return toc file for EPUB 2.""" 55 | node = root.find("{*}spine[@toc]") 56 | if node is None: 57 | raise ValueError("EPUB 2 NCX ID not found") 58 | 59 | toc_id = node.get("toc") 60 | if toc_id is None: 61 | raise ValueError("EPUB 2 NCX ID not found") 62 | 63 | toc_file = manifest.get(toc_id) 64 | 65 | if toc_file is None: 66 | raise ValueError("EPUB 2 NCX file not found") 67 | 68 | return toc_file 69 | 70 | 71 | def nov_content_epub3_toc_file(root, manifest): 72 | """Return toc file for EPUB 3.""" 73 | node = root.find("{*}manifest/{*}item[@properties='nav']") 74 | if node is None: 75 | raise ValueError("EPUB 3