Showing preview only (529K chars total). Download the full file or copy to clipboard to get everything.
Repository: MangaManagerORG/Manga-Manager
Branch: main
Commit: df2a88b71b92
Files: 146
Total size: 25.1 MB
Directory structure:
gitextract_z3pdl1we/
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ └── workflows/
│ └── Run_Tests.yml
├── .gitignore
├── BUILD.ps1
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── DEVELOPMENT.MD
├── Dockerfile
├── LICENSE
├── MangaManager/
│ ├── Extensions/
│ │ ├── CoverDownloader/
│ │ │ ├── CoverDownloader.py
│ │ │ └── __init__.py
│ │ ├── IExtensionApp.py
│ │ ├── Template.py
│ │ ├── WebpConverter/
│ │ │ ├── WebpConverter.py
│ │ │ └── __init__.py
│ │ └── __init__.py
│ ├── ExternalSources/
│ │ ├── CoverSources/
│ │ │ ├── MangaDex/
│ │ │ │ ├── MangaDex.py
│ │ │ │ └── __init__.py
│ │ │ └── __init__.py
│ │ ├── MetadataSources/
│ │ │ ├── MetadataSourceFactory.py
│ │ │ ├── Providers/
│ │ │ │ ├── AniList.py
│ │ │ │ ├── ComicVine.py
│ │ │ │ ├── MangaUpdates.py
│ │ │ │ └── __init__.py
│ │ │ └── __init__.py
│ │ └── __init__.py
│ ├── common/
│ │ ├── __init__.py
│ │ └── models/
│ │ ├── AgeRating.py
│ │ ├── ComicInfo.py
│ │ ├── ComicInfo.xds
│ │ ├── ComicInfoTag.py
│ │ ├── ComicPageType.py
│ │ ├── Manga.py
│ │ ├── YesNo.py
│ │ └── __init__.py
│ ├── logging_setup.py
│ ├── main.py
│ ├── pyinstaller_hooks/
│ │ └── hook-tkinterdnd2.py
│ ├── res/
│ │ └── languages.json
│ ├── src/
│ │ ├── Common/
│ │ │ ├── LoadedComicInfo/
│ │ │ │ ├── ArchiveFile.py
│ │ │ │ ├── CoverActions.py
│ │ │ │ ├── ILoadedComicInfo.py
│ │ │ │ ├── LoadedComicInfo.py
│ │ │ │ ├── LoadedFileCoverData.py
│ │ │ │ ├── LoadedFileMetadata.py
│ │ │ │ └── __init__.py
│ │ │ ├── ResourceLoader.py
│ │ │ ├── __init__.py
│ │ │ ├── errors.py
│ │ │ ├── naturalsorter.py
│ │ │ ├── parser.py
│ │ │ ├── progressbar.py
│ │ │ ├── terminalcolors.py
│ │ │ └── utils.py
│ │ ├── DynamicLibController/
│ │ │ ├── __init__.py
│ │ │ ├── extension_manager.py
│ │ │ └── models/
│ │ │ ├── CoverSourceInterface.py
│ │ │ ├── ExtensionsInterface.py
│ │ │ ├── IMetadataSource.py
│ │ │ └── __init__.py
│ │ ├── MetadataManager/
│ │ │ ├── CoverManager/
│ │ │ │ ├── CoverManager.py
│ │ │ │ └── __init__.py
│ │ │ ├── GUI/
│ │ │ │ ├── ControlManager.py
│ │ │ │ ├── ExceptionWindow.py
│ │ │ │ ├── FileChooserWindow.py
│ │ │ │ ├── MessageBox.py
│ │ │ │ ├── OneTimeMessageBox.py
│ │ │ │ ├── __init__.py
│ │ │ │ ├── scrolledframe.py
│ │ │ │ ├── utils.py
│ │ │ │ ├── widgets/
│ │ │ │ │ ├── AutocompleteComboboxWidget.py
│ │ │ │ │ ├── ButtonWidget.py
│ │ │ │ │ ├── CanvasCoverWidget.py
│ │ │ │ │ ├── ComboBoxWidget.py
│ │ │ │ │ ├── FileMultiSelectWidget.py
│ │ │ │ │ ├── FormBundleWidget.py
│ │ │ │ │ ├── HyperlinkLabelWidget.py
│ │ │ │ │ ├── LongTextWidget.py
│ │ │ │ │ ├── MMWidget.py
│ │ │ │ │ ├── MessageBoxWidget.py
│ │ │ │ │ ├── OptionMenuWidget.py
│ │ │ │ │ ├── ProgressBarWidget.py
│ │ │ │ │ ├── ScrolledFrameWidget.py
│ │ │ │ │ ├── WidgetManager.py
│ │ │ │ │ └── __init__.py
│ │ │ │ └── windows/
│ │ │ │ ├── AboutWindow.py
│ │ │ │ ├── DragAndDrop.py
│ │ │ │ ├── LoadingWindow.py
│ │ │ │ ├── MainWindow.py
│ │ │ │ ├── SettingsWindow.py
│ │ │ │ └── __init__.py
│ │ │ ├── MetadataManagerCLI.py
│ │ │ ├── MetadataManagerGUI.py
│ │ │ ├── MetadataManagerLib.py
│ │ │ └── __init__.py
│ │ ├── Settings/
│ │ │ ├── SettingControl.py
│ │ │ ├── SettingControlType.py
│ │ │ ├── SettingSection.py
│ │ │ ├── Settings.py
│ │ │ ├── SettingsDefault.py
│ │ │ └── __init__.py
│ │ ├── __init__.py
│ │ └── __version__.py
│ └── tests/
│ ├── Common/
│ │ ├── __init__.py
│ │ ├── test_ComicInfo.py
│ │ └── test_utils.py
│ ├── ExtensionsTests/
│ │ ├── __init__.py
│ │ └── test_WebpConverter.py
│ ├── ExternalMetadataTests/
│ │ ├── __init__.py
│ │ └── test_AniList.py
│ ├── LoadedComicInfo/
│ │ ├── __init__.py
│ │ ├── test_Covers.py
│ │ ├── test_LoadedCInfo.py
│ │ ├── test_LoadedCInfo_backup.py
│ │ └── test_moveto.py
│ ├── MetadataManagerTests/
│ │ ├── GUI/
│ │ │ ├── __init__.py
│ │ │ ├── test_MetadataEditorGUI.py
│ │ │ ├── test_dinamic_layouts.py
│ │ │ └── test_fetch_metadata.py
│ │ ├── __init__.py
│ │ └── test_MetadataEditorCore.py
│ ├── Settings/
│ │ ├── __init__.py
│ │ └── test_Settings.py
│ ├── __init__.py
│ ├── common.py
│ ├── data/
│ │ ├── !00_SAMPLE_FILE.CBZ
│ │ └── test.py
│ └── test_comicinfo.py
├── README.md
├── docker-compose.yml
├── docker-root/
│ ├── config/
│ │ ├── .config/
│ │ │ └── xfce4/
│ │ │ ├── panel/
│ │ │ │ └── launcher-7/
│ │ │ │ └── MM Launcher.desktop
│ │ │ └── xfconf/
│ │ │ └── xfce-perchannel-xml/
│ │ │ ├── xfce4-desktop.xml
│ │ │ ├── xfce4-panel.xml
│ │ │ └── xsettings.xml
│ │ ├── Desktop/
│ │ │ ├── MangaManager_23_02_02_Beta_linux_01
│ │ │ ├── covers-folder-link.desktop
│ │ │ ├── manga-folder-link.desktop
│ │ │ └── manga-manager-link.desktop
│ │ └── custom-cont-init.d/
│ │ └── prepare-app-permissions.sh
│ └── defaults/
│ ├── autostart
│ └── startwm.sh
├── requirements.txt
└── sonar-project.properties
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: thepromidius
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: "[Feature Request]"
labels: Feature Request
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
================================================
FILE: .github/workflows/Run_Tests.yml
================================================
# This workflow file will install Python dependencies,
# create a desktop, and test the application's GUI on multiple versions of Python
name: Python tests & Build
on:
- push
- pull_request
env:
$$_ENV_DEVELOPMENT_MM_$$: true
IMAGE_NAME: "thepromidius/manga-manager"
jobs:
test_linux:
env:
DISPLAY: ":99.0"
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ '3.11' ]
name: Python ${{ matrix.python-version }} - Linux
steps:
-
uses: actions/checkout@v3
-
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
-
run: sudo apt install xvfb
-
run: pip install -r requirements.txt
-
name: Start xvfb
run: |
Xvfb :99 -screen 0 1920x1080x24 &disown
-
name: Run the tests
run: |
cd MangaManager
python -m unittest discover -s tests -t .
# test_windows:
# env:
# DISPLAY: ":99.0"
# runs-on: windows-latest
# if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/test' }}
# strategy:
# matrix:
# python-version: [ '3.11' ]
# name: Python ${{ matrix.python-version }} - Windows
# steps:
# -
# uses: actions/checkout@v3
# -
# uses: actions/setup-python@v4
# with:
# python-version: ${{ matrix.python-version }}
# cache: 'pip'
# -
# run: pip install -r requirements.txt
# -
# name: Run the tests
# run: |
# cd MangaManager
# python -m unittest discover -s tests -t .
sonarcloud:
name: SonarCloud
runs-on: ubuntu-latest
steps:
-
uses: actions/checkout@v3
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
parse_versions:
name: Parse Versions Env
runs-on: ubuntu-latest
outputs:
latest_commit: ${{ steps.latest_commit.outputs.latest_commit }}
release_commit: ${{ steps.release_commit.outputs.release_commit }}
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set latest release commit hash (latest)
id: release_commit
run: |
release_url=$(curl -s https://api.github.com/repos/${{ github.repository }}/releases/latest)
tag_name=$(echo $release_url | jq -r '.tag_name')
tag_url=$(curl -s https://api.github.com/repos/${{ github.repository }}/git/ref/tags/$tag_name)
release_commit=$(echo $tag_url | jq -r '.object.sha')
short_release_commit=$(git rev-parse --short $release_commit)
echo "release_commit=$short_release_commit" >> $GITHUB_OUTPUT
- name: Set latest commit hash (develop)
id: latest_commit
run: |
latest_commit=$(git rev-parse --short HEAD)
echo "latest_commit=$latest_commit" >> $GITHUB_OUTPUT
docker_test:
name: Test and Build - Test Version
needs: [test_linux, sonarcloud, parse_versions]
runs-on: ubuntu-latest
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/test' }}
steps:
-
name: Checkout
uses: actions/checkout@v3
-
name: Replace "nightly" with commit hash
run: |
file_contents=$(head -n 1 MangaManager/src/__version__.py)
new_contents="${file_contents/nightly/nightly:${{ needs.parse_versions.outputs.latest_commit }}}"
echo "$new_contents" > MangaManager/src/__version__.py
-
name: Set up QEMU
uses: docker/setup-qemu-action@v2
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
-
name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Build and push
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ env.IMAGE_NAME }}:test
docker_nightly:
name: Test and Build - Nightly Version
needs: [test_linux, sonarcloud, parse_versions ]
runs-on: ubuntu-latest
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
steps:
-
name: Checkout
uses: actions/checkout@v3
-
name: Replace "nightly" with commit hash
run: |
file_contents=$(head -n 1 MangaManager/src/__version__.py)
new_contents="${file_contents/nightly/nightly:${{ needs.parse_versions.outputs.latest_commit }}}"
echo "$new_contents" > MangaManager/src/__version__.py
-
name: Set up QEMU
uses: docker/setup-qemu-action@v2
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
-
name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Build and push
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ env.IMAGE_NAME }}:nightly
docker_stable:
name: Stable Build
needs: [test_linux, sonarcloud ]
runs-on: ubuntu-latest
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
steps:
-
name: Set up QEMU
uses: docker/setup-qemu-action@v2
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
-
name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Build and push
uses: docker/build-push-action@v3
with:
push: true
tags: ${{ env.IMAGE_NAME }}:latest
================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
## Application settings
settings.ini
/.coverage
/build/**/*
/dist/**/*
/config.ini
/**/*.log*
*.pdf
/instructions_benchmarks.txt
/.idea/sonarlint/
/MangaManager/build
/MangaManager/dist
# From Tests
/MangaManager/*.cbz
/.idea
/PYTHON_MANGA_MANAGER_WORKFLOW.vsdx
/.idea/sonarlint/
================================================
FILE: BUILD.ps1
================================================
##$repoName = "Manga-Manager"
##$ownerName = "MangaManagerOrg"
### Get the latest release
##$latestRelease = Invoke-RestMethod -Uri "https://api.github.com/repos/$ownerName/$repoName/releases/latest"
### Get the branch or tag name of the commit where the latest release is tied to
##$latestReleaseBranchOrTagName = $latestRelease.target_commitish
### Get the short hash of the commit where the latest release is tied to
##$latestReleaseCommitHash = git rev-parse --short $latestReleaseBranchOrTagName
### Get the short hash of the latest commit in the develop branch
##$latestDevelopHash = git rev-parse --short develop
##
##$content = Get-Content .\MangaManager\src\__version__.py
##if ($content -match '(?<=__version__ = ")[^:"]+') {
## $newContent = $content -replace '__version__ = ".*"', "__version__ = `"$versionNumber:nightly--$latestReleaseCommitHash->$latestDevelopHash`""
## $newContent | Set-Content .\MangaManager\src\__version__.py
##}
##Write-Output $newContent
#$repoName = "Manga-Manager"
#$ownerName = "MangaManagerOrg"
## Get the latest release
#$latestRelease = Invoke-RestMethod -Uri "https://api.github.com/repos/$ownerName/$repoName/releases/latest"
## Get the branch or tag name of the commit where the latest release is tied to
#$latestReleaseBranchOrTagName = $latestRelease.target_commitish
## Get the short hash of the commit where the latest release is tied to
#$latestReleaseCommitHash = git rev-parse --short $latestReleaseBranchOrTagName
## Get the short hash of the latest commit in the develop branch
#$latestDevelopHash = git rev-parse --short develop
#
#$content = Get-Content .\MangaManager\src\__version__.py
#$versionRegex = '(?<=__version__ = ")[^:"]+'
#if ($content -match $versionRegex) {
# $versionNumber = $matches[0]
# $newContent = $content -replace '__version__ = ".*"', "__version__ = `"$versionNumber:nightly--$latestReleaseCommitHash->$latestDevelopHash`""
# $newContent | Set-Content .\MangaManager\src\__version__.py
#}
#Write-Output $newContent
$repoName = "Manga-Manager"
$ownerName = "MangaManagerOrg"
# Get the latest release
$latestRelease = Invoke-RestMethod -Uri "https://api.github.com/repos/$ownerName/$repoName/releases/latest"
# Get the branch or tag name of the commit where the latest release is tied to
$latestReleaseBranchOrTagName = $latestRelease.target_commitish
# Get the short hash of the commit where the latest release is tied to
$latestReleaseCommitHash = git rev-parse --short $latestReleaseBranchOrTagName
# Get the short hash of the latest commit in the develop branch
$latestDevelopHash = git rev-parse --short develop
$content = Get-Content .\MangaManager\src\__version__.py
$versionFile = ".\MangaManager\src\__version__.py"
# Read the current contents of the version file
$content = Get-Content $versionFile
# Update the commit hashes in the version file
$content | ForEach-Object {
if ($_ -match "^__version__ = '.*:stable$'") {
$_ -replace "(?<=[^-])-?[0-9a-f]{7,}?(?=-|->)", $latestReleaseCommitHash
} elseif ($_ -match "^__version__ = '.*:nightly$'") {
$_ -replace "(?<=[^-])-?[0-9a-f]{7,}?(?=-|->)", $latestDevelopHash
} else {
$_
}
} | Set-Content $versionFile
Write-Output $content
================================================
FILE: CHANGELOG.md
================================================
## 1.0.0-beta.1
### Features
* Select multiple files and preview the covers of each file.
* Select a folder and open files recursively
* Choose the files you want to change metadata (without needing to open again) Just click and edit!
* Bulk edit metadata for all files at once
* Changes are kept in one sesion. You can edit a single file, edit a different one and edit again without needing to save/write the files each time.
* Apply all changes (save to file) all at once when you are done editing.
* Edit cover or backcover from the metadata view itself. Append, replace, or delete.
* Cover manager to batch edit covers (the old cover manager but improved significantly)
* Online Metadata scraping
* Webp converter
* Errors and warnings log inside the UI itself!
## 0.4.6
### Features
* Added settings button in main menu
* Added .bmp support to webp converter (previously it skipped it)
### Fixed
* A bug in docker that wouldn't select files
* Handled exception when no cover is selected and process is clicked
* Reencode to UTF-8 if file encoding is wrong reading ComicInfo.xml
* Keep original file if recompressed one is bigger. Closes #106
* Skip image if recompress fails. Closes #115
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
thepromidiusyt@gmail.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing guidelines
We welcome any kind of contribution to our software, from simple comment or question to a full fledged [pull request](https://help.github.com/articles/about-pull-requests/). Please read and follow our [Code of Conduct](CODE_OF_CONDUCT.md).
A contribution can be one of the following cases:
1. you have a question;
2. you think you may have found a bug (including unexpected behavior);
3. you want to make some kind of change to the code base (e.g. to fix a bug, to add a new feature, to update documentation).
The sections below outline the steps in each case.
## You have a question
1. use the search functionality [here](https://github.com/MangaManagerORG/Manga-Manager/issues) to see if someone already filed the same issue;
2. if your issue search did not yield any relevant results, make a new issue;
3. apply the "Question" label; apply other labels when relevant.
## You think you may have found a bug
1. use the search functionality [here](https://github.com/MangaManagerORG/Manga-Manager/issues) to see if someone already filed the same issue;
2. if your issue search did not yield any relevant results, make a new issue, making sure to provide enough information to the rest of the community to understand the cause and context of the problem. Depending on the issue, you may want to include:
- the [SHA hashcode](https://help.github.com/articles/autolinked-references-and-urls/#commit-shas) of the commit that is causing your problem;
- some identifying information (name and version number) for dependencies you're using;
- information about the operating system;
3. apply relevant labels to the newly created issue.
## You want to make some kind of change to the code base
1. (**important**) announce your plan to the rest of the community _before you start working_. This announcement should be in the form of a (new) issue;
2. (**important**) wait until some kind of consensus is reached about your idea being a good idea;
3. if needed, fork the repository to your own GitHub profile and create your own feature branch off of the latest main commit. While working on your feature branch, make sure to stay up to date with the main branch by pulling in changes, possibly from the 'upstream' repository (follow the instructions [here](https://help.github.com/articles/configuring-a-remote-for-a-fork/) and [here](https://help.github.com/articles/syncing-a-fork/));
4. Install dependencies with `pip3 install -r requirements.txt`;
5. make sure the existing tests still work by running ``pytest``. If project tests fails use ``pytest --keep-baked-projects`` to keep generated project in /tmp/pytest-* and investigate;
6. add your own tests (if necessary);
7. update or expand the documentation;
8. push your feature branch to (your fork of) the Python Template repository on GitHub;
9. create the pull request, e.g. following the instructions [here](https://help.github.com/articles/creating-a-pull-request/).
In case you feel like you've made a valuable contribution, but you don't know how to write or run tests for it, or how to generate the documentation: don't let this discourage you from making the pull request; we can help you! Just go ahead and submit the pull request, but keep in mind that you might be asked to append additional commits to your pull request.
================================================
FILE: DEVELOPMENT.MD
================================================
## Versioning and building
When a build is to be made, copy the short hash of last commit and update it in `src/__version__.py`. After that make commit and push the version bump.
After that you can now create the build with the command below
## How to build:
`python -m PyInstaller .\MangaManager.spec --clean`
## Errors building with pyinstaller
If you can not run the build make sure all requirements are installed.
Pyinstaller does not use the virtual env requirements. So make sure the base python has them installed
Some of the requirements that gave me issues are:
- `chardet`
================================================
FILE: Dockerfile
================================================
FROM ghcr.io/linuxserver/baseimage-rdesktop-web:jammy
ENV DEBIAN_FRONTEND=noninteractive
ENV UID=1000
ENV GID=1000
ENV NO_UPDATE_NOTIFIER=true
ENV GUIAUTOSTART=true
WORKDIR /tmp
COPY requirements.txt /tmp/
# Copy App
COPY --chown=$UID:$GID [ "/MangaManager", "/app" ]
# Setup Dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends \
# Desktop Environment
mousepad \
xfce4-terminal \
xfce4 \
xubuntu-default-settings \
xubuntu-icon-theme \
unrar\
# Python \
idle-python3.11 \
python3-tk \
python3-pip && \
# Manga Manager Dependencies
python3.11 -m pip install -r requirements.txt && \
# Cleanup
apt-get autoclean && \
rm -rf \
/var/lib/apt/lists/* \
/var/tmp/* \
/tmp/* && \
# Try making python3 callable by just running "python" on Ubuntu :) (optional)
ln -s /usr/bin/python3.11 /usr/bin/python || true && \
chmod -R +x /app
# Setup environment & branding/customization
COPY /docker-root /
RUN \
chmod -R +x /config/Desktop && \
chmod -R +x /config/.config/xfce4/panel
WORKDIR /app
EXPOSE 3000
VOLUME /manga
VOLUME /covers
================================================
FILE: LICENSE
================================================
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
================================================
FILE: MangaManager/Extensions/CoverDownloader/CoverDownloader.py
================================================
from tkinter import Label, Frame, Entry
def get_cover_from_source_dummy() -> list:
...
class CoverDownloader():#IExtensionApp):
name = "Cover Downloader"
def serve_gui(self):
if not self.master:
return Exception("Tried to initialize ui with no master window")
frame = Frame(self.master)
frame.pack()
Label(frame, text="Manga identifier").pack()
Entry(frame).pack()
# Combobox(frame, state="readonly",values=sources_factory["CoverSources"]).pack()
covers = get_cover_from_source_dummy()
================================================
FILE: MangaManager/Extensions/CoverDownloader/__init__.py
================================================
================================================
FILE: MangaManager/Extensions/IExtensionApp.py
================================================
import abc
import tkinter
from typing import final
class IExtensionApp(tkinter.Toplevel, metaclass=abc.ABCMeta):
"""
"""
name = None
embedded_ui = False
master_frame = None
master = None
_super = None
@final
def __init__(self, master, super_=None, **kwargs):
"""
Initializes the toplevel window but hides the window.
"""
if self.name is None: # Check if the "name" attribute has been set
raise ValueError(f"Error initializing the {self.__class__.__name__} Extension. The 'name' attribute must be set in the ExtensionApp class.")
# if self.embedded_ui:
super().__init__(master=master,**kwargs)
self.title(self.__class__.name)
self.master = self.master_frame = self
if super_ is not None:
self._super = super_
# else:
# frame = tkinter.Frame()
# self.master_frame = frame
self.serve_gui()
# self.withdraw() # Hide the window
def _initialize(self):
...
# """
# Sets the new master window and displays the Toplevel window
# :param master:
# :return:
# """
# self.master = master
# self.deiconify()
# self.serve_gui()
@abc.abstractmethod
def serve_gui(self):
...
================================================
FILE: MangaManager/Extensions/Template.py
================================================
import logging
from Extensions.IExtensionApp import IExtensionApp
logger = logging.getLogger()
class ExtensionTemplate(IExtensionApp):
name = "Webp Converter"
def serve_gui(self):
if not self.master:
return Exception("Tried to initialize ui with no master window")
================================================
FILE: MangaManager/Extensions/WebpConverter/WebpConverter.py
================================================
from __future__ import annotations
import glob
import logging
import os
import pathlib
import threading
import tkinter
import tkinter.ttk as ttk
from tkinter import filedialog
from Extensions.IExtensionApp import IExtensionApp
from src.Common.LoadedComicInfo.LoadedComicInfo import LoadedComicInfo
from src.Common.utils import ShowPathTreeAsDict
from src.MetadataManager.GUI.widgets import ScrolledFrameWidget, ProgressBarWidget
from src.Settings.Settings import Settings
logger = logging.getLogger()
def start_processing(_selected_files,_progress_bar):
processing_thread = threading.Thread(target=_run_process, args=(_selected_files,_progress_bar))
processing_thread.start()
def _run_process(list_of_files,progress_bar:ProgressBarWidget):
for file in list_of_files:
logger.info(f"[Extension][WebpConvert] Processing file",
extra={"processed_filename":file})
try:
# time.sleep(20)
LoadedComicInfo(file, load_default_metadata=False).convert_to_webp()
progress_bar.increase_processed()
except Exception:
logger.exception(f"Failed to convert to webp '{file}'")
progress_bar.increase_failed()
progress_bar.running = False
class WebpConverter(IExtensionApp):
name = "Webp Converter"
embedded_ui = True
base_path: str = ""
glob: str = "**/*.cbz"
_selected_files: list[str | pathlib.Path] = []
treeview_frame: ScrolledFrameWidget = None
nodes: dict
_progress_bar: ProgressBarWidget
def pb_update(self):
if self._progress_bar.running:
self._progress_bar._update()
self.after(20, self.pb_update)
@property
def selected_files(self):
self._set_input()
return self._selected_files
def process(self):
if not self._selected_files:
return
self._progress_bar.start(len(self._selected_files))
self._progress_bar.running = True
self.pb_update()
self.after(0, self.pb_update)
start_processing(self._selected_files,self._progress_bar)
def select_base(self):
self.base_path = filedialog.askdirectory(parent=self) # select directory
self.selected_base_path.set(str(self.base_path))
def _on_file(self, parent, file):
self.tree.insert(self.nodes.get(str(parent.get("current"))), 'end', text=file, open=True)
def _on_folder(self, parent_dic, folder):
parent_path = str(pathlib.Path(parent_dic.get("current")))
node = self.tree.insert(self.nodes[parent_path], 'end', text=folder, open=True)
self.nodes[str(pathlib.Path(parent_path, folder))] = node
def _clear(self):
self.tree.delete(*self.tree.get_children())
self.nodes = dict()
def _set_input(self):
self.glob = self.path_glob.get() or "*.cbz"
os.chdir(self.base_path)
logger.debug(f"Looking up files for glob: '{self.glob}'")
self._selected_files = [
pathlib.Path(self.base_path, globbed_file) for globbed_file in glob.glob(self.glob, recursive=True)]
logger.debug(f"Found {len(self._selected_files)} files")
def preview(self):
if not self.base_path:
return
self._clear()
self._set_input()
abspath = os.path.abspath(self.base_path)
node = self.tree.insert("", 'end', abspath, text=self.base_path, open=True)
self.nodes[abspath] = node
treeview = ShowPathTreeAsDict
treeview.on_file = self._on_file
treeview.on_subfolder = self._on_folder
self.treeview_files = treeview(base_path=self.base_path, paths=self.selected_files).get()
def serve_gui(self):
self.geometry("300x400")
frame = tkinter.Frame(self.master)
frame.pack(fill="both", expand=True, padx=20, pady=20)
default_base_setting = Settings().get('Webp Converter', 'default_base_path')
self.selected_base_path = tkinter.StringVar(None, value=default_base_setting)
self.base_path = default_base_setting
tkinter.Button(frame, text="Select Base Directory", command=self.select_base, width=50).pack()
self.base_path_entry = tkinter.Entry(frame, state="readonly", textvariable=self.selected_base_path, width=50)
self.base_path_entry.pack()
tkinter.Label(frame, text="Glob to apply:", width=50).pack(side="top")
self.path_glob = tkinter.Entry(frame, width=50)
self.path_glob.pack()
#
tkinter.Button(frame, text="Preview selected files", pady=6, command=self.preview, width=50).pack(side="top", pady=10)
tkinter.Button(frame, text="Process", command=self.process, pady=6, width=50).pack(side="top", pady=10)
pb_frame = tkinter.Frame(frame, pady=10, width=60)
pb_frame.pack()
self._progress_bar = ProgressBarWidget(pb_frame)
self.tree = ttk.Treeview(frame)
self.tree.heading('#0', text='Project tree', anchor='n')
self.tree.pack(expand=True, fill="both", side="top")
================================================
FILE: MangaManager/Extensions/WebpConverter/__init__.py
================================================
================================================
FILE: MangaManager/Extensions/__init__.py
================================================
================================================
FILE: MangaManager/ExternalSources/CoverSources/MangaDex/MangaDex.py
================================================
import logging
import os
import urllib
from pathlib import Path
import requests
from src.Common.utils import clean_filename
from src.DynamicLibController.models.CoverSourceInterface import ICoverSource, Cover
from src.Settings import SettingHeading
from src.Settings.Settings import Settings
logger = logging.getLogger()
class MangaDex(ICoverSource):
name = "MangaDex"
@staticmethod
def parse_identifier(identifier) -> str:
...
@classmethod
def get_covers(cls, identifier: str) -> list[Cover]:
"""
Downloads the covers from manga_id from mangadex.
If the cover is already downloaded it won't re-download
:param identifier: The manga identifier only.
"""
manga_id = cls.parse_identifier(identifier)
data = {"manga[]": [manga_id], "includes[]": ["manga"], "limit": 50}
# Request the list of covers in the provided manga
r = requests.get(f"https://api.mangadex.org/cover", params=data)
if r.status_code == 400:
logger.error("MangaDex api returned 400 for ")
raise Exception("status code 400")
r_json = r.json()
cover_attributes = r_json.get("data")[0].get("relationships")[0].get("attributes")
ja_title = list(filter(lambda p: p.get("ja-ro"),
cover_attributes.get("altTitles")))
if ja_title:
ja_title = ja_title[0].get("ja-ro")
normalized_manga_name = (ja_title or cover_attributes.get("title").get("en"))
destination_dirpath = Path(Settings().get(SettingHeading.Main, 'covers_folder_path'), clean_filename(
normalized_manga_name)) # The covers get stored in their own series folder inside the covers directory
total = len(r_json.get("data"))
# Todo: Implement progress bar
for i, cover_data in enumerate(r_json.get("data")):
try:
cover_filename = cover_data.get("attributes").get("fileName")
filename, file_extension = os.path.splitext(cover_filename)
cover_volume = cover_data.get("attributes").get("volume")
cover_loc = cover_data.get("attributes").get("locale")
destination_filename = f"Cover_Vol.{str(cover_volume).zfill(2)}_{cover_loc}{file_extension}"
destination_filepath = Path(destination_dirpath, destination_filename)
if (not destination_filepath.exists() or force_overwrite) and not test_run:
image_url = f"https://mangadex.org/covers/{manga_id}/{cover_filename}"
urllib.request.urlretrieve(image_url, destination_filepath)
logger.debug(f"Downloaded {destination_filename}")
elif test_run:
image_url = f"https://mangadex.org/covers/{manga_id}/{cover_filename}"
print(f"Asserting if valid url: '{image_url}' ")
return check_url_isImage(image_url)
else:
logger.info(f"Skipped 'https://mangadex.org/covers/{manga_id}/{cover_filename}' -> Already exists")
except Exception as e:
logger.error(e)
logger.info(f"Files saved to: '{destination_dirpath}'")
@classmethod
def parse_input(cls, value) -> str:
"""
Accepts a mangadex id or URL
:param value:
:return: mangadex managa id
"""
if "https://mangadex.org/title/" in value:
value = value.replace("https://mangadex.org/title/", "").split("/")[0]
return value
================================================
FILE: MangaManager/ExternalSources/CoverSources/MangaDex/__init__.py
================================================
================================================
FILE: MangaManager/ExternalSources/CoverSources/__init__.py
================================================
================================================
FILE: MangaManager/ExternalSources/MetadataSources/MetadataSourceFactory.py
================================================
import logging
# Import all the scrapers here to ensure globals() has the key in it for dynamic instantiation
from .Providers.AniList import AniList
from .Providers.ComicVine import ComicVine
from .Providers.MangaUpdates import MangaUpdates
logger = logging.getLogger()
# Avoid IDE cleaning imports
MangaUpdates.__dont_clean = ""
AniList.__dont_clean = ""
ComicVine.__dont_clean = ""
# NOTE: This is a stopgap solution until dynamic loader is implemented
class ScraperFactory:
""" Singleton Factory of metadata providers. Pass in the name defined in the provider .name() and an instance will be returned. """
__instance = None
providers = {}
def __new__(cls):
if ScraperFactory.__instance is None:
ScraperFactory.__instance = object.__new__(cls)
return ScraperFactory.__instance
def __init__(self):
pass
def get_scraper(self, setting_name):
if setting_name not in self.providers:
try:
cls = globals()[setting_name]
except KeyError:
logger.exception(f"Failed to load setting name '{setting_name}'")
return
self.providers[setting_name] = cls()
return self.providers[setting_name]
================================================
FILE: MangaManager/ExternalSources/MetadataSources/Providers/AniList.py
================================================
from __future__ import annotations
import logging
import re
import requests
from enum import StrEnum
from typing import Optional
from common import get_invalid_person_tag
from common.models import ComicInfo
from src.Common.errors import MangaNotFoundError
from src.DynamicLibController.models.IMetadataSource import IMetadataSource
from src.Settings.SettingControl import SettingControl
from src.Settings.SettingControlType import SettingControlType
from src.Settings.SettingSection import SettingSection
from src.Settings.Settings import Settings
pattern = r"anilist.com/manga/(\d+)"
class AniListPerson(StrEnum):
OriginalStory = "original_story", # Original Story
CharacterDesign = "character_design", # Character Design
Story = "story", # Story
Art = "art", # Art
Assistant = "assistant", # Assistant
class AniListSetting(StrEnum):
SeriesTitleLanguage = "series_title_language",
class AniList(IMetadataSource):
name = "AniList"
_log = logging.getLogger()
# Map the Role from API to the ComicInfo tags to write
person_mapper = {}
_HOW_METADATA_MAPS_TOOLTIP = "How metadata field will map to ComicInfo fields"
romaji_as_series = True
def init_settings(self):
self.settings = [
SettingSection(self.name, self.name, [
SettingControl(key=AniListSetting.SeriesTitleLanguage, name="Prefer Romaji Series Title Language",
control_type=SettingControlType.Bool, value=True,
tooltip="How metadata field will map to Series and LocalizedSeries fields\n"
"true: Romaji->Series, English->LocalizedSeries\n"
"false: English->Series, Romaji->LocalizedSeries\n"
"Always Romaji->Series when no English"),
SettingControl(key=AniListPerson.OriginalStory, name="Original Story",
control_type=SettingControlType.Text, value="Writer",
tooltip=self._HOW_METADATA_MAPS_TOOLTIP,
validate=self.is_valid_person_tag, format_value=self.trim),
SettingControl(key=AniListPerson.CharacterDesign, name="Character Design",
control_type=SettingControlType.Text, value="Penciller",
tooltip=self._HOW_METADATA_MAPS_TOOLTIP,
validate=self.is_valid_person_tag, format_value=self.trim),
SettingControl(key=AniListPerson.Story, name="Story",
control_type=SettingControlType.Text, value="Writer",
tooltip=self._HOW_METADATA_MAPS_TOOLTIP,
validate=self.is_valid_person_tag, format_value=self.trim),
SettingControl(key=AniListPerson.Art, name="Art",
control_type=SettingControlType.Text, value="Penciller, Inker, CoverArtist",
tooltip=self._HOW_METADATA_MAPS_TOOLTIP,
validate=self.is_valid_person_tag, format_value=self.trim),
SettingControl(key=AniListPerson.Assistant, name="Assistant",
control_type=SettingControlType.Text, value="",
tooltip=self._HOW_METADATA_MAPS_TOOLTIP,
validate=self.is_valid_person_tag, format_value=self.trim),
])
]
super().init_settings()
def save_settings(self):
self.romaji_as_series = Settings().get(self.name, AniListSetting.SeriesTitleLanguage)
self.person_mapper["Original Story"] = Settings().get(self.name, AniListPerson.OriginalStory).split(',')
self.person_mapper["Original Creator"] = Settings().get(self.name, AniListPerson.OriginalStory).split(',')
self.person_mapper["Character Design"] = Settings().get(self.name, AniListPerson.CharacterDesign).split(',')
self.person_mapper["Story"] = Settings().get(self.name, AniListPerson.Story).split(',')
self.person_mapper["Art"] = Settings().get(self.name, AniListPerson.Art).split(',')
self.person_mapper["Story & Art"] = Settings().get(self.name, AniListPerson.Story).split(',') + Settings().get(
self.name, AniListPerson.Art).split(',')
self.person_mapper["Assistant"] = Settings().get(self.name, AniListPerson.Assistant).split(',')
@staticmethod
def is_valid_person_tag(key, value):
invalid_people = get_invalid_person_tag(value)
if len(invalid_people) == 0:
return ""
return ", ".join(invalid_people) + " are not a valid tags"
@staticmethod
def get_manga_id_from_url(url):
pattern = r"https:\/\/anilist\.co\/manga\/(\d+)"
match = re.search(pattern, url)
if match:
return match.group(1)
return None
@classmethod
def _get_id_from_series(cls, cinfo: ComicInfo) -> Optional[int]:
manga_id = cls.get_manga_id_from_url(cinfo.web)
if manga_id is not None:
return manga_id
try:
content = cls._search_for_manga_title_by_manga_title(cinfo.series, "MANGA", {})
except MangaNotFoundError:
content = cls.search_for_manga_title_by_manga_title_with_adult(cinfo.series, "MANGA", {})
if content is None:
return None
return content.get("id")
@classmethod
def get_cinfo(cls, comic_info_from_ui: ComicInfo) -> ComicInfo | None:
comicinfo = ComicInfo()
serie_id = cls._get_id_from_series(comic_info_from_ui)
if serie_id is None:
return None
data = cls._search_details_by_series_id(serie_id, "MANGA", {})
startdate = data.get("startDate")
comicinfo.day = startdate.get("day")
comicinfo.month = startdate.get("month")
comicinfo.year = startdate.get("year")
comicinfo.genre = ", ".join(data.get("genres")).strip()
comicinfo.web = data.get("siteUrl").strip()
if data.get("volumes"):
comicinfo.count = data.get("volumes")
# Title (Series & LocalizedSeries)
title = data.get("title")
cls._log.info("[AniList] Fetch Data found title " + str(title) + " for " + comic_info_from_ui.series)
title_english = (data.get("title").get("english") or "").strip()
title_romaji = (data.get("title").get("romaji") or "").strip()
if cls.romaji_as_series:
comicinfo.series = title_romaji
if title_romaji != title_english:
comicinfo.localized_series = title_english
else:
comicinfo.series = title_english
if title_romaji != title_english:
comicinfo.localized_series = title_romaji
# Summary
comicinfo.summary = IMetadataSource.clean_description(data.get("description"), remove_source=True)
# People
cls.update_people_from_mapping(data["staff"]["edges"], cls.person_mapper, comicinfo,
lambda item: item["node"]["name"]["full"],
lambda item: item["role"])
return comicinfo
@classmethod
def _post(cls, query, variables, logging_info):
try:
response = requests.post('https://graphql.anilist.co', json={'query': query, 'variables': variables})
if response.status_code == 429: # Anilist rate-limit code
raise AniListRateLimit()
except AniListRateLimit:
cls._log.exception("Hitted anilist ratelimit")
return None
except Exception:
cls._log.exception("Unhandled exception making the request to anilist")
return None
cls._log.debug(f'Query: {query}')
cls._log.debug(f'Variables: {variables}')
# self.logger.debug(f'Response JSON: {response.json()}')
try:
return response.json()['data']['Media']
except TypeError:
cls._log.exception("Wrong data format recieved when parsing response json")
return None
@classmethod
def _search_for_manga_title_by_id(cls, manga_id, logging_info):
query = '''
query search_for_manga_title_by_id ($manga_id: Int) {
Media (id: $manga_id, type: MANGA) {
id
title {
romaji
english
native
}
synonyms
}
}
'''
variables = {
'manga_id': manga_id,
}
return cls._post(query, variables, logging_info)
@classmethod
def _search_for_manga_title_by_manga_title(cls, manga_title, format_, logging_info):
query = '''
query search_manga_by_manga_title ($manga_title: String, $format: MediaFormat) {
Media (search: $manga_title, type: MANGA, format: $format, isAdult: false) {
id
title {
romaji
english
native
}
synonyms
}
}
'''
variables = {
'manga_title': manga_title,
'format': format_
}
ret = cls._post(query, variables, logging_info)
if ret is None:
raise MangaNotFoundError("AniList", manga_title)
return ret
@classmethod
def search_for_manga_title_by_manga_title_with_adult(cls, manga_title, format_, logging_info):
query = '''
query search_manga_by_manga_title ($manga_title: String, $format: MediaFormat) {
Media (search: $manga_title, type: MANGA, format: $format) {
id
title {
romaji
english
native
}
synonyms
}
}
'''
variables = {
'manga_title': manga_title,
'format': format_
}
return cls._post(query, variables, logging_info)
@classmethod
def _search_details_by_series_id(cls, series_id, format_, logging_info):
query = '''
query search_details_by_series_id ($series_id: Int, $format: MediaFormat) {
Media (id: $series_id, type: MANGA, format: $format) {
id
status
volumes
siteUrl
title {
romaji
english
native
}
type
genres
synonyms
startDate {
day
month
year
}
coverImage {
extraLarge
}
staff {
edges {
node{
name {
first
last
full
alternative
}
siteUrl
}
role
}
}
description
}
}
'''
variables = {
'series_id': series_id,
'format': format_
}
return cls._post(query, variables, logging_info)
class AniListRateLimit(Exception):
"""
Exception raised when AniList rate-limit is breached.
"""
================================================
FILE: MangaManager/ExternalSources/MetadataSources/Providers/ComicVine.py
================================================
import logging
from abc import ABC
import requests
from common.models import ComicInfo
from src.Common.errors import MangaNotFoundError
from src.DynamicLibController.models.IMetadataSource import IMetadataSource
from src.Settings import SettingSection, SettingControl, SettingControlType, Settings
class ComicVine(IMetadataSource, ABC):
name = 'ComicVine'
_log = logging.getLogger()
def __init__(self):
self.settings = [
SettingSection(self.name, self.name, [
SettingControl('api_key', "API Key", SettingControlType.Text, "",
"API Key to communicate with ComicVine. This is required for the source"),
])
]
super(ComicVine, self).__init__()
self._log = logging.getLogger(f'{self.__module__}.{self.name}')
def save_settings(self):
pass
def get_cinfo(self, comic_info_from_ui: ComicInfo) -> ComicInfo | None:
comicinfo = ComicInfo()
try:
content = self._search_by_title(comic_info_from_ui.series)
except MangaNotFoundError:
content = self._search_by_issue(comic_info_from_ui.series)
if content is None:
return None
# content = content.get("id")
# data = self._search_details_by_series_id(content, "MANGA", {})
#
# startdate = data.get("startDate")
# comicinfo.summary = data.get("description").strip()
# comicinfo.day = startdate.get("day")
# comicinfo.month = startdate.get("month")
# comicinfo.year = startdate.get("year")
# comicinfo.series = data.get("title").get("romaji").strip()
# comicinfo.genre = ", ".join(data.get("genres")).strip()
# comicinfo.web = data.get("siteUrl").strip()
# People
# self.update_people_from_mapping(data["staff"]["edges"], self.person_mapper, comicinfo,
# lambda item: item["node"]["name"]["full"],
# lambda item: item["role"])
return comicinfo
def _search_by_title(self, series_name, publish_year=""):
url = f"{self._build_url_base('series')}&name={series_name}"
try:
response = requests.get(url)
# if response.status_code == 429: # Anilist rate-limit code
# raise AniListRateLimit()
except Exception as e:
self._log.warning('Manga Manager is unfamiliar with this error. Please log an issue for investigation.', e)
return None
self._log.debug(f'Query: {url}')
try:
return response.json()['results']
except TypeError:
return None
pass
def _search_by_issue(self, series_name, issue_number):
pass
def _build_url_base(self, entity):
return f"http://api.comicvine.com/{entity}/?api_key={Settings().get(self.name, 'API Key')}&format=json"
================================================
FILE: MangaManager/ExternalSources/MetadataSources/Providers/MangaUpdates.py
================================================
import logging
from enum import StrEnum
import requests
from common import get_invalid_person_tag
from common.models import ComicInfo
from src.Common.errors import MangaNotFoundError
from src.DynamicLibController.models.IMetadataSource import IMetadataSource
from src.Settings.SettingControl import SettingControl
from src.Settings.SettingControlType import SettingControlType
from src.Settings.SettingSection import SettingSection
from src.Settings.Settings import Settings
class MangaUpdatesPerson(StrEnum):
Author = "author",
Artist = "artist",
class MangaUpdates(IMetadataSource):
name = "MangaUpdates"
_log = logging.getLogger()
person_mapper = {
"Author": [
"Writer"
],
"Artist": [
"Penciller",
"Inker",
"CoverArtist"
]
}
def init_settings(self):
self.settings = [
SettingSection(self.name, self.name, [
SettingControl(MangaUpdatesPerson.Author, "Author", SettingControlType.Text, "Writer",
"How metadata field will map to ComicInfo fields", self.is_valid_person_tag, self.trim),
SettingControl(MangaUpdatesPerson.Artist, "Artist", SettingControlType.Text,
"Penciller, Inker, CoverArtist", "How metadata field will map to ComicInfo fields",
self.is_valid_person_tag, self.trim),
])
]
def save_settings(self):
# Update person_mapper when this is called as it indicates the settings for the provider might have changed
self.person_mapper[MangaUpdatesPerson.Author] = Settings().get(self.name, MangaUpdatesPerson.Author).split(',')
self.person_mapper[MangaUpdatesPerson.Artist] = Settings().get(self.name, MangaUpdatesPerson.Artist).split(',')
@staticmethod
def is_valid_person_tag(key, value):
invalid_people = get_invalid_person_tag(value)
if len(invalid_people) == 0:
return ""
return ", ".join(invalid_people) + " are not a valid tags"
@classmethod
def get_cinfo(cls, comic_info_from_ui) -> ComicInfo | None:
# We need to take what's already in the UI and allow fetching to merge the data in
comicinfo = ComicInfo()
data = cls._get_series_details(comic_info_from_ui.series, {})
# Basic Info
comicinfo.series = data["title"].strip()
comicinfo.summary = IMetadataSource.clean_description(data.get("description"), remove_source=True)
comicinfo.genre = ", ".join([i["genre"] for i in data["genres"]]).strip()
comicinfo.tags = ", ".join([i["category"] for i in data["categories"]])
comicinfo.web = data["url"].strip()
comicinfo.manga = "Yes" if data["type"] == "Manga" else "No"
comicinfo.year = data["year"]
# People Info
cls.update_people_from_mapping(cls, data["authors"], cls.person_mapper, comicinfo,
lambda item: item["name"],
lambda item: item["type"])
comicinfo.publisher = (", ".join([ i["publisher_name"] for i in data["publishers"] ]))
# Extended
comicinfo.community_rating = round(data["bayesian_rating"]/2, 1)
return comicinfo
@classmethod
def _get_series_id(cls, search_params, logging_info):
try:
response = requests.post('https://api.mangaupdates.com/v1/series/search', json=search_params)
except Exception as e:
cls._log.exception(e, extra=logging_info)
cls._log.warning('Manga Manager is unfamiliar with this error. Please log an issue for investigation.',
extra=logging_info)
return None
cls._log.debug(f'Search Params: {search_params}')
# cls.logger.debug(f'Response JSON: {response.json()}')
if len(response.json()['results']) == 0:
raise MangaNotFoundError("MangaUpdates",search_params['search'])
try:
return response.json()['results'][0]['record']['series_id']
except TypeError:
return None
@classmethod
def _get_series_details(cls, manga_title, logging_info):
search_params = {
"search": manga_title,
"page": 1,
"per_page": 1
}
try:
series_details = requests.get('https://api.mangaupdates.com/v1/series/' + str(cls._get_series_id(search_params, {})))
except Exception as e:
cls._log.exception(e, extra=logging_info)
cls._log.warning('Manga Manager is unfamiliar with this error. Please log an issue for investigation.',
extra=logging_info)
return None
return series_details.json()
================================================
FILE: MangaManager/ExternalSources/MetadataSources/Providers/__init__.py
================================================
================================================
FILE: MangaManager/ExternalSources/MetadataSources/__init__.py
================================================
from .MetadataSourceFactory import ScraperFactory
from .Providers.AniList import AniList
from .Providers.MangaUpdates import MangaUpdates
print("MetadataSources module loaded")
================================================
FILE: MangaManager/ExternalSources/__init__.py
================================================
================================================
FILE: MangaManager/common/__init__.py
================================================
from .models import PeopleTags
def get_invalid_person_tag(people: str):
""" Validates that a common separated list or single person is a valid Person tag"""
invalid_people = []
for person in [p.strip() for p in people.split(",") if p != ""]:
if person not in list(PeopleTags):
invalid_people.append(person)
return invalid_people
================================================
FILE: MangaManager/common/models/AgeRating.py
================================================
from enum import Enum
# Keep this ordered in terms of progressing ratings, rather than alphabetical
class AgeRating(str, Enum):
UNKNOWN = 'Unknown'
RATING_PENDING = 'Rating Pending'
EARLY_CHILDHOOD = 'Early Childhood'
EVERYONE = 'Everyone'
G = 'G'
EVERYONE_10 = 'Everyone 10+'
PG = 'PG'
KIDSTO_ADULTS = 'Kids to Adults'
TEEN = 'Teen'
MA_15 = 'MA15+'
MATURE_17 = 'Mature 17+'
M = 'M'
R_18 = 'R18+'
ADULTS_ONLY_18 = 'Adults Only 18+'
X_18 = 'X18+'
@classmethod
def list(cls):
return list(map(lambda c: c.value, cls))
================================================
FILE: MangaManager/common/models/ComicInfo.py
================================================
from io import BytesIO
from xml.etree import ElementTree as ET
comic_info_tag_map = {
"series": "Series",
"localized_series": "LocalizedSeries",
"series_sort": "SeriesSort",
"count": "Count",
"writer": "Writer",
"penciller": "Penciller",
"inker": "Inker",
"colorist": "Colorist",
"letterer": "Letterer",
"cover_artist": "CoverArtist",
"editor": "Editor",
"translator": "Translator",
"publisher": "Publisher",
"imprint": "Imprint",
"characters": "Characters",
"teams": "Teams",
"locations": "Locations",
"main_character_or_team": "MainCharacterOrTeam",
"other": "Other",
"genre": "Genre",
"age_rating": "AgeRating",
"series_group": "SeriesGroup",
"alternate_series": "AlternateSeries",
"story_arc": "StoryArc",
"story_arc_number": "StoryArcNumber",
"alternate_count": "AlternateCount",
"alternate_number": "AlternateNumber",
"title": "Title",
"summary": "Summary",
"review": "Review",
"tags": "Tags",
"web": "Web",
"number": "Number",
"volume": "Volume",
"format": "Format",
"manga": "Manga",
"year": "Year",
"month": "Month",
"day": "Day",
"language_iso": "LanguageISO",
"notes": "Notes",
"community_rating": "CommunityRating",
"black_and_white": "BlackAndWhite",
"page_count": "PageCount",
"scan_information": "ScanInformation",
"gtin": "GTIN"
}
class ComicInfo:
series = ""
localized_series = ""
count = ""
writer = ""
penciller = ""
inker = ""
colorist = ""
letterer = ""
cover_artist = ""
editor = ""
translator = ""
publisher = ""
imprint = ""
characters = ""
teams = ""
locations = ""
main_character_or_team = ""
genre = ""
age_rating = ""
series_sort = ""
series_group = ""
alternate_series = ""
story_arc = ""
story_arc_number = ""
alternate_count = ""
alternate_number = ""
title = ""
summary = ""
review = ""
tags = ""
web = ""
number = ""
volume = ""
format = ""
manga = ""
year = ""
month = ""
day = ""
language_iso = ""
notes = ""
community_rating = ""
black_and_white = ""
page_count = ""
scan_information = ""
other = ""
gtin = ""
def __init__(self):
pass
def set_by_tag_name(self, tag, value):
for key, v in comic_info_tag_map.items():
if tag == v:
if value is None:
value = ""
self.__setattr__(key, value)
def get_by_tag_name(self, name) -> str:
for key, value in comic_info_tag_map.items():
if name == value:
ret = getattr(self, key)
if ret is None:
return ""
return ret
return ""
@classmethod
def from_xml(cls, xml_string):
root = ET.ElementTree(ET.fromstring(xml_string.encode("utf-8"), parser=ET.XMLParser(encoding='utf-8')))
comic_info = cls()
for prop in [a for a in dir(comic_info) if not a.startswith('__') and not callable(getattr(comic_info, a))]:
comic_info.__setattr__(prop, root.findtext(comic_info_tag_map[prop]))
return comic_info
def to_xml(self):
root = ET.Element("ComicInfo")
for key, mapped_key in comic_info_tag_map.items():
value = str(self.get_by_tag_name(mapped_key))
if value:
ET.SubElement(root, mapped_key).text = value
# prevent creation of self-closing tags
for node in root.iter():
if node.text is None:
node.text = ""
f = BytesIO()
et = ET.ElementTree(root)
ET.indent(et)
et.write(f, encoding='utf-8', xml_declaration=True)
ret_xml = f.getvalue()
return str(ret_xml, encoding="utf-8")
# print(f.getvalue()) # your XML file, encoded as UTF-8
# output_xml = ET.tostring(root, encoding="UTF-8", xml_declaration=True, method='xml').decode("utf8")
# return output_xml
"""Returns TRUE if it has changes"""
def has_changes(self, other):
for key in comic_info_tag_map.keys():
if getattr(self, key) != getattr(other, key):
return True
return False
================================================
FILE: MangaManager/common/models/ComicInfo.xds
================================================
<?xml version="1.0" encoding="utf-8"?>
<xs:schema elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="ComicInfo" nillable="true" type="ComicInfo" />
<xs:complexType name="ComicInfo">
<xs:sequence>
<xs:element minOccurs="0" maxOccurs="1" default="" name="Title" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Series" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="LocalizedSeries" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="SeriesSort" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Number" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="-1" name="Count" type="xs:int" />
<xs:element minOccurs="0" maxOccurs="1" default="-1" name="Volume" type="xs:int" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="AlternateSeries" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="AlternateNumber" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="-1" name="AlternateCount" type="xs:int" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Summary" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Notes" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="-1" name="Year" type="xs:int" />
<xs:element minOccurs="0" maxOccurs="1" default="-1" name="Month" type="xs:int" />
<xs:element minOccurs="0" maxOccurs="1" default="-1" name="Day" type="xs:int" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Writer" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Penciller" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Inker" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Colorist" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Letterer" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="CoverArtist" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Editor" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Translator" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Publisher" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Imprint" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Genre" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Tags" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Web" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="0" name="PageCount" type="xs:int" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="LanguageISO" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Format" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="Unknown" name="BlackAndWhite" type="YesNo" />
<xs:element minOccurs="0" maxOccurs="1" default="Unknown" name="Manga" type="Manga" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Characters" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Teams" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Locations" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="ScanInformation" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="StoryArc" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="StoryArcNumber" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="SeriesGroup" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="Unknown" name="AgeRating" type="AgeRating" />
<xs:element minOccurs="0" maxOccurs="1" name="Pages" type="ArrayOfComicPageInfo" />
<xs:element minOccurs="0" maxOccurs="1" name="CommunityRating" type="Rating" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Other" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="MainCharacterOrTeam" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Review" type="xs:string" />
</xs:sequence>
</xs:complexType>
<xs:simpleType name="YesNo">
<xs:restriction base="xs:string">
<xs:enumeration value="Unknown" />
<xs:enumeration value="No" />
<xs:enumeration value="Yes" />
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="Manga">
<xs:restriction base="xs:string">
<xs:enumeration value="Unknown" />
<xs:enumeration value="No" />
<xs:enumeration value="Yes" />
<xs:enumeration value="YesAndRightToLeft" />
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="Rating">
<xs:restriction base="xs:decimal">
<xs:minInclusive value="0"/>
<xs:maxInclusive value="5"/>
<xs:fractionDigits value="1"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="AgeRating">
<xs:restriction base="xs:string">
<xs:enumeration value="Unknown" />
<xs:enumeration value="Adults Only 18+" />
<xs:enumeration value="Early Childhood" />
<xs:enumeration value="Everyone" />
<xs:enumeration value="Everyone 10+" />
<xs:enumeration value="G" />
<xs:enumeration value="Kids to Adults" />
<xs:enumeration value="M" />
<xs:enumeration value="MA15+" />
<xs:enumeration value="Mature 17+" />
<xs:enumeration value="PG" />
<xs:enumeration value="R18+" />
<xs:enumeration value="Rating Pending" />
<xs:enumeration value="Teen" />
<xs:enumeration value="X18+" />
</xs:restriction>
</xs:simpleType>
<xs:complexType name="ArrayOfComicPageInfo">
<xs:sequence>
<xs:element minOccurs="0" maxOccurs="unbounded" name="Page" nillable="true" type="ComicPageInfo" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="ComicPageInfo">
<xs:attribute name="Image" type="xs:int" use="required" />
<xs:attribute default="Story" name="Type" type="ComicPageType" />
<xs:attribute default="false" name="DoublePage" type="xs:boolean" />
<xs:attribute default="0" name="ImageSize" type="xs:long" />
<xs:attribute default="" name="Key" type="xs:string" />
<xs:attribute default="" name="Bookmark" type="xs:string" />
<xs:attribute default="-1" name="ImageWidth" type="xs:int" />
<xs:attribute default="-1" name="ImageHeight" type="xs:int" />
</xs:complexType>
<xs:simpleType name="ComicPageType">
<xs:list>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:enumeration value="FrontCover" />
<xs:enumeration value="InnerCover" />
<xs:enumeration value="Roundup" />
<xs:enumeration value="Story" />
<xs:enumeration value="Advertisement" />
<xs:enumeration value="Editorial" />
<xs:enumeration value="Letters" />
<xs:enumeration value="Preview" />
<xs:enumeration value="BackCover" />
<xs:enumeration value="Other" />
<xs:enumeration value="Deleted" />
</xs:restriction>
</xs:simpleType>
</xs:list>
</xs:simpleType>
</xs:schema>
================================================
FILE: MangaManager/common/models/ComicInfoTag.py
================================================
================================================
FILE: MangaManager/common/models/ComicPageType.py
================================================
from enum import Enum
class ComicPageType(str, Enum):
FRONT_COVER = 'FrontCover'
INNER_COVER = 'InnerCover'
ROUNDUP = 'Roundup'
STORY = 'Story'
ADVERTISMENT = 'Advertisment'
EDITORIAL = 'Editorial'
LETTERS = 'Letters'
PREVIEW = 'Preview'
BACK_COVER = 'BackCover'
OTHER = 'Other'
DELETED = 'Deleted'
@classmethod
def list(cls): # pragma: no cover
return list(map(lambda c: c.value, cls))
================================================
FILE: MangaManager/common/models/Manga.py
================================================
from enum import Enum
class Manga(str, Enum):
UNKNOWN = 'Unknown'
NO = 'No'
YES = 'Yes'
YES_AND_RIGHT_TO_LEFT = 'YesAndRightToLeft'
@classmethod
def list(cls): # pragma: no cover
return list(map(lambda c: c.value, cls))
================================================
FILE: MangaManager/common/models/YesNo.py
================================================
from enum import Enum
class YesNo(str, Enum):
UNKNOWN = 'Unknown'
NO = 'No'
YES = 'Yes'
@classmethod
def list(cls): # pragma: no cover
return list(map(lambda c: c.value, cls))
================================================
FILE: MangaManager/common/models/__init__.py
================================================
from .AgeRating import AgeRating
from .ComicPageType import ComicPageType
from .Manga import Manga
from .YesNo import YesNo
from .ComicInfo import ComicInfo
Formats = (
"", "Special", "Reference", "Director's Cut", "Box Set", "Annual", "Anthology", "Epilogue", "One-Shot", "Prologue",
"TPB", "Trade Paper Back", "Omnibus", "Compendium", "Absolute", "Graphic Novel", "GN", "FCB"
)
PeopleTags = (
"Writer", "Penciller", "Inker", "Letterer", "CoverArtist", "Editor", "Translator",
"Publisher", "Imprint", "Other",
)
================================================
FILE: MangaManager/logging_setup.py
================================================
import logging
import sys
from logging.handlers import RotatingFileHandler
def trace(self, message, *args, **kws):
# Yes, logger takes its '*args' as 'args'.
self._log(logging.TRACE, message, args, **kws)
def add_trace_level():
logging.TRACE = 9
logging.addLevelName(logging.TRACE, "TRACE")
logging.Logger.trace = trace
class UmpumpedLogHandler(logging.Handler):
def emit(self, record):
logging.umpumped_events.append(record)
ei = record.exc_info
def setup_logging(LOGFILE_PATH,level=logging.DEBUG):
# Create our own implementation to have trace logging
# Setup Logger
logging.umpumped_events = []
umpumped_handler = logging.umpumped_handler = UmpumpedLogHandler(logging.INFO)
logging.getLogger('PIL').setLevel(logging.WARNING)
rotating_file_handler = RotatingFileHandler(LOGFILE_PATH, maxBytes=10_000_000,
backupCount=2)
rotating_file_handler.setLevel(level)
stream_handler = logging.StreamHandler(sys.stdout)
stream_handler.setLevel(level)
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s - %(name)20s - %(levelname)8s - %(message)s',
handlers=[stream_handler, rotating_file_handler, umpumped_handler]
# filename='/tmp/myapp.log'
)
logger = logging.getLogger()
logger.debug('DEBUG LEVEL - MAIN MODULE')
logger.info('INFO LEVEL - MAIN MODULE')
logger.trace('TRACE LEVEL - MAIN MODULE')
================================================
FILE: MangaManager/main.py
================================================
import argparse
import enum
import glob
import logging
from pathlib import Path
from logging_setup import add_trace_level, setup_logging
add_trace_level()
parser = argparse.ArgumentParser()
############
# Logging arguments
###########
parser.add_argument(
'--debug',
help="Print lots of debugging statements",
action="store_const", dest="loglevel", const=logging.DEBUG,
default=logging.INFO)
parser.add_argument(
'--trace',
help="Prints INSANE ammount of debug statements",
action="store_const", dest="loglevel", const=logging.TRACE,
)
parser.add_argument('-d', help="Debug Level", action="store", dest="selected_files_cli",
metavar="--cli <glob-like-path>", required=False, default=False)
parser.add_argument('--cli', help="Metadata Editor in CLI mode", action="store", dest="selected_files_cli",
metavar="--cli <glob-like-path>", required=False, default=False)
parser.add_argument('--webp', help="Webp converter in CLI mode", action="store", dest="selected_files_webp",
metavar="--webp <glob-like-path>", required=False, default=False)
args = parser.parse_args()
# Setup logger
mm_path = Path(Path.home(), "MangaManager")
mm_path.mkdir(exist_ok=True, parents=True)
LOGS_PATH = Path(f"{mm_path}/logs/")
LOGS_PATH.mkdir(parents=True, exist_ok=True)
LOGFILE_PATH = Path(LOGS_PATH, "MangaManager.log")
setup_logging(LOGFILE_PATH, args.loglevel)
logger = logging.getLogger()
from src.Settings.Settings import Settings
# Create initial ini with defaults else load existing
Settings().load()
from src.Common.errors import NoFilesSelected
from src.MetadataManager.MetadataManagerCLI import App as CLIMetadataApp
from src.__version__ import __version__ as version
# <Arguments parser>
class ToolS(enum.Enum):
NONE = 0
METADATA = 1
WEBP = 5
@classmethod
def list(cls):
return list(map(lambda c: c.name, cls))
def get_selected_files(glob_path) -> list[str]:
file_paths = glob.glob(glob_path)
if not file_paths:
raise NoFilesSelected()
return file_paths
if __name__ == '__main__':
if args.selected_files_cli:
logger.info(f"Starting: CLI Metadata app")
selected_files = get_selected_files(args.selected_files_cli)
app = CLIMetadataApp(selected_files)
elif args.selected_files_webp:
logger.info(f"Starting: CLI Webp converter app")
# app = glob.glob(args.selected_files))
selected_files = get_selected_files(args.selected_files_cli)
else:
logger.info(f"Starting: GUI Manga Manager v{version}. Welcome")
from src.MetadataManager import execute_gui
execute_gui()
================================================
FILE: MangaManager/pyinstaller_hooks/hook-tkinterdnd2.py
================================================
from PyInstaller.utils.hooks import collect_data_files
datas = collect_data_files('tkinterdnd2')
================================================
FILE: MangaManager/res/languages.json
================================================
[{
"isoCode": "aa",
"title": "Afar"
}, {
"isoCode": "aa-DJ",
"title": "Afar (Djibouti)"
}, {
"isoCode": "aa-ER",
"title": "Afar (Eritrea)"
}, {
"isoCode": "aa-ET",
"title": "Afar (Ethiopia)"
}, {
"isoCode": "af",
"title": "Afrikaans"
}, {
"isoCode": "af-NA",
"title": "Afrikaans (Namibia)"
}, {
"isoCode": "af-ZA",
"title": "Afrikaans (South Africa)"
}, {
"isoCode": "agq",
"title": "Aghem"
}, {
"isoCode": "agq-CM",
"title": "Aghem (Cameroon)"
}, {
"isoCode": "ak",
"title": "Akan"
}, {
"isoCode": "ak-GH",
"title": "Akan (Ghana)"
}, {
"isoCode": "am",
"title": "Amharic"
}, {
"isoCode": "am-ET",
"title": "Amharic (Ethiopia)"
}, {
"isoCode": "ar",
"title": "Arabic"
}, {
"isoCode": "ar-001",
"title": "Arabic (World)"
}, {
"isoCode": "ar-AE",
"title": "Arabic (United Arab Emirates)"
}, {
"isoCode": "ar-BH",
"title": "Arabic (Bahrain)"
}, {
"isoCode": "ar-DJ",
"title": "Arabic (Djibouti)"
}, {
"isoCode": "ar-DZ",
"title": "Arabic (Algeria)"
}, {
"isoCode": "ar-EG",
"title": "Arabic (Egypt)"
}, {
"isoCode": "ar-ER",
"title": "Arabic (Eritrea)"
}, {
"isoCode": "ar-IL",
"title": "Arabic (Israel)"
}, {
"isoCode": "ar-IQ",
"title": "Arabic (Iraq)"
}, {
"isoCode": "ar-JO",
"title": "Arabic (Jordan)"
}, {
"isoCode": "ar-KM",
"title": "Arabic (Comoros)"
}, {
"isoCode": "ar-KW",
"title": "Arabic (Kuwait)"
}, {
"isoCode": "ar-LB",
"title": "Arabic (Lebanon)"
}, {
"isoCode": "ar-LY",
"title": "Arabic (Libya)"
}, {
"isoCode": "ar-MA",
"title": "Arabic (Morocco)"
}, {
"isoCode": "ar-MR",
"title": "Arabic (Mauritania)"
}, {
"isoCode": "ar-OM",
"title": "Arabic (Oman)"
}, {
"isoCode": "ar-PS",
"title": "Arabic (Palestinian Authority)"
}, {
"isoCode": "ar-QA",
"title": "Arabic (Qatar)"
}, {
"isoCode": "ar-SA",
"title": "Arabic (Saudi Arabia)"
}, {
"isoCode": "ar-SD",
"title": "Arabic (Sudan)"
}, {
"isoCode": "ar-SO",
"title": "Arabic (Somalia)"
}, {
"isoCode": "ar-SS",
"title": "Arabic (South Sudan)"
}, {
"isoCode": "ar-SY",
"title": "Arabic (Syria)"
}, {
"isoCode": "ar-TD",
"title": "Arabic (Chad)"
}, {
"isoCode": "ar-TN",
"title": "Arabic (Tunisia)"
}, {
"isoCode": "ar-YE",
"title": "Arabic (Yemen)"
}, {
"isoCode": "arn",
"title": "Mapuche"
}, {
"isoCode": "arn-CL",
"title": "Mapuche (Chile)"
}, {
"isoCode": "as",
"title": "Assamese"
}, {
"isoCode": "as-IN",
"title": "Assamese (India)"
}, {
"isoCode": "asa",
"title": "Asu"
}, {
"isoCode": "asa-TZ",
"title": "Asu (Tanzania)"
}, {
"isoCode": "ast",
"title": "Asturian"
}, {
"isoCode": "ast-ES",
"title": "Asturian (Spain)"
}, {
"isoCode": "az",
"title": "Azerbaijani"
}, {
"isoCode": "az-Cyrl",
"title": "Azerbaijani (Cyrillic)"
}, {
"isoCode": "az-Cyrl-AZ",
"title": "Azerbaijani (Cyrillic, Azerbaijan)"
}, {
"isoCode": "az-Latn",
"title": "Azerbaijani (Latin)"
}, {
"isoCode": "az-Latn-AZ",
"title": "Azerbaijani (Latin, Azerbaijan)"
}, {
"isoCode": "ba",
"title": "Bashkir"
}, {
"isoCode": "ba-RU",
"title": "Bashkir (Russia)"
}, {
"isoCode": "bas",
"title": "Basaa"
}, {
"isoCode": "bas-CM",
"title": "Basaa (Cameroon)"
}, {
"isoCode": "be",
"title": "Belarusian"
}, {
"isoCode": "be-BY",
"title": "Belarusian (Belarus)"
}, {
"isoCode": "bem",
"title": "Bemba"
}, {
"isoCode": "bem-ZM",
"title": "Bemba (Zambia)"
}, {
"isoCode": "bez",
"title": "Bena"
}, {
"isoCode": "bez-TZ",
"title": "Bena (Tanzania)"
}, {
"isoCode": "bg",
"title": "Bulgarian"
}, {
"isoCode": "bg-BG",
"title": "Bulgarian (Bulgaria)"
}, {
"isoCode": "bm",
"title": "Bamanankan"
}, {
"isoCode": "bm-ML",
"title": "Bamanankan (Mali)"
}, {
"isoCode": "bn",
"title": "Bangla"
}, {
"isoCode": "bn-BD",
"title": "Bangla (Bangladesh)"
}, {
"isoCode": "bn-IN",
"title": "Bangla (India)"
}, {
"isoCode": "bo",
"title": "Tibetan"
}, {
"isoCode": "bo-CN",
"title": "Tibetan (China)"
}, {
"isoCode": "bo-IN",
"title": "Tibetan (India)"
}, {
"isoCode": "br",
"title": "Breton"
}, {
"isoCode": "br-FR",
"title": "Breton (France)"
}, {
"isoCode": "brx",
"title": "Bodo"
}, {
"isoCode": "brx-IN",
"title": "Bodo (India)"
}, {
"isoCode": "bs",
"title": "Bosnian"
}, {
"isoCode": "bs-Cyrl",
"title": "Bosnian (Cyrillic)"
}, {
"isoCode": "bs-Cyrl-BA",
"title": "Bosnian (Cyrillic, Bosnia & Herzegovina)"
}, {
"isoCode": "bs-Latn",
"title": "Bosnian (Latin)"
}, {
"isoCode": "bs-Latn-BA",
"title": "Bosnian (Latin, Bosnia & Herzegovina)"
}, {
"isoCode": "byn",
"title": "Blin"
}, {
"isoCode": "byn-ER",
"title": "Blin (Eritrea)"
}, {
"isoCode": "ca",
"title": "Catalan"
}, {
"isoCode": "ca-AD",
"title": "Catalan (Andorra)"
}, {
"isoCode": "ca-ES",
"title": "Catalan (Spain)"
}, {
"isoCode": "ca-ES-VALENCIA",
"title": "Catalan (Spain, Valencian)"
}, {
"isoCode": "ca-FR",
"title": "Catalan (France)"
}, {
"isoCode": "ca-IT",
"title": "Catalan (Italy)"
}, {
"isoCode": "ccp",
"title": "Chakma"
}, {
"isoCode": "ccp-BD",
"title": "Chakma (Bangladesh)"
}, {
"isoCode": "ccp-IN",
"title": "Chakma (India)"
}, {
"isoCode": "ce",
"title": "Chechen"
}, {
"isoCode": "ce-RU",
"title": "Chechen (Russia)"
}, {
"isoCode": "ceb",
"title": "Cebuano"
}, {
"isoCode": "ceb-PH",
"title": "Cebuano (Philippines)"
}, {
"isoCode": "cgg",
"title": "Chiga"
}, {
"isoCode": "cgg-UG",
"title": "Chiga (Uganda)"
}, {
"isoCode": "chr",
"title": "Cherokee"
}, {
"isoCode": "chr-US",
"title": "Cherokee (United States)"
}, {
"isoCode": "ckb",
"title": "Central Kurdish"
}, {
"isoCode": "ckb-IQ",
"title": "Central Kurdish (Iraq)"
}, {
"isoCode": "ckb-IR",
"title": "Central Kurdish (Iran)"
}, {
"isoCode": "co",
"title": "Corsican"
}, {
"isoCode": "co-FR",
"title": "Corsican (France)"
}, {
"isoCode": "cs",
"title": "Czech"
}, {
"isoCode": "cs-CZ",
"title": "Czech (Czechia)"
}, {
"isoCode": "cu",
"title": "Church Slavic"
}, {
"isoCode": "cu-RU",
"title": "Church Slavic (Russia)"
}, {
"isoCode": "cy",
"title": "Welsh"
}, {
"isoCode": "cy-GB",
"title": "Welsh (United Kingdom)"
}, {
"isoCode": "da",
"title": "Danish"
}, {
"isoCode": "da-DK",
"title": "Danish (Denmark)"
}, {
"isoCode": "da-GL",
"title": "Danish (Greenland)"
}, {
"isoCode": "dav",
"title": "Taita"
}, {
"isoCode": "dav-KE",
"title": "Taita (Kenya)"
}, {
"isoCode": "de",
"title": "German"
}, {
"isoCode": "de-AT",
"title": "German (Austria)"
}, {
"isoCode": "de-BE",
"title": "German (Belgium)"
}, {
"isoCode": "de-CH",
"title": "German (Switzerland)"
}, {
"isoCode": "de-DE",
"title": "German (Germany)"
}, {
"isoCode": "de-IT",
"title": "German (Italy)"
}, {
"isoCode": "de-LI",
"title": "German (Liechtenstein)"
}, {
"isoCode": "de-LU",
"title": "German (Luxembourg)"
}, {
"isoCode": "dje",
"title": "Zarma"
}, {
"isoCode": "dje-NE",
"title": "Zarma (Niger)"
}, {
"isoCode": "dsb",
"title": "Lower Sorbian"
}, {
"isoCode": "dsb-DE",
"title": "Lower Sorbian (Germany)"
}, {
"isoCode": "dua",
"title": "Duala"
}, {
"isoCode": "dua-CM",
"title": "Duala (Cameroon)"
}, {
"isoCode": "dv",
"title": "Divehi"
}, {
"isoCode": "dv-MV",
"title": "Divehi (Maldives)"
}, {
"isoCode": "dyo",
"title": "Jola-Fonyi"
}, {
"isoCode": "dyo-SN",
"title": "Jola-Fonyi (Senegal)"
}, {
"isoCode": "dz",
"title": "Dzongkha"
}, {
"isoCode": "dz-BT",
"title": "Dzongkha (Bhutan)"
}, {
"isoCode": "ebu",
"title": "Embu"
}, {
"isoCode": "ebu-KE",
"title": "Embu (Kenya)"
}, {
"isoCode": "ee",
"title": "Ewe"
}, {
"isoCode": "ee-GH",
"title": "Ewe (Ghana)"
}, {
"isoCode": "ee-TG",
"title": "Ewe (Togo)"
}, {
"isoCode": "el",
"title": "Greek"
}, {
"isoCode": "el-CY",
"title": "Greek (Cyprus)"
}, {
"isoCode": "el-GR",
"title": "Greek (Greece)"
}, {
"isoCode": "en",
"title": "English"
}, {
"isoCode": "en-001",
"title": "English (World)"
}, {
"isoCode": "en-150",
"title": "English (Europe)"
}, {
"isoCode": "en-AE",
"title": "English (United Arab Emirates)"
}, {
"isoCode": "en-AG",
"title": "English (Antigua & Barbuda)"
}, {
"isoCode": "en-AI",
"title": "English (Anguilla)"
}, {
"isoCode": "en-AS",
"title": "English (American Samoa)"
}, {
"isoCode": "en-AT",
"title": "English (Austria)"
}, {
"isoCode": "en-AU",
"title": "English (Australia)"
}, {
"isoCode": "en-BB",
"title": "English (Barbados)"
}, {
"isoCode": "en-BE",
"title": "English (Belgium)"
}, {
"isoCode": "en-BI",
"title": "English (Burundi)"
}, {
"isoCode": "en-BM",
"title": "English (Bermuda)"
}, {
"isoCode": "en-BS",
"title": "English (Bahamas)"
}, {
"isoCode": "en-BW",
"title": "English (Botswana)"
}, {
"isoCode": "en-BZ",
"title": "English (Belize)"
}, {
"isoCode": "en-CA",
"title": "English (Canada)"
}, {
"isoCode": "en-CC",
"title": "English (Cocos [Keeling] Islands)"
}, {
"isoCode": "en-CH",
"title": "English (Switzerland)"
}, {
"isoCode": "en-CK",
"title": "English (Cook Islands)"
}, {
"isoCode": "en-CM",
"title": "English (Cameroon)"
}, {
"isoCode": "en-CX",
"title": "English (Christmas Island)"
}, {
"isoCode": "en-CY",
"title": "English (Cyprus)"
}, {
"isoCode": "en-DE",
"title": "English (Germany)"
}, {
"isoCode": "en-DK",
"title": "English (Denmark)"
}, {
"isoCode": "en-DM",
"title": "English (Dominica)"
}, {
"isoCode": "en-ER",
"title": "English (Eritrea)"
}, {
"isoCode": "en-FI",
"title": "English (Finland)"
}, {
"isoCode": "en-FJ",
"title": "English (Fiji)"
}, {
"isoCode": "en-FK",
"title": "English (Falkland Islands)"
}, {
"isoCode": "en-FM",
"title": "English (Micronesia)"
}, {
"isoCode": "en-GB",
"title": "English (United Kingdom)"
}, {
"isoCode": "en-GD",
"title": "English (Grenada)"
}, {
"isoCode": "en-GG",
"title": "English (Guernsey)"
}, {
"isoCode": "en-GH",
"title": "English (Ghana)"
}, {
"isoCode": "en-GI",
"title": "English (Gibraltar)"
}, {
"isoCode": "en-GM",
"title": "English (Gambia)"
}, {
"isoCode": "en-GU",
"title": "English (Guam)"
}, {
"isoCode": "en-GY",
"title": "English (Guyana)"
}, {
"isoCode": "en-HK",
"title": "English (Hong Kong SAR)"
}, {
"isoCode": "en-IE",
"title": "English (Ireland)"
}, {
"isoCode": "en-IL",
"title": "English (Israel)"
}, {
"isoCode": "en-IM",
"title": "English (Isle of Man)"
}, {
"isoCode": "en-IN",
"title": "English (India)"
}, {
"isoCode": "en-IO",
"title": "English (British Indian Ocean Territory)"
}, {
"isoCode": "en-JE",
"title": "English (Jersey)"
}, {
"isoCode": "en-JM",
"title": "English (Jamaica)"
}, {
"isoCode": "en-KE",
"title": "English (Kenya)"
}, {
"isoCode": "en-KI",
"title": "English (Kiribati)"
}, {
"isoCode": "en-KN",
"title": "English (St. Kitts & Nevis)"
}, {
"isoCode": "en-KY",
"title": "English (Cayman Islands)"
}, {
"isoCode": "en-LC",
"title": "English (St. Lucia)"
}, {
"isoCode": "en-LR",
"title": "English (Liberia)"
}, {
"isoCode": "en-LS",
"title": "English (Lesotho)"
}, {
"isoCode": "en-MG",
"title": "English (Madagascar)"
}, {
"isoCode": "en-MH",
"title": "English (Marshall Islands)"
}, {
"isoCode": "en-MO",
"title": "English (Macao SAR)"
}, {
"isoCode": "en-MP",
"title": "English (Northern Mariana Islands)"
}, {
"isoCode": "en-MS",
"title": "English (Montserrat)"
}, {
"isoCode": "en-MT",
"title": "English (Malta)"
}, {
"isoCode": "en-MU",
"title": "English (Mauritius)"
}, {
"isoCode": "en-MW",
"title": "English (Malawi)"
}, {
"isoCode": "en-MY",
"title": "English (Malaysia)"
}, {
"isoCode": "en-NA",
"title": "English (Namibia)"
}, {
"isoCode": "en-NF",
"title": "English (Norfolk Island)"
}, {
"isoCode": "en-NG",
"title": "English (Nigeria)"
}, {
"isoCode": "en-NL",
"title": "English (Netherlands)"
}, {
"isoCode": "en-NR",
"title": "English (Nauru)"
}, {
"isoCode": "en-NU",
"title": "English (Niue)"
}, {
"isoCode": "en-NZ",
"title": "English (New Zealand)"
}, {
"isoCode": "en-PG",
"title": "English (Papua New Guinea)"
}, {
"isoCode": "en-PH",
"title": "English (Philippines)"
}, {
"isoCode": "en-PK",
"title": "English (Pakistan)"
}, {
"isoCode": "en-PN",
"title": "English (Pitcairn Islands)"
}, {
"isoCode": "en-PR",
"title": "English (Puerto Rico)"
}, {
"isoCode": "en-PW",
"title": "English (Palau)"
}, {
"isoCode": "en-RW",
"title": "English (Rwanda)"
}, {
"isoCode": "en-SB",
"title": "English (Solomon Islands)"
}, {
"isoCode": "en-SC",
"title": "English (Seychelles)"
}, {
"isoCode": "en-SD",
"title": "English (Sudan)"
}, {
"isoCode": "en-SE",
"title": "English (Sweden)"
}, {
"isoCode": "en-SG",
"title": "English (Singapore)"
}, {
"isoCode": "en-SH",
"title": "English (St Helena, Ascension, Tristan da Cunha)"
}, {
"isoCode": "en-SI",
"title": "English (Slovenia)"
}, {
"isoCode": "en-SL",
"title": "English (Sierra Leone)"
}, {
"isoCode": "en-SS",
"title": "English (South Sudan)"
}, {
"isoCode": "en-SX",
"title": "English (Sint Maarten)"
}, {
"isoCode": "en-SZ",
"title": "English (Eswatini)"
}, {
"isoCode": "en-TC",
"title": "English (Turks & Caicos Islands)"
}, {
"isoCode": "en-TK",
"title": "English (Tokelau)"
}, {
"isoCode": "en-TO",
"title": "English (Tonga)"
}, {
"isoCode": "en-TT",
"title": "English (Trinidad & Tobago)"
}, {
"isoCode": "en-TV",
"title": "English (Tuvalu)"
}, {
"isoCode": "en-TZ",
"title": "English (Tanzania)"
}, {
"isoCode": "en-UG",
"title": "English (Uganda)"
}, {
"isoCode": "en-UM",
"title": "English (U.S. Outlying Islands)"
}, {
"isoCode": "en-US",
"title": "English (United States)"
}, {
"isoCode": "en-US-POSIX",
"title": "English (United States, Computer)"
}, {
"isoCode": "en-VC",
"title": "English (St. Vincent & Grenadines)"
}, {
"isoCode": "en-VG",
"title": "English (British Virgin Islands)"
}, {
"isoCode": "en-VI",
"title": "English (U.S. Virgin Islands)"
}, {
"isoCode": "en-VU",
"title": "English (Vanuatu)"
}, {
"isoCode": "en-WS",
"title": "English (Samoa)"
}, {
"isoCode": "en-ZA",
"title": "English (South Africa)"
}, {
"isoCode": "en-ZM",
"title": "English (Zambia)"
}, {
"isoCode": "en-ZW",
"title": "English (Zimbabwe)"
}, {
"isoCode": "eo",
"title": "Esperanto"
}, {
"isoCode": "eo-001",
"title": "Esperanto (World)"
}, {
"isoCode": "es",
"title": "Spanish"
}, {
"isoCode": "es-419",
"title": "Spanish (Latin America)"
}, {
"isoCode": "es-AR",
"title": "Spanish (Argentina)"
}, {
"isoCode": "es-BO",
"title": "Spanish (Bolivia)"
}, {
"isoCode": "es-BR",
"title": "Spanish (Brazil)"
}, {
"isoCode": "es-BZ",
"title": "Spanish (Belize)"
}, {
"isoCode": "es-CL",
"title": "Spanish (Chile)"
}, {
"isoCode": "es-CO",
"title": "Spanish (Colombia)"
}, {
"isoCode": "es-CR",
"title": "Spanish (Costa Rica)"
}, {
"isoCode": "es-CU",
"title": "Spanish (Cuba)"
}, {
"isoCode": "es-DO",
"title": "Spanish (Dominican Republic)"
}, {
"isoCode": "es-EC",
"title": "Spanish (Ecuador)"
}, {
"isoCode": "es-ES",
"title": "Spanish (Spain)"
}, {
"isoCode": "es-GQ",
"title": "Spanish (Equatorial Guinea)"
}, {
"isoCode": "es-GT",
"title": "Spanish (Guatemala)"
}, {
"isoCode": "es-HN",
"title": "Spanish (Honduras)"
}, {
"isoCode": "es-MX",
"title": "Spanish (Mexico)"
}, {
"isoCode": "es-NI",
"title": "Spanish (Nicaragua)"
}, {
"isoCode": "es-PA",
"title": "Spanish (Panama)"
}, {
"isoCode": "es-PE",
"title": "Spanish (Peru)"
}, {
"isoCode": "es-PH",
"title": "Spanish (Philippines)"
}, {
"isoCode": "es-PR",
"title": "Spanish (Puerto Rico)"
}, {
"isoCode": "es-PY",
"title": "Spanish (Paraguay)"
}, {
"isoCode": "es-SV",
"title": "Spanish (El Salvador)"
}, {
"isoCode": "es-US",
"title": "Spanish (United States)"
}, {
"isoCode": "es-UY",
"title": "Spanish (Uruguay)"
}, {
"isoCode": "es-VE",
"title": "Spanish (Venezuela)"
}, {
"isoCode": "et",
"title": "Estonian"
}, {
"isoCode": "et-EE",
"title": "Estonian (Estonia)"
}, {
"isoCode": "eu",
"title": "Basque"
}, {
"isoCode": "eu-ES",
"title": "Basque (Spain)"
}, {
"isoCode": "ewo",
"title": "Ewondo"
}, {
"isoCode": "ewo-CM",
"title": "Ewondo (Cameroon)"
}, {
"isoCode": "fa",
"title": "Persian"
}, {
"isoCode": "fa-AF",
"title": "Persian (Afghanistan)"
}, {
"isoCode": "fa-IR",
"title": "Persian (Iran)"
}, {
"isoCode": "ff",
"title": "Fulah"
}, {
"isoCode": "ff-Latn",
"title": "Fulah (Latin)"
}, {
"isoCode": "ff-Latn-BF",
"title": "Fulah (Latin, Burkina Faso)"
}, {
"isoCode": "ff-Latn-CM",
"title": "Fulah (Latin, Cameroon)"
}, {
"isoCode": "ff-Latn-GH",
"title": "Fulah (Latin, Ghana)"
}, {
"isoCode": "ff-Latn-GM",
"title": "Fulah (Latin, Gambia)"
}, {
"isoCode": "ff-Latn-GN",
"title": "Fulah (Latin, Guinea)"
}, {
"isoCode": "ff-Latn-GW",
"title": "Fulah (Latin, Guinea-Bissau)"
}, {
"isoCode": "ff-Latn-LR",
"title": "Fulah (Latin, Liberia)"
}, {
"isoCode": "ff-Latn-MR",
"title": "Fulah (Latin, Mauritania)"
}, {
"isoCode": "ff-Latn-NE",
"title": "Fulah (Latin, Niger)"
}, {
"isoCode": "ff-Latn-NG",
"title": "Fulah (Latin, Nigeria)"
}, {
"isoCode": "ff-Latn-SL",
"title": "Fulah (Latin, Sierra Leone)"
}, {
"isoCode": "ff-Latn-SN",
"title": "Fulah (Latin, Senegal)"
}, {
"isoCode": "fi",
"title": "Finnish"
}, {
"isoCode": "fi-FI",
"title": "Finnish (Finland)"
}, {
"isoCode": "fil",
"title": "Filipino"
}, {
"isoCode": "fil-PH",
"title": "Filipino (Philippines)"
}, {
"isoCode": "fo",
"title": "Faroese"
}, {
"isoCode": "fo-DK",
"title": "Faroese (Denmark)"
}, {
"isoCode": "fo-FO",
"title": "Faroese (Faroe Islands)"
}, {
"isoCode": "fr",
"title": "French"
}, {
"isoCode": "fr-BE",
"title": "French (Belgium)"
}, {
"isoCode": "fr-BF",
"title": "French (Burkina Faso)"
}, {
"isoCode": "fr-BI",
"title": "French (Burundi)"
}, {
"isoCode": "fr-BJ",
"title": "French (Benin)"
}, {
"isoCode": "fr-BL",
"title": "French (St. Barthélemy)"
}, {
"isoCode": "fr-CA",
"title": "French (Canada)"
}, {
"isoCode": "fr-CD",
"title": "French (Congo [DRC])"
}, {
"isoCode": "fr-CF",
"title": "French (Central African Republic)"
}, {
"isoCode": "fr-CG",
"title": "French (Congo)"
}, {
"isoCode": "fr-CH",
"title": "French (Switzerland)"
}, {
"isoCode": "fr-CI",
"title": "French (Côte d’Ivoire)"
}, {
"isoCode": "fr-CM",
"title": "French (Cameroon)"
}, {
"isoCode": "fr-DJ",
"title": "French (Djibouti)"
}, {
"isoCode": "fr-DZ",
"title": "French (Algeria)"
}, {
"isoCode": "fr-FR",
"title": "French (France)"
}, {
"isoCode": "fr-GA",
"title": "French (Gabon)"
}, {
"isoCode": "fr-GF",
"title": "French (French Guiana)"
}, {
"isoCode": "fr-GN",
"title": "French (Guinea)"
}, {
"isoCode": "fr-GP",
"title": "French (Guadeloupe)"
}, {
"isoCode": "fr-GQ",
"title": "French (Equatorial Guinea)"
}, {
"isoCode": "fr-HT",
"title": "French (Haiti)"
}, {
"isoCode": "fr-KM",
"title": "French (Comoros)"
}, {
"isoCode": "fr-LU",
"title": "French (Luxembourg)"
}, {
"isoCode": "fr-MA",
"title": "French (Morocco)"
}, {
"isoCode": "fr-MC",
"title": "French (Monaco)"
}, {
"isoCode": "fr-MF",
"title": "French (St. Martin)"
}, {
"isoCode": "fr-MG",
"title": "French (Madagascar)"
}, {
"isoCode": "fr-ML",
"title": "French (Mali)"
}, {
"isoCode": "fr-MQ",
"title": "French (Martinique)"
}, {
"isoCode": "fr-MR",
"title": "French (Mauritania)"
}, {
"isoCode": "fr-MU",
"title": "French (Mauritius)"
}, {
"isoCode": "fr-NC",
"title": "French (New Caledonia)"
}, {
"isoCode": "fr-NE",
"title": "French (Niger)"
}, {
"isoCode": "fr-PF",
"title": "French (French Polynesia)"
}, {
"isoCode": "fr-PM",
"title": "French (St. Pierre & Miquelon)"
}, {
"isoCode": "fr-RE",
"title": "French (Réunion)"
}, {
"isoCode": "fr-RW",
"title": "French (Rwanda)"
}, {
"isoCode": "fr-SC",
"title": "French (Seychelles)"
}, {
"isoCode": "fr-SN",
"title": "French (Senegal)"
}, {
"isoCode": "fr-SY",
"title": "French (Syria)"
}, {
"isoCode": "fr-TD",
"title": "French (Chad)"
}, {
"isoCode": "fr-TG",
"title": "French (Togo)"
}, {
"isoCode": "fr-TN",
"title": "French (Tunisia)"
}, {
"isoCode": "fr-VU",
"title": "French (Vanuatu)"
}, {
"isoCode": "fr-WF",
"title": "French (Wallis & Futuna)"
}, {
"isoCode": "fr-YT",
"title": "French (Mayotte)"
}, {
"isoCode": "fur",
"title": "Friulian"
}, {
"isoCode": "fur-IT",
"title": "Friulian (Italy)"
}, {
"isoCode": "fy",
"title": "Western Frisian"
}, {
"isoCode": "fy-NL",
"title": "Western Frisian (Netherlands)"
}, {
"isoCode": "ga",
"title": "Irish"
}, {
"isoCode": "ga-IE",
"title": "Irish (Ireland)"
}, {
"isoCode": "gd",
"title": "Scottish Gaelic"
}, {
"isoCode": "gd-GB",
"title": "Scottish Gaelic (United Kingdom)"
}, {
"isoCode": "gl",
"title": "Galician"
}, {
"isoCode": "gl-ES",
"title": "Galician (Spain)"
}, {
"isoCode": "gn",
"title": "Guarani"
}, {
"isoCode": "gn-PY",
"title": "Guarani (Paraguay)"
}, {
"isoCode": "gsw",
"title": "Swiss German"
}, {
"isoCode": "gsw-CH",
"title": "Swiss German (Switzerland)"
}, {
"isoCode": "gsw-FR",
"title": "Swiss German (France)"
}, {
"isoCode": "gsw-LI",
"title": "Swiss German (Liechtenstein)"
}, {
"isoCode": "gu",
"title": "Gujarati"
}, {
"isoCode": "gu-IN",
"title": "Gujarati (India)"
}, {
"isoCode": "guz",
"title": "Gusii"
}, {
"isoCode": "guz-KE",
"title": "Gusii (Kenya)"
}, {
"isoCode": "gv",
"title": "Manx"
}, {
"isoCode": "gv-IM",
"title": "Manx (Isle of Man)"
}, {
"isoCode": "ha",
"title": "Hausa"
}, {
"isoCode": "ha-GH",
"title": "Hausa (Ghana)"
}, {
"isoCode": "ha-NE",
"title": "Hausa (Niger)"
}, {
"isoCode": "ha-NG",
"title": "Hausa (Nigeria)"
}, {
"isoCode": "haw",
"title": "Hawaiian"
}, {
"isoCode": "haw-US",
"title": "Hawaiian (United States)"
}, {
"isoCode": "he",
"title": "Hebrew"
}, {
"isoCode": "he-IL",
"title": "Hebrew (Israel)"
}, {
"isoCode": "hi",
"title": "Hindi"
}, {
"isoCode": "hi-IN",
"title": "Hindi (India)"
}, {
"isoCode": "hr",
"title": "Croatian"
}, {
"isoCode": "hr-BA",
"title": "Croatian (Bosnia & Herzegovina)"
}, {
"isoCode": "hr-HR",
"title": "Croatian (Croatia)"
}, {
"isoCode": "hsb",
"title": "Upper Sorbian"
}, {
"isoCode": "hsb-DE",
"title": "Upper Sorbian (Germany)"
}, {
"isoCode": "hu",
"title": "Hungarian"
}, {
"isoCode": "hu-HU",
"title": "Hungarian (Hungary)"
}, {
"isoCode": "hy",
"title": "Armenian"
}, {
"isoCode": "hy-AM",
"title": "Armenian (Armenia)"
}, {
"isoCode": "ia",
"title": "Interlingua"
}, {
"isoCode": "ia-001",
"title": "Interlingua (World)"
}, {
"isoCode": "id",
"title": "Indonesian"
}, {
"isoCode": "id-ID",
"title": "Indonesian (Indonesia)"
}, {
"isoCode": "ig",
"title": "Igbo"
}, {
"isoCode": "ig-NG",
"title": "Igbo (Nigeria)"
}, {
"isoCode": "ii",
"title": "Yi"
}, {
"isoCode": "ii-CN",
"title": "Yi (China)"
}, {
"isoCode": "is",
"title": "Icelandic"
}, {
"isoCode": "is-IS",
"title": "Icelandic (Iceland)"
}, {
"isoCode": "it",
"title": "Italian"
}, {
"isoCode": "it-CH",
"title": "Italian (Switzerland)"
}, {
"isoCode": "it-IT",
"title": "Italian (Italy)"
}, {
"isoCode": "it-SM",
"title": "Italian (San Marino)"
}, {
"isoCode": "it-VA",
"title": "Italian (Vatican City)"
}, {
"isoCode": "iu",
"title": "Inuktitut"
}, {
"isoCode": "iu-CA",
"title": "Inuktitut (Canada)"
}, {
"isoCode": "iu-Latn",
"title": "Inuktitut (Latin)"
}, {
"isoCode": "iu-Latn-CA",
"title": "Inuktitut (Latin, Canada)"
}, {
"isoCode": "ja",
"title": "Japanese"
}, {
"isoCode": "ja-JP",
"title": "Japanese (Japan)"
}, {
"isoCode": "jgo",
"title": "Ngomba"
}, {
"isoCode": "jgo-CM",
"title": "Ngomba (Cameroon)"
}, {
"isoCode": "jmc",
"title": "Machame"
}, {
"isoCode": "jmc-TZ",
"title": "Machame (Tanzania)"
}, {
"isoCode": "jv",
"title": "Javanese"
}, {
"isoCode": "jv-ID",
"title": "Javanese (Indonesia)"
}, {
"isoCode": "ka",
"title": "Georgian"
}, {
"isoCode": "ka-GE",
"title": "Georgian (Georgia)"
}, {
"isoCode": "kab",
"title": "Kabyle"
}, {
"isoCode": "kab-DZ",
"title": "Kabyle (Algeria)"
}, {
"isoCode": "kam",
"title": "Kamba"
}, {
"isoCode": "kam-KE",
"title": "Kamba (Kenya)"
}, {
"isoCode": "kde",
"title": "Makonde"
}, {
"isoCode": "kde-TZ",
"title": "Makonde (Tanzania)"
}, {
"isoCode": "kea",
"title": "Kabuverdianu"
}, {
"isoCode": "kea-CV",
"title": "Kabuverdianu (Cabo Verde)"
}, {
"isoCode": "khq",
"title": "Koyra Chiini"
}, {
"isoCode": "khq-ML",
"title": "Koyra Chiini (Mali)"
}, {
"isoCode": "ki",
"title": "Kikuyu"
}, {
"isoCode": "ki-KE",
"title": "Kikuyu (Kenya)"
}, {
"isoCode": "kk",
"title": "Kazakh"
}, {
"isoCode": "kk-KZ",
"title": "Kazakh (Kazakhstan)"
}, {
"isoCode": "kkj",
"title": "Kako"
}, {
"isoCode": "kkj-CM",
"title": "Kako (Cameroon)"
}, {
"isoCode": "kl",
"title": "Kalaallisut"
}, {
"isoCode": "kl-GL",
"title": "Kalaallisut (Greenland)"
}, {
"isoCode": "kln",
"title": "Kalenjin"
}, {
"isoCode": "kln-KE",
"title": "Kalenjin (Kenya)"
}, {
"isoCode": "km",
"title": "Khmer"
}, {
"isoCode": "km-KH",
"title": "Khmer (Cambodia)"
}, {
"isoCode": "kn",
"title": "Kannada"
}, {
"isoCode": "kn-IN",
"title": "Kannada (India)"
}, {
"isoCode": "ko",
"title": "Korean"
}, {
"isoCode": "ko-KP",
"title": "Korean (North Korea)"
}, {
"isoCode": "ko-KR",
"title": "Korean (Korea)"
}, {
"isoCode": "kok",
"title": "Konkani"
}, {
"isoCode": "kok-IN",
"title": "Konkani (India)"
}, {
"isoCode": "ks",
"title": "Kashmiri"
}, {
"isoCode": "ks-IN",
"title": "Kashmiri (India)"
}, {
"isoCode": "ksb",
"title": "Shambala"
}, {
"isoCode": "ksb-TZ",
"title": "Shambala (Tanzania)"
}, {
"isoCode": "ksf",
"title": "Bafia"
}, {
"isoCode": "ksf-CM",
"title": "Bafia (Cameroon)"
}, {
"isoCode": "ksh",
"title": "Colognian"
}, {
"isoCode": "ksh-DE",
"title": "Colognian (Germany)"
}, {
"isoCode": "kw",
"title": "Cornish"
}, {
"isoCode": "kw-GB",
"title": "Cornish (United Kingdom)"
}, {
"isoCode": "ky",
"title": "Kyrgyz"
}, {
"isoCode": "ky-KG",
"title": "Kyrgyz (Kyrgyzstan)"
}, {
"isoCode": "lag",
"title": "Langi"
}, {
"isoCode": "lag-TZ",
"title": "Langi (Tanzania)"
}, {
"isoCode": "lb",
"title": "Luxembourgish"
}, {
"isoCode": "lb-LU",
"title": "Luxembourgish (Luxembourg)"
}, {
"isoCode": "lg",
"title": "Ganda"
}, {
"isoCode": "lg-UG",
"title": "Ganda (Uganda)"
}, {
"isoCode": "lkt",
"title": "Lakota"
}, {
"isoCode": "lkt-US",
"title": "Lakota (United States)"
}, {
"isoCode": "ln",
"title": "Lingala"
}, {
"isoCode": "ln-AO",
"title": "Lingala (Angola)"
}, {
"isoCode": "ln-CD",
"title": "Lingala (Congo [DRC])"
}, {
"isoCode": "ln-CF",
"title": "Lingala (Central African Republic)"
}, {
"isoCode": "ln-CG",
"title": "Lingala (Congo)"
}, {
"isoCode": "lo",
"title": "Lao"
}, {
"isoCode": "lo-LA",
"title": "Lao (Laos)"
}, {
"isoCode": "lrc",
"title": "Northern Luri"
}, {
"isoCode": "lrc-IQ",
"title": "Northern Luri (Iraq)"
}, {
"isoCode": "lrc-IR",
"title": "Northern Luri (Iran)"
}, {
"isoCode": "lt",
"title": "Lithuanian"
}, {
"isoCode": "lt-LT",
"title": "Lithuanian (Lithuania)"
}, {
"isoCode": "lu",
"title": "Luba-Katanga"
}, {
"isoCode": "lu-CD",
"title": "Luba-Katanga (Congo [DRC])"
}, {
"isoCode": "luo",
"title": "Luo"
}, {
"isoCode": "luo-KE",
"title": "Luo (Kenya)"
}, {
"isoCode": "luy",
"title": "Luyia"
}, {
"isoCode": "luy-KE",
"title": "Luyia (Kenya)"
}, {
"isoCode": "lv",
"title": "Latvian"
}, {
"isoCode": "lv-LV",
"title": "Latvian (Latvia)"
}, {
"isoCode": "mas",
"title": "Masai"
}, {
"isoCode": "mas-KE",
"title": "Masai (Kenya)"
}, {
"isoCode": "mas-TZ",
"title": "Masai (Tanzania)"
}, {
"isoCode": "mer",
"title": "Meru"
}, {
"isoCode": "mer-KE",
"title": "Meru (Kenya)"
}, {
"isoCode": "mfe",
"title": "Morisyen"
}, {
"isoCode": "mfe-MU",
"title": "Morisyen (Mauritius)"
}, {
"isoCode": "mg",
"title": "Malagasy"
}, {
"isoCode": "mg-MG",
"title": "Malagasy (Madagascar)"
}, {
"isoCode": "mgh",
"title": "Makhuwa-Meetto"
}, {
"isoCode": "mgh-MZ",
"title": "Makhuwa-Meetto (Mozambique)"
}, {
"isoCode": "mgo",
"title": "Metaʼ"
}, {
"isoCode": "mgo-CM",
"title": "Metaʼ (Cameroon)"
}, {
"isoCode": "mi",
"title": "Maori"
}, {
"isoCode": "mi-NZ",
"title": "Maori (New Zealand)"
}, {
"isoCode": "mk",
"title": "Macedonian"
}, {
"isoCode": "mk-MK",
"title": "Macedonian (North Macedonia)"
}, {
"isoCode": "ml",
"title": "Malayalam"
}, {
"isoCode": "ml-IN",
"title": "Malayalam (India)"
}, {
"isoCode": "mn",
"title": "Mongolian"
}, {
"isoCode": "mn-MN",
"title": "Mongolian (Mongolia)"
}, {
"isoCode": "mn-Mong",
"title": "Mongolian (Mongolian)"
}, {
"isoCode": "mn-Mong-CN",
"title": "Mongolian (Mongolian, China)"
}, {
"isoCode": "mn-Mong-MN",
"title": "Mongolian (Mongolian, Mongolia)"
}, {
"isoCode": "moh",
"title": "Mohawk"
}, {
"isoCode": "moh-CA",
"title": "Mohawk (Canada)"
}, {
"isoCode": "mr",
"title": "Marathi"
}, {
"isoCode": "mr-IN",
"title": "Marathi (India)"
}, {
"isoCode": "ms",
"title": "Malay"
}, {
"isoCode": "ms-BN",
"title": "Malay (Brunei)"
}, {
"isoCode": "ms-MY",
"title": "Malay (Malaysia)"
}, {
"isoCode": "ms-SG",
"title": "Malay (Singapore)"
}, {
"isoCode": "mt",
"title": "Maltese"
}, {
"isoCode": "mt-MT",
"title": "Maltese (Malta)"
}, {
"isoCode": "mua",
"title": "Mundang"
}, {
"isoCode": "mua-CM",
"title": "Mundang (Cameroon)"
}, {
"isoCode": "my",
"title": "Burmese"
}, {
"isoCode": "my-MM",
"title": "Burmese (Myanmar)"
}, {
"isoCode": "mzn",
"title": "Mazanderani"
}, {
"isoCode": "mzn-IR",
"title": "Mazanderani (Iran)"
}, {
"isoCode": "naq",
"title": "Nama"
}, {
"isoCode": "naq-NA",
"title": "Nama (Namibia)"
}, {
"isoCode": "nb",
"title": "Norwegian Bokmål"
}, {
"isoCode": "nb-NO",
"title": "Norwegian Bokmål (Norway)"
}, {
"isoCode": "nb-SJ",
"title": "Norwegian Bokmål (Svalbard & Jan Mayen)"
}, {
"isoCode": "nd",
"title": "North Ndebele"
}, {
"isoCode": "nd-ZW",
"title": "North Ndebele (Zimbabwe)"
}, {
"isoCode": "nds",
"title": "Low German"
}, {
"isoCode": "nds-DE",
"title": "Low German (Germany)"
}, {
"isoCode": "nds-NL",
"title": "Low German (Netherlands)"
}, {
"isoCode": "ne",
"title": "Nepali"
}, {
"isoCode": "ne-IN",
"title": "Nepali (India)"
}, {
"isoCode": "ne-NP",
"title": "Nepali (Nepal)"
}, {
"isoCode": "nl",
"title": "Dutch"
}, {
"isoCode": "nl-AW",
"title": "Dutch (Aruba)"
}, {
"isoCode": "nl-BE",
"title": "Dutch (Belgium)"
}, {
"isoCode": "nl-BQ",
"title": "Dutch (Bonaire, Sint Eustatius and Saba)"
}, {
"isoCode": "nl-CW",
"title": "Dutch (Curaçao)"
}, {
"isoCode": "nl-NL",
"title": "Dutch (Netherlands)"
}, {
"isoCode": "nl-SR",
"title": "Dutch (Suriname)"
}, {
"isoCode": "nl-SX",
"title": "Dutch (Sint Maarten)"
}, {
"isoCode": "nmg",
"title": "Kwasio"
}, {
"isoCode": "nmg-CM",
"title": "Kwasio (Cameroon)"
}, {
"isoCode": "nn",
"title": "Norwegian Nynorsk"
}, {
"isoCode": "nn-NO",
"title": "Norwegian Nynorsk (Norway)"
}, {
"isoCode": "nnh",
"title": "Ngiemboon"
}, {
"isoCode": "nnh-CM",
"title": "Ngiemboon (Cameroon)"
}, {
"isoCode": "nqo",
"title": "N’Ko"
}, {
"isoCode": "nqo-GN",
"title": "N’Ko (Guinea)"
}, {
"isoCode": "nr",
"title": "South Ndebele"
}, {
"isoCode": "nr-ZA",
"title": "South Ndebele (South Africa)"
}, {
"isoCode": "nso",
"title": "Sesotho sa Leboa"
}, {
"isoCode": "nso-ZA",
"title": "Sesotho sa Leboa (South Africa)"
}, {
"isoCode": "nus",
"title": "Nuer"
}, {
"isoCode": "nus-SS",
"title": "Nuer (South Sudan)"
}, {
"isoCode": "nyn",
"title": "Nyankole"
}, {
"isoCode": "nyn-UG",
"title": "Nyankole (Uganda)"
}, {
"isoCode": "oc",
"title": "Occitan"
}, {
"isoCode": "oc-FR",
"title": "Occitan (France)"
}, {
"isoCode": "om",
"title": "Oromo"
}, {
"isoCode": "om-ET",
"title": "Oromo (Ethiopia)"
}, {
"isoCode": "om-KE",
"title": "Oromo (Kenya)"
}, {
"isoCode": "or",
"title": "Odia"
}, {
"isoCode": "or-IN",
"title": "Odia (India)"
}, {
"isoCode": "os",
"title": "Ossetic"
}, {
"isoCode": "os-GE",
"title": "Ossetic (Georgia)"
}, {
"isoCode": "os-RU",
"title": "Ossetic (Russia)"
}, {
"isoCode": "pa",
"title": "Punjabi"
}, {
"isoCode": "pa-Arab",
"title": "Punjabi (Arabic)"
}, {
"isoCode": "pa-Arab-PK",
"title": "Punjabi (Arabic, Pakistan)"
}, {
"isoCode": "pa-Guru",
"title": "Punjabi (Gurmukhi)"
}, {
"isoCode": "pa-Guru-IN",
"title": "Punjabi (Gurmukhi, India)"
}, {
"isoCode": "pl",
"title": "Polish"
}, {
"isoCode": "pl-PL",
"title": "Polish (Poland)"
}, {
"isoCode": "prg",
"title": "Prussian"
}, {
"isoCode": "prg-001",
"title": "Prussian (World)"
}, {
"isoCode": "ps",
"title": "Pashto"
}, {
"isoCode": "ps-AF",
"title": "Pashto (Afghanistan)"
}, {
"isoCode": "ps-PK",
"title": "Pashto (Pakistan)"
}, {
"isoCode": "pt",
"title": "Portuguese"
}, {
"isoCode": "pt-AO",
"title": "Portuguese (Angola)"
}, {
"isoCode": "pt-BR",
"title": "Portuguese (Brazil)"
}, {
"isoCode": "pt-CH",
"title": "Portuguese (Switzerland)"
}, {
"isoCode": "pt-CV",
"title": "Portuguese (Cabo Verde)"
}, {
"isoCode": "pt-GQ",
"title": "Portuguese (Equatorial Guinea)"
}, {
"isoCode": "pt-GW",
"title": "Portuguese (Guinea-Bissau)"
}, {
"isoCode": "pt-LU",
"title": "Portuguese (Luxembourg)"
}, {
"isoCode": "pt-MO",
"title": "Portuguese (Macao SAR)"
}, {
"isoCode": "pt-MZ",
"title": "Portuguese (Mozambique)"
}, {
"isoCode": "pt-PT",
"title": "Portuguese (Portugal)"
}, {
"isoCode": "pt-ST",
"title": "Portuguese (São Tomé & Príncipe)"
}, {
"isoCode": "pt-TL",
"title": "Portuguese (Timor-Leste)"
}, {
"isoCode": "qu",
"title": "Quechua"
}, {
"isoCode": "qu-BO",
"title": "Quechua (Bolivia)"
}, {
"isoCode": "qu-EC",
"title": "Quechua (Ecuador)"
}, {
"isoCode": "qu-PE",
"title": "Quechua (Peru)"
}, {
"isoCode": "quc",
"title": "Kʼicheʼ"
}, {
"isoCode": "quc-GT",
"title": "Kʼicheʼ (Guatemala)"
}, {
"isoCode": "rm",
"title": "Romansh"
}, {
"isoCode": "rm-CH",
"title": "Romansh (Switzerland)"
}, {
"isoCode": "rn",
"title": "Rundi"
}, {
"isoCode": "rn-BI",
"title": "Rundi (Burundi)"
}, {
"isoCode": "ro",
"title": "Romanian"
}, {
"isoCode": "ro-MD",
"title": "Romanian (Moldova)"
}, {
"isoCode": "ro-RO",
"title": "Romanian (Romania)"
}, {
"isoCode": "rof",
"title": "Rombo"
}, {
"isoCode": "rof-TZ",
"title": "Rombo (Tanzania)"
}, {
"isoCode": "ru",
"title": "Russian"
}, {
"isoCode": "ru-BY",
"title": "Russian (Belarus)"
}, {
"isoCode": "ru-KG",
"title": "Russian (Kyrgyzstan)"
}, {
"isoCode": "ru-KZ",
"title": "Russian (Kazakhstan)"
}, {
"isoCode": "ru-MD",
"title": "Russian (Moldova)"
}, {
"isoCode": "ru-RU",
"title": "Russian (Russia)"
}, {
"isoCode": "ru-UA",
"title": "Russian (Ukraine)"
}, {
"isoCode": "rw",
"title": "Kinyarwanda"
}, {
"isoCode": "rw-RW",
"title": "Kinyarwanda (Rwanda)"
}, {
"isoCode": "rwk",
"title": "Rwa"
}, {
"isoCode": "rwk-TZ",
"title": "Rwa (Tanzania)"
}, {
"isoCode": "sa",
"title": "Sanskrit"
}, {
"isoCode": "sa-IN",
"title": "Sanskrit (India)"
}, {
"isoCode": "sah",
"title": "Sakha"
}, {
"isoCode": "sah-RU",
"title": "Sakha (Russia)"
}, {
"isoCode": "saq",
"title": "Samburu"
}, {
"isoCode": "saq-KE",
"title": "Samburu (Kenya)"
}, {
"isoCode": "sbp",
"title": "Sangu"
}, {
"isoCode": "sbp-TZ",
"title": "Sangu (Tanzania)"
}, {
"isoCode": "sd",
"title": "Sindhi"
}, {
"isoCode": "sd-PK",
"title": "Sindhi (Pakistan)"
}, {
"isoCode": "se",
"title": "Northern Sami"
}, {
"isoCode": "se-FI",
"title": "Northern Sami (Finland)"
}, {
"isoCode": "se-NO",
"title": "Northern Sami (Norway)"
}, {
"isoCode": "se-SE",
"title": "Northern Sami (Sweden)"
}, {
"isoCode": "seh",
"title": "Sena"
}, {
"isoCode": "seh-MZ",
"title": "Sena (Mozambique)"
}, {
"isoCode": "ses",
"title": "Koyraboro Senni"
}, {
"isoCode": "ses-ML",
"title": "Koyraboro Senni (Mali)"
}, {
"isoCode": "sg",
"title": "Sango"
}, {
"isoCode": "sg-CF",
"title": "Sango (Central African Republic)"
}, {
"isoCode": "shi",
"title": "Tachelhit"
}, {
"isoCode": "shi-Latn",
"title": "Tachelhit (Latin)"
}, {
"isoCode": "shi-Latn-MA",
"title": "Tachelhit (Latin, Morocco)"
}, {
"isoCode": "shi-Tfng",
"title": "Tachelhit (Tifinagh)"
}, {
"isoCode": "shi-Tfng-MA",
"title": "Tachelhit (Tifinagh, Morocco)"
}, {
"isoCode": "si",
"title": "Sinhala"
}, {
"isoCode": "si-LK",
"title": "Sinhala (Sri Lanka)"
}, {
"isoCode": "sk",
"title": "Slovak"
}, {
"isoCode": "sk-SK",
"title": "Slovak (Slovakia)"
}, {
"isoCode": "sl",
"title": "Slovenian"
}, {
"isoCode": "sl-SI",
"title": "Slovenian (Slovenia)"
}, {
"isoCode": "sma",
"title": "Southern Sami"
}, {
"isoCode": "sma-NO",
"title": "Southern Sami (Norway)"
}, {
"isoCode": "sma-SE",
"title": "Southern Sami (Sweden)"
}, {
"isoCode": "smj",
"title": "Lule Sami"
}, {
"isoCode": "smj-NO",
"title": "Lule Sami (Norway)"
}, {
"isoCode": "smj-SE",
"title": "Lule Sami (Sweden)"
}, {
"isoCode": "smn",
"title": "Inari Sami"
}, {
"isoCode": "smn-FI",
"title": "Inari Sami (Finland)"
}, {
"isoCode": "sms",
"title": "Skolt Sami"
}, {
"isoCode": "sms-FI",
"title": "Skolt Sami (Finland)"
}, {
"isoCode": "sn",
"title": "Shona"
}, {
"isoCode": "sn-ZW",
"title": "Shona (Zimbabwe)"
}, {
"isoCode": "so",
"title": "Somali"
}, {
"isoCode": "so-DJ",
"title": "Somali (Djibouti)"
}, {
"isoCode": "so-ET",
"title": "Somali (Ethiopia)"
}, {
"isoCode": "so-KE",
"title": "Somali (Kenya)"
}, {
"isoCode": "so-SO",
"title": "Somali (Somalia)"
}, {
"isoCode": "sq",
"title": "Albanian"
}, {
"isoCode": "sq-AL",
"title": "Albanian (Albania)"
}, {
"isoCode": "sq-MK",
"title": "Albanian (North Macedonia)"
}, {
"isoCode": "sq-XK",
"title": "Albanian (Kosovo)"
}, {
"isoCode": "sr",
"title": "Serbian"
}, {
"isoCode": "sr-Cyrl",
"title": "Serbian (Cyrillic)"
}, {
"isoCode": "sr-Cyrl-BA",
"title": "Serbian (Cyrillic, Bosnia & Herzegovina)"
}, {
"isoCode": "sr-Cyrl-ME",
"title": "Serbian (Cyrillic, Montenegro)"
}, {
"isoCode": "sr-Cyrl-RS",
"title": "Serbian (Cyrillic, Serbia)"
}, {
"isoCode": "sr-Cyrl-XK",
"title": "Serbian (Cyrillic, Kosovo)"
}, {
"isoCode": "sr-Latn",
"title": "Serbian (Latin)"
}, {
"isoCode": "sr-Latn-BA",
"title": "Serbian (Latin, Bosnia & Herzegovina)"
}, {
"isoCode": "sr-Latn-ME",
"title": "Serbian (Latin, Montenegro)"
}, {
"isoCode": "sr-Latn-RS",
"title": "Serbian (Latin, Serbia)"
}, {
"isoCode": "sr-Latn-XK",
"title": "Serbian (Latin, Kosovo)"
}, {
"isoCode": "ss",
"title": "siSwati"
}, {
"isoCode": "ss-SZ",
"title": "siSwati (Eswatini)"
}, {
"isoCode": "ss-ZA",
"title": "siSwati (South Africa)"
}, {
"isoCode": "ssy",
"title": "Saho"
}, {
"isoCode": "ssy-ER",
"title": "Saho (Eritrea)"
}, {
"isoCode": "st",
"title": "Sesotho"
}, {
"isoCode": "st-LS",
"title": "Sesotho (Lesotho)"
}, {
"isoCode": "st-ZA",
"title": "Sesotho (South Africa)"
}, {
"isoCode": "sv",
"title": "Swedish"
}, {
"isoCode": "sv-AX",
"title": "Swedish (Åland Islands)"
}, {
"isoCode": "sv-FI",
"title": "Swedish (Finland)"
}, {
"isoCode": "sv-SE",
"title": "Swedish (Sweden)"
}, {
"isoCode": "sw",
"title": "Kiswahili"
}, {
"isoCode": "sw-CD",
"title": "Kiswahili (Congo [DRC])"
}, {
"isoCode": "sw-KE",
"title": "Kiswahili (Kenya)"
}, {
"isoCode": "sw-TZ",
"title": "Kiswahili (Tanzania)"
}, {
"isoCode": "sw-UG",
"title": "Kiswahili (Uganda)"
}, {
"isoCode": "syr",
"title": "Syriac"
}, {
"isoCode": "syr-SY",
"title": "Syriac (Syria)"
}, {
"isoCode": "ta",
"title": "Tamil"
}, {
"isoCode": "ta-IN",
"title": "Tamil (India)"
}, {
"isoCode": "ta-LK",
"title": "Tamil (Sri Lanka)"
}, {
"isoCode": "ta-MY",
"title": "Tamil (Malaysia)"
}, {
"isoCode": "ta-SG",
"title": "Tamil (Singapore)"
}, {
"isoCode": "te",
"title": "Telugu"
}, {
"isoCode": "te-IN",
"title": "Telugu (India)"
}, {
"isoCode": "teo",
"title": "Teso"
}, {
"isoCode": "teo-KE",
"title": "Teso (Kenya)"
}, {
"isoCode": "teo-UG",
"title": "Teso (Uganda)"
}, {
"isoCode": "tg",
"title": "Tajik"
}, {
"isoCode": "tg-TJ",
"title": "Tajik (Tajikistan)"
}, {
"isoCode": "th",
"title": "Thai"
}, {
"isoCode": "th-TH",
"title": "Thai (Thailand)"
}, {
"isoCode": "ti",
"title": "Tigrinya"
}, {
"isoCode": "ti-ER",
"title": "Tigrinya (Eritrea)"
}, {
"isoCode": "ti-ET",
"title": "Tigrinya (Ethiopia)"
}, {
"isoCode": "tig",
"title": "Tigre"
}, {
"isoCode": "tig-ER",
"title": "Tigre (Eritrea)"
}, {
"isoCode": "tk",
"title": "Turkmen"
}, {
"isoCode": "tk-TM",
"title": "Turkmen (Turkmenistan)"
}, {
"isoCode": "tn",
"title": "Setswana"
}, {
"isoCode": "tn-BW",
"title": "Setswana (Botswana)"
}, {
"isoCode": "tn-ZA",
"title": "Setswana (South Africa)"
}, {
"isoCode": "to",
"title": "Tongan"
}, {
"isoCode": "to-TO",
"title": "Tongan (Tonga)"
}, {
"isoCode": "tr",
"title": "Turkish"
}, {
"isoCode": "tr-CY",
"title": "Turkish (Cyprus)"
}, {
"isoCode": "tr-TR",
"title": "Turkish (Turkey)"
}, {
"isoCode": "ts",
"title": "Xitsonga"
}, {
"isoCode": "ts-ZA",
"title": "Xitsonga (South Africa)"
}, {
"isoCode": "tt",
"title": "Tatar"
}, {
"isoCode": "tt-RU",
"title": "Tatar (Russia)"
}, {
"isoCode": "twq",
"title": "Tasawaq"
}, {
"isoCode": "twq-NE",
"title": "Tasawaq (Niger)"
}, {
"isoCode": "tzm",
"title": "Central Atlas Tamazight"
}, {
"isoCode": "tzm-MA",
"title": "Central Atlas Tamazight (Morocco)"
}, {
"isoCode": "ug",
"title": "Uyghur"
}, {
"isoCode": "ug-CN",
"title": "Uyghur (China)"
}, {
"isoCode": "uk",
"title": "Ukrainian"
}, {
"isoCode": "uk-UA",
"title": "Ukrainian (Ukraine)"
}, {
"isoCode": "ur",
"title": "Urdu"
}, {
"isoCode": "ur-IN",
"title": "Urdu (India)"
}, {
"isoCode": "ur-PK",
"title": "Urdu (Pakistan)"
}, {
"isoCode": "uz",
"title": "Uzbek"
}, {
"isoCode": "uz-Arab",
"title": "Uzbek (Arabic)"
}, {
"isoCode": "uz-Arab-AF",
"title": "Uzbek (Arabic, Afghanistan)"
}, {
"isoCode": "uz-Cyrl",
"title": "Uzbek (Cyrillic)"
}, {
"isoCode": "uz-Cyrl-UZ",
"title": "Uzbek (Cyrillic, Uzbekistan)"
}, {
"isoCode": "uz-Latn",
"title": "Uzbek (Latin)"
}, {
"isoCode": "uz-Latn-UZ",
"title": "Uzbek (Latin, Uzbekistan)"
}, {
"isoCode": "vai",
"title": "Vai"
}, {
"isoCode": "vai-Latn",
"title": "Vai (Latin)"
}, {
"isoCode": "vai-Latn-LR",
"title": "Vai (Latin, Liberia)"
}, {
"isoCode": "vai-Vaii",
"title": "Vai (Vai)"
}, {
"isoCode": "vai-Vaii-LR",
"title": "Vai (Vai, Liberia)"
}, {
"isoCode": "ve",
"title": "Venda"
}, {
"isoCode": "ve-ZA",
"title": "Venda (South Africa)"
}, {
"isoCode": "vi",
"title": "Vietnamese"
}, {
"isoCode": "vi-VN",
"title": "Vietnamese (Vietnam)"
}, {
"isoCode": "vo",
"title": "Volapük"
}, {
"isoCode": "vo-001",
"title": "Volapük (World)"
}, {
"isoCode": "vun",
"title": "Vunjo"
}, {
"isoCode": "vun-TZ",
"title": "Vunjo (Tanzania)"
}, {
"isoCode": "wae",
"title": "Walser"
}, {
"isoCode": "wae-CH",
"title": "Walser (Switzerland)"
}, {
"isoCode": "wal",
"title": "Wolaytta"
}, {
"isoCode": "wal-ET",
"title": "Wolaytta (Ethiopia)"
}, {
"isoCode": "wo",
"title": "Wolof"
}, {
"isoCode": "wo-SN",
"title": "Wolof (Senegal)"
}, {
"isoCode": "xh",
"title": "isiXhosa"
}, {
"isoCode": "xh-ZA",
"title": "isiXhosa (South Africa)"
}, {
"isoCode": "xog",
"title": "Soga"
}, {
"isoCode": "xog-UG",
"title": "Soga (Uganda)"
}, {
"isoCode": "yav",
"title": "Yangben"
}, {
"isoCode": "yav-CM",
"title": "Yangben (Cameroon)"
}, {
"isoCode": "yi",
"title": "Yiddish"
}, {
"isoCode": "yi-001",
"title": "Yiddish (World)"
}, {
"isoCode": "yo",
"title": "Yoruba"
}, {
"isoCode": "yo-BJ",
"title": "Yoruba (Benin)"
}, {
"isoCode": "yo-NG",
"title": "Yoruba (Nigeria)"
}, {
"isoCode": "zgh",
"title": "Standard Moroccan Tamazight"
}, {
"isoCode": "zgh-MA",
"title": "Standard Moroccan Tamazight (Morocco)"
}, {
"isoCode": "zh",
"title": "Chinese"
}, {
"isoCode": "zh-Hans",
"title": "Chinese (Simplified)"
}, {
"isoCode": "zh-Hans-CN",
"title": "Chinese (Simplified, China)"
}, {
"isoCode": "zh-Hans-HK",
"title": "Chinese (Simplified, Hong Kong SAR)"
}, {
"isoCode": "zh-Hans-MO",
"title": "Chinese (Simplified, Macao SAR)"
}, {
"isoCode": "zh-Hans-SG",
"title": "Chinese (Simplified, Singapore)"
}, {
"isoCode": "zh-Hant",
"title": "Chinese (Traditional)"
}, {
"isoCode": "zh-Hant-HK",
"title": "Chinese (Traditional, Hong Kong SAR)"
}, {
"isoCode": "zh-Hant-MO",
"title": "Chinese (Traditional, Macao SAR)"
}, {
"isoCode": "zh-Hant-TW",
"title": "Chinese (Traditional, Taiwan)"
}, {
"isoCode": "zu",
"title": "isiZulu"
}, {
"isoCode": "zu-ZA",
"title": "isiZulu (South Africa)"
}]
================================================
FILE: MangaManager/src/Common/LoadedComicInfo/ArchiveFile.py
================================================
import os
import zipfile
import rarfile
class ArchiveFile:
"""
A class that provides a unified interface to read and write archive files.
It automatically chooses between ZipFile and RarFile based on the
file extension.
"""
is_cbr = False
def __init__(self, filename, mode='r', password=None):
self.filename = filename
self.mode = mode
self.password = password
self.archive = None
ext = os.path.splitext(filename)[1].lower()
if ext in ('.cbz', '.zip'):
self.archive = zipfile.ZipFile(filename, mode)
elif ext in ('.cbr', '.rar'):
self.is_cbr = True
self.archive = rarfile.RarFile(filename, mode)
if password:
self.archive.setpassword(password)
else:
raise ValueError('Unsupported file type: %s' % ext)
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
if self.archive is not None:
self.archive.close()
def namelist(self):
return self.archive.namelist()
def infolist(self):
return self.archive.infolist()
def getinfo(self, name):
return self.archive.getinfo(name)
def read(self, name):
return self.archive.read(name)
def open(self, name):
# if self.is_cbr:
# return self.archive.read(name)
return self.archive.open(name)
def extract(self, member, path=None, password=None):
if password:
self.archive.setpassword(password)
self.archive.extract(member, path)
def extractall(self, path=None, members=None, password=None):
if password:
self.archive.setpassword(password)
self.archive.extractall(path, members)
================================================
FILE: MangaManager/src/Common/LoadedComicInfo/CoverActions.py
================================================
import enum
class CoverActions(enum.Enum):
RESET = 0 # Cancel current selected action
REPLACE = 1
DELETE = 2
APPEND = 3
================================================
FILE: MangaManager/src/Common/LoadedComicInfo/ILoadedComicInfo.py
================================================
class ILoadedComicInfo:
"""
Helper class that loads the info that is required by the tools
file_path : str
Path of the file
cinfo_object : ComicInfo
The class where the metadata is stored
cover_filename : str
The filename of the image that gets parsed as series cover
has_metadata : bool
If false, we only need to append metadata.
No need to back up ComicInfo.xml because it doesn't exist
volume : int
The volume from the metadata. If not set then it tries to parse from filename
chapter : str
The volume from the metadata. If not set then it tries to parse from filename
"""
file_path: str
file_name: str
has_metadata: bool = False
is_cinfo_at_root: bool = False
has_changes = False
changed_tags = []
================================================
FILE: MangaManager/src/Common/LoadedComicInfo/LoadedComicInfo.py
================================================
from __future__ import annotations
import copy
import io
import logging
import os
import tempfile
import zipfile
from typing import IO
from common.models import ComicInfo
from src.Common.errors import BadZipFile
from src.Common.utils import IS_IMAGE_PATTERN, get_new_webp_name, convert_to_webp
from .ArchiveFile import ArchiveFile
from .CoverActions import CoverActions
from .ILoadedComicInfo import ILoadedComicInfo
from .LoadedFileCoverData import LoadedFileCoverData
from .LoadedFileMetadata import LoadedFileMetadata
from ...Settings import Settings, SettingHeading
logger = logging.getLogger("LoadedCInfo")
COMICINFO_FILE = 'ComicInfo.xml'
COMICINFO_FILE_BACKUP = 'Old_ComicInfo.xml.bak'
COVER_NAME = "!0000_Cover"
BACKCOVER_NAME = "9999_Back"
_LOG_TAG_WEBP = "Convert Webp"
_LOG_TAG_WRITE_META = 'Write Meta'
_LOG_TAG_RECOMPRESSING = "Recompressing"
move_to_value = ""
class LoadedComicInfo(LoadedFileMetadata, LoadedFileCoverData, ILoadedComicInfo):
@property
def _logging_extra(self):
return {"processed_filename": self.file_name}
def __init__(self, path, comicinfo: ComicInfo = None, load_default_metadata=True):
"""
:param path:
:param comicinfo: The data class to be applied
:raises BadZipFile: The file can't be read or is not a valid zip file
"""
self.file_path = path or None
self.file_name = None if path is None else os.path.basename(path)
logger.debug(f"[{'Loading File':13s}] '{self.file_name}'")
self.cinfo_object = comicinfo
if load_default_metadata:
self.load_metadata()
def get_template_values(self) -> dict:
"""
Returns a dict with predefined values to fill in string templates
:return:
"""
return {
"filename": self.file_name,
"series": self.cinfo_object.series or "",
"series_sort": self.cinfo_object.series_sort or "",
"title": self.cinfo_object.title or "",
"chapter": self.cinfo_object.number or "",
"volume": self.cinfo_object.volume or "",
"publisher": self.cinfo_object.publisher or ""
}
def get_template_filename(self, input_template: str) -> str|None:
"""
Fills the provided template with the available values in the comicinfo
:param input_template: A string representing the input template ("{series} - {chapter}")
:return: None if there's a missing key in the template
"""
try:
return input_template.format_map(self.get_template_values()).replace(" ", " ")
except KeyError as e:
logger.error(f"Could not get {list(e.args)} keys when filling template values")
return None
###############################
# LOADING METHODS
###############################
def load_all(self):
try:
# Fixme: skip folders
# Update: 05-01-23 At this point i don't remember why the fix me. I'm leaving it there.
self.load_cover_info()
with ArchiveFile(self.file_path, 'r') as self.archive:
if not self.cinfo_object:
self._load_metadata()
except zipfile.BadZipFile:
logger.error(f"[{'Loading File':13s}] Failed to read file. File is not a zip file or is broken.",
exc_info=False)
raise BadZipFile()
return self
def load_metadata(self):
try:
with ArchiveFile(self.file_path, 'r') as self.archive:
if not self.cinfo_object:
self._load_metadata()
except zipfile.BadZipFile:
logger.error(f"[{'Loading File':13s}] Failed to read file. File is not a zip file or is broken.",
exc_info=False)
raise BadZipFile()
return self
###############################
# PROCESSING METHODS
###############################
# INTERFACE METHODS
def write_metadata(self, auto_unmark_changes=False):
# print(self.cinfo_object.__dict__)
self.has_changes = self.cinfo_object.has_changes(self.original_cinfo_object)
logger.debug(f"[{'BEGIN WRITE':13s}] Writing metadata to file '{self.file_path}'")
try:
self._process(write_metadata=self.has_changes)
finally:
if auto_unmark_changes:
self.has_changes = False
def convert_to_webp(self):
logger.debug(f"[{'BEGIN CONVERT':13s}] Converting to webp: '{self.file_path}'")
self._process(do_convert_to_webp=True)
def _export_metadata(self) -> str:
return str(self.cinfo_object.to_xml())
# ACTUAL LOGIC
def _process(self, write_metadata=False, do_convert_to_webp=False, **_):
logger.info(f"[{'PROCESSING':13s}] Processing file '{self.file_path}'")
if write_metadata and not do_convert_to_webp and not self.has_metadata:
with zipfile.ZipFile(self.file_path, mode='a', compression=zipfile.ZIP_STORED) as zf:
# We finally append our new ComicInfo file
zf.writestr(COMICINFO_FILE, self._export_metadata())
logger.debug(f"[{_LOG_TAG_WRITE_META:13s}] New ComicInfo.xml appended to the file",
extra=self._logging_extra)
self.has_metadata = True
# Creates a tempfile in the directory the original file is at
tmpfd, tmpname = tempfile.mkstemp(dir=os.path.dirname(self.file_path))
os.close(tmpfd)
has_cover_action = self.cover_action not in (CoverActions.RESET, None) or self.backcover_action not in (
CoverActions.RESET, None)
original_size = os.path.getsize(self.file_path)
with ArchiveFile(self.file_path, 'r') as zin:
initial_file_count = len(zin.namelist())
for s in zin.infolist():
if s.file_size != 0:
orig_comp_type = s.compress_type
break
with zipfile.ZipFile(tmpname, "w",compression=orig_comp_type) as zout: # The temp file where changes will be saved to
self._recompress(zin, zout, write_metadata=write_metadata, do_convert_webp=do_convert_to_webp)
newfile_size = os.path.getsize(tmpname)
# If the new file is smaller than the original file, we process again with no webp conversion.
# Some source files have better png compression than webp
if original_size < newfile_size and do_convert_to_webp:
logger.warning(f"[{'Processing':13s}] New converted file is bigger than original file",
extra=self._logging_extra)
os.remove(tmpname)
if not has_cover_action and not write_metadata:
logger.warning(f"[{'Processing':13s}] ⤷ Keeping original files. No additional processing left")
return
logger.warning(f"[{'Processing':13s}] ⤷ Cover action or new metadata detected. Processing new covers without converting source to webp")
with zipfile.ZipFile(tmpname, "w") as zout: # The temp file where changes will be saved to
self._recompress(zin, zout, write_metadata=write_metadata, do_convert_webp=False)
# Reset cover flags
self.cover_action = CoverActions.RESET
self.backcover_action = CoverActions.RESET
logger.debug(f"[{'Processing':13s}] Data from old file copied to new file",
extra=self._logging_extra)
# Delete old file and rename new file to old name
try:
with ArchiveFile(self.file_path, 'r') as zin:
assert initial_file_count == len(zin.namelist())
os.remove(self.file_path)
os.rename(tmpname, self.file_path)
logger.debug(f"[{'Processing':13s}] Successfully deleted old file and named tempfile as the old file",
extra=self._logging_extra)
# If we fail to delete original file we delete temp file effecively aborting the metadata update
except PermissionError:
logger.exception(f"[{'Processing':13s}] Permission error. Aborting and clearing temp files",
extra=self._logging_extra)
os.remove(
tmpname) # Could be moved to a 'finally'? Don't want to risk it not clearing temp files properly
raise
except FileNotFoundError:
try:
logger.exception(f"[{'Processing':13s}] File not found. Aborting and clearing temp files",
extra=self._logging_extra)
os.remove(tmpname)
except FileNotFoundError:
pass
except Exception:
logger.exception(f"[{'Processing':13s}] Unhandled exception. Create an issue so this gets investigated."
f" Aborting and clearing temp files",
extra=self._logging_extra)
os.remove(tmpname)
raise
self.original_cinfo_object = copy.copy(self.cinfo_object)
logger.info(f"[{'Processing':13s}] Successfully recompressed file",
extra=self._logging_extra)
if (self.cover_cache or self.backcover_cache) and has_cover_action:
logger.info("[{'Processing':13s}] Updating covers")
self.load_cover_info()
def _recompress(self, zin, zout, write_metadata, do_convert_webp):
"""
Given 2 input and output zipfiles copy content of one zipfile to the new one.
Files that matches certain criteria gets skipped and not copied over, hence deleted.
:param zin: The zipfile object of the zip that's going to be read
:param zout: The ZipFile object of the new zip to copy stuff to
:param write_metadata: Should update metadata
:param do_convert_webp: Should convert images before adding to new zipfile
:return:
"""
is_metadata_backed = False
# Write the new metadata once
if write_metadata:
zout.writestr(COMICINFO_FILE, self._export_metadata())
logger.debug(f"[{_LOG_TAG_WRITE_META:13s}] New ComicInfo.xml appended to the file")
# Directly backup the metadata if it's at root.
if self.is_cinfo_at_root:
if Settings().get(SettingHeading.Main, "create_backup_comicinfo") == 'True' and self.had_metadata_on_open:
zout.writestr(f"Old_{COMICINFO_FILE}.bak", zin.read(COMICINFO_FILE))
logger.debug(f"[{_LOG_TAG_WRITE_META:13s}] Backup for comicinfo.xml created")
is_metadata_backed = True
self.has_metadata = True
# Append the cover if the action is append
if self.cover_action == CoverActions.APPEND:
self._append_image(zout, self.new_cover_path, False, do_convert_webp,
current_backcover_filename=self.backcover_filename)
if self.backcover_action == CoverActions.APPEND:
self._append_image(zout, self.new_backcover_path, True, do_convert_webp,
current_backcover_filename=self.backcover_filename)
# Start iterating files.
total = len(zin.namelist())
for i, item in enumerate(zin.infolist()):
counter = f"{i}/{total}"
if write_metadata:
# Discard old backup
if item.filename.endswith(
COMICINFO_FILE_BACKUP): # Skip file, effectively deleting old backup
logger.debug(f"[{_LOG_TAG_WRITE_META:13s}] Skipped old backup file")
continue
if item.filename.endswith(COMICINFO_FILE):
# A root-level comicinfo was backed up already. This one is likely not where it should
if is_metadata_backed:
logger.info(f"[{_LOG_TAG_WRITE_META:13s}] Skipping non compliant ComicInfo.xml")
continue
# If filename is comicinfo save as old_comicinfo.xml
if Settings().get(SettingHeading.Main, "create_backup_comicinfo") == 'True' and self.had_metadata_on_open:
zout.writestr(f"Old_{item.filename}.bak", zin.read(item.filename))
logger.debug(f"[{_LOG_TAG_WRITE_META:13s}] Backup for comicinfo.xml created")
# Stop accepting more comicinfo files.
is_metadata_backed = True
continue
# Handle Cover Changes:
if item.filename == self.cover_filename:
match self.cover_action:
case None:
self._move_image(zin, zout=zout, item_=item, do_convert_to_webp=do_convert_webp)
case CoverActions.DELETE:
logger.trace(
f"[{_LOG_TAG_RECOMPRESSING:13}] Skipping cover to effectively delete it. File: '{item.filename}'")
case CoverActions.REPLACE:
with open(self.new_cover_path, "rb") as opened_image:
opened_image_io = io.BytesIO(opened_image.read())
self._move_image(zin, zout=zout, item_=item, do_convert_to_webp=do_convert_webp,
image=opened_image_io)
case _:
self._move_image(zin, zout=zout, item_=item, do_convert_to_webp=do_convert_webp)
continue
# Handle BackCover Change
elif item.filename == self.backcover_filename:
match self.backcover_action:
case None:
self._move_image(zin, zout=zout, item_=item, do_convert_to_webp=do_convert_webp)
case CoverActions.DELETE:
logger.trace(
f"[{_LOG_TAG_RECOMPRESSING:13}] Skipping back cover to efectively delete it. File: '{item.filename}'")
case CoverActions.REPLACE:
with open(self.new_backcover_path, "rb") as opened_image:
opened_image_io = io.BytesIO(opened_image.read())
self._move_image(zin, zout=zout, item_=item, do_convert_to_webp=do_convert_webp,
image=opened_image_io)
case _:
self._move_image(zin, zout=zout, item_=item, do_convert_to_webp=do_convert_webp)
continue
# Copy the rest of the images as they are
self._move_image(zin, zout=zout, item_=item, do_convert_to_webp=do_convert_webp)
# Recompressing methods
@staticmethod
def _move_image(zin: zipfile.ZipFile, zout: zipfile.ZipFile, item_: zipfile.ZipInfo,
do_convert_to_webp: bool,
new_filename=None, image: IO[bytes] = None):
"""
Given an input and output ZipFile copy the passed item to the new zipfile. Also converts image to webp if set to true
:param zin: The input zipfile object
:param zout: The output zipfile where the bytes will be copied over
:param item_: The zipfile 'item' object
:param do_convert_to_webp: Should the bytes be converted to webp formate
:param new_filename: If a new filename is desired this should be set. Else it will use original filename
:param image: Bytes of the image if the data wants to be overwritten
:return:
"""
# Convert to webp if option enabled and file is image
if do_convert_to_webp and IS_IMAGE_PATTERN.match(item_.filename):
with zin.open(item_) as opened_image:
new_filename = get_new_webp_name(new_filename if new_filename is not None else item_.filename)
zout.writestr(new_filename, convert_to_webp(opened_image if image is None else image))
logger.trace(f"[{_LOG_TAG_RECOMPRESSING:13s}] Adding converted file '{new_filename}' to new tempfile"
f" back to the new tempfile")
# Keep the rest of the files.
else:
zout.writestr(item_.filename if new_filename is None else new_filename,
zin.read(item_.filename) if image is None else image.read())
logger.trace(f"[{_LOG_TAG_RECOMPRESSING:13s}] Adding '{item_.filename}' back to the new tempfile")
@staticmethod
def _append_image(zout, cover_path, is_backcover=False, do_convert_to_webp=False, current_backcover_filename=''):
"""
Given a zipfile object, append (Add image and make it be the first one when natural sorting. Make it last if is_backcover is true) the image in the provided path
:param zout: The zipfile object where the image is going to be added to
:param cover_path: The path to the image file
:param is_backcover: Whether we are "appending" a cover or backcover
:param do_convert_to_webp: Whether the provided image should be converted to webp
:return:
"""
file_name, ext = os.path.splitext(os.path.basename(cover_path))
new_filename = f"{os.path.join(os.path.dirname(current_backcover_filename), '~') if is_backcover else ''}{BACKCOVER_NAME if is_backcover else COVER_NAME}{ext}"
logger.trace(
f"[{_LOG_TAG_RECOMPRESSING:13}] Apending cover to efectively delete it. Loading '{cover_path}'")
if do_convert_to_webp:
with open(cover_path, "rb") as opened_image:
opened_image_io = io.BytesIO(opened_image.read())
new_filename = get_new_webp_name(new_filename)
zout.writestr(new_filename, convert_to_webp(opened_image_io))
logger.trace(
f"[{_LOG_TAG_RECOMPRESSING:13s}] Adding converted file '{new_filename}' to new tempfile")
else:
zout.write(cover_path, new_filename)
logger.trace(
f"[{_LOG_TAG_RECOMPRESSING:13s}] Adding file '{new_filename}' to new tempfile")
================================================
FILE: MangaManager/src/Common/LoadedComicInfo/LoadedFileCoverData.py
================================================
from __future__ import annotations
import io
import logging
import zipfile
from typing import IO
from PIL import ImageTk, Image
from src.Common.errors import BadZipFile
from src.Common.utils import obtain_cover_filename
from .ArchiveFile import ArchiveFile
from .CoverActions import CoverActions
from .ILoadedComicInfo import ILoadedComicInfo
from ...Settings import Settings, SettingHeading
logger = logging.getLogger("LoadedCInfo")
COMICINFO_FILE = 'ComicInfo.xml'
COVER_NAME = "!0000_Cover"
BACKCOVER_NAME = "9999_Back"
_LOG_TAG_WEBP = "Convert Webp"
_LOG_TAG_WRITE_META = 'Write Meta'
_LOG_TAG_RECOMPRESSING = "Recompressing"
move_to_value = ""
class LoadedFileCoverData(ILoadedComicInfo):
cover_filename: str | None = None
cover_cache: ImageTk.PhotoImage = None
backcover_filename: str | None = None
backcover_cache: ImageTk.PhotoImage = None
_cover_action: CoverActions | None = None
# Path to the new cover selected by the user
_new_cover_path: str | None = None
new_cover_cache: ImageTk.PhotoImage | None = None
# Path to the new backcover selected by the user
_backcover_action: CoverActions | None = None
_new_backcover_path: str | None = None
new_backcover_cache: ImageTk.PhotoImage | None = None
def get_cover_cache(self, is_backcover=False) -> ImageTk.PhotoImage | None:
if self._cover_action is None:
return self.backcover_cache if is_backcover else self.cover_cache
else:
return self.new_backcover_cache if is_backcover else self.new_cover_cache
@property
def cover_action(self):
return self._cover_action
@cover_action.setter
def cover_action(self, value: CoverActions):
if value == CoverActions.RESET:
self._new_cover_path = None
self.new_cover_cache = None
self._cover_action = None
else:
self._cover_action = value
self.has_changes = True
@property
def backcover_action(self):
return self._backcover_action
@backcover_action.setter
def backcover_action(self, value: CoverActions):
if value == CoverActions.RESET:
self._new_backcover_path = None
self.new_backcover_cache = None
self._backcover_action = None
else:
self._backcover_action = value
self.has_changes = True
@property
def new_cover_path(self):
return self._new_cover_path
@new_cover_path.setter
def new_cover_path(self, path):
if path is None:
self._new_cover_path = None
self.new_cover_cache = None
return
image = Image.open(path)
image = image.resize((190, 260), Image.NEAREST)
try:
self.new_cover_cache = ImageTk.PhotoImage(image)
except RuntimeError:
self.new_cover_cache = None
self._new_cover_path = path
@property
def new_backcover_path(self):
return self._new_backcover_path
@new_backcover_path.setter
def new_backcover_path(self, path):
if path is None:
self._new_backcover_path = None
self.new_backcover_cache = None
return
image = Image.open(path)
image = image.resize((190, 260), Image.NEAREST)
try:
self.new_backcover_cache = ImageTk.PhotoImage(image)
except RuntimeError:
self.new_cover_cache = None
self._new_backcover_path = path
def load_cover_info(self, load_images=True):
try:
with ArchiveFile(self.file_path,'r') as self.archive:
cover_info = obtain_cover_filename(self.archive.namelist())
if not cover_info:
return
self.cover_filename, self.backcover_filename = cover_info
if not self.cover_filename:
logger.warning(f"[{'CoverParsing':13s}] Couldn't parse any cover")
else:
logger.info(f"[{'CoverParsing':13s}] Cover parsed as '{self.cover_filename}'")
if bool(Settings().get(SettingHeading.Main, 'cache_cover_images')):
self.get_cover_image_bytes()
if not self.backcover_filename:
logger.warning(f"[{'CoverParsing':13s}] Couldn't parse any back cover")
else:
logger.info(f"[{'CoverParsing':13s}] Back Cover parsed as '{self.backcover_filename}'")
if load_images:
self.get_cover_image_bytes(back_cover=True)
except zipfile.BadZipFile:
logger.error(f"[{'Loading File':13s}] Failed to read file. File is not a zip file or is broken.",
exc_info=False)
raise BadZipFile()
except Exception:
logger.exception(f"Unhandled error loading cover info for file: '{self.file_name}'")
return self
def get_cover_image_bytes(self, resized=False, back_cover=False) -> IO[bytes] | None:
"""
Opens the cbz and returns the bytes for the parsed cover image
:return:
"""
if not self.file_path or not self.cover_filename:
return None
if back_cover and not self.backcover_filename:
return None
try:
with ArchiveFile(self.file_path,'r') as zin:
img_bytes = zin.open(self.cover_filename if not back_cover else self.backcover_filename)
image = Image.open(img_bytes)
image = image.resize((190, 260), Image.NEAREST)
try:
if not back_cover:
self.cover_cache = ImageTk.PhotoImage(image)
else:
self.backcover_cache = ImageTk.PhotoImage(image)
except RuntimeError as e:
print(e) # Random patch for some error when running tests
...
if resized:
return io.BytesIO(image.tobytes())
return img_bytes
except Exception:
logger.exception(f"Error getting cover bytes. BackCover = {'True' if back_cover else 'False'} File: {self.file_name}")
================================================
FILE: MangaManager/src/Common/LoadedComicInfo/LoadedFileMetadata.py
================================================
import copy
import logging
import rarfile
from lxml.etree import XMLSyntaxError
from common.models import ComicInfo
from src.Common.LoadedComicInfo.ILoadedComicInfo import ILoadedComicInfo
from src.Common.errors import MissingRarTool
logger = logging.getLogger("LoadedCInfo")
COMICINFO_FILE = 'ComicInfo.xml'
COVER_NAME = "!0000_Cover"
BACKCOVER_NAME = "9999_Back"
_LOG_TAG_WEBP = "Convert Webp"
_LOG_TAG_WRITE_META = 'Write Meta'
_LOG_TAG_RECOMPRESSING = "Recompressing"
move_to_value = ""
class LoadedFileMetadata(ILoadedComicInfo):
_cinfo_object: ComicInfo
original_cinfo_object: ComicInfo
# Used to keep original state after being loaded for the first time. Useful to undo sesion changes
original_cinfo_object_before_session: ComicInfo | None = None
had_metadata_on_open = False
@property
def cinfo_object(self):
return self._cinfo_object
@cinfo_object.setter
def cinfo_object(self, value: ComicInfo):
self._cinfo_object = value
@property
def volume(self):
if self.cinfo_object:
return self.cinfo_object.volume
@property
def chapter(self):
if self.cinfo_object:
return self.cinfo_object.number
@volume.setter
def volume(self, value):
self.cinfo_object.volume = value
@chapter.setter
def chapter(self, value):
self.cinfo_object.number = value
def _load_metadata(self):
"""
Reads the metadata from the ComicInfo.xml at root level
:raises CorruptedComicInfo If the metadata file exists but can't be parsed
:return:
"""
LOG_TAG = f"[{'Reading Meta':13s}] "
try:
# If Comicinfo is not at root try to grab any ComicInfo.xml in the file
if COMICINFO_FILE not in self.archive.namelist():
cinfo_file = [filename.endswith(COMICINFO_FILE) for filename in self.archive.namelist()][
0] or COMICINFO_FILE
else:
cinfo_file = COMICINFO_FILE
self.is_cinfo_at_root = True
xml_string = self.archive.read(cinfo_file).decode('utf-8')
self.has_metad
gitextract_z3pdl1we/ ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows/ │ └── Run_Tests.yml ├── .gitignore ├── BUILD.ps1 ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DEVELOPMENT.MD ├── Dockerfile ├── LICENSE ├── MangaManager/ │ ├── Extensions/ │ │ ├── CoverDownloader/ │ │ │ ├── CoverDownloader.py │ │ │ └── __init__.py │ │ ├── IExtensionApp.py │ │ ├── Template.py │ │ ├── WebpConverter/ │ │ │ ├── WebpConverter.py │ │ │ └── __init__.py │ │ └── __init__.py │ ├── ExternalSources/ │ │ ├── CoverSources/ │ │ │ ├── MangaDex/ │ │ │ │ ├── MangaDex.py │ │ │ │ └── __init__.py │ │ │ └── __init__.py │ │ ├── MetadataSources/ │ │ │ ├── MetadataSourceFactory.py │ │ │ ├── Providers/ │ │ │ │ ├── AniList.py │ │ │ │ ├── ComicVine.py │ │ │ │ ├── MangaUpdates.py │ │ │ │ └── __init__.py │ │ │ └── __init__.py │ │ └── __init__.py │ ├── common/ │ │ ├── __init__.py │ │ └── models/ │ │ ├── AgeRating.py │ │ ├── ComicInfo.py │ │ ├── ComicInfo.xds │ │ ├── ComicInfoTag.py │ │ ├── ComicPageType.py │ │ ├── Manga.py │ │ ├── YesNo.py │ │ └── __init__.py │ ├── logging_setup.py │ ├── main.py │ ├── pyinstaller_hooks/ │ │ └── hook-tkinterdnd2.py │ ├── res/ │ │ └── languages.json │ ├── src/ │ │ ├── Common/ │ │ │ ├── LoadedComicInfo/ │ │ │ │ ├── ArchiveFile.py │ │ │ │ ├── CoverActions.py │ │ │ │ ├── ILoadedComicInfo.py │ │ │ │ ├── LoadedComicInfo.py │ │ │ │ ├── LoadedFileCoverData.py │ │ │ │ ├── LoadedFileMetadata.py │ │ │ │ └── __init__.py │ │ │ ├── ResourceLoader.py │ │ │ ├── __init__.py │ │ │ ├── errors.py │ │ │ ├── naturalsorter.py │ │ │ ├── parser.py │ │ │ ├── progressbar.py │ │ │ ├── terminalcolors.py │ │ │ └── utils.py │ │ ├── DynamicLibController/ │ │ │ ├── __init__.py │ │ │ ├── extension_manager.py │ │ │ └── models/ │ │ │ ├── CoverSourceInterface.py │ │ │ ├── ExtensionsInterface.py │ │ │ ├── IMetadataSource.py │ │ │ └── __init__.py │ │ ├── MetadataManager/ │ │ │ ├── CoverManager/ │ │ │ │ ├── CoverManager.py │ │ │ │ └── __init__.py │ │ │ ├── GUI/ │ │ │ │ ├── ControlManager.py │ │ │ │ ├── ExceptionWindow.py │ │ │ │ ├── FileChooserWindow.py │ │ │ │ ├── MessageBox.py │ │ │ │ ├── OneTimeMessageBox.py │ │ │ │ ├── __init__.py │ │ │ │ ├── scrolledframe.py │ │ │ │ ├── utils.py │ │ │ │ ├── widgets/ │ │ │ │ │ ├── AutocompleteComboboxWidget.py │ │ │ │ │ ├── ButtonWidget.py │ │ │ │ │ ├── CanvasCoverWidget.py │ │ │ │ │ ├── ComboBoxWidget.py │ │ │ │ │ ├── FileMultiSelectWidget.py │ │ │ │ │ ├── FormBundleWidget.py │ │ │ │ │ ├── HyperlinkLabelWidget.py │ │ │ │ │ ├── LongTextWidget.py │ │ │ │ │ ├── MMWidget.py │ │ │ │ │ ├── MessageBoxWidget.py │ │ │ │ │ ├── OptionMenuWidget.py │ │ │ │ │ ├── ProgressBarWidget.py │ │ │ │ │ ├── ScrolledFrameWidget.py │ │ │ │ │ ├── WidgetManager.py │ │ │ │ │ └── __init__.py │ │ │ │ └── windows/ │ │ │ │ ├── AboutWindow.py │ │ │ │ ├── DragAndDrop.py │ │ │ │ ├── LoadingWindow.py │ │ │ │ ├── MainWindow.py │ │ │ │ ├── SettingsWindow.py │ │ │ │ └── __init__.py │ │ │ ├── MetadataManagerCLI.py │ │ │ ├── MetadataManagerGUI.py │ │ │ ├── MetadataManagerLib.py │ │ │ └── __init__.py │ │ ├── Settings/ │ │ │ ├── SettingControl.py │ │ │ ├── SettingControlType.py │ │ │ ├── SettingSection.py │ │ │ ├── Settings.py │ │ │ ├── SettingsDefault.py │ │ │ └── __init__.py │ │ ├── __init__.py │ │ └── __version__.py │ └── tests/ │ ├── Common/ │ │ ├── __init__.py │ │ ├── test_ComicInfo.py │ │ └── test_utils.py │ ├── ExtensionsTests/ │ │ ├── __init__.py │ │ └── test_WebpConverter.py │ ├── ExternalMetadataTests/ │ │ ├── __init__.py │ │ └── test_AniList.py │ ├── LoadedComicInfo/ │ │ ├── __init__.py │ │ ├── test_Covers.py │ │ ├── test_LoadedCInfo.py │ │ ├── test_LoadedCInfo_backup.py │ │ └── test_moveto.py │ ├── MetadataManagerTests/ │ │ ├── GUI/ │ │ │ ├── __init__.py │ │ │ ├── test_MetadataEditorGUI.py │ │ │ ├── test_dinamic_layouts.py │ │ │ └── test_fetch_metadata.py │ │ ├── __init__.py │ │ └── test_MetadataEditorCore.py │ ├── Settings/ │ │ ├── __init__.py │ │ └── test_Settings.py │ ├── __init__.py │ ├── common.py │ ├── data/ │ │ ├── !00_SAMPLE_FILE.CBZ │ │ └── test.py │ └── test_comicinfo.py ├── README.md ├── docker-compose.yml ├── docker-root/ │ ├── config/ │ │ ├── .config/ │ │ │ └── xfce4/ │ │ │ ├── panel/ │ │ │ │ └── launcher-7/ │ │ │ │ └── MM Launcher.desktop │ │ │ └── xfconf/ │ │ │ └── xfce-perchannel-xml/ │ │ │ ├── xfce4-desktop.xml │ │ │ ├── xfce4-panel.xml │ │ │ └── xsettings.xml │ │ ├── Desktop/ │ │ │ ├── MangaManager_23_02_02_Beta_linux_01 │ │ │ ├── covers-folder-link.desktop │ │ │ ├── manga-folder-link.desktop │ │ │ └── manga-manager-link.desktop │ │ └── custom-cont-init.d/ │ │ └── prepare-app-permissions.sh │ └── defaults/ │ ├── autostart │ └── startwm.sh ├── requirements.txt └── sonar-project.properties
SYMBOL INDEX (634 symbols across 84 files)
FILE: MangaManager/Extensions/CoverDownloader/CoverDownloader.py
function get_cover_from_source_dummy (line 5) | def get_cover_from_source_dummy() -> list:
class CoverDownloader (line 9) | class CoverDownloader():#IExtensionApp):
method serve_gui (line 12) | def serve_gui(self):
FILE: MangaManager/Extensions/IExtensionApp.py
class IExtensionApp (line 6) | class IExtensionApp(tkinter.Toplevel, metaclass=abc.ABCMeta):
method __init__ (line 17) | def __init__(self, master, super_=None, **kwargs):
method _initialize (line 36) | def _initialize(self):
method serve_gui (line 48) | def serve_gui(self):
FILE: MangaManager/Extensions/Template.py
class ExtensionTemplate (line 8) | class ExtensionTemplate(IExtensionApp):
method serve_gui (line 11) | def serve_gui(self):
FILE: MangaManager/Extensions/WebpConverter/WebpConverter.py
function start_processing (line 20) | def start_processing(_selected_files,_progress_bar):
function _run_process (line 23) | def _run_process(list_of_files,progress_bar:ProgressBarWidget):
class WebpConverter (line 39) | class WebpConverter(IExtensionApp):
method pb_update (line 50) | def pb_update(self):
method selected_files (line 56) | def selected_files(self):
method process (line 60) | def process(self):
method select_base (line 72) | def select_base(self):
method _on_file (line 76) | def _on_file(self, parent, file):
method _on_folder (line 79) | def _on_folder(self, parent_dic, folder):
method _clear (line 84) | def _clear(self):
method _set_input (line 88) | def _set_input(self):
method preview (line 96) | def preview(self):
method serve_gui (line 109) | def serve_gui(self):
FILE: MangaManager/ExternalSources/CoverSources/MangaDex/MangaDex.py
class MangaDex (line 16) | class MangaDex(ICoverSource):
method parse_identifier (line 20) | def parse_identifier(identifier) -> str:
method get_covers (line 24) | def get_covers(cls, identifier: str) -> list[Cover]:
method parse_input (line 84) | def parse_input(cls, value) -> str:
FILE: MangaManager/ExternalSources/MetadataSources/MetadataSourceFactory.py
class ScraperFactory (line 16) | class ScraperFactory:
method __new__ (line 21) | def __new__(cls):
method __init__ (line 27) | def __init__(self):
method get_scraper (line 30) | def get_scraper(self, setting_name):
FILE: MangaManager/ExternalSources/MetadataSources/Providers/AniList.py
class AniListPerson (line 20) | class AniListPerson(StrEnum):
class AniListSetting (line 28) | class AniListSetting(StrEnum):
class AniList (line 32) | class AniList(IMetadataSource):
method init_settings (line 40) | def init_settings(self):
method save_settings (line 73) | def save_settings(self):
method is_valid_person_tag (line 85) | def is_valid_person_tag(key, value):
method get_manga_id_from_url (line 92) | def get_manga_id_from_url(url):
method _get_id_from_series (line 99) | def _get_id_from_series(cls, cinfo: ComicInfo) -> Optional[int]:
method get_cinfo (line 115) | def get_cinfo(cls, comic_info_from_ui: ComicInfo) -> ComicInfo | None:
method _post (line 156) | def _post(cls, query, variables, logging_info):
method _search_for_manga_title_by_id (line 178) | def _search_for_manga_title_by_id(cls, manga_id, logging_info):
method _search_for_manga_title_by_manga_title (line 200) | def _search_for_manga_title_by_manga_title(cls, manga_title, format_, ...
method search_for_manga_title_by_manga_title_with_adult (line 226) | def search_for_manga_title_by_manga_title_with_adult(cls, manga_title,...
method _search_details_by_series_id (line 249) | def _search_details_by_series_id(cls, series_id, format_, logging_info):
class AniListRateLimit (line 300) | class AniListRateLimit(Exception):
FILE: MangaManager/ExternalSources/MetadataSources/Providers/ComicVine.py
class ComicVine (line 12) | class ComicVine(IMetadataSource, ABC):
method __init__ (line 16) | def __init__(self):
method save_settings (line 28) | def save_settings(self):
method get_cinfo (line 31) | def get_cinfo(self, comic_info_from_ui: ComicInfo) -> ComicInfo | None:
method _search_by_title (line 59) | def _search_by_title(self, series_name, publish_year=""):
method _search_by_issue (line 76) | def _search_by_issue(self, series_name, issue_number):
method _build_url_base (line 79) | def _build_url_base(self, entity):
FILE: MangaManager/ExternalSources/MetadataSources/Providers/MangaUpdates.py
class MangaUpdatesPerson (line 16) | class MangaUpdatesPerson(StrEnum):
class MangaUpdates (line 21) | class MangaUpdates(IMetadataSource):
method init_settings (line 35) | def init_settings(self):
method save_settings (line 46) | def save_settings(self):
method is_valid_person_tag (line 52) | def is_valid_person_tag(key, value):
method get_cinfo (line 60) | def get_cinfo(cls, comic_info_from_ui) -> ComicInfo | None:
method _get_series_id (line 88) | def _get_series_id(cls, search_params, logging_info):
method _get_series_details (line 108) | def _get_series_details(cls, manga_title, logging_info):
FILE: MangaManager/common/__init__.py
function get_invalid_person_tag (line 4) | def get_invalid_person_tag(people: str):
FILE: MangaManager/common/models/AgeRating.py
class AgeRating (line 4) | class AgeRating(str, Enum):
method list (line 22) | def list(cls):
FILE: MangaManager/common/models/ComicInfo.py
class ComicInfo (line 54) | class ComicInfo:
method __init__ (line 102) | def __init__(self):
method set_by_tag_name (line 105) | def set_by_tag_name(self, tag, value):
method get_by_tag_name (line 112) | def get_by_tag_name(self, name) -> str:
method from_xml (line 122) | def from_xml(cls, xml_string):
method to_xml (line 130) | def to_xml(self):
method has_changes (line 152) | def has_changes(self, other):
FILE: MangaManager/common/models/ComicPageType.py
class ComicPageType (line 4) | class ComicPageType(str, Enum):
method list (line 18) | def list(cls): # pragma: no cover
FILE: MangaManager/common/models/Manga.py
class Manga (line 4) | class Manga(str, Enum):
method list (line 11) | def list(cls): # pragma: no cover
FILE: MangaManager/common/models/YesNo.py
class YesNo (line 4) | class YesNo(str, Enum):
method list (line 10) | def list(cls): # pragma: no cover
FILE: MangaManager/logging_setup.py
function trace (line 6) | def trace(self, message, *args, **kws):
function add_trace_level (line 11) | def add_trace_level():
class UmpumpedLogHandler (line 17) | class UmpumpedLogHandler(logging.Handler):
method emit (line 18) | def emit(self, record):
function setup_logging (line 22) | def setup_logging(LOGFILE_PATH,level=logging.DEBUG):
FILE: MangaManager/main.py
class ToolS (line 58) | class ToolS(enum.Enum):
method list (line 64) | def list(cls):
function get_selected_files (line 68) | def get_selected_files(glob_path) -> list[str]:
FILE: MangaManager/src/Common/LoadedComicInfo/ArchiveFile.py
class ArchiveFile (line 7) | class ArchiveFile:
method __init__ (line 15) | def __init__(self, filename, mode='r', password=None):
method __enter__ (line 32) | def __enter__(self):
method __exit__ (line 35) | def __exit__(self, exc_type, exc_value, traceback):
method namelist (line 39) | def namelist(self):
method infolist (line 42) | def infolist(self):
method getinfo (line 45) | def getinfo(self, name):
method read (line 48) | def read(self, name):
method open (line 51) | def open(self, name):
method extract (line 56) | def extract(self, member, path=None, password=None):
method extractall (line 61) | def extractall(self, path=None, members=None, password=None):
FILE: MangaManager/src/Common/LoadedComicInfo/CoverActions.py
class CoverActions (line 4) | class CoverActions(enum.Enum):
FILE: MangaManager/src/Common/LoadedComicInfo/ILoadedComicInfo.py
class ILoadedComicInfo (line 1) | class ILoadedComicInfo:
FILE: MangaManager/src/Common/LoadedComicInfo/LoadedComicInfo.py
class LoadedComicInfo (line 32) | class LoadedComicInfo(LoadedFileMetadata, LoadedFileCoverData, ILoadedCo...
method _logging_extra (line 35) | def _logging_extra(self):
method __init__ (line 38) | def __init__(self, path, comicinfo: ComicInfo = None, load_default_met...
method get_template_values (line 53) | def get_template_values(self) -> dict:
method get_template_filename (line 68) | def get_template_filename(self, input_template: str) -> str|None:
method load_all (line 83) | def load_all(self):
method load_metadata (line 98) | def load_metadata(self):
method write_metadata (line 114) | def write_metadata(self, auto_unmark_changes=False):
method convert_to_webp (line 124) | def convert_to_webp(self):
method _export_metadata (line 128) | def _export_metadata(self) -> str:
method _process (line 132) | def _process(self, write_metadata=False, do_convert_to_webp=False, **_):
method _recompress (line 214) | def _recompress(self, zin, zout, write_metadata, do_convert_webp):
method _move_image (line 310) | def _move_image(zin: zipfile.ZipFile, zout: zipfile.ZipFile, item_: zi...
method _append_image (line 337) | def _append_image(zout, cover_path, is_backcover=False, do_convert_to_...
FILE: MangaManager/src/Common/LoadedComicInfo/LoadedFileCoverData.py
class LoadedFileCoverData (line 29) | class LoadedFileCoverData(ILoadedComicInfo):
method get_cover_cache (line 45) | def get_cover_cache(self, is_backcover=False) -> ImageTk.PhotoImage | ...
method cover_action (line 52) | def cover_action(self):
method cover_action (line 56) | def cover_action(self, value: CoverActions):
method backcover_action (line 66) | def backcover_action(self):
method backcover_action (line 70) | def backcover_action(self, value: CoverActions):
method new_cover_path (line 80) | def new_cover_path(self):
method new_cover_path (line 84) | def new_cover_path(self, path):
method new_backcover_path (line 98) | def new_backcover_path(self):
method new_backcover_path (line 102) | def new_backcover_path(self, path):
method load_cover_info (line 115) | def load_cover_info(self, load_images=True):
method get_cover_image_bytes (line 144) | def get_cover_image_bytes(self, resized=False, back_cover=False) -> IO...
FILE: MangaManager/src/Common/LoadedComicInfo/LoadedFileMetadata.py
class LoadedFileMetadata (line 21) | class LoadedFileMetadata(ILoadedComicInfo):
method cinfo_object (line 30) | def cinfo_object(self):
method cinfo_object (line 34) | def cinfo_object(self, value: ComicInfo):
method volume (line 38) | def volume(self):
method chapter (line 43) | def chapter(self):
method volume (line 48) | def volume(self, value):
method chapter (line 52) | def chapter(self, value):
method _load_metadata (line 55) | def _load_metadata(self):
method reset_metadata (line 100) | def reset_metadata(self):
FILE: MangaManager/src/Common/ResourceLoader.py
class ResourceLoader (line 9) | class ResourceLoader:
method get (line 15) | def get(filename):
FILE: MangaManager/src/Common/errors.py
class NoMetadataFileFound (line 1) | class NoMetadataFileFound(Exception):
method __init__ (line 6) | def __init__(self, cbz_path):
class MangaNotFoundError (line 10) | class MangaNotFoundError(Exception):
method __init__ (line 14) | def __init__(self, source, manga_title):
class EditedCinfoNotSet (line 19) | class EditedCinfoNotSet(RuntimeError):
method __init__ (line 20) | def __init__(self, message=None):
class CorruptedComicInfo (line 24) | class CorruptedComicInfo(Exception):
method __init__ (line 29) | def __init__(self, cbz_path):
class CancelComicInfoLoad (line 33) | class CancelComicInfoLoad(Exception):
method __init__ (line 39) | def __init__(self):
class CancelComicInfoSave (line 43) | class CancelComicInfoSave(Exception):
method __init__ (line 49) | def __init__(self):
class NoFilesSelected (line 53) | class NoFilesSelected(Exception):
method __init__ (line 58) | def __init__(self):
class BadZipFile (line 62) | class BadZipFile(Exception):
method __init__ (line 67) | def __init__(self):
class NoComicInfoLoaded (line 71) | class NoComicInfoLoaded(Exception):
method __init__ (line 76) | def __init__(self, info=""):
class NoModifiedCinfo (line 80) | class NoModifiedCinfo(Exception):
method __init__ (line 85) | def __init__(self):
class FailedBackup (line 89) | class FailedBackup(RuntimeError):
method __init__ (line 94) | def __init__(self):
class MissingRarTool (line 97) | class MissingRarTool(Exception):
method __init__ (line 100) | def __init__(self):
FILE: MangaManager/src/Common/naturalsorter.py
function decompose_path_into_components (line 8) | def decompose_path_into_components(x):
function natsort_key_with_path_support (line 23) | def natsort_key_with_path_support(x):
FILE: MangaManager/src/Common/parser.py
function _parse (line 74) | def _parse(patterns, group, filename):
function parse_volume (line 83) | def parse_volume(filename: str) -> str:
function parse_series (line 88) | def parse_series(filename: str) -> str:
function parse_number (line 93) | def parse_number(filename: str) -> str:
FILE: MangaManager/src/Common/progressbar.py
class RepeatedTimer (line 11) | class RepeatedTimer(object):
method __init__ (line 12) | def __init__(self, interval = 1):
method register_callable (line 26) | def register_callable(self, function: callable):
method unregister_callable (line 34) | def unregister_callable(self, function: callable):
method _run (line 37) | def _run(self):
method _call_hooks (line 42) | def _call_hooks(self):
method start (line 49) | def start(self):
method stop (line 55) | def stop(self):
class ProgressBar (line 62) | class ProgressBar(abc.ABC):
method __init__ (line 70) | def __init__(self):
method set_template (line 87) | def set_template(self,new_value:str):
method label_text (line 92) | def label_text(self):
method percentage (line 101) | def percentage(self):
method update_progress_label (line 104) | def update_progress_label(self):
method _update (line 107) | def _update(self):
method start (line 110) | def start(self,total):
method stop (line 119) | def stop(self):
method increase_processed (line 122) | def increase_processed(self):
method increase_failed (line 128) | def increase_failed(self):
method reset (line 132) | def reset(self):
FILE: MangaManager/src/Common/terminalcolors.py
class TerminalColors (line 1) | class TerminalColors:
FILE: MangaManager/src/Common/utils.py
function remove_text_inside_brackets (line 30) | def remove_text_inside_brackets(text, brackets="()[]"):
function normalize_filename (line 49) | def normalize_filename(filename):
function clean_filename (line 56) | def clean_filename(sourcestring, removestring=" %:/,.\\[]<>*?\""):
function find_chapter (line 77) | def find_chapter(text):
function fetch_chapter (line 85) | def fetch_chapter(text):
function fetch_volume (line 90) | def fetch_volume(text):
function obtain_cover_filename (line 95) | def obtain_cover_filename(file_list) -> (str, str):
function get_new_webp_name (line 129) | def get_new_webp_name(currentName: str) -> str:
function convert_to_webp (line 136) | def convert_to_webp(image_bytes_to_convert: IO[bytes]) -> bytes:
function get_platform (line 152) | def get_platform():
class ShowPathTreeAsDict (line 166) | class ShowPathTreeAsDict:
method display_tree (line 168) | def display_tree(self) -> int:
method __init__ (line 180) | def __init__(self,paths: list, base_path = None):
method _recurse (line 191) | def _recurse(self, parent_dic: dict, breaked_subpath):
method get (line 209) | def get(self):
method on_file (line 212) | def on_file(self, parent_dict: dict, breaked_subpath):
method on_subfolder (line 215) | def on_subfolder(self, parent_dict: dict, subfolder):
method _build_tree (line 218) | def _build_tree(self, parent, data):
function get_elapsed_time (line 229) | def get_elapsed_time(start_time: float) -> str:
function get_estimated_time (line 245) | def get_estimated_time(start_time: float, processed_files: int, total_fi...
function open_folder (line 270) | def open_folder(folder_path, selected_file: str = None):
function get_language_iso_list (line 288) | def get_language_iso_list():
function extract_folder_and_module (line 314) | def extract_folder_and_module(file_path):
function match_pyfiles_with_foldername (line 320) | def match_pyfiles_with_foldername(file_path):
function parse_bool (line 325) | def parse_bool(value: str) -> bool:
FILE: MangaManager/src/DynamicLibController/extension_manager.py
function extract_folder_and_module (line 15) | def extract_folder_and_module(file_path):
function match_pyfiles_with_foldername (line 21) | def match_pyfiles_with_foldername(file_path):
function load_extensions (line 30) | def load_extensions(extensions_directory,) -> list[IExtensionApp]:
FILE: MangaManager/src/DynamicLibController/models/CoverSourceInterface.py
class ICoverSource (line 6) | class ICoverSource(abc.ABC):
method download (line 11) | def download(cls, identifier: str):
method __init__ (line 15) | def __init__(self, master, super_=None, **kwargs):
class Cover (line 27) | class Cover:
FILE: MangaManager/src/DynamicLibController/models/ExtensionsInterface.py
class IMMExtension (line 4) | class IMMExtension(abc.ABC):
method save_settings (line 19) | def save_settings(self):
FILE: MangaManager/src/DynamicLibController/models/IMetadataSource.py
function _merge (line 12) | def _merge(value1, value2):
class MLStripper (line 17) | class MLStripper(HTMLParser):
method __init__ (line 18) | def __init__(self):
method handle_data (line 25) | def handle_data(self, d):
method get_data (line 28) | def get_data(self):
class IMetadataSource (line 32) | class IMetadataSource(IMMExtension):
method get_cinfo (line 42) | def get_cinfo(cls, comic_info_from_ui: ComicInfo) -> ComicInfo:
method save_settings (line 45) | def save_settings(self):
method trim (line 52) | def trim(value):
method update_people_from_mapping (line 59) | def update_people_from_mapping(people: list[object], mapping, comicinf...
method clean_description (line 80) | def clean_description(summary: str, remove_source: bool) -> str:
method init_settings (line 108) | def init_settings(self):
method __init__ (line 124) | def __init__(self):
FILE: MangaManager/src/MetadataManager/CoverManager/CoverManager.py
class ComicFrame (line 29) | class ComicFrame(CoverFrame):
method __init__ (line 30) | def __init__(self, master, loaded_cinfo: LoadedComicInfo):
class CoverManager (line 130) | class CoverManager(tkinter.Toplevel):
method __init__ (line 136) | def __init__(self, master, super_: GUIApp = None, **kwargs):
method redraw (line 163) | def redraw(self, event):
method exit_btn (line 209) | def exit_btn(self):
method serve_gui (line 214) | def serve_gui(self):
method select_frame (line 295) | def select_frame(self, _, frame: ComicFrame, pos: str):
method run_bulk_action (line 321) | def run_bulk_action(self, action: CoverActions):
method clear_selection (line 376) | def clear_selection(self):
method select_similar (line 394) | def select_similar(self):
method _scan_images (line 422) | def _scan_images(self, x, lcinfo:LoadedComicInfo, comicframe, is_backc...
method _compare_images (line 438) | def _compare_images(self, x, compared_image, comicframe, pos):
method compare_image (line 445) | def compare_image(x, y, delta:float):
FILE: MangaManager/src/MetadataManager/GUI/ControlManager.py
class ControlManager (line 9) | class ControlManager:
method add (line 15) | def add(self, widget: tkinter.Widget):
method append (line 18) | def append(self, widget: tkinter.Widget):
method toggle (line 21) | def toggle(self, enabled=True):
method lock (line 28) | def lock(self):
method unlock (line 31) | def unlock(self):
FILE: MangaManager/src/MetadataManager/GUI/ExceptionWindow.py
class ExceptionHandler (line 13) | class ExceptionHandler(logging.Handler):
method __init__ (line 14) | def __init__(self, tree_widget):
method emit (line 18) | def emit(self, record):
class ExceptionFrame (line 35) | class ExceptionFrame(Frame):
method __init__ (line 36) | def __init__(self, master=None, is_test=False, **kwargs):
method update_handler_level (line 66) | def update_handler_level(self,*args):
method clear_treeview (line 70) | def clear_treeview(self):
method __del__ (line 74) | def __del__(self):
FILE: MangaManager/src/MetadataManager/GUI/FileChooserWindow.py
class DummyFile (line 14) | class DummyFile:
method __str__ (line 16) | def __str__(self):
class TreeAutocompleteCombobox (line 19) | class TreeAutocompleteCombobox(ttk.Combobox):
method set_completion_list (line 20) | def set_completion_list(self,path, completion_list):
method autocomplete (line 32) | def autocomplete(self, delta=0):
method handle_keyrelease (line 56) | def handle_keyrelease(self, event):
class TreeviewExplorerWidget (line 75) | class TreeviewExplorerWidget(ttk.Treeview):
method __init__ (line 77) | def __init__(self,master, *_, **__):
method nothing (line 90) | def nothing(self, *event):
method _on_select (line 94) | def _on_select(self,*args):
method clear (line 102) | def clear(self):
method show_nested_items (line 108) | def show_nested_items(self, current_path,glob="*.cbz"):
class FileChooser (line 126) | class FileChooser(tkinter.Toplevel):
method __init__ (line 127) | def __init__(self, parent, initialdir=None,*_, **__):
method update_suggestions (line 176) | def update_suggestions(self):
method change_to_entry (line 184) | def change_to_entry(self, *_):
method clear_search_chilren (line 197) | def clear_search_chilren(self, *_):
method update_search_bar (line 206) | def update_search_bar(self, new_path):
method on_treeview_select (line 240) | def on_treeview_select(self, *_):
method get_selection (line 258) | def get_selection(self):
method get_selected_files (line 262) | def get_selected_files(self, *_):
method exit_btn (line 268) | def exit_btn(self):
function askopenfiles (line 273) | def askopenfiles(parent, *args, **kwargs):
function askdirectory (line 281) | def askdirectory(*args, **kwargs):
FILE: MangaManager/src/MetadataManager/GUI/MessageBox.py
class MessageBoxWidgetFactory (line 7) | class MessageBoxWidgetFactory:
method yes_or_no (line 13) | def yes_or_no(parent, title, description):
method showerror (line 22) | def showerror(parent, title, description):
method showwarning (line 32) | def showwarning(parent, title, description):
method get_onetime_messagebox (line 41) | def get_onetime_messagebox() -> Type[OneTimeMessageBox]:
method get_box_button (line 45) | def get_box_button() -> Type[MessageBoxButton]:
FILE: MangaManager/src/MetadataManager/GUI/OneTimeMessageBox.py
class OneTimeMessageBox (line 10) | class OneTimeMessageBox(MessageBoxWidget):
method __new__ (line 14) | def __new__(cls, mb_id, *args, **kwargs):
method __init__ (line 24) | def __init__(self, mb_id=None, *args, **kwargs):
method with_dontshowagain (line 29) | def with_dontshowagain(self):
method prompt (line 34) | def prompt(self):
FILE: MangaManager/src/MetadataManager/GUI/scrolledframe.py
function bindings (line 13) | def bindings(widget, seq):
function _funcid (line 17) | def _funcid(binding):
function remove_binding (line 21) | def remove_binding(widget, seq, index=None, funcid=None):
class ApplicationLevelBindManager (line 49) | class ApplicationLevelBindManager(object):
method on_mousewheel (line 55) | def on_mousewheel(event):
method mousewheel_bind (line 60) | def mousewheel_bind(widget):
method mousewheel_unbind (line 64) | def mousewheel_unbind():
method init_mousewheel_binding (line 68) | def init_mousewheel_binding(master):
method make_onmousewheel_cb (line 85) | def make_onmousewheel_cb(widget, orient, factor=1):
class ScrolledFrame (line 117) | class ScrolledFrame(ttk.Frame):
method __init__ (line 126) | def __init__(self, master=None, **kw):
method reposition (line 173) | def reposition(self):
method xview (line 180) | def xview(self, mode=None, value=None, units=None):
method yview (line 201) | def yview(self, mode=None, value=None, units=None):
method _reposition (line 220) | def _reposition(self, *_):
method _getxview (line 223) | def _getxview(self):
method _getyview (line 251) | def _getyview(self):
method _scrollBothNow (line 282) | def _scrollBothNow(self):
method _toggleHorizScrollbar (line 323) | def _toggleHorizScrollbar(self):
method _toggleVertScrollbar (line 332) | def _toggleVertScrollbar(self):
method configure (line 341) | def configure(self, cnf=None, **kw):
method cget (line 353) | def cget(self, key):
method _configure_mousewheel (line 361) | def _configure_mousewheel(self):
FILE: MangaManager/src/MetadataManager/GUI/utils.py
function validate_int (line 5) | def validate_int(value) -> bool:
function center (line 15) | def center(win):
FILE: MangaManager/src/MetadataManager/GUI/widgets/AutocompleteComboboxWidget.py
class AutocompleteComboboxWidget (line 6) | class AutocompleteComboboxWidget(MMWidget):
method __init__ (line 7) | def __init__(self, master, cinfo_name, label_text=None, default_values...
method autocomplete (line 25) | def autocomplete(self, delta=0):
method handle_keyrelease (line 49) | def handle_keyrelease(self, event):
FILE: MangaManager/src/MetadataManager/GUI/widgets/ButtonWidget.py
class ButtonWidget (line 5) | class ButtonWidget(tkinter.Button):
method __init__ (line 6) | def __init__(self, tooltip=None,image=None, *args, **kwargs):
FILE: MangaManager/src/MetadataManager/GUI/widgets/CanvasCoverWidget.py
class CanvasCoverWidget (line 24) | class CanvasCoverWidget(Canvas):
class CoverFrame (line 33) | class CoverFrame(Frame):
method get_canvas (line 43) | def get_canvas(self, cover_else_backcover: bool = True) -> CanvasCover...
method get_cinfo_cover_data (line 49) | def get_cinfo_cover_data(self):
method resized (line 52) | def resized(self, event: Event):
method __init__ (line 66) | def __init__(self, master):
method cover_action (line 173) | def cover_action(self, loaded_cinfo: LoadedComicInfo = None, auto_trig...
method backcover_action (line 223) | def backcover_action(self, loaded_cinfo: LoadedComicInfo = None, auto_...
method clear (line 275) | def clear(self):
method update_cover_image (line 290) | def update_cover_image(self, loadedcomicinfo_list: list[LoadedComicInf...
method hide_actions (line 326) | def hide_actions(self):
method display_action (line 333) | def display_action(self, _: str = None):
method hide_back_image (line 354) | def hide_back_image(self):
method show_back_image (line 358) | def show_back_image(self):
method opencovers (line 362) | def opencovers(self):
method display_next_cover (line 365) | def display_next_cover(self, event):
method toggle_action_buttons (line 368) | def toggle_action_buttons(self, enabled=True):
FILE: MangaManager/src/MetadataManager/GUI/widgets/ComboBoxWidget.py
class ComboBoxWidget (line 7) | class ComboBoxWidget(MMWidget):
method __init__ (line 8) | def __init__(self, master, cinfo_name:str, label_text=None, default_va...
FILE: MangaManager/src/MetadataManager/GUI/widgets/FileMultiSelectWidget.py
class FileMultiSelectWidget (line 11) | class FileMultiSelectWidget(Treeview):
method __init__ (line 12) | def __init__(self, *args, **kwargs):
method clear (line 29) | def clear(self):
method select_all (line 32) | def select_all(self, *_):
method get_selected (line 36) | def get_selected(self) -> list[LoadedComicInfo]:
method insert (line 39) | def insert(self, loaded_cinfo: LoadedComicInfo, *args, **kwargs):
method _on_select (line 46) | def _on_select(self, *_):
method add_hook_item_selected (line 57) | def add_hook_item_selected(self, function: callable):
method add_hook_item_inserted (line 60) | def add_hook_item_inserted(self, function: callable):
method _call_hook_item_selected (line 63) | def _call_hook_item_selected(self, loaded_cinfo_list: list[LoadedComic...
method _call_hook_item_inserted (line 66) | def _call_hook_item_inserted(self, loaded_comicinfo: LoadedComicInfo):
method popup (line 69) | def popup(self, event):
method open_in_explorer (line 85) | def open_in_explorer(self, event=None):
method reset_loadedcinfo_changes (line 88) | def reset_loadedcinfo_changes(self, event=None):
method _run_hook (line 92) | def _run_hook(source: list[callable], *args):
FILE: MangaManager/src/MetadataManager/GUI/widgets/FormBundleWidget.py
class FormBundleWidget (line 8) | class FormBundleWidget(Frame):
method __init__ (line 22) | def __init__(self, master, mapper_fn,name=None, *_, **kwargs):
method with_label (line 35) | def with_label(self, title, tooltip=""):
method with_input (line 43) | def with_input(self, control: SettingControl, section: SettingSection):
method build (line 52) | def build(self):
method validate (line 62) | def validate(self):
method format_output (line 77) | def format_output(self):
FILE: MangaManager/src/MetadataManager/GUI/widgets/HyperlinkLabelWidget.py
class HyperlinkLabelWidget (line 5) | class HyperlinkLabelWidget(Frame):
method __init__ (line 6) | def __init__(self, master=None, text="", url="", url_text=None, **kwar...
method open_url (line 18) | def open_url(self):
method set_text (line 21) | def set_text(self, text):
method set_url (line 24) | def set_url(self, url):
FILE: MangaManager/src/MetadataManager/GUI/widgets/LongTextWidget.py
class LongTextWidget (line 7) | class LongTextWidget(MMWidget):
method __init__ (line 8) | def __init__(self, master, cinfo_name, label_text=None, width: int = N...
FILE: MangaManager/src/MetadataManager/GUI/widgets/MMWidget.py
class _LongText (line 9) | class _LongText:
method __init__ (line 18) | def __init__(self, name=None):
method set (line 22) | def set(self, value: str):
method clear (line 34) | def clear(self):
method get (line 44) | def get(self) -> str:
method __str__ (line 54) | def __str__(self):
class MMWidget (line 57) | class MMWidget(Frame):
method __init__ (line 64) | def __init__(self, master,name):
method set (line 67) | def set(self, value):
method set_default (line 79) | def set_default(self):
method get (line 82) | def get(self):
method pack (line 85) | def pack(self, **kwargs):
method grid (line 92) | def grid(self, row=None, column=None, **kwargs):
method set_label (line 99) | def set_label(self, text, tooltip=None):
FILE: MangaManager/src/MetadataManager/GUI/widgets/MessageBoxWidget.py
class MessageBoxButton (line 7) | class MessageBoxButton:
method __init__ (line 8) | def __init__(self, id, title):
class MessageBoxWidget (line 13) | class MessageBoxWidget(Toplevel):
method __init__ (line 24) | def __init__(self, *args, **kwargs):
method with_icon (line 63) | def with_icon(self, icon_path):
method with_title (line 73) | def with_title(self, title):
method with_description (line 85) | def with_description(self, description):
method with_content (line 90) | def with_content(self, content_frame: Frame):
method with_actions (line 103) | def with_actions(self, action_buttons: list[MessageBoxButton]):
method build (line 117) | def build(self):
method prompt (line 124) | def prompt(self):
method _set_selected_value (line 136) | def _set_selected_value(self, value):
FILE: MangaManager/src/MetadataManager/GUI/widgets/OptionMenuWidget.py
class OptionMenuWidget (line 11) | class OptionMenuWidget(MMWidget):
method __init__ (line 12) | def __init__(self, master: tkinter.Frame, cinfo_name, label_text=None,...
method update_listed_values (line 32) | def update_listed_values(self, default_selected, values) -> None:
method get_options (line 36) | def get_options(self) -> list[str]:
method append_first (line 51) | def append_first(self, value: str):
method remove_first (line 54) | def remove_first(self):
FILE: MangaManager/src/MetadataManager/GUI/widgets/ProgressBarWidget.py
class ProgressBarWidget (line 10) | class ProgressBarWidget(ProgressBar):
method __init__ (line 11) | def __init__(self, parent):
method update_progress_label (line 49) | def update_progress_label(self):
method _update (line 52) | def _update(self):
FILE: MangaManager/src/MetadataManager/GUI/widgets/ScrolledFrameWidget.py
class ScrolledFrameWidget (line 7) | class ScrolledFrameWidget(ScrolledFrame):
method __init__ (line 8) | def __init__(self, master, *_, **kwargs):
method create_frame (line 15) | def create_frame(self, **kwargs):
FILE: MangaManager/src/MetadataManager/GUI/widgets/WidgetManager.py
class WidgetManager (line 6) | class WidgetManager:
method get_widget (line 9) | def get_widget(self, name) -> ComboBoxWidget | LongTextWidget | Option...
method add_widget (line 12) | def add_widget(self, name, widget_frame: ComboBoxWidget | LongTextWidg...
method __setattr__ (line 16) | def __setattr__(self, key, value):
method clean_widgets (line 20) | def clean_widgets(self):
method toggle_widgets (line 27) | def toggle_widgets(self, enabled=True):
method get_tags (line 38) | def get_tags(self):
FILE: MangaManager/src/MetadataManager/GUI/windows/AboutWindow.py
function get_release_tag (line 14) | def get_release_tag() -> Versions:
class AboutWindow (line 58) | class AboutWindow:
method __init__ (line 62) | def __init__(self, parent):
method close (line 100) | def close(self):
FILE: MangaManager/src/MetadataManager/GUI/windows/DragAndDrop.py
function extract_paths_from_string (line 4) | def extract_paths_from_string(input_string):
class DragAndDropFilesApp (line 6) | class DragAndDropFilesApp(TkinterDnD.Tk):
method __init__ (line 7) | def __init__(self):
method on_drop (line 22) | def on_drop(self, event):
FILE: MangaManager/src/MetadataManager/GUI/windows/LoadingWindow.py
class LoadingWindow (line 9) | class LoadingWindow(tkinter.Toplevel):
method __new__ (line 12) | def __new__(cls, total, *args, **kwargs):
method __init__ (line 22) | def __init__(self, total):
method is_abort (line 52) | def is_abort(self):
method set_abort (line 54) | def set_abort(self,*_):
method loaded_file (line 63) | def loaded_file(self, value: str):
method finish_loading (line 68) | def finish_loading(self):
FILE: MangaManager/src/MetadataManager/GUI/windows/MainWindow.py
class MainWindow (line 24) | class MainWindow(GUIApp):
method __init__ (line 32) | def __init__(self):
method display_side_bar (line 62) | def display_side_bar(self) -> None:
method display_menu_bar (line 91) | def display_menu_bar(self) -> None:
method init_main_content_frame (line 136) | def init_main_content_frame(self) -> None:
method display_main_content_widgets (line 171) | def display_main_content_widgets(self) -> None:
method display_bottom_frame (line 338) | def display_bottom_frame(self):
method on_file_selection_preview (line 353) | def on_file_selection_preview(self, *args):
method on_drop (line 369) | def on_drop(self,event):
FILE: MangaManager/src/MetadataManager/GUI/windows/SettingsWindow.py
function template_validation (line 27) | def template_validation(key_list):
function populate_default_settings (line 72) | def populate_default_settings():
class SettingsWindow (line 94) | class SettingsWindow:
method __init__ (line 95) | def __init__(self, parent):
method build_setting_entry (line 163) | def build_setting_entry(self, parent_frame, control: SettingControl, s...
method build_setting_entries (line 174) | def build_setting_entries(self, parent_frame, settings, section):
method save_settings (line 178) | def save_settings(self):
method setting_control_to_widget (line 203) | def setting_control_to_widget(parent_frame: tkinter.Frame, control: Se...
FILE: MangaManager/src/MetadataManager/MetadataManagerCLI.py
function prompt_autocomplete (line 21) | def prompt_autocomplete():
function grouper (line 30) | def grouper(n, iterable, fillvalue=None):
class bcolors (line 37) | class bcolors:
function _ (line 54) | def _(event: prompt_toolkit.key_binding.KeyPressEvent):
function _ (line 61) | def _(event: prompt_toolkit.key_binding.KeyPressEvent):
function _ (line 70) | def _(event: prompt_toolkit.key_binding.KeyPressEvent):
class App (line 87) | class App(MetadataManagerLib):
method on_processed_item (line 88) | def on_processed_item(self, loaded_info: LoadedComicInfo):
method on_manga_not_found (line 91) | def on_manga_not_found(self, exception, series_name):
method __init__ (line 94) | def __init__(self, file_paths: list[str]):
method _parse_lcinfo_list_to_gui (line 102) | def _parse_lcinfo_list_to_gui(self, loaded_cinfo_list) -> ComicInfo:
method serve_ui (line 124) | def serve_ui(self):
method restart (line 232) | def restart(self):
method clear (line 235) | def clear(self):
method quit (line 239) | def quit(self):
method process (line 244) | def process(self):
method tree_selected (line 254) | def tree_selected(self) -> int:
method _is_valid_tool (line 260) | def _is_valid_tool(self, value):
method on_badzipfile_error (line 264) | def on_badzipfile_error(self, exception, file_path):
method on_corruped_metadata_error (line 267) | def on_corruped_metadata_error(self, exception, loaded_info: LoadedCom...
method on_writing_error (line 270) | def on_writing_error(self, exception, loaded_info: LoadedComicInfo):
method on_writing_exception (line 273) | def on_writing_exception(self, exception, loaded_info: LoadedComicInfo):
FILE: MangaManager/src/MetadataManager/MetadataManagerGUI.py
class GUIApp (line 32) | class GUIApp(Tk, MetadataManagerLib):
method __init__ (line 45) | def __init__(self):
method report_callback_exception (line 97) | def report_callback_exception(self, *_):
method cinfo_tags (line 106) | def cinfo_tags(self):
method selected_items (line 110) | def selected_items(self):
method select_files (line 121) | def select_files(self):
method select_folder (line 145) | def select_folder(self):
method load_selected_files (line 162) | def load_selected_files(self,new_selection:list=None,is_event_dragdrop...
method show_settings (line 190) | def show_settings(self):
method show_about (line 193) | def show_about(self):
method are_unsaved_changes (line 196) | def are_unsaved_changes(self, exist_unsaved_changes=False):
method update_item_saved_status (line 206) | def update_item_saved_status(self, loaded_cinfo):
method show_not_saved_indicator (line 218) | def show_not_saved_indicator(self, loaded_cinfo_list=None):
method on_item_loaded (line 238) | def on_item_loaded(self, loaded_cinfo: LoadedComicInfo, cursor, total)...
method on_processed_item (line 255) | def on_processed_item(self, loaded_info: LoadedComicInfo):
method on_badzipfile_error (line 260) | def on_badzipfile_error(self, exception, file_path: LoadedComicInfo): ...
method on_writing_exception (line 267) | def on_writing_exception(self, exception, loaded_info: LoadedComicInfo...
method on_writing_error (line 273) | def on_writing_error(self, exception, loaded_info: LoadedComicInfo): ...
method on_corruped_metadata_error (line 278) | def on_corruped_metadata_error(self, exception, loaded_info: LoadedCom...
method on_manga_not_found (line 284) | def on_manga_not_found(self, exception, series_name): # pragma: no cover
method on_missing_rar_tools (line 288) | def on_missing_rar_tools(self, exception):
method _serialize_cinfolist_to_gui (line 300) | def _serialize_cinfolist_to_gui(self, loaded_cinfo_list=None):
method _serialize_gui_to_cinfo (line 345) | def _serialize_gui_to_cinfo(self) -> ComicInfo:
method process_gui_update (line 375) | def process_gui_update(self, old_selection: list[LoadedComicInfo], new...
method fill_from_filename (line 385) | def fill_from_filename(self) -> None:
method pre_process (line 429) | def pre_process(self) -> None:
method _fill_filename (line 451) | def _fill_filename(self):
method _fill_foldername (line 455) | def _fill_foldername(self):
method _treeview_open_explorer (line 469) | def _treeview_open_explorer(self, file):
method _treview_reset (line 473) | def _treview_reset(self, event=None):
method display_extensions (line 476) | def display_extensions(self, parent_frame):
method process_fetch_online (line 482) | def process_fetch_online(self, *_):
method clean_selected (line 508) | def clean_selected(self):
FILE: MangaManager/src/MetadataManager/MetadataManagerLib.py
class _IMetadataManagerLib (line 19) | class _IMetadataManagerLib(abc.ABC):
method on_item_loaded (line 20) | def on_item_loaded(self, loaded_cinfo: LoadedComicInfo,cursor,total):
method on_badzipfile_error (line 27) | def on_badzipfile_error(self, exception, file_path):
method on_processed_item (line 33) | def on_processed_item(self, loaded_info: LoadedComicInfo):
method on_corruped_metadata_error (line 39) | def on_corruped_metadata_error(self, exception, loaded_info: LoadedCom...
method on_writing_error (line 45) | def on_writing_error(self, exception, loaded_info: LoadedComicInfo):
method on_writing_exception (line 52) | def on_writing_exception(self, exception, loaded_info: LoadedComicInfo):
method on_manga_not_found (line 58) | def on_manga_not_found(self, exception, series_name):
method on_missing_rar_tools (line 63) | def on_missing_rar_tools(self,exception):
class MetadataManagerLib (line 68) | class MetadataManagerLib(_IMetadataManagerLib, ABC):
method loaded_cinfo_list_to_process (line 90) | def loaded_cinfo_list_to_process(self) -> list[LoadedComicInfo]:
method process (line 93) | def process(self):
method merge_changed_metadata (line 128) | def merge_changed_metadata(self, loaded_cinfo_list: list[LoadedComicIn...
method open_cinfo_list (line 176) | def open_cinfo_list(self, abort_load_check:callable,append_items=False...
method preview_export (line 235) | def preview_export(self, loaded_cinfo):
method fetch_online (line 243) | def fetch_online(self, partial_comic_info):
FILE: MangaManager/src/MetadataManager/__init__.py
function load_extensions (line 14) | def load_extensions():
function execute_gui (line 21) | def execute_gui():
FILE: MangaManager/src/Settings/SettingControl.py
class SettingControl (line 7) | class SettingControl(abc.ABC):
method __init__ (line 22) | def __init__(self, key, name, control_type, value='', tooltip='', vali...
method set_values (line 34) | def set_values(self, values):
FILE: MangaManager/src/Settings/SettingControlType.py
class SettingControlType (line 4) | class SettingControlType(Enum):
FILE: MangaManager/src/Settings/SettingSection.py
class SettingSection (line 4) | class SettingSection:
method __init__ (line 11) | def __init__(self, name, key, values=None):
method get_control (line 18) | def get_control(self, key):
FILE: MangaManager/src/Settings/Settings.py
class Settings (line 11) | class Settings:
method config_file (line 18) | def config_file(self):
method __new__ (line 20) | def __new__(cls):
method __init__ (line 31) | def __init__(self):
method save (line 42) | def save(self):
method load (line 47) | def load(self,override_settings_from=None):
method get (line 63) | def get(self, section, key):
method set_default (line 77) | def set_default(self, section, key, value):
method get_default (line 83) | def get_default(self, section, key, default_value):
method set (line 90) | def set(self, section, key, value):
method _create_section (line 97) | def _create_section(self, section):
method _load_test (line 100) | def _load_test(self):
FILE: MangaManager/src/Settings/SettingsDefault.py
class SettingHeading (line 4) | class SettingHeading(StrEnum):
FILE: MangaManager/tests/Common/test_ComicInfo.py
class ComicInfoTests (line 7) | class ComicInfoTests(unittest.TestCase):
method test_sample_xml_isvalid (line 8) | def test_sample_xml_isvalid(self):
method test_valid_xml (line 14) | def test_valid_xml(self):
FILE: MangaManager/tests/Common/test_utils.py
class MyTestCase (line 7) | class MyTestCase(unittest.TestCase):
method test_update_people_from_mapping (line 8) | def test_update_people_from_mapping(self):
FILE: MangaManager/tests/ExternalMetadataTests/test_AniList.py
class TestSources (line 6) | class TestSources(unittest.TestCase):
method test_AnilistReturnMatches (line 7) | def test_AnilistReturnMatches(self):
method test_AnilistReturnMatches_url (line 19) | def test_AnilistReturnMatches_url(self):
FILE: MangaManager/tests/LoadedComicInfo/test_Covers.py
class LoadedCInfo_Utils (line 12) | class LoadedCInfo_Utils(unittest.TestCase):
method test_CoverParsing (line 13) | def test_CoverParsing(self):
class CoverHandling_Recompressing_Tests (line 29) | class CoverHandling_Recompressing_Tests(CBZManipulationTests):
method setUp (line 30) | def setUp(self) -> None:
method test_delete_cover (line 40) | def test_delete_cover(self):
method test_delete_backcover (line 51) | def test_delete_backcover(self):
method test_append_cover (line 62) | def test_append_cover(self):
method test_append_backcover (line 74) | def test_append_backcover(self):
method test_replace_cover (line 88) | def test_replace_cover(self):
method test_replace_backcover (line 104) | def test_replace_backcover(self):
FILE: MangaManager/tests/LoadedComicInfo/test_LoadedCInfo.py
class LoadedComicInfo_MetadataTests (line 41) | class LoadedComicInfo_MetadataTests(unittest.TestCase):
method setUp (line 49) | def setUp(self) -> None:
method tearDown (line 87) | def tearDown(self) -> None:
method test_simple_read (line 97) | def test_simple_read(self):
method test_simple_write (line 104) | def test_simple_write(self):
method test_simple_backup (line 126) | def test_simple_backup(self):
method test_simple_backup_nometadata (line 143) | def test_simple_backup_nometadata(self):
FILE: MangaManager/tests/LoadedComicInfo/test_LoadedCInfo_backup.py
class LoadedComicInfo_SaveTests (line 41) | class LoadedComicInfo_SaveTests(unittest.TestCase):
method setUp (line 49) | def setUp(self) -> None:
method tearDown (line 79) | def tearDown(self) -> None:
FILE: MangaManager/tests/LoadedComicInfo/test_moveto.py
class MoveToTemplate (line 7) | class MoveToTemplate(unittest.TestCase):
method test_template (line 8) | def test_template(self):
FILE: MangaManager/tests/MetadataManagerTests/GUI/test_MetadataEditorGUI.py
class UiToCinfoTest (line 29) | class UiToCinfoTest(TKinterTestCase):
method setUp (line 32) | def setUp(self) -> None:
method tearDown (line 38) | def tearDown(self) -> None:
method test_all_ui_fields_loaded (line 48) | def test_all_ui_fields_loaded(self):
method test_all_fields_map_to_cinfo (line 58) | def test_all_fields_map_to_cinfo(self):
method test_full_flow (line 96) | def test_full_flow(self):
class CinfoToUiTest (line 130) | class CinfoToUiTest(TKinterTestCase):
method setUp (line 133) | def setUp(self) -> None:
method tearDown (line 143) | def tearDown(self) -> None:
method test_one_field_empty_should_not_be_overwritten_by_data_from_other_cinfo_with_field_filled (line 157) | def test_one_field_empty_should_not_be_overwritten_by_data_from_other_...
class BulkLoadingTest (line 193) | class BulkLoadingTest(TKinterTestCase):
method setUp (line 195) | def setUp(self) -> None:
method tearDown (line 203) | def tearDown(self) -> None:
method test_bulk_selection (line 217) | def test_bulk_selection(self):
class GenericUITest (line 249) | class GenericUITest(TKinterTestCase):
method setUp (line 250) | def setUp(self):
method test_settings_window_correctly_displayed (line 254) | def test_settings_window_correctly_displayed(self):
FILE: MangaManager/tests/MetadataManagerTests/GUI/test_dinamic_layouts.py
class DinamicLayoutTests (line 21) | class DinamicLayoutTests(TKinterTestCase):
method setUp (line 24) | def setUp(self) -> None:
method tearDown (line 35) | def tearDown(self) -> None:
method test_all_fields_are_populated (line 49) | def test_all_fields_are_populated(self):
FILE: MangaManager/tests/MetadataManagerTests/GUI/test_fetch_metadata.py
class FetchMetadataFlowTest (line 24) | class FetchMetadataFlowTest(TKinterTestCase):
method test_fetch_online_button_flow (line 26) | def test_fetch_online_button_flow(self):
FILE: MangaManager/tests/MetadataManagerTests/test_MetadataEditorCore.py
class CoreTesting (line 18) | class CoreTesting(unittest.TestCase):
method setUp (line 22) | def setUp(self) -> None:
method tearDown (line 28) | def tearDown(self) -> None:
method test (line 39) | def test(self):
method test_selected_files_loaded (line 91) | def test_selected_files_loaded(self):
method test_process_should_raise_exception_if_no_new_cinfo (line 99) | def test_process_should_raise_exception_if_no_new_cinfo(self):
class ErrorHandlingTests (line 106) | class ErrorHandlingTests(unittest.TestCase):
method setUp (line 112) | def setUp(self) -> None:
method tearDown (line 118) | def tearDown(self) -> None:
method test_load_files_should_handle_broken_zipfile (line 130) | def test_load_files_should_handle_broken_zipfile(self):
method test_on_badzipfile_error (line 150) | def test_on_badzipfile_error(self):
method test_on_writing_error (line 172) | def test_on_writing_error(self):
method test_on_writing_exception (line 197) | def test_on_writing_exception(self):
FILE: MangaManager/tests/Settings/test_Settings.py
class SettingsTest (line 7) | class SettingsTest(unittest.TestCase):
method tearDown (line 9) | def tearDown(self):
method test_Settings_will_create_if_nothing_on_disk (line 14) | def test_Settings_will_create_if_nothing_on_disk(self):
method test_Settings_will_set_values (line 18) | def test_Settings_will_set_values(self):
method test_Settings_will_write_default_tag_if_not_exists (line 26) | def test_Settings_will_write_default_tag_if_not_exists(self):
FILE: MangaManager/tests/common.py
function create_dummy_files (line 17) | def create_dummy_files(nfiles):
function create_test_cbz (line 27) | def create_test_cbz(nfiles, nimages=4, loaded_cinfo: LoadedComicInfo = N...
class CBZManipulationTests (line 46) | class CBZManipulationTests(unittest.TestCase):
method setUp (line 50) | def setUp(self) -> None:
method tearDown (line 56) | def tearDown(self) -> None:
class TKinterTestCase (line 70) | class TKinterTestCase(unittest.TestCase):
method setUp (line 76) | def setUp(self):
method tearDown (line 79) | def tearDown(self):
method pump_events (line 87) | def pump_events(self):
function parameterized_class (line 92) | def parameterized_class(attrs, input_values=None, classname_func=None, ...
function custom_get_item (line 157) | def custom_get_item(key):
class CustomConfigParser1 (line 164) | class CustomConfigParser1(configparser.ConfigParser):
method __getitem__ (line 165) | def __getitem__(self, key):
class CustomConfigParser2 (line 172) | class CustomConfigParser2(configparser.ConfigParser):
method read (line 173) | def read(self, filenames, *args, **kwargs):
function is_valid_xml (line 185) | def is_valid_xml(xml:str) -> bool:
FILE: MangaManager/tests/test_comicinfo.py
class LoadedCInfo_Utils (line 6) | class LoadedCInfo_Utils(unittest.TestCase):
method test_ComicInfo_ToList_methods_work (line 7) | def test_ComicInfo_ToList_methods_work(self):
Condensed preview — 146 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (536K chars).
[
{
"path": ".github/FUNDING.yml",
"chars": 777,
"preview": "# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [u"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 833,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the b"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 624,
"preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: \"[Feature Request]\"\nlabels: Feature Request\nass"
},
{
"path": ".github/workflows/Run_Tests.yml",
"chars": 5984,
"preview": "# This workflow file will install Python dependencies,\n# create a desktop, and test the application's GUI on multiple ve"
},
{
"path": ".gitignore",
"chars": 4954,
"preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packagi"
},
{
"path": "BUILD.ps1",
"chars": 3227,
"preview": "##$repoName = \"Manga-Manager\"\n##$ownerName = \"MangaManagerOrg\"\n### Get the latest release\n##$latestRelease = Invoke-Rest"
},
{
"path": "CHANGELOG.md",
"chars": 1201,
"preview": "## 1.0.0-beta.1\n\n### Features\n\n* Select multiple files and preview the covers of each file.\n* Select a folder and open f"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 5225,
"preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participa"
},
{
"path": "CONTRIBUTING.md",
"chars": 3331,
"preview": "# Contributing guidelines\n\nWe welcome any kind of contribution to our software, from simple comment or question to a ful"
},
{
"path": "DEVELOPMENT.MD",
"chars": 586,
"preview": "\n## Versioning and building\nWhen a build is to be made, copy the short hash of last commit and update it in `src/__versi"
},
{
"path": "Dockerfile",
"chars": 1157,
"preview": "FROM ghcr.io/linuxserver/baseimage-rdesktop-web:jammy\n\nENV DEBIAN_FRONTEND=noninteractive\nENV UID=1000\nENV GID=1000\nENV "
},
{
"path": "LICENSE",
"chars": 35149,
"preview": " GNU GENERAL PUBLIC LICENSE\n Version 3, 29 June 2007\n\n Copyright (C) 2007 Free "
},
{
"path": "MangaManager/Extensions/CoverDownloader/CoverDownloader.py",
"chars": 579,
"preview": "from tkinter import Label, Frame, Entry\n\n\n\ndef get_cover_from_source_dummy() -> list:\n ...\n\n\nclass CoverDownloader():"
},
{
"path": "MangaManager/Extensions/CoverDownloader/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "MangaManager/Extensions/IExtensionApp.py",
"chars": 1349,
"preview": "import abc\nimport tkinter\nfrom typing import final\n\n\nclass IExtensionApp(tkinter.Toplevel, metaclass=abc.ABCMeta):\n \""
},
{
"path": "MangaManager/Extensions/Template.py",
"chars": 297,
"preview": "import logging\n\nfrom Extensions.IExtensionApp import IExtensionApp\n\nlogger = logging.getLogger()\n\n\nclass ExtensionTempla"
},
{
"path": "MangaManager/Extensions/WebpConverter/WebpConverter.py",
"chars": 5048,
"preview": "from __future__ import annotations\n\nimport glob\nimport logging\nimport os\nimport pathlib\nimport threading\nimport tkinter\n"
},
{
"path": "MangaManager/Extensions/WebpConverter/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "MangaManager/Extensions/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "MangaManager/ExternalSources/CoverSources/MangaDex/MangaDex.py",
"chars": 3603,
"preview": "import logging\nimport os\nimport urllib\nfrom pathlib import Path\n\nimport requests\n\nfrom src.Common.utils import clean_fil"
},
{
"path": "MangaManager/ExternalSources/CoverSources/MangaDex/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "MangaManager/ExternalSources/CoverSources/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "MangaManager/ExternalSources/MetadataSources/MetadataSourceFactory.py",
"chars": 1246,
"preview": "import logging\n\n# Import all the scrapers here to ensure globals() has the key in it for dynamic instantiation\nfrom .Pro"
},
{
"path": "MangaManager/ExternalSources/MetadataSources/Providers/AniList.py",
"chars": 11675,
"preview": "from __future__ import annotations\n\nimport logging\nimport re\nimport requests\nfrom enum import StrEnum\nfrom typing import"
},
{
"path": "MangaManager/ExternalSources/MetadataSources/Providers/ComicVine.py",
"chars": 2943,
"preview": "import logging\nfrom abc import ABC\n\nimport requests\n\nfrom common.models import ComicInfo\nfrom src.Common.errors import M"
},
{
"path": "MangaManager/ExternalSources/MetadataSources/Providers/MangaUpdates.py",
"chars": 4847,
"preview": "import logging\nfrom enum import StrEnum\n\nimport requests\n\nfrom common import get_invalid_person_tag\nfrom common.models i"
},
{
"path": "MangaManager/ExternalSources/MetadataSources/Providers/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "MangaManager/ExternalSources/MetadataSources/__init__.py",
"chars": 178,
"preview": "\nfrom .MetadataSourceFactory import ScraperFactory\nfrom .Providers.AniList import AniList\nfrom .Providers.MangaUpdates i"
},
{
"path": "MangaManager/ExternalSources/__init__.py",
"chars": 1,
"preview": "\n"
},
{
"path": "MangaManager/common/__init__.py",
"chars": 367,
"preview": "from .models import PeopleTags\n\n\ndef get_invalid_person_tag(people: str):\n \"\"\" Validates that a common separated list"
},
{
"path": "MangaManager/common/models/AgeRating.py",
"chars": 593,
"preview": "from enum import Enum\n\n# Keep this ordered in terms of progressing ratings, rather than alphabetical\nclass AgeRating(str"
},
{
"path": "MangaManager/common/models/ComicInfo.py",
"chars": 4334,
"preview": "from io import BytesIO\nfrom xml.etree import ElementTree as ET\n\ncomic_info_tag_map = {\n \"series\": \"Series\",\n \"loca"
},
{
"path": "MangaManager/common/models/ComicInfo.xds",
"chars": 8243,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<xs:schema elementFormDefault=\"qualified\" xmlns:xs=\"http://www.w3.org/2001/XMLSch"
},
{
"path": "MangaManager/common/models/ComicInfoTag.py",
"chars": 0,
"preview": ""
},
{
"path": "MangaManager/common/models/ComicPageType.py",
"chars": 450,
"preview": "from enum import Enum\n\n\nclass ComicPageType(str, Enum):\n FRONT_COVER = 'FrontCover'\n INNER_COVER = 'InnerCover'\n "
},
{
"path": "MangaManager/common/models/Manga.py",
"chars": 256,
"preview": "from enum import Enum\n\n\nclass Manga(str, Enum):\n UNKNOWN = 'Unknown'\n NO = 'No'\n YES = 'Yes'\n YES_AND_RIGHT_"
},
{
"path": "MangaManager/common/models/YesNo.py",
"chars": 207,
"preview": "from enum import Enum\n\n\nclass YesNo(str, Enum):\n UNKNOWN = 'Unknown'\n NO = 'No'\n YES = 'Yes'\n\n @classmethod\n"
},
{
"path": "MangaManager/common/models/__init__.py",
"chars": 533,
"preview": "from .AgeRating import AgeRating\nfrom .ComicPageType import ComicPageType\nfrom .Manga import Manga\nfrom .YesNo import Ye"
},
{
"path": "MangaManager/logging_setup.py",
"chars": 1553,
"preview": "import logging\nimport sys\nfrom logging.handlers import RotatingFileHandler\n\n\ndef trace(self, message, *args, **kws):\n "
},
{
"path": "MangaManager/main.py",
"chars": 2691,
"preview": "import argparse\nimport enum\nimport glob\nimport logging\nfrom pathlib import Path\n\nfrom logging_setup import add_trace_lev"
},
{
"path": "MangaManager/pyinstaller_hooks/hook-tkinterdnd2.py",
"chars": 98,
"preview": "from PyInstaller.utils.hooks import collect_data_files\n\ndatas = collect_data_files('tkinterdnd2')\n"
},
{
"path": "MangaManager/res/languages.json",
"chars": 49161,
"preview": "[{\n \"isoCode\": \"aa\",\n \"title\": \"Afar\"\n}, {\n \"isoCode\": \"aa-DJ\",\n \"title\": \"Afar (Djibouti)\"\n}, {\n \"isoCod"
},
{
"path": "MangaManager/src/Common/LoadedComicInfo/ArchiveFile.py",
"chars": 1802,
"preview": "import os\nimport zipfile\n\nimport rarfile\n\n\nclass ArchiveFile:\n \"\"\"\n A class that provides a unified interface to r"
},
{
"path": "MangaManager/src/Common/LoadedComicInfo/CoverActions.py",
"chars": 138,
"preview": "import enum\n\n\nclass CoverActions(enum.Enum):\n RESET = 0 # Cancel current selected action\n REPLACE = 1\n DELETE "
},
{
"path": "MangaManager/src/Common/LoadedComicInfo/ILoadedComicInfo.py",
"chars": 881,
"preview": "class ILoadedComicInfo:\n \"\"\"\n Helper class that loads the info that is required by the tools\n\n file_pat"
},
{
"path": "MangaManager/src/Common/LoadedComicInfo/LoadedComicInfo.py",
"chars": 18324,
"preview": "from __future__ import annotations\n\nimport copy\nimport io\nimport logging\nimport os\nimport tempfile\nimport zipfile\nfrom t"
},
{
"path": "MangaManager/src/Common/LoadedComicInfo/LoadedFileCoverData.py",
"chars": 6260,
"preview": "from __future__ import annotations\n\nimport io\nimport logging\nimport zipfile\nfrom typing import IO\n\nfrom PIL import Image"
},
{
"path": "MangaManager/src/Common/LoadedComicInfo/LoadedFileMetadata.py",
"chars": 3526,
"preview": "import copy\nimport logging\n\nimport rarfile\nfrom lxml.etree import XMLSyntaxError\n\nfrom common.models import ComicInfo\nfr"
},
{
"path": "MangaManager/src/Common/LoadedComicInfo/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "MangaManager/src/Common/ResourceLoader.py",
"chars": 352,
"preview": "import os\nfrom os.path import abspath\n\nfrom pkg_resources import resource_filename\n\nres_path = abspath(resource_filename"
},
{
"path": "MangaManager/src/Common/__init__.py",
"chars": 43,
"preview": "from .ResourceLoader import ResourceLoader\n"
},
{
"path": "MangaManager/src/Common/errors.py",
"chars": 2677,
"preview": "class NoMetadataFileFound(Exception):\n \"\"\"\n Exception raised when not enough data is given to create a Metadata ob"
},
{
"path": "MangaManager/src/Common/naturalsorter.py",
"chars": 822,
"preview": "from __future__ import annotations\n\nimport pathlib\n\nfrom natsort import natsort_key\n\n\ndef decompose_path_into_components"
},
{
"path": "MangaManager/src/Common/parser.py",
"chars": 5643,
"preview": "import re\n\n\"\"\"\nRegex Patterns adapted from Kavita: https://github.com/Kareadita/Kavita\n\"\"\"\nNumber = \"\\d+(\\.\\d)?\"\nNumberR"
},
{
"path": "MangaManager/src/Common/progressbar.py",
"chars": 3889,
"preview": "import abc\nimport logging\nimport time\nfrom string import Template\nfrom threading import Timer\n\nfrom src.Common.utils imp"
},
{
"path": "MangaManager/src/Common/terminalcolors.py",
"chars": 1894,
"preview": "class TerminalColors:\n RESET = \"\\x1b[0m\"\n BOLD = \"\\x1b[1m\"\n CURSIVE = "
},
{
"path": "MangaManager/src/Common/utils.py",
"chars": 11266,
"preview": "import logging\nimport os\nimport re\nimport subprocess\nimport sys\nimport time\nimport urllib.request\nfrom io import BytesIO"
},
{
"path": "MangaManager/src/DynamicLibController/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "MangaManager/src/DynamicLibController/extension_manager.py",
"chars": 2575,
"preview": "import glob\nimport importlib\nimport logging\nimport os\nimport sys\nfrom pathlib import Path\n\nfrom Extensions.IExtensionApp"
},
{
"path": "MangaManager/src/DynamicLibController/models/CoverSourceInterface.py",
"chars": 802,
"preview": "import abc\nimport dataclasses\nfrom typing import final\n\n\nclass ICoverSource(abc.ABC):\n name = None\n\n @classmethod\n"
},
{
"path": "MangaManager/src/DynamicLibController/models/ExtensionsInterface.py",
"chars": 710,
"preview": "import abc\n\n\nclass IMMExtension(abc.ABC):\n \"\"\"\n The basic interface that all extensions must implement. An ext"
},
{
"path": "MangaManager/src/DynamicLibController/models/IMetadataSource.py",
"chars": 4165,
"preview": "import abc\nimport logging\nfrom html.parser import HTMLParser\nfrom io import StringIO\nfrom typing import final\n\nfrom comm"
},
{
"path": "MangaManager/src/DynamicLibController/models/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "MangaManager/src/MetadataManager/CoverManager/CoverManager.py",
"chars": 22425,
"preview": "import copy\nimport logging\nimport platform\nimport tkinter\nfrom idlelib.tooltip import Hovertip\nfrom tkinter import Frame"
},
{
"path": "MangaManager/src/MetadataManager/CoverManager/__init__.py",
"chars": 1,
"preview": "\n"
},
{
"path": "MangaManager/src/MetadataManager/GUI/ControlManager.py",
"chars": 815,
"preview": "import logging\nimport tkinter\n\nimport _tkinter\n\nlogger = logging.getLogger()\n\n\nclass ControlManager:\n \"\"\"\n \"\"\"\n "
},
{
"path": "MangaManager/src/MetadataManager/GUI/ExceptionWindow.py",
"chars": 3270,
"preview": "import logging\nimport tkinter\nimport traceback\nfrom tkinter import Frame\nfrom tkinter.font import Font\nfrom tkinter.ttk "
},
{
"path": "MangaManager/src/MetadataManager/GUI/FileChooserWindow.py",
"chars": 11905,
"preview": "import dataclasses\nimport fnmatch\nimport os\nimport tkinter\nfrom idlelib.tooltip import Hovertip\nfrom pathlib import Path"
},
{
"path": "MangaManager/src/MetadataManager/GUI/MessageBox.py",
"chars": 1566,
"preview": "from typing import Type\n\nfrom src.MetadataManager.GUI.OneTimeMessageBox import OneTimeMessageBox\nfrom src.MetadataManage"
},
{
"path": "MangaManager/src/MetadataManager/GUI/OneTimeMessageBox.py",
"chars": 1518,
"preview": "from tkinter import Checkbutton, BooleanVar\n\nfrom src.Common.utils import parse_bool\nfrom src.MetadataManager.GUI.widget"
},
{
"path": "MangaManager/src/MetadataManager/GUI/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "MangaManager/src/MetadataManager/GUI/scrolledframe.py",
"chars": 13988,
"preview": "# Extracted from: https://github.com/alejandroautalan/pygubu/tree/master/pygubu\n\n# encoding: utf8\nfrom __future__ import"
},
{
"path": "MangaManager/src/MetadataManager/GUI/utils.py",
"chars": 955,
"preview": "import re\n\nINT_PATTERN = re.compile(\"^-*\\d*(?:,?\\d+|\\.?\\d+)?$\")\n\ndef validate_int(value) -> bool:\n \"\"\"\n Validates "
},
{
"path": "MangaManager/src/MetadataManager/GUI/widgets/AutocompleteComboboxWidget.py",
"chars": 3049,
"preview": "import tkinter\nfrom .MMWidget import MMWidget\nfrom tkinter.ttk import Combobox\n\n\nclass AutocompleteComboboxWidget(MMWidg"
},
{
"path": "MangaManager/src/MetadataManager/GUI/widgets/ButtonWidget.py",
"chars": 363,
"preview": "import tkinter\nfrom idlelib.tooltip import Hovertip\n\n\nclass ButtonWidget(tkinter.Button):\n def __init__(self, tooltip"
},
{
"path": "MangaManager/src/MetadataManager/GUI/widgets/CanvasCoverWidget.py",
"chars": 18009,
"preview": "import logging\nimport pathlib\nimport tkinter\nfrom idlelib.tooltip import Hovertip\nfrom os.path import basename\nfrom tkin"
},
{
"path": "MangaManager/src/MetadataManager/GUI/widgets/ComboBoxWidget.py",
"chars": 1149,
"preview": "from tkinter.ttk import Combobox\n\nfrom src.MetadataManager.GUI.utils import validate_int\nfrom .MMWidget import MMWidget\n"
},
{
"path": "MangaManager/src/MetadataManager/GUI/widgets/FileMultiSelectWidget.py",
"chars": 3761,
"preview": "import copy\nimport logging\nimport tkinter\nfrom tkinter.ttk import Treeview\n\nfrom src.Common.LoadedComicInfo.LoadedComicI"
},
{
"path": "MangaManager/src/MetadataManager/GUI/widgets/FormBundleWidget.py",
"chars": 2635,
"preview": "from idlelib.tooltip import Hovertip\nfrom tkinter import Frame, Label, Entry, Checkbutton, StringVar, BooleanVar\n\nfrom s"
},
{
"path": "MangaManager/src/MetadataManager/GUI/widgets/HyperlinkLabelWidget.py",
"chars": 974,
"preview": "import webbrowser\nfrom tkinter import Frame, Label\n\n\nclass HyperlinkLabelWidget(Frame):\n def __init__(self, master=No"
},
{
"path": "MangaManager/src/MetadataManager/GUI/widgets/LongTextWidget.py",
"chars": 728,
"preview": "from tkinter.scrolledtext import ScrolledText\n\n\nfrom .MMWidget import MMWidget, _LongText\n\n\nclass LongTextWidget(MMWidge"
},
{
"path": "MangaManager/src/MetadataManager/GUI/widgets/MMWidget.py",
"chars": 3195,
"preview": "from idlelib.tooltip import Hovertip\nfrom tkinter.ttk import Combobox, OptionMenu, Frame, Label\nfrom tkinter import Text"
},
{
"path": "MangaManager/src/MetadataManager/GUI/widgets/MessageBoxWidget.py",
"chars": 4615,
"preview": "from tkinter import Button, Label, Frame, Toplevel\n\nfrom src.MetadataManager.GUI.scrolledframe import ScrolledFrame\nfrom"
},
{
"path": "MangaManager/src/MetadataManager/GUI/widgets/OptionMenuWidget.py",
"chars": 2081,
"preview": "import logging\nimport tkinter\nfrom tkinter.ttk import Combobox\n\nfrom common.models import AgeRating, Formats, YesNo, Man"
},
{
"path": "MangaManager/src/MetadataManager/GUI/widgets/ProgressBarWidget.py",
"chars": 2492,
"preview": "import logging\nimport tkinter\nfrom tkinter.ttk import Progressbar, Style\n\nfrom src.Common.progressbar import ProgressBar"
},
{
"path": "MangaManager/src/MetadataManager/GUI/widgets/ScrolledFrameWidget.py",
"chars": 728,
"preview": "import tkinter\nfrom tkinter import Frame\n\nfrom src.MetadataManager.GUI.scrolledframe import ScrolledFrame\n\n\nclass Scroll"
},
{
"path": "MangaManager/src/MetadataManager/GUI/widgets/WidgetManager.py",
"chars": 1549,
"preview": "from .OptionMenuWidget import OptionMenuWidget\nfrom .LongTextWidget import LongTextWidget\nfrom .ComboBoxWidget import Co"
},
{
"path": "MangaManager/src/MetadataManager/GUI/widgets/__init__.py",
"chars": 574,
"preview": "from .MMWidget import MMWidget\nfrom .OptionMenuWidget import OptionMenuWidget\nfrom .LongTextWidget import LongTextWidget"
},
{
"path": "MangaManager/src/MetadataManager/GUI/windows/AboutWindow.py",
"chars": 4394,
"preview": "import logging\nimport tkinter\nfrom typing import NamedTuple\n\nimport requests\n\nfrom src.MetadataManager.GUI.widgets impor"
},
{
"path": "MangaManager/src/MetadataManager/GUI/windows/DragAndDrop.py",
"chars": 1051,
"preview": "import re\nimport tkinter as tk\nfrom tkinterdnd2 import DND_FILES, TkinterDnD\ndef extract_paths_from_string(input_string)"
},
{
"path": "MangaManager/src/MetadataManager/GUI/windows/LoadingWindow.py",
"chars": 2702,
"preview": "import tkinter\nfrom unittest.mock import Mock\n\nfrom src.Common.progressbar import ProgressBar\nfrom src.MetadataManager.G"
},
{
"path": "MangaManager/src/MetadataManager/GUI/windows/MainWindow.py",
"chars": 21022,
"preview": "import json\nimport re\nimport tkinter\nfrom tkinter import Frame, ttk\nfrom tkinter.ttk import Notebook\n\nfrom tkinterdnd2 i"
},
{
"path": "MangaManager/src/MetadataManager/GUI/windows/SettingsWindow.py",
"chars": 11445,
"preview": "from __future__ import annotations\n\nimport logging\nimport re\nimport tkinter\nfrom tkinter import ttk, Frame\nfrom tkinter."
},
{
"path": "MangaManager/src/MetadataManager/GUI/windows/__init__.py",
"chars": 37,
"preview": "from .AboutWindow import AboutWindow\n"
},
{
"path": "MangaManager/src/MetadataManager/MetadataManagerCLI.py",
"chars": 11478,
"preview": "import itertools\nimport logging\nimport shutil\nimport sys\nimport textwrap\nimport time\n\nimport prompt_toolkit\nfrom prompt_"
},
{
"path": "MangaManager/src/MetadataManager/MetadataManagerGUI.py",
"chars": 23229,
"preview": "from __future__ import annotations\n\nimport glob\nimport logging\nimport os\nimport tkinter\nfrom tkinter import Tk, Frame\n\nf"
},
{
"path": "MangaManager/src/MetadataManager/MetadataManagerLib.py",
"chars": 11293,
"preview": "from __future__ import annotations\n\nimport abc\nimport logging\nfrom abc import ABC\n\nfrom ExternalSources.MetadataSources "
},
{
"path": "MangaManager/src/MetadataManager/__init__.py",
"chars": 1276,
"preview": "import logging\nimport src\nfrom src.Common import ResourceLoader\nfrom src.MetadataManager.GUI.windows.MainWindow import M"
},
{
"path": "MangaManager/src/Settings/SettingControl.py",
"chars": 1116,
"preview": "import abc\nfrom typing import Callable\n\nfrom .SettingControlType import SettingControlType\n\n\nclass SettingControl(abc.AB"
},
{
"path": "MangaManager/src/Settings/SettingControlType.py",
"chars": 99,
"preview": "from enum import Enum\n\n\nclass SettingControlType(Enum):\n Bool = 0,\n Text = 1,\n Options = 2"
},
{
"path": "MangaManager/src/Settings/SettingSection.py",
"chars": 552,
"preview": "from .SettingControl import SettingControl\n\n\nclass SettingSection:\n \"\"\"\n A section of config controls. Will rende"
},
{
"path": "MangaManager/src/Settings/Settings.py",
"chars": 3855,
"preview": "import configparser\nimport logging\nimport os\nfrom pathlib import Path\n\nfrom src.Settings.SettingsDefault import default_"
},
{
"path": "MangaManager/src/Settings/SettingsDefault.py",
"chars": 775,
"preview": "from enum import StrEnum\n\n\nclass SettingHeading(StrEnum):\n Main = \"Main\",\n WebpConverter = \"Webp Converter\",\n E"
},
{
"path": "MangaManager/src/Settings/__init__.py",
"chars": 212,
"preview": "from .SettingControl import SettingControl\nfrom .SettingControlType import SettingControlType\nfrom .SettingSection impor"
},
{
"path": "MangaManager/src/__init__.py",
"chars": 766,
"preview": "import logging\nfrom os import environ\nfrom os.path import abspath\nfrom pathlib import Path\n\nimport requests # Needed fo"
},
{
"path": "MangaManager/src/__version__.py",
"chars": 38,
"preview": "__version__ = \"1.0.4:stable:fd7b72b0\"\n"
},
{
"path": "MangaManager/tests/Common/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "MangaManager/tests/Common/test_ComicInfo.py",
"chars": 1591,
"preview": "import unittest\n\nfrom common.models import ComicInfo\nfrom tests.common import is_valid_xml\n\n\nclass ComicInfoTests(unitte"
},
{
"path": "MangaManager/tests/Common/test_utils.py",
"chars": 1173,
"preview": "import unittest\n\nfrom common.models import ComicInfo\nfrom src.DynamicLibController.models.IMetadataSource import IMetada"
},
{
"path": "MangaManager/tests/ExtensionsTests/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "MangaManager/tests/ExtensionsTests/test_WebpConverter.py",
"chars": 2673,
"preview": "# from src.Common.loadedcomicinfo import LoadedComicInfo\nfrom logging_setup import add_trace_level\n\nadd_trace_level()\n\n#"
},
{
"path": "MangaManager/tests/ExternalMetadataTests/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "MangaManager/tests/ExternalMetadataTests/test_AniList.py",
"chars": 1114,
"preview": "import unittest\n\nfrom common.models import ComicInfo\n\n\nclass TestSources(unittest.TestCase):\n def test_AnilistReturnM"
},
{
"path": "MangaManager/tests/LoadedComicInfo/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "MangaManager/tests/LoadedComicInfo/test_Covers.py",
"chars": 5441,
"preview": "import io\nimport unittest\nimport zipfile\n\nfrom PIL import Image\n\nfrom src.Common.LoadedComicInfo.LoadedComicInfo import "
},
{
"path": "MangaManager/tests/LoadedComicInfo/test_LoadedCInfo.py",
"chars": 6930,
"preview": "import os\nimport random\nimport tempfile\nimport unittest\nimport zipfile\nfrom unittest import skip\n\nfrom common.models imp"
},
{
"path": "MangaManager/tests/LoadedComicInfo/test_LoadedCInfo_backup.py",
"chars": 4118,
"preview": "import os\nimport random\nimport tempfile\nimport unittest\nimport zipfile\n\nfrom common.models import ComicInfo\nfrom src.Com"
},
{
"path": "MangaManager/tests/LoadedComicInfo/test_moveto.py",
"chars": 819,
"preview": "import unittest\n\nfrom common.models import ComicInfo\nfrom src.Common.LoadedComicInfo.LoadedComicInfo import LoadedComicI"
},
{
"path": "MangaManager/tests/MetadataManagerTests/GUI/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "MangaManager/tests/MetadataManagerTests/GUI/test_MetadataEditorGUI.py",
"chars": 10045,
"preview": "import glob\nimport importlib\nimport os\nimport random\nfrom tkinter.filedialog import askopenfiles\n\nfrom common.models imp"
},
{
"path": "MangaManager/tests/MetadataManagerTests/GUI/test_dinamic_layouts.py",
"chars": 1963,
"preview": "import glob\nimport importlib\nimport os\n\nfrom src.MetadataManager import MetadataManagerGUI\nfrom tests.common import crea"
},
{
"path": "MangaManager/tests/MetadataManagerTests/GUI/test_fetch_metadata.py",
"chars": 1496,
"preview": "import glob\nimport importlib\nimport os\n\nfrom logging_setup import add_trace_level\nfrom src.MetadataManager.MetadataManag"
},
{
"path": "MangaManager/tests/MetadataManagerTests/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "MangaManager/tests/MetadataManagerTests/test_MetadataEditorCore.py",
"chars": 9546,
"preview": "import os\nimport unittest\nimport zipfile\nfrom unittest.mock import patch, MagicMock\n\nimport src.Common.LoadedComicInfo.L"
},
{
"path": "MangaManager/tests/Settings/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "MangaManager/tests/Settings/test_Settings.py",
"chars": 971,
"preview": "import os.path\nimport unittest\n\nfrom src.Settings import SettingHeading, Settings\n\n\nclass SettingsTest(unittest.TestCase"
},
{
"path": "MangaManager/tests/__init__.py",
"chars": 60,
"preview": "from logging_setup import add_trace_level\n\nadd_trace_level()"
},
{
"path": "MangaManager/tests/common.py",
"chars": 6474,
"preview": "import configparser\nimport io\nimport os\nimport random\nimport sys\nimport unittest\nimport warnings\nimport zipfile\n\nimport "
},
{
"path": "MangaManager/tests/data/test.py",
"chars": 359,
"preview": "from src.Common.LoadedComicInfo.ArchiveFile import ArchiveFile\n\nif __name__ == '__main__':\n with ArchiveFile(\"!00_SAM"
},
{
"path": "MangaManager/tests/test_comicinfo.py",
"chars": 537,
"preview": "import unittest\n\nfrom common.models import AgeRating, Manga, YesNo, Formats\n\n\nclass LoadedCInfo_Utils(unittest.TestCase)"
},
{
"path": "README.md",
"chars": 3579,
"preview": "[](https://githu"
},
{
"path": "docker-compose.yml",
"chars": 1197,
"preview": "# This Docker is based on Linuxserver.io's Webtop Base Image.\n# For information about more available options to use, ple"
},
{
"path": "docker-root/config/.config/xfce4/panel/launcher-7/MM Launcher.desktop",
"chars": 571,
"preview": "[Desktop Entry]\nVersion=1.0\nType=Application\n# Exec=python /app/main.py\nExec=/config/Desktop/MangaManager_23_02_02_Beta_"
},
{
"path": "docker-root/config/.config/xfce4/xfconf/xfce-perchannel-xml/xfce4-desktop.xml",
"chars": 2040,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<channel name=\"xfce4-desktop\" version=\"1.0\">\n <property name=\"backdrop\" type=\"e"
},
{
"path": "docker-root/config/.config/xfce4/xfconf/xfce-perchannel-xml/xfce4-panel.xml",
"chars": 4414,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<channel name=\"xfce4-panel\" version=\"1.0\">\n <property name=\"configver\" type=\"in"
},
{
"path": "docker-root/config/.config/xfce4/xfconf/xfce-perchannel-xml/xsettings.xml",
"chars": 2003,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<channel name=\"xsettings\" version=\"1.0\">\n <property name=\"Net\" type=\"empty\">\n "
},
{
"path": "docker-root/config/Desktop/covers-folder-link.desktop",
"chars": 175,
"preview": "[Desktop Entry]\nVersion=1.0\nType=Application\nName=Covers Folder\nComment=Open /covers\nExec=/usr/bin/Thunar\nIcon=folder-pi"
},
{
"path": "docker-root/config/Desktop/manga-folder-link.desktop",
"chars": 179,
"preview": "[Desktop Entry]\nVersion=1.0\nType=Application\nName=Manga Folder\nComment=Open /manga\nExec=/usr/bin/Thunar\nIcon=application"
},
{
"path": "docker-root/config/Desktop/manga-manager-link.desktop",
"chars": 207,
"preview": "[Desktop Entry]\nVersion=1.0\nType=Application\nName=Manga Manager\nComment=\n# Exec=python /app/main.py\nExec=/config/Desktop"
},
{
"path": "docker-root/config/custom-cont-init.d/prepare-app-permissions.sh",
"chars": 79,
"preview": "#!/bin/bash\n\necho \"Preparing permissions for /app folder\"\nchown -R abc:abc /app"
},
{
"path": "docker-root/defaults/autostart",
"chars": 46,
"preview": "startxfce4\nMangaManager_23_02_02_Beta_linux_01"
},
{
"path": "docker-root/defaults/startwm.sh",
"chars": 85,
"preview": "#!/bin/bash\n/startpulse.sh &\n/usr/bin/startxfce4 > /dev/null 2>&1\npython /app/main.py"
},
{
"path": "requirements.txt",
"chars": 154,
"preview": "prompt_toolkit>=3.0.31\npillow>=9.4.0\nPillow\nnatsort>=8.2.0\nlxml >= 4.9.1\nsix >= 1.16.0\nrequests >= 2.31.0\nanytree~=2.8.0"
},
{
"path": "sonar-project.properties",
"chars": 892,
"preview": "sonar.projectKey=ThePromidius_Manga-Manager\nsonar.organization=thepromidius\n# This is the name and version displayed in "
}
]
// ... and 2 more files (download for full content)
About this extraction
This page contains the full source code of the MangaManagerORG/Manga-Manager GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 146 files (25.1 MB), approximately 123.1k tokens, and a symbol index with 634 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.