├── .flake8 ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── anitopy ├── __init__.py ├── anitopy.py ├── element.py ├── keyword.py ├── parser.py ├── parser_helper.py ├── parser_number.py ├── token.py └── tokenizer.py ├── requirements.txt ├── requirements_dev.txt ├── setup.py └── tests ├── __init__.py ├── fixtures ├── __init__.py ├── failing_table.py └── table.py └── test_anitopy.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/python 2 | 3 | ### Python ### 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # IPython 80 | profile_default/ 81 | ipython_config.py 82 | 83 | # pyenv 84 | .python-version 85 | 86 | # celery beat schedule file 87 | celerybeat-schedule 88 | 89 | # SageMath parsed files 90 | *.sage.py 91 | 92 | # Environments 93 | .env 94 | .venv 95 | env/ 96 | venv/ 97 | ENV/ 98 | env.bak/ 99 | venv.bak/ 100 | 101 | # Spyder project settings 102 | .spyderproject 103 | .spyproject 104 | 105 | # Rope project settings 106 | .ropeproject 107 | 108 | # mkdocs documentation 109 | /site 110 | 111 | # mypy 112 | .mypy_cache/ 113 | .dmypy.json 114 | dmypy.json 115 | 116 | ### Python Patch ### 117 | .venv/ 118 | 119 | ### Python.VirtualEnv Stack ### 120 | # Virtualenv 121 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 122 | [Bb]in 123 | [Ii]nclude 124 | [Ll]ib 125 | [Ll]ib64 126 | [Ll]ocal 127 | [Ss]cripts 128 | pyvenv.cfg 129 | pip-selfcheck.json 130 | 131 | 132 | # End of https://www.gitignore.io/api/python 133 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.3" 5 | - "3.4" 6 | - "3.5" 7 | - "3.6" 8 | - "3.7" 9 | - "3.8" 10 | - "3.9" 11 | 12 | install: make setup 13 | 14 | script: make test 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | ### Added 9 | - Support 2K, 4K and 8K video resolutions. 10 | - Add support to multi threading. This was not possible since the parsed tokens and elements where stored globally with the use of singletons. 11 | 12 | ### Fixed 13 | - Increased the maximum amount of numbers to be considered as an episode from 3 to 4 since One Piece and Detective Conan have more than 999 episodes. 14 | - In some cases a 0 was being parsed as the season number. For example, for the release group 0x539. 15 | - An exception was being raised when the episode number was at the very end of the filename. 16 | 17 | ## [2.1.1] - 2022-07-24 18 | ### Fixed 19 | - Fix a bug where the pattern `SEv` was not being parsed correctly due to the version at the end. 20 | 21 | ## [2.1.0] - 2021-12-03 22 | ### Added 23 | - Support for new keywords for audio and subtitles: DUAL-AUDIO, MULTIAUDIO, MULTI AUDIO, MULTI-AUDIO, MULTIPLE SUBTITLE, MULTI SUBS and MULTI-SUBS. 24 | 25 | ## [2.0.2] - 2021-11-14 26 | ### Fixed 27 | - Parsing a filename containing the same anime type repeated once or more was causing a KeyError if this type was also parsed as a title. 28 | 29 | ## [2.0.1] - 2021-03-14 30 | ### Fixed 31 | - Numbers not related to the episode number enclosed by brackets at the end of the filename caused an AttributeError while trying to parse one of the episode number patterns. 32 | 33 | ## [2.0.0] - 2019-03-19 34 | ### Changed 35 | - The season pattern `S` is now parsed and removed from the title. 36 | 37 | ## [1.3.0] - 2018-10-13 38 | ### Added 39 | - Support TS video file extension keyword. 40 | 41 | ## [1.2.0] - 2018-08-16 42 | ### Added 43 | - Support new keywords for audio, video and subtitles: EAC3, E-AC-3, Hardsubs, HEVC2, Hi444, Hi444P and Hi444PP. 44 | 45 | ### Fixed 46 | - Add requirement for module enum34 for python below version 3.4. 47 | 48 | ## [1.1.0] - 2018-01-10 49 | ### Added 50 | - Support to python 2 with absolute imports and unicode strings. Also use regex match instead of fullmatch. 51 | 52 | ## [1.0.1] - 2017-12-09 53 | ### Changed 54 | - Options are now passed as a dictionary. 55 | 56 | ### Fixed 57 | - Removed import loops between `parser_helper.py` and `parser_number.py`. 58 | 59 | ## [0.3.0] - 2017-09-29 60 | ### Added 61 | - Identify some elements during tokenization. This cover some cases where some keywords are separated by uncommon delimiters or no delimiters at all creating tokens that doesn't match with any keyword. 62 | 63 | ### Fixed 64 | - Remove accents from strings before trying to match it with a keyword. 65 | 66 | ## [0.2.0] - 2017-09-19 67 | ### Added 68 | - Parse alternative episode numbers. 69 | 70 | ### Fixed 71 | - Fix a bug where passing some special characters to the allowed delimiters may result in regex parsing strings incorrectly. 72 | - Ignored strings option is now working. 73 | 74 | ## [0.1.1] - 2017-09-18 75 | ### Fixed 76 | - Expose the `Options` class through the `__init__.py`. 77 | 78 | ## 0.1.0 - 2017-09-17 79 | ### Added 80 | - Working parser for the majority of anime filenames. 81 | 82 | [Unreleased]: https://github.com/igorcmoura/anitopy/compare/v2.1.1...HEAD 83 | [2.1.1]: https://github.com/igorcmoura/anitopy/compare/v2.1.0...HEAD 84 | [2.1.0]: https://github.com/igorcmoura/anitopy/compare/v2.0.2...v2.1.0 85 | [2.0.2]: https://github.com/igorcmoura/anitopy/compare/v2.0.1...v2.0.2 86 | [2.0.1]: https://github.com/igorcmoura/anitopy/compare/v2.0.0...v2.0.1 87 | [2.0.0]: https://github.com/igorcmoura/anitopy/compare/v1.3.0...v2.0.0 88 | [1.3.0]: https://github.com/igorcmoura/anitopy/compare/v1.2.0...v1.3.0 89 | [1.2.0]: https://github.com/igorcmoura/anitopy/compare/v1.1.0...v1.2.0 90 | [1.1.0]: https://github.com/igorcmoura/anitopy/compare/v1.0.1...v1.1.0 91 | [1.0.1]: https://github.com/igorcmoura/anitopy/compare/v0.3.0...v1.0.1 92 | [0.3.0]: https://github.com/igorcmoura/anitopy/compare/v0.2.0...v0.3.0 93 | [0.2.0]: https://github.com/igorcmoura/anitopy/compare/v0.1.1...v0.2.0 94 | [0.1.1]: https://github.com/igorcmoura/anitopy/compare/v0.1.0...v0.1.1 95 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 ================================== 2 | 3 | 1. Definitions -------------- 4 | 5 | 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 6 | 7 | 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 8 | 9 | 1.3. "Contribution" means Covered Software of a particular Contributor. 10 | 11 | 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 12 | 13 | 1.5. "Incompatible With Secondary Licenses" means 14 | 15 | (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or 16 | 17 | (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 18 | 19 | 1.6. "Executable Form" means any form of the work other than Source Code Form. 20 | 21 | 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 22 | 23 | 1.8. "License" means this document. 24 | 25 | 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 26 | 27 | 1.10. "Modifications" means any of the following: 28 | 29 | (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or 30 | 31 | (b) any new file in Source Code Form that contains any Covered Software. 32 | 33 | 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 34 | 35 | 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 36 | 37 | 1.13. "Source Code Form" means the form of the work preferred for making modifications. 38 | 39 | 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 40 | 41 | 2. License Grants and Conditions -------------------------------- 42 | 43 | 2.1. Grants 44 | 45 | Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: 46 | 47 | (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and 48 | 49 | (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 50 | 51 | 2.2. Effective Date 52 | 53 | The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 54 | 55 | 2.3. Limitations on Grant Scope 56 | 57 | The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: 58 | 59 | (a) for any code that a Contributor has removed from Covered Software; or 60 | 61 | (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or 62 | 63 | (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. 64 | 65 | This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 66 | 67 | 2.4. Subsequent Licenses 68 | 69 | No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 70 | 71 | 2.5. Representation 72 | 73 | Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 74 | 75 | 2.6. Fair Use 76 | 77 | This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 78 | 79 | 2.7. Conditions 80 | 81 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 82 | 83 | 3. Responsibilities ------------------- 84 | 85 | 3.1. Distribution of Source Form 86 | 87 | All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 88 | 89 | 3.2. Distribution of Executable Form 90 | 91 | If You distribute Covered Software in Executable Form then: 92 | 93 | (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and 94 | 95 | (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 96 | 97 | 3.3. Distribution of a Larger Work 98 | 99 | You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 100 | 101 | 3.4. Notices 102 | 103 | You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 104 | 105 | 3.5. Application of Additional Terms 106 | 107 | You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 108 | 109 | 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- 110 | 111 | If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 112 | 113 | 5. Termination -------------- 114 | 115 | 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 116 | 117 | 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 118 | 119 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. 120 | 121 | ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ 122 | 123 | ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 124 | 125 | 8. Litigation ------------- 126 | 127 | Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 128 | 129 | 9. Miscellaneous ---------------- 130 | 131 | This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 132 | 133 | 10. Versions of the License --------------------------- 134 | 135 | 10.1. New Versions 136 | 137 | Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 138 | 139 | 10.2. Effect of New Versions 140 | 141 | You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 142 | 143 | 10.3. Modified Versions 144 | 145 | If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 146 | 147 | 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses 148 | 149 | If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. 150 | 151 | Exhibit A - Source Code Form License Notice ------------------------------------------- 152 | 153 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. 154 | 155 | If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. 156 | 157 | You may add additional accurate notices of copyright ownership. 158 | 159 | Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- 160 | 161 | This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | recursive-include anitopy *.py 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | setup: 2 | pip install -r requirements_dev.txt 3 | 4 | test: 5 | python -m unittest discover 6 | 7 | build-dist: 8 | python setup.py sdist 9 | 10 | upload-pypi: build-dist 11 | twine upload --skip-existing dist/* 12 | 13 | upload-pypitest: build-dist 14 | twine upload --skip-existing --repository-url https://test.pypi.org/legacy/ dist/* 15 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Anitopy 3 | ======= 4 | 5 | Anitopy is a Python library for parsing anime video filenames. It's simple to use and it's based on the C++ library `Anitomy `_. 6 | 7 | Example 8 | ------- 9 | The following filename... 10 | 11 | :: 12 | 13 | [TaigaSubs]_Toradora!_(2008)_-_01v2_-_Tiger_and_Dragon_[1280x720_H.264_FLAC][1234ABCD].mkv 14 | 15 | ...can be parsed using the following code: 16 | 17 | .. code-block:: python 18 | 19 | >>> import anitopy 20 | >>> anitopy.parse('[TaigaSubs]_Toradora!_(2008)_-_01v2_-_Tiger_and_Dragon_[1280x720_H.264_FLAC][1234ABCD].mkv') 21 | { 22 | 'anime_title': 'Toradora!', 23 | 'anime_year': '2008', 24 | 'audio_term': 'FLAC', 25 | 'episode_number': '01', 26 | 'episode_title': 'Tiger and Dragon', 27 | 'file_checksum': '1234ABCD', 28 | 'file_extension': 'mkv', 29 | 'file_name': '[TaigaSubs]_Toradora!_(2008)_-_01v2_-_Tiger_and_Dragon_[1280x720_H.264_FLAC][1234ABCD].mkv', 30 | 'release_group': 'TaigaSubs', 31 | 'release_version': '2', 32 | 'video_resolution': '1280x720', 33 | 'video_term': 'H.264' 34 | } 35 | 36 | The :code:`parse` function receives a string and returns a dictionary containing all found elements. It can also receive parsing options, this will be explained below. 37 | 38 | Installation 39 | ------------ 40 | 41 | To install Anitopy, simply use pip: 42 | 43 | .. code-block:: bash 44 | 45 | pip install anitopy 46 | 47 | Or download the source code and inside the source code's folder run: 48 | 49 | .. code-block:: bash 50 | 51 | python setup.py install 52 | 53 | Options 54 | ------- 55 | 56 | The :code:`parse` function can receive the :code:`options` parameter. E.g.: 57 | 58 | .. code-block:: python 59 | 60 | >>> import anitopy 61 | >>> anitopy_options = {'allowed_delimiters': ' '} 62 | >>> anitopy.parse('DRAMAtical Murder Episode 1 - Data_01_Login', options=anitopy_options) 63 | { 64 | 'anime_title': 'DRAMAtical Murder', 65 | 'episode_number': '1', 66 | 'episode_title': 'Data_01_Login', 67 | 'file_name': 'DRAMAtical Murder Episode 1 - Data_01_Login' 68 | } 69 | 70 | If the default options had been used, the parser would have considered :code:`_` as a delimiter and replaced it with space in the episode title. 71 | 72 | The options contain the following attributes: 73 | 74 | +----------------------+-----------------+-----------------------------------------------------------------+-------------------+ 75 | | **Attribute name** | **Type** | **Description** | **Default value** | 76 | +----------------------+-----------------+-----------------------------------------------------------------+-------------------+ 77 | | allowed_delimiters | string | The list of character to be considered as delimiters. | ' _.&+,|' | 78 | +----------------------+-----------------+-----------------------------------------------------------------+-------------------+ 79 | | ignored_strings | list of strings | A list of strings to be removed from the filename during parse. | [] | 80 | +----------------------+-----------------+-----------------------------------------------------------------+-------------------+ 81 | | parse_episode_number | boolean | If the episode number should be parsed. | True | 82 | +----------------------+-----------------+-----------------------------------------------------------------+-------------------+ 83 | | parse_episode_title | boolean | If the episode title should be parsed. | True | 84 | +----------------------+-----------------+-----------------------------------------------------------------+-------------------+ 85 | | parse_file_extension | boolean | If the file extension should be parsed. | True | 86 | +----------------------+-----------------+-----------------------------------------------------------------+-------------------+ 87 | | parse_release_group | boolean | If the release group should be parsed. | True | 88 | +----------------------+-----------------+-----------------------------------------------------------------+-------------------+ 89 | -------------------------------------------------------------------------------- /anitopy/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Anitopy 4 | ~~~~~~~ 5 | 6 | Anitopy is a python library for parsing anime video filenames. 7 | 8 | :copyright: (c) 2018 by Igor Cescon de Moura. 9 | :license: Mozilla Public License Version 2.0, see LICENSE for more details. 10 | """ 11 | from __future__ import absolute_import 12 | 13 | from anitopy.anitopy import parse 14 | 15 | 16 | __all__ = ['parse'] 17 | -------------------------------------------------------------------------------- /anitopy/anitopy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals, absolute_import 4 | 5 | from anitopy.element import Elements, ElementCategory 6 | from anitopy.keyword import keyword_manager 7 | from anitopy.parser import Parser 8 | from anitopy.token import Tokens 9 | from anitopy.tokenizer import Tokenizer 10 | 11 | 12 | default_options = { 13 | 'allowed_delimiters': ' _.&+,|', 14 | 'ignored_strings': [], 15 | 'parse_episode_number': True, 16 | 'parse_episode_title': True, 17 | 'parse_file_extension': True, 18 | 'parse_release_group': True 19 | } 20 | 21 | 22 | def parse(filename, options=default_options): 23 | elements = Elements() 24 | tokens = Tokens() 25 | 26 | # Add missing options 27 | for key, value in default_options.items(): 28 | options.setdefault(key, value) 29 | 30 | elements.insert(ElementCategory.FILE_NAME, filename) 31 | if options['parse_file_extension']: 32 | filename, extension = remove_extension_from_filename(filename) 33 | if extension: 34 | elements.insert(ElementCategory.FILE_EXTENSION, extension) 35 | 36 | if options['ignored_strings']: 37 | filename = remove_ignored_strings_from_filename( 38 | filename, options['ignored_strings']) 39 | 40 | if not filename: 41 | return None 42 | 43 | tokenizer = Tokenizer(filename, options, elements, tokens) 44 | if not tokenizer.tokenize(): 45 | return None 46 | 47 | parser = Parser(options, elements, tokens) 48 | if not parser.parse(): 49 | return None 50 | 51 | return elements.get_dictionary() 52 | 53 | 54 | def remove_extension_from_filename(filename): 55 | split_filename = filename.rsplit('.', 1) 56 | 57 | if len(split_filename) < 2: 58 | return filename, None 59 | 60 | new_filename, extension = split_filename 61 | 62 | max_length = 4 63 | if len(extension) > max_length: 64 | return filename, None 65 | 66 | if not extension.isalnum(): 67 | return filename, None 68 | 69 | keyword = keyword_manager.normalize(extension) 70 | if not keyword_manager.find(keyword, ElementCategory.FILE_EXTENSION): 71 | return filename, None 72 | 73 | return new_filename, extension 74 | 75 | 76 | def remove_ignored_strings_from_filename(filename, ignored_strings): 77 | for string in ignored_strings: 78 | filename = filename.replace(string, '') 79 | return filename 80 | -------------------------------------------------------------------------------- /anitopy/element.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | from enum import Enum 6 | 7 | 8 | class ElementCategory(Enum): 9 | ANIME_SEASON = 'anime_season' 10 | ANIME_SEASON_PREFIX = 'anime_season_prefix' 11 | ANIME_TITLE = 'anime_title' 12 | ANIME_TYPE = 'anime_type' 13 | ANIME_YEAR = 'anime_year' 14 | AUDIO_TERM = 'audio_term' 15 | DEVICE_COMPATIBILITY = 'device_compatibility' 16 | EPISODE_NUMBER = 'episode_number' 17 | EPISODE_NUMBER_ALT = 'episode_number_alt' 18 | EPISODE_PREFIX = 'episode_prefix' 19 | EPISODE_TITLE = 'episode_title' 20 | FILE_CHECKSUM = 'file_checksum' 21 | FILE_EXTENSION = 'file_extension' 22 | FILE_NAME = 'file_name' 23 | LANGUAGE = 'language' 24 | OTHER = 'other' 25 | RELEASE_GROUP = 'release_group' 26 | RELEASE_INFORMATION = 'release_information' 27 | RELEASE_VERSION = 'release_version' 28 | SOURCE = 'source' 29 | SUBTITLES = 'subtitles' 30 | VIDEO_RESOLUTION = 'video_resolution' 31 | VIDEO_TERM = 'video_term' 32 | VOLUME_NUMBER = 'volume_number' 33 | VOLUME_PREFIX = 'volume_prefix' 34 | UNKNOWN = 'unknown' 35 | 36 | @classmethod 37 | def is_searchable(cls, category): 38 | searchable_categories = [ 39 | cls.ANIME_SEASON_PREFIX, 40 | cls.ANIME_TYPE, 41 | cls.AUDIO_TERM, 42 | cls.DEVICE_COMPATIBILITY, 43 | cls.EPISODE_PREFIX, 44 | cls.FILE_CHECKSUM, 45 | cls.LANGUAGE, 46 | cls.OTHER, 47 | cls.RELEASE_GROUP, 48 | cls.RELEASE_INFORMATION, 49 | cls.RELEASE_VERSION, 50 | cls.SOURCE, 51 | cls.SUBTITLES, 52 | cls.VIDEO_RESOLUTION, 53 | cls.VIDEO_TERM, 54 | cls.VOLUME_PREFIX 55 | ] 56 | return category in searchable_categories 57 | 58 | @classmethod 59 | def is_singular(cls, category): 60 | non_singular_categories = [ 61 | cls.ANIME_SEASON, 62 | cls.ANIME_TYPE, 63 | cls.AUDIO_TERM, 64 | cls.DEVICE_COMPATIBILITY, 65 | cls.EPISODE_NUMBER, 66 | cls.LANGUAGE, 67 | cls.OTHER, 68 | cls.RELEASE_INFORMATION, 69 | cls.SOURCE, 70 | cls.VIDEO_TERM 71 | ] 72 | return category not in non_singular_categories 73 | 74 | 75 | class Elements: 76 | def __init__(self): 77 | self._elements = {} 78 | self._check_alt_number = False 79 | 80 | def get_check_alt_number(self): 81 | return self._check_alt_number 82 | 83 | def set_check_alt_number(self, value): 84 | self._check_alt_number = value 85 | 86 | def insert(self, category, content): 87 | self._elements.setdefault(category.value, []).append(content) 88 | 89 | def erase(self, category): 90 | elements = self._elements 91 | if category.value in elements: 92 | del elements[category.value] 93 | 94 | def remove(self, category, content): 95 | elements = self._elements 96 | elements[category.value].remove(content) 97 | if len(elements[category.value]) == 0: 98 | del elements[category.value] 99 | 100 | def contains(self, category): 101 | return bool(category.value in self._elements.keys() and 102 | self._elements[category.value]) 103 | 104 | def empty(self): 105 | return not bool(self._elements) 106 | 107 | def get(self, category): 108 | return self._elements.get( 109 | category.value, '' if ElementCategory.is_singular(category) 110 | else []) 111 | 112 | def get_dictionary(self): 113 | # Convert single element lists to the element itself 114 | elements = dict([ 115 | (category, value[0]) if len(value) == 1 else (category, value) 116 | for category, value in self._elements.items() 117 | ]) 118 | return elements 119 | -------------------------------------------------------------------------------- /anitopy/keyword.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals, absolute_import 4 | 5 | import unicodedata as ud 6 | 7 | from anitopy.element import ElementCategory 8 | 9 | 10 | class KeywordOption: 11 | def __init__(self, identifiable=True, searchable=True, valid=True): 12 | self.identifiable = identifiable 13 | self.searchable = searchable 14 | self.valid = valid 15 | 16 | 17 | class Keyword: 18 | def __init__(self, category, options): 19 | self.category = category 20 | self.options = options 21 | 22 | 23 | class KeywordManager: 24 | def __init__(self): 25 | options_default = KeywordOption() 26 | options_invalid = KeywordOption(valid=False) 27 | options_unidentifiable = KeywordOption(identifiable=False) 28 | options_unidentifiable_invalid = KeywordOption(identifiable=False, 29 | valid=False) 30 | options_unidentifiable_unsearchable = KeywordOption(identifiable=False, 31 | searchable=False) 32 | 33 | self._file_extensions = {} 34 | self._keys = {} 35 | 36 | self.add(ElementCategory.ANIME_SEASON_PREFIX, options_unidentifiable, [ 37 | 'S', 'SAISON', 'SEASON']) 38 | 39 | self.add(ElementCategory.ANIME_TYPE, options_unidentifiable, [ 40 | 'GEKIJOUBAN', 'MOVIE', 41 | 'OAD', 'OAV', 'ONA', 'OVA', 42 | 'SPECIAL', 'SPECIALS', 43 | 'TV']) 44 | self.add(ElementCategory.ANIME_TYPE, 45 | options_unidentifiable_unsearchable, 46 | ['SP']) # e.g. "Yumeiro Patissiere SP Professional" 47 | self.add(ElementCategory.ANIME_TYPE, options_unidentifiable_invalid, [ 48 | 'ED', 'ENDING', 'NCED', 49 | 'NCOP', 'OP', 'OPENING', 50 | 'PREVIEW', 'PV']) 51 | 52 | self.add(ElementCategory.AUDIO_TERM, options_default, [ 53 | # Audio channels 54 | '2.0CH', '2CH', '5.1', '5.1CH', 'DTS', 'DTS-ES', 'DTS5.1', 55 | 'TRUEHD5.1', 56 | # Audio codec 57 | 'AAC', 'AACX2', 'AACX3', 'AACX4', 'AC3', 'EAC3', 'E-AC-3', 58 | 'FLAC', 'FLACX2', 'FLACX3', 'FLACX4', 'LOSSLESS', 'MP3', 'OGG', 59 | 'VORBIS', 60 | # Audio language 61 | 'DUALAUDIO', 'DUAL AUDIO', 'DUAL-AUDIO', 62 | 'MULTIAUDIO', 'MULTI AUDIO', 'MULTI-AUDIO']) 63 | 64 | self.add(ElementCategory.DEVICE_COMPATIBILITY, options_default, [ 65 | 'IPAD3', 'IPHONE5', 'IPOD', 'PS3', 'XBOX', 'XBOX360']) 66 | self.add(ElementCategory.DEVICE_COMPATIBILITY, options_unidentifiable, 67 | ['ANDROID']) 68 | 69 | self.add(ElementCategory.EPISODE_PREFIX, options_default, [ 70 | 'EP', 'EP.', 'EPS', 'EPS.', 'EPISODE', 'EPISODE.', 'EPISODES', 71 | 'CAPITULO', 'EPISODIO', 'FOLGE']) 72 | self.add(ElementCategory.EPISODE_PREFIX, options_invalid, [ 73 | 'E', '\x7B2C']) # single-letter episode keywords are not valid 74 | 75 | self.add(ElementCategory.FILE_EXTENSION, options_default, [ 76 | '3GP', 'AVI', 'DIVX', 'FLV', 'M2TS', 'MKV', 'MOV', 'MP4', 'MPG', 77 | 'OGM', 'RM', 'RMVB', 'TS', 'WEBM', 'WMV']) 78 | self.add(ElementCategory.FILE_EXTENSION, options_invalid, [ 79 | 'AAC', 'AIFF', 'FLAC', 'M4A', 'MP3', 'MKA', 'OGG', 'WAV', 'WMA', 80 | '7Z', 'RAR', 'ZIP', 81 | 'ASS', 'SRT']) 82 | 83 | self.add(ElementCategory.LANGUAGE, options_default, [ 84 | 'ENG', 'ENGLISH', 'ESPANOL', 'JAP', 'PT-BR', 'SPANISH', 'VOSTFR']) 85 | self.add(ElementCategory.LANGUAGE, options_unidentifiable, [ 86 | 'ESP', 'ITA']) # e.g. "Tokyo ESP", "Bokura ga Ita" 87 | 88 | self.add(ElementCategory.OTHER, options_default, [ 89 | 'REMASTER', 'REMASTERED', 'UNCENSORED', 'UNCUT', 90 | 'TS', 'VFR', 'WIDESCREEN', 'WS']) 91 | 92 | self.add(ElementCategory.RELEASE_GROUP, options_default, [ 93 | 'THORA']) 94 | 95 | self.add(ElementCategory.RELEASE_INFORMATION, options_default, [ 96 | 'BATCH', 'COMPLETE', 'PATCH', 'REMUX']) 97 | self.add(ElementCategory.RELEASE_INFORMATION, options_unidentifiable, [ 98 | 'END', 'FINAL']) # e.g. "The End of Evangelion", "Final Approach" 99 | 100 | self.add(ElementCategory.RELEASE_VERSION, options_default, [ 101 | 'V0', 'V1', 'V2', 'V3', 'V4']) 102 | 103 | self.add(ElementCategory.SOURCE, options_default, [ 104 | 'BD', 'BDRIP', 'BLURAY', 'BLU-RAY', 105 | 'DVD', 'DVD5', 'DVD9', 'DVD-R2J', 'DVDRIP', 'DVD-RIP', 106 | 'R2DVD', 'R2J', 'R2JDVD', 'R2JDVDRIP', 107 | 'HDTV', 'HDTVRIP', 'TVRIP', 'TV-RIP', 108 | 'WEBCAST', 'WEBRIP']) 109 | 110 | self.add(ElementCategory.SUBTITLES, options_default, [ 111 | 'ASS', 'BIG5', 'DUB', 'DUBBED', 'HARDSUB', 'HARDSUBS', 'RAW', 112 | 'SOFTSUB', 'SOFTSUBS', 'SUB', 'SUBBED', 'SUBTITLED', 113 | 'MULTIPLE SUBTITLE', 'MULTI SUBS', 'MULTI-SUBS']) 114 | 115 | self.add(ElementCategory.VIDEO_TERM, options_default, [ 116 | # Frame rate 117 | '23.976FPS', '24FPS', '29.97FPS', '30FPS', '60FPS', '120FPS', 118 | # Video codec 119 | '8BIT', '8-BIT', '10BIT', '10BITS', '10-BIT', '10-BITS', 120 | 'HI10', 'HI10P', 'HI444', 'HI444P', 'HI444PP', 121 | 'H264', 'H265', 'H.264', 'H.265', 'X264', 'X265', 'X.264', 122 | 'AVC', 'HEVC', 'HEVC2', 'DIVX', 'DIVX5', 'DIVX6', 'XVID', 123 | # Video format 124 | 'AVI', 'RMVB', 'WMV', 'WMV3', 'WMV9', 125 | # Video quality 126 | 'HQ', 'LQ', 127 | # Video resolution 128 | 'HD', 'SD']) 129 | 130 | self.add(ElementCategory.VOLUME_PREFIX, options_default, [ 131 | 'VOL', 'VOL.', 'VOLUME']) 132 | 133 | def add(self, category, options, keywords): 134 | keyword_container = self._get_keyword_container(category) 135 | for keyword in keywords: 136 | if not keyword: 137 | continue 138 | if keyword in keyword_container.keys(): 139 | continue 140 | keyword_container[keyword] = Keyword(category, options) 141 | 142 | def find(self, string, category=ElementCategory.UNKNOWN): 143 | keyword_container = self._get_keyword_container(category) 144 | if string not in keyword_container.keys(): 145 | return None 146 | keyword = keyword_container[string] 147 | if category != ElementCategory.UNKNOWN and \ 148 | keyword.category != category: 149 | return None 150 | return keyword 151 | 152 | @staticmethod 153 | def peek(elements, string): 154 | entries = [ 155 | (ElementCategory.AUDIO_TERM, ['Dual Audio', 'Multi Audio']), 156 | (ElementCategory.VIDEO_TERM, ['H264', 'H.264', 'h264', 'h.264']), 157 | (ElementCategory.VIDEO_RESOLUTION, ['480p', '720p', '1080p']), 158 | (ElementCategory.SUBTITLES, ['Multiple Subtitle', 'Multi Subs']), 159 | (ElementCategory.SOURCE, ['Blu-Ray']) 160 | ] 161 | 162 | preidentified_tokens = [] 163 | 164 | for category, keywords in entries: 165 | for keyword in keywords: 166 | keyword_begin_pos = string.find(keyword) 167 | if keyword_begin_pos != -1: # Found the keyword in the string 168 | elements.insert(category, keyword) 169 | 170 | keyword_end_pos = keyword_begin_pos + len(keyword) 171 | preidentified_tokens.append( 172 | (keyword_begin_pos, keyword_end_pos)) 173 | 174 | return sorted(preidentified_tokens) 175 | 176 | @staticmethod 177 | def normalize(string): 178 | # Remove accents and other special symbols 179 | nfkd = ud.normalize('NFKD', string) 180 | without_accents = ''.join([c for c in nfkd if not ud.combining(c)]) 181 | 182 | return without_accents.upper() 183 | 184 | def _get_keyword_container(self, category): 185 | return self._file_extensions \ 186 | if category == ElementCategory.FILE_EXTENSION \ 187 | else self._keys 188 | 189 | 190 | keyword_manager = KeywordManager() 191 | -------------------------------------------------------------------------------- /anitopy/parser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals, absolute_import 4 | 5 | from anitopy import parser_helper, parser_number 6 | from anitopy.element import ElementCategory 7 | from anitopy.keyword import keyword_manager 8 | from anitopy.token import TokenCategory, TokenFlags 9 | 10 | 11 | class Parser: 12 | def __init__(self, options, elements, tokens): 13 | self.options = options 14 | self.elements = elements 15 | self.tokens = tokens 16 | 17 | def parse(self): 18 | self.search_for_keywords() 19 | 20 | self.search_for_isolated_numbers() 21 | 22 | if self.options['parse_episode_number']: 23 | self.search_for_episode_number() 24 | 25 | self.search_for_anime_title() 26 | 27 | if self.options['parse_release_group'] and \ 28 | not self.elements.contains(ElementCategory.RELEASE_GROUP): 29 | self.search_for_release_group() 30 | 31 | if self.options['parse_episode_title'] and \ 32 | self.elements.contains(ElementCategory.EPISODE_NUMBER): 33 | self.search_for_episode_title() 34 | 35 | self.validate_elements() 36 | 37 | return not self.elements.empty() 38 | 39 | def search_for_keywords(self): 40 | for token in self.tokens.get_list(TokenFlags.UNKNOWN): 41 | word = token.content 42 | word = word.strip(' -') 43 | 44 | if not word: 45 | continue 46 | # Don't bother if the word is a number that cannot be CRC 47 | if len(word) != 8 and word.isdigit(): 48 | continue 49 | 50 | category = ElementCategory.UNKNOWN 51 | keyword = keyword_manager.find(keyword_manager.normalize(word)) 52 | if keyword: 53 | category = keyword.category 54 | if not self.options['parse_release_group'] and \ 55 | category == ElementCategory.RELEASE_GROUP: 56 | continue 57 | if not ElementCategory.is_searchable(category) or \ 58 | not keyword.options.searchable: 59 | continue 60 | if ElementCategory.is_singular(category) and \ 61 | self.elements.contains(category): 62 | continue 63 | 64 | if category == ElementCategory.ANIME_SEASON_PREFIX: 65 | parser_helper.check_anime_season_keyword(self.elements, self.tokens, token) 66 | continue 67 | elif category == ElementCategory.EPISODE_PREFIX: 68 | if keyword.options.valid: 69 | parser_number.check_extent_keyword( 70 | self.elements, self.tokens, ElementCategory.EPISODE_NUMBER, token) 71 | continue 72 | elif category == ElementCategory.RELEASE_VERSION: 73 | word = word[1:] # number without "v" 74 | elif category == ElementCategory.VOLUME_PREFIX: 75 | parser_number.check_extent_keyword( 76 | self.elements, self.tokens, ElementCategory.VOLUME_NUMBER, token) 77 | continue 78 | else: 79 | if not self.elements.contains(ElementCategory.FILE_CHECKSUM) and \ 80 | parser_helper.is_crc32(word): 81 | category = ElementCategory.FILE_CHECKSUM 82 | elif not self.elements.contains(ElementCategory.VIDEO_RESOLUTION) \ 83 | and parser_helper.is_resolution(word): 84 | category = ElementCategory.VIDEO_RESOLUTION 85 | 86 | if category != ElementCategory.UNKNOWN: 87 | self.elements.insert(category, word) 88 | if keyword is None or keyword.options.identifiable: 89 | token.category = TokenCategory.IDENTIFIER 90 | 91 | def search_for_isolated_numbers(self): 92 | for token in self.tokens.get_list(TokenFlags.UNKNOWN): 93 | if not token.content.isdigit() or \ 94 | not parser_helper.is_token_isolated(self.tokens, token): 95 | continue 96 | 97 | number = int(token.content) 98 | 99 | # Anime year 100 | if number >= parser_number.ANIME_YEAR_MIN and \ 101 | number <= parser_number.ANIME_YEAR_MAX: 102 | if not self.elements.contains(ElementCategory.ANIME_YEAR): 103 | self.elements.insert(ElementCategory.ANIME_YEAR, token.content) 104 | token.category = TokenCategory.IDENTIFIER 105 | continue 106 | 107 | # Video resolution 108 | if number == 480 or number == 720 or number == 1080: 109 | # If these numbers are isolated, it's more likely for them to 110 | # be the video resolution rather than the episode number. Some 111 | # fansub groups use these without the "p" suffix. 112 | if not self.elements.contains(ElementCategory.VIDEO_RESOLUTION): 113 | self.elements.insert( 114 | ElementCategory.VIDEO_RESOLUTION, token.content) 115 | token.category = TokenCategory.IDENTIFIER 116 | continue 117 | 118 | def search_for_episode_number(self): 119 | # List all unknown tokens that contain a number 120 | tokens = [token for token in self.tokens.get_list(TokenFlags.UNKNOWN) 121 | if parser_helper.find_number_in_string(token.content) is not 122 | None] 123 | 124 | if not tokens: 125 | return 126 | 127 | self.elements.set_check_alt_number( 128 | self.elements.contains(ElementCategory.EPISODE_NUMBER)) 129 | 130 | # If a token matches a known episode pattern, it has to be the episode 131 | # number 132 | if parser_number.search_for_episode_patterns(self.elements, self.tokens, tokens): 133 | return 134 | 135 | if self.elements.contains(ElementCategory.EPISODE_NUMBER): 136 | return # We have previously found an episode number via keywords 137 | 138 | # From now on, we're only interested in numeric tokens 139 | tokens = [token for token in tokens if token.content.isdigit()] 140 | 141 | if not tokens: 142 | return 143 | 144 | # e.g. "01 (176)", "29 (04)" 145 | if parser_number.search_for_equivalent_numbers(self.elements, self.tokens, tokens): 146 | return 147 | 148 | # e.g. " - 08" 149 | if parser_number.search_for_separated_numbers(self.elements, self.tokens, tokens): 150 | return 151 | 152 | # e.g. "[12]", "(2006)" 153 | if parser_number.search_for_isolated_numbers(self.elements, self.tokens, tokens): 154 | return 155 | 156 | # Consider using the last number as a last resort 157 | parser_number.search_for_last_number(self.elements, self.tokens, tokens) 158 | 159 | def search_for_anime_title(self): 160 | enclosed_title = False 161 | 162 | # Find the first non-enclosed unknown token 163 | token_begin = self.tokens.find(TokenFlags.NOT_ENCLOSED | TokenFlags.UNKNOWN) 164 | 165 | # If that doesn't work, find the first unknown token in the second 166 | # enclosed group, assuming that the first one is the release group 167 | if token_begin is None: 168 | enclosed_title = True 169 | token_begin = self.tokens.get(0) 170 | skipped_previous_group = False 171 | while token_begin is not None: 172 | token_begin = self.tokens.find_next(token_begin, TokenFlags.UNKNOWN) 173 | if token_begin is None: 174 | break 175 | # Ignore groups that are composed of non-Latin characters 176 | if parser_helper.is_mostly_latin_string(token_begin.content): 177 | if skipped_previous_group: 178 | break # Found it 179 | # Get the first unknown token of the next group 180 | token_begin = self.tokens.find_next(token_begin, TokenFlags.BRACKET) 181 | skipped_previous_group = True 182 | 183 | if token_begin is None: 184 | return 185 | 186 | # Continue until an identifier (or a bracket, if the title is enclosed) 187 | # is found 188 | token_end = self.tokens.find_next( 189 | token_begin, TokenFlags.IDENTIFIER | ( 190 | TokenFlags.BRACKET if enclosed_title else TokenFlags.NONE 191 | )) 192 | 193 | # If within the interval there's an open bracket without its matching 194 | # pair, move the upper endpoint back to the bracket 195 | if not enclosed_title: 196 | last_bracket = token_end 197 | bracket_open = False 198 | for token in self.tokens.get_list(TokenFlags.BRACKET, begin=token_begin, end=token_end): 199 | last_bracket = token 200 | bracket_open = not bracket_open 201 | if bracket_open: 202 | token_end = last_bracket 203 | 204 | # If the interval ends with an enclosed group (e.g. "Anime Title 205 | # [Fansub]"), move the upper endpoint back to the beginning of the 206 | # group. We ignore parentheses in order to keep certain groups (e.g. 207 | # "(TV)") intact. 208 | if not enclosed_title: 209 | token = self.tokens.find_previous(token_end, TokenFlags.NOT_DELIMITER) 210 | while token.category == TokenCategory.BRACKET and \ 211 | token.content != ')': 212 | token = self.tokens.find_previous(token, TokenFlags.BRACKET) 213 | if token is not None: 214 | token_end = token 215 | token = self.tokens.find_previous( 216 | token_end, TokenFlags.NOT_DELIMITER) 217 | 218 | # Token end is a bracket, so we get the previous token to be included 219 | # in the element 220 | token_end = self.tokens.find_previous(token_end, TokenFlags.VALID) 221 | parser_helper.build_element(self.elements, self.tokens, ElementCategory.ANIME_TITLE, token_begin, 222 | token_end, keep_delimiters=False) 223 | 224 | def search_for_release_group(self): 225 | token_end = None 226 | while True: 227 | # Find the first enclosed unknown token 228 | if token_end: 229 | token_begin = self.tokens.find_next( 230 | token_end, TokenFlags.ENCLOSED | TokenFlags.UNKNOWN) 231 | else: 232 | token_begin = self.tokens.find( 233 | TokenFlags.ENCLOSED | TokenFlags.UNKNOWN) 234 | if token_begin is None: 235 | return 236 | 237 | # Continue until a bracket or identifier is found 238 | token_end = self.tokens.find_next( 239 | token_begin, TokenFlags.BRACKET | TokenFlags.IDENTIFIER) 240 | if token_end is None: 241 | return 242 | if token_end.category != TokenCategory.BRACKET: 243 | continue 244 | 245 | # Ignore if it's not the first non-delimiter token in group 246 | previous_token = self.tokens.find_previous( 247 | token_begin, TokenFlags.NOT_DELIMITER) 248 | if previous_token is not None and \ 249 | previous_token.category != TokenCategory.BRACKET: 250 | continue 251 | 252 | # Build release group, token end is a bracket, so we get the 253 | # previous token to be included in the element 254 | token_end = self.tokens.find_previous(token_end, TokenFlags.VALID) 255 | parser_helper.build_element( 256 | self.elements, self.tokens, 257 | ElementCategory.RELEASE_GROUP, token_begin, token_end, 258 | keep_delimiters=True) 259 | return 260 | 261 | def search_for_episode_title(self): 262 | token_end = None 263 | while True: 264 | # Find the first non-enclosed unknown token 265 | if token_end: 266 | token_begin = self.tokens.find_next( 267 | token_end, TokenFlags.NOT_ENCLOSED | TokenFlags.UNKNOWN) 268 | else: 269 | token_begin = self.tokens.find( 270 | TokenFlags.NOT_ENCLOSED | TokenFlags.UNKNOWN) 271 | if token_begin is None: 272 | return 273 | 274 | # Continue until a bracket or identifier is found 275 | token_end = self.tokens.find_next( 276 | token_begin, TokenFlags.BRACKET | TokenFlags.IDENTIFIER) 277 | 278 | # Ignore if it's only a dash 279 | if self.tokens.distance(token_begin, token_end) <= 2 and \ 280 | parser_helper.is_dash_character(token_begin.content): 281 | continue 282 | 283 | # If token end is a bracket, then we get the previous token to be 284 | # included in the element 285 | if token_end and token_end.category == TokenCategory.BRACKET: 286 | token_end = self.tokens.find_previous(token_end, TokenFlags.VALID) 287 | # Build episode title 288 | parser_helper.build_element( 289 | self.elements, self.tokens, 290 | ElementCategory.EPISODE_TITLE, token_begin, token_end, 291 | keep_delimiters=False) 292 | return 293 | 294 | def validate_elements(self): 295 | # Validate anime type and episode title 296 | if self.elements.contains(ElementCategory.ANIME_TYPE) and \ 297 | self.elements.contains(ElementCategory.EPISODE_TITLE): 298 | # Here we check whether the episode title contains an anime type 299 | episode_title = self.elements.get(ElementCategory.EPISODE_TITLE)[0] 300 | # Copy list because we may modify it 301 | anime_type_list = list(self.elements.get(ElementCategory.ANIME_TYPE)) 302 | for anime_type in anime_type_list: 303 | if anime_type == episode_title: 304 | # Invalid episode title 305 | self.elements.erase(ElementCategory.EPISODE_TITLE) 306 | elif anime_type in episode_title: 307 | norm_anime_type = keyword_manager.normalize(anime_type) 308 | if keyword_manager.find( 309 | norm_anime_type, ElementCategory.ANIME_TYPE): 310 | self.elements.remove(ElementCategory.ANIME_TYPE, anime_type) 311 | continue 312 | -------------------------------------------------------------------------------- /anitopy/parser_helper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals, absolute_import 4 | 5 | import re 6 | import unicodedata as ud 7 | 8 | from anitopy.element import ElementCategory 9 | from anitopy.token import TokenCategory, TokenFlags 10 | 11 | DASHES = '-\u2010\u2011\u2012\u2013\u2014\u2015' 12 | 13 | 14 | def find_number_in_string(string): 15 | is_number = [char.isdigit() for char in string] 16 | if any(is_number): 17 | return is_number.index(True) 18 | return None 19 | 20 | 21 | def find_non_number_in_string(string): 22 | is_number = [char.isdigit() for char in string] 23 | if not all(is_number): 24 | return is_number.index(False) 25 | return None 26 | 27 | 28 | def is_hexadecimal_string(string): 29 | return all([char in '1234567890abcdefABCDEF' for char in string]) 30 | 31 | 32 | def get_number_from_ordinal(string): 33 | ordinals = { 34 | '1st': '1', 'First': '1', 35 | '2nd': '2', 'Second': '2', 36 | '3rd': '3', 'Third': '3', 37 | '4th': '4', 'Fourth': '4', 38 | '5th': '5', 'Fifth': '5', 39 | '6th': '6', 'Sixth': '6', 40 | '7th': '7', 'Seventh': '7', 41 | '8th': '8', 'Eighth': '8', 42 | '9th': '9', 'Ninth': '9' 43 | } 44 | return ordinals.get(string, None) 45 | 46 | 47 | def is_crc32(string): 48 | return len(string) == 8 and is_hexadecimal_string(string) 49 | 50 | 51 | def is_dash_character(string): 52 | if len(string) != 1: 53 | return False 54 | return string in DASHES 55 | 56 | 57 | def is_latin_char(char): 58 | return is_latin_char.cache.setdefault(char, 'LATIN' in ud.name(char)) 59 | is_latin_char.cache = {} # noqa: E305 60 | 61 | 62 | def is_mostly_latin_string(string): 63 | if len(string) == 0: 64 | return False 65 | latin_length = len([char for char in string if is_latin_char(char)]) 66 | return latin_length / len(string) >= 0.5 67 | 68 | 69 | def is_resolution(string): 70 | pattern = '\\d{3,4}([pP]|([xX\u00D7]\\d{3,4}))$|^[248]K$' 71 | return bool(re.match(pattern, string)) 72 | 73 | 74 | def check_anime_season_keyword(elements, parsed_tokens, token): 75 | def set_anime_season(first, second, content): 76 | elements.insert(ElementCategory.ANIME_SEASON, content) 77 | first.category = TokenCategory.IDENTIFIER 78 | second.category = TokenCategory.IDENTIFIER 79 | 80 | previous_token = parsed_tokens.find_previous(token, TokenFlags.NOT_DELIMITER) 81 | if previous_token: 82 | number = get_number_from_ordinal(previous_token.content) 83 | if number: 84 | set_anime_season(previous_token, token, number) 85 | return True 86 | 87 | next_token = parsed_tokens.find_next(token, TokenFlags.NOT_DELIMITER) 88 | if next_token and next_token.content.isdigit(): 89 | set_anime_season(token, next_token, next_token.content) 90 | return True 91 | 92 | return False 93 | 94 | 95 | def is_token_isolated(parsed_tokens, token): 96 | previous_token = parsed_tokens.find_previous(token, TokenFlags.NOT_DELIMITER) 97 | if previous_token.category != TokenCategory.BRACKET: 98 | return False 99 | 100 | next_token = parsed_tokens.find_next(token, TokenFlags.NOT_DELIMITER) 101 | if next_token is not None and next_token.category != TokenCategory.BRACKET: 102 | return False 103 | 104 | return True 105 | 106 | ############################################################################### 107 | 108 | 109 | def build_element(elements, parsed_tokens, category, token_begin=None, token_end=None, 110 | keep_delimiters=False): 111 | element = '' 112 | 113 | for token in parsed_tokens.get_list(begin=token_begin, end=token_end): 114 | if token.category == TokenCategory.UNKNOWN: 115 | element += token.content 116 | token.category = TokenCategory.IDENTIFIER 117 | elif token.category == TokenCategory.BRACKET: 118 | element += token.content 119 | elif token.category == TokenCategory.DELIMITER: 120 | delimiter = token.content 121 | if keep_delimiters: 122 | element += delimiter 123 | elif token != token_begin and token != token_end: 124 | if delimiter == ',' or delimiter == '&': 125 | element += delimiter 126 | else: 127 | element += ' ' 128 | 129 | if not keep_delimiters: 130 | element = element.strip(' ' + DASHES) 131 | 132 | if element: 133 | elements.insert(category, element) 134 | -------------------------------------------------------------------------------- /anitopy/parser_number.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals, absolute_import 4 | 5 | import re 6 | 7 | from anitopy import parser_helper 8 | from anitopy.element import ElementCategory 9 | from anitopy.keyword import keyword_manager 10 | from anitopy.token import TokenCategory, TokenFlags, Token 11 | 12 | ANIME_YEAR_MIN = 1900 13 | ANIME_YEAR_MAX = 2050 14 | EPISODE_NUMBER_MAX = ANIME_YEAR_MIN - 1 15 | VOLUME_NUMBER_MAX = 20 16 | 17 | 18 | def str2int(string): 19 | try: 20 | return int(string) 21 | except ValueError: 22 | return 0 23 | 24 | 25 | def is_valid_episode_number(number): 26 | return str2int(number) <= EPISODE_NUMBER_MAX 27 | 28 | 29 | def set_episode_number(elements, number, token, validate): 30 | if validate and not is_valid_episode_number(number): 31 | return False 32 | 33 | token.category = TokenCategory.IDENTIFIER 34 | 35 | category = ElementCategory.EPISODE_NUMBER 36 | 37 | # Handle equivalent numbers 38 | if elements.get_check_alt_number(): 39 | # TODO: check if getting only the first episode number is enough 40 | episode_number = elements.get(ElementCategory.EPISODE_NUMBER)[0] 41 | if str2int(number) > str2int(episode_number): 42 | category = ElementCategory.EPISODE_NUMBER_ALT 43 | elif str2int(number) < str2int(episode_number): 44 | elements.remove(ElementCategory.EPISODE_NUMBER, episode_number) 45 | elements.insert(ElementCategory.EPISODE_NUMBER_ALT, episode_number) 46 | else: 47 | return False 48 | 49 | elements.insert(category, number) 50 | return True 51 | 52 | 53 | def set_alternative_episode_number(elements, number, token): 54 | elements.insert(ElementCategory.EPISODE_NUMBER_ALT, number) 55 | token.category = TokenCategory.IDENTIFIER 56 | 57 | return True 58 | 59 | 60 | def check_extent_keyword(elements, parsed_tokens, category, token): 61 | next_token = parsed_tokens.find_next(token, TokenFlags.NOT_DELIMITER) 62 | 63 | if next_token.category == TokenCategory.UNKNOWN: 64 | if next_token and \ 65 | parser_helper.find_number_in_string(next_token.content) \ 66 | is not None: 67 | if category == ElementCategory.EPISODE_NUMBER: 68 | if not match_episode_patterns( 69 | elements, parsed_tokens, next_token.content, next_token): 70 | set_episode_number( 71 | elements, next_token.content, next_token, validate=False) 72 | elif category == ElementCategory.VOLUME_NUMBER: 73 | if not match_volume_patterns( 74 | elements, next_token.content, next_token): 75 | set_volume_number( 76 | elements, next_token.content, next_token, validate=False) 77 | else: 78 | return False 79 | token.category = TokenCategory.IDENTIFIER 80 | return True 81 | 82 | return False 83 | 84 | ############################################################################### 85 | 86 | 87 | def number_comes_after_prefix(elements, parsed_tokens, category, token): 88 | number_begin = parser_helper.find_number_in_string(token.content) 89 | prefix = token.content[:number_begin] 90 | 91 | keyword = keyword_manager.find(keyword_manager.normalize(prefix), category) 92 | if keyword: 93 | number = token.content[number_begin:] 94 | if category == ElementCategory.EPISODE_PREFIX: 95 | if match_episode_patterns(elements, parsed_tokens, number, token): 96 | return True 97 | return set_episode_number(elements, number, token, validate=False) 98 | if category == ElementCategory.VOLUME_PREFIX: 99 | if match_volume_patterns(elements, number, token): 100 | return True 101 | return set_volume_number(elements, number, token, validate=False) 102 | if category == ElementCategory.ANIME_SEASON_PREFIX: 103 | return set_season_number(elements, number, token) 104 | 105 | return False 106 | 107 | 108 | def number_comes_before_another_number(elements, parsed_tokens, token): 109 | separator_token = parsed_tokens.find_next(token, TokenFlags.NOT_DELIMITER) 110 | 111 | if separator_token: 112 | separator = separator_token.content 113 | if separator == '&' or separator == 'of': 114 | other_token = parsed_tokens.find_next( 115 | separator_token, TokenFlags.NOT_DELIMITER) 116 | if other_token and other_token.content.isdigit(): 117 | set_episode_number(elements, token.content, token, validate=False) 118 | if separator == '&': 119 | set_episode_number( 120 | elements, other_token.content, token, validate=False) 121 | separator_token.category = TokenCategory.IDENTIFIER 122 | other_token.category = TokenCategory.IDENTIFIER 123 | return True 124 | 125 | return False 126 | 127 | 128 | def search_for_episode_patterns(elements, parsed_tokens, tokens): 129 | for token in tokens: 130 | numeric_front = token.content[0].isdigit() 131 | 132 | if not numeric_front: 133 | # e.g. "EP.1", "Vol.1" 134 | if number_comes_after_prefix( 135 | elements, parsed_tokens, ElementCategory.EPISODE_PREFIX, token): 136 | return True 137 | if number_comes_after_prefix( 138 | elements, parsed_tokens, ElementCategory.VOLUME_PREFIX, token): 139 | continue 140 | if number_comes_after_prefix( 141 | elements, parsed_tokens, ElementCategory.ANIME_SEASON_PREFIX, token): 142 | continue 143 | else: 144 | # e.g. "8 & 10", "01 of 24" 145 | if number_comes_before_another_number(elements, parsed_tokens, token): 146 | return True 147 | # Look for other patterns 148 | if match_episode_patterns(elements, parsed_tokens, token.content, token): 149 | return True 150 | 151 | return False 152 | 153 | ############################################################################### 154 | 155 | 156 | def match_single_episode_pattern(elements, word, token): 157 | pattern = '(\\d{1,4})[vV](\\d)$' 158 | match = re.match(pattern, word) 159 | if match: 160 | set_episode_number(elements, match.group(1), token, validate=False) 161 | elements.insert(ElementCategory.RELEASE_VERSION, match.group(2)) 162 | return True 163 | 164 | return False 165 | 166 | 167 | def match_multi_episode_pattern(elements, word, token): 168 | pattern = '(\\d{1,4})(?:[vV](\\d))?[-~&+](\\d{1,4})(?:[vV](\\d))?$' 169 | match = re.match(pattern, word) 170 | if match: 171 | lower_bound = match.group(1) 172 | upper_bound = match.group(3) 173 | # Avoid matching expressions such as "009-1" or "5-2" 174 | if int(lower_bound) < int(upper_bound): 175 | if set_episode_number(elements, lower_bound, token, validate=True): 176 | set_episode_number(elements, upper_bound, token, validate=False) 177 | if match.group(2): 178 | elements.insert( 179 | ElementCategory.RELEASE_VERSION, match.group(2)) 180 | if match.group(4): 181 | elements.insert( 182 | ElementCategory.RELEASE_VERSION, match.group(4)) 183 | return True 184 | 185 | return False 186 | 187 | 188 | def match_season_and_episode_pattern(elements, word, token): 189 | pattern = 'S?(\\d{1,2})(?:-S?(\\d{1,2}))?' +\ 190 | '(?:x|[ ._-x]?E)(\\d{1,4})(?:-E?(\\d{1,4}))?' +\ 191 | '(?:[vV](\\d))?$' 192 | match = re.match(pattern, word, flags=re.IGNORECASE) 193 | 194 | if match: 195 | if int(match.group(1)) == 0: 196 | return False 197 | elements.insert(ElementCategory.ANIME_SEASON, match.group(1)) 198 | if match.group(2): 199 | elements.insert(ElementCategory.ANIME_SEASON, match.group(2)) 200 | set_episode_number(elements, match.group(3), token, validate=False) 201 | if match.group(4): 202 | set_episode_number(elements, match.group(4), token, validate=False) 203 | if match.group(5): 204 | elements.insert(ElementCategory.RELEASE_VERSION, match.group(5)) 205 | return True 206 | 207 | return False 208 | 209 | 210 | def match_type_and_episode_pattern(elements, parsed_tokens, word, token): 211 | number_begin = parser_helper.find_number_in_string(word) 212 | prefix = word[:number_begin] 213 | 214 | keyword = keyword_manager.find( 215 | keyword_manager.normalize(prefix), ElementCategory.ANIME_TYPE) 216 | 217 | if keyword: 218 | elements.insert(ElementCategory.ANIME_TYPE, prefix) 219 | number = word[number_begin:] 220 | if match_episode_patterns(elements, parsed_tokens, number, token) or \ 221 | set_episode_number(elements, number, token, validate=True): 222 | # Split token (we do this last in order to avoid invalidating our 223 | # token reference earlier) 224 | token_index = parsed_tokens.get_index(token) 225 | token.content = number 226 | parsed_tokens.insert(token_index, Token( 227 | TokenCategory.IDENTIFIER if keyword.options.identifiable 228 | else TokenCategory.UNKNOWN, 229 | prefix, token.enclosed)) 230 | return True 231 | 232 | return False 233 | 234 | 235 | def match_fractional_episode_pattern(elements, word, token): 236 | # We don't allow any fractional part other than ".5", because there are 237 | # cases where such a number is a part of the anime title (e.g. "Evangelion: 238 | # 1.11", "Tokyo Magnitude 8.0") or a keyword (e.g. "5.1"). 239 | pattern = '\\d+\\.5$' 240 | match = re.match(pattern, word) 241 | if match: 242 | if set_episode_number(elements, word, token, validate=True): 243 | return True 244 | 245 | return False 246 | 247 | 248 | def match_partial_episode_pattern(elements, word, token): 249 | non_number_begin = parser_helper.find_non_number_in_string(word) 250 | suffix = word[non_number_begin:] 251 | 252 | def is_valid_suffix(s): 253 | return len(s) == 1 and s in 'ABCabc' 254 | 255 | if is_valid_suffix(suffix): 256 | if set_episode_number(elements, word, token, validate=True): 257 | return True 258 | 259 | return False 260 | 261 | 262 | def match_number_sign_pattern(elements, word, token): 263 | if word[0] != '#': 264 | return False 265 | 266 | pattern = '#(\\d{1,4})(?:[-~&+](\\d{1,4}))?(?:[vV](\\d))?$' 267 | match = re.match(pattern, word) 268 | 269 | if match: 270 | if set_episode_number(elements, match.group(1), token, validate=True): 271 | if match.group(2): 272 | set_episode_number(elements, match.group(2), token, validate=True) 273 | if match.group(3): 274 | elements.insert(ElementCategory.RELEASE_VERSION, 275 | match.group(3)) 276 | return True 277 | 278 | return False 279 | 280 | 281 | def match_japanese_counter_pattern(elements, word, token): 282 | if word[-1] != '\u8A71': 283 | return False 284 | 285 | pattern = '(\\d{1,4})\u8A71$' 286 | match = re.match(pattern, word) 287 | 288 | if match: 289 | if set_episode_number(elements, match.group(1), token, validate=False): 290 | return True 291 | 292 | return False 293 | 294 | 295 | def match_episode_patterns(elements, parsed_tokens, word, token): 296 | if word.isdigit(): 297 | return False 298 | 299 | word = word.strip(' -') 300 | 301 | numeric_front = word[0].isdigit() 302 | numeric_back = word[-1].isdigit() 303 | 304 | # e.g. "01v2" 305 | if numeric_front and numeric_back: 306 | if match_single_episode_pattern(elements, word, token): 307 | return True 308 | # e.g. "01-02", "03-05v2" 309 | if numeric_front and numeric_back: 310 | if match_multi_episode_pattern(elements, word, token): 311 | return True 312 | # e.g. "2x01", "S01E03", "S01-02xE001-150", "S01E06v2" 313 | if numeric_back: 314 | if match_season_and_episode_pattern(elements, word, token): 315 | return True 316 | # e.g. "ED1", "OP4a", "OVA2" 317 | if not numeric_front: 318 | if match_type_and_episode_pattern(elements, parsed_tokens, word, token): 319 | return True 320 | # e.g. "07.5" 321 | if numeric_front and numeric_back: 322 | if match_fractional_episode_pattern(elements, word, token): 323 | return True 324 | # e.g. "4a", "111C" 325 | if numeric_front and not numeric_back: 326 | if match_partial_episode_pattern(elements, word, token): 327 | return True 328 | # e.g. "#01", "#02-03v2" 329 | if numeric_back: 330 | if match_number_sign_pattern(elements, word, token): 331 | return True 332 | # U+8A71 is used as counter for stories, episodes of TV series, etc. 333 | if numeric_front: 334 | if match_japanese_counter_pattern(elements, word, token): 335 | return True 336 | 337 | return False 338 | 339 | ############################################################################### 340 | 341 | 342 | def is_valid_volume_number(number): 343 | return int(number) <= VOLUME_NUMBER_MAX 344 | 345 | 346 | def set_volume_number(elements, number, token, validate): 347 | if validate: 348 | if not is_valid_volume_number(number): 349 | return False 350 | 351 | elements.insert(ElementCategory.VOLUME_NUMBER, number) 352 | token.category = TokenCategory.IDENTIFIER 353 | return True 354 | 355 | 356 | def match_single_volume_pattern(elements, word, token): 357 | pattern = '(\\d{1,2})[vV](\\d)$' 358 | match = re.match(pattern, word) 359 | 360 | if match: 361 | set_volume_number(elements, match.group(1), token, validate=False) 362 | elements.insert(ElementCategory.RELEASE_VERSION, match.group(2)) 363 | return True 364 | 365 | return False 366 | 367 | 368 | def match_multi_volume_pattern(elements, word, token): 369 | pattern = '(\\d{1,2})[-~&+](\\d{1,2})(?:[vV](\\d))?$' 370 | match = re.match(pattern, word) 371 | 372 | if match: 373 | lower_bound = match.group(1) 374 | upper_bound = match.group(2) 375 | if int(lower_bound) < int(upper_bound): 376 | if set_volume_number(elements, lower_bound, token, validate=True): 377 | set_volume_number(elements, upper_bound, token, validate=False) 378 | if match.group(3): 379 | elements.insert(ElementCategory.RELEASE_VERSION, 380 | match.group(3)) 381 | return True 382 | 383 | return False 384 | 385 | 386 | def match_volume_patterns(elements, word, token): 387 | # All patterns contain at least one non-numeric character 388 | if word.isdigit(): 389 | return False 390 | 391 | word = word.strip(' -') 392 | 393 | numeric_front = word[0].isdigit() 394 | numeric_back = word[-1].isdigit() 395 | 396 | # e.g. "01v2" 397 | if numeric_front and numeric_back: 398 | if match_single_volume_pattern(elements, word, token): 399 | return True 400 | # e.g. "01-02", "03-05v2" 401 | if numeric_front and numeric_back: 402 | if match_multi_volume_pattern(elements, word, token): 403 | return True 404 | 405 | return False 406 | 407 | ############################################################################### 408 | 409 | 410 | def set_season_number(elements, number, token): 411 | if not number.isdigit(): 412 | return False 413 | 414 | elements.insert(ElementCategory.ANIME_SEASON, number) 415 | token.category = TokenCategory.IDENTIFIER 416 | return True 417 | 418 | ############################################################################### 419 | 420 | 421 | def search_for_equivalent_numbers(elements, parsed_tokens, tokens): 422 | for token in tokens: 423 | if parser_helper.is_token_isolated(parsed_tokens, token) or \ 424 | not is_valid_episode_number(token.content): 425 | continue 426 | 427 | # Find the first enclosed, non-delimiter token 428 | next_token = parsed_tokens.find_next(token, TokenFlags.NOT_DELIMITER) 429 | if next_token is None or next_token.category != TokenCategory.BRACKET: 430 | continue 431 | next_token = parsed_tokens.find_next( 432 | next_token, TokenFlags.ENCLOSED | TokenFlags.NOT_DELIMITER) 433 | if next_token is None or next_token.category != TokenCategory.UNKNOWN: 434 | continue 435 | 436 | # Check if it's an isolated number 437 | if not parser_helper.is_token_isolated(parsed_tokens, next_token) or \ 438 | not next_token.content.isdigit() or \ 439 | not is_valid_episode_number(next_token.content): 440 | continue 441 | 442 | episode = min(token, next_token, key=lambda t: int(t.content)) 443 | alt_episode = max(token, next_token, key=lambda t: int(t.content)) 444 | 445 | set_episode_number(elements, episode.content, episode, validate=False) 446 | set_alternative_episode_number(elements, alt_episode.content, alt_episode) 447 | 448 | return True 449 | 450 | return False 451 | 452 | 453 | def search_for_separated_numbers(elements, parsed_tokens, tokens): 454 | for token in tokens: 455 | previous_token = parsed_tokens.find_previous(token, TokenFlags.NOT_DELIMITER) 456 | 457 | # See if the number has a preceding "-" separator 458 | if previous_token.category == TokenCategory.UNKNOWN and \ 459 | parser_helper.is_dash_character(previous_token.content): 460 | if set_episode_number(elements, token.content, token, validate=True): 461 | previous_token.category = TokenCategory.IDENTIFIER 462 | return True 463 | 464 | return False 465 | 466 | 467 | def search_for_isolated_numbers(elements, parsed_tokens, tokens): 468 | for token in tokens: 469 | if not token.enclosed or not parser_helper.is_token_isolated(parsed_tokens, token): 470 | continue 471 | 472 | if set_episode_number(elements, token.content, token, validate=True): 473 | return True 474 | 475 | return False 476 | 477 | 478 | def search_for_last_number(elements, parsed_tokens, tokens): 479 | for token in tokens: 480 | token_index = parsed_tokens.get_index(token) 481 | 482 | # Assuming that episode number always comes after the title, first 483 | # token cannot be what we're looking for 484 | if token_index == 0: 485 | continue 486 | 487 | # An enclosed token is unlikely to be the episode number at this point 488 | if token.enclosed: 489 | continue 490 | 491 | # Ignore if it's the first non-enclosed, non-delimiter token 492 | if all([t.enclosed or t.category == TokenCategory.DELIMITER 493 | for t in parsed_tokens.get_list()[:token_index]]): 494 | continue 495 | 496 | # Ignore if the previous token is "Movie" or "Part" 497 | previous_token = parsed_tokens.find_previous(token, TokenFlags.NOT_DELIMITER) 498 | if previous_token.category == TokenCategory.UNKNOWN: 499 | if previous_token.content.lower() == 'movie' or \ 500 | previous_token.content.lower() == 'part': 501 | continue 502 | 503 | # We'll use this number after all 504 | if set_episode_number(elements, token.content, token, validate=True): 505 | return True 506 | 507 | return False 508 | -------------------------------------------------------------------------------- /anitopy/token.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | from enum import Enum 6 | 7 | 8 | class TokenCategory(Enum): 9 | # Auto enumerate elements 10 | def __new__(cls): 11 | value = len(cls.__members__) + 1 12 | obj = object.__new__(cls) 13 | obj._value_ = value 14 | return obj 15 | 16 | UNKNOWN = () 17 | BRACKET = () 18 | DELIMITER = () 19 | IDENTIFIER = () 20 | INVALID = () 21 | 22 | 23 | class TokenFlags: 24 | NONE = 0 25 | # Categories 26 | BRACKET = 1 << 0 27 | NOT_BRACKET = 1 << 1 28 | DELIMITER = 1 << 2 29 | NOT_DELIMITER = 1 << 3 30 | IDENTIFIER = 1 << 4 31 | NOT_IDENTIFIER = 1 << 5 32 | UNKNOWN = 1 << 6 33 | NOT_UNKNOWN = 1 << 7 34 | VALID = 1 << 8 35 | NOT_VALID = 1 << 9 36 | # Enclosed 37 | ENCLOSED = 1 << 10 38 | NOT_ENCLOSED = 1 << 11 39 | # Masks 40 | MASK_CATEGORIES = \ 41 | BRACKET | NOT_BRACKET | \ 42 | DELIMITER | NOT_DELIMITER | \ 43 | IDENTIFIER | NOT_IDENTIFIER | \ 44 | UNKNOWN | NOT_UNKNOWN | \ 45 | VALID | NOT_VALID 46 | MASK_ENCLOSED = ENCLOSED | NOT_ENCLOSED 47 | 48 | 49 | class Token: 50 | def __init__(self, category=TokenCategory.UNKNOWN, content=None, 51 | enclosed=False): 52 | self.category = category 53 | self.content = content 54 | self.enclosed = enclosed 55 | 56 | def __repr__(self): 57 | return 'Token(category = {0}, content = "{1}", enclosed = {2}'.format( 58 | self.category, self.content, self.enclosed 59 | ) 60 | 61 | def check_flags(self, flags): 62 | def check_flag(flag): 63 | return (flags & flag) == flag 64 | 65 | if flags & TokenFlags.MASK_ENCLOSED: 66 | success = self.enclosed if check_flag(TokenFlags.ENCLOSED) \ 67 | else not self.enclosed 68 | if not success: 69 | return False 70 | 71 | if flags & TokenFlags.MASK_CATEGORIES: 72 | def check_category(fe, fn, cat): 73 | return self.category == cat if check_flag(fe) else \ 74 | self.category != cat if check_flag(fn) else False 75 | if check_category(TokenFlags.BRACKET, TokenFlags.NOT_BRACKET, 76 | TokenCategory.BRACKET): 77 | return True 78 | if check_category(TokenFlags.DELIMITER, TokenFlags.NOT_DELIMITER, 79 | TokenCategory.DELIMITER): 80 | return True 81 | if check_category(TokenFlags.IDENTIFIER, TokenFlags.NOT_IDENTIFIER, 82 | TokenCategory.IDENTIFIER): 83 | return True 84 | if check_category(TokenFlags.UNKNOWN, TokenFlags.NOT_UNKNOWN, 85 | TokenCategory.UNKNOWN): 86 | return True 87 | if check_category(TokenFlags.NOT_VALID, TokenFlags.VALID, 88 | TokenCategory.INVALID): 89 | return True 90 | return False 91 | 92 | return True 93 | 94 | 95 | class Tokens: 96 | def __init__(self): 97 | self._tokens = [] 98 | 99 | def empty(self): 100 | return len(self._tokens) == 0 101 | 102 | def append(self, token): 103 | self._tokens.append(token) 104 | 105 | def insert(self, index, token): 106 | self._tokens.insert(index, token) 107 | 108 | def update(self, tokens): 109 | self._tokens = tokens 110 | 111 | def get(self, index): 112 | return self._tokens[index] 113 | 114 | def get_list(self, flags=None, begin=None, end=None): 115 | tokens = self._tokens 116 | begin_index = 0 if begin is None else self.get_index(begin) 117 | end_index = len(tokens) if end is None else self.get_index(end) 118 | if flags is None: 119 | return tokens[begin_index:end_index+1] 120 | else: 121 | return [token for token in tokens[begin_index:end_index+1] 122 | if token.check_flags(flags)] 123 | 124 | def get_index(self, token): 125 | return self._tokens.index(token) 126 | 127 | def distance(self, token_begin, token_end): 128 | begin_index = 0 if token_begin is None else self.get_index(token_begin) 129 | end_index = len(self._tokens) if token_end is None else \ 130 | self.get_index(token_end) 131 | return end_index - begin_index 132 | 133 | @staticmethod 134 | def _find_in_tokens(tokens, flags): 135 | for token in tokens: 136 | if token.check_flags(flags): 137 | return token 138 | return None 139 | 140 | def find(self, flags): 141 | return self._find_in_tokens(self._tokens, flags) 142 | 143 | def find_previous(self, token, flags): 144 | tokens = self._tokens 145 | if token is None: 146 | tokens = tokens[::-1] 147 | else: 148 | token_index = self.get_index(token) 149 | tokens = tokens[token_index-1::-1] 150 | return self._find_in_tokens(tokens, flags) 151 | 152 | def find_next(self, token, flags): 153 | tokens = self._tokens 154 | if token is not None: 155 | token_index = self.get_index(token) 156 | tokens = tokens[token_index+1:] 157 | return self._find_in_tokens(tokens, flags) 158 | -------------------------------------------------------------------------------- /anitopy/tokenizer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals, absolute_import 4 | 5 | import re 6 | 7 | from anitopy.keyword import keyword_manager 8 | from anitopy.token import TokenCategory, TokenFlags, Token 9 | 10 | 11 | class Tokenizer: 12 | def __init__(self, filename, options, elements, tokens): 13 | self.filename = filename 14 | self.options = options 15 | self.elements = elements 16 | self.tokens = tokens 17 | 18 | def tokenize(self): 19 | self._tokenize_by_brackets() 20 | return not self.tokens.empty() 21 | 22 | def _add_token(self, category, content, enclosed): 23 | self.tokens.append(Token(category, content, enclosed)) 24 | 25 | def _tokenize_by_brackets(self): 26 | brackets = [ 27 | ('(', ')'), # U+0028-U+0029 Parenthesis 28 | ('[', ']'), # U+005B-U+005D Square bracket 29 | ('{', '}'), # U+007B-U+007D Curly bracket 30 | ('\u300C', '\u300D'), # Corner bracket 31 | ('\u300E', '\u300F'), # White corner bracket 32 | ('\u3010', '\u3011'), # Black lenticular bracket 33 | ('\uFF08', '\uFF09'), # Fullwidth parenthesis 34 | ] 35 | 36 | text = self.filename 37 | is_bracket_open = False 38 | matching_bracket = '' 39 | 40 | def find_first_bracket(): 41 | open_brackets = [bracket_pair[0] for bracket_pair in brackets] 42 | 43 | index = next( 44 | (idx for idx, bracket in enumerate(text) 45 | if bracket in open_brackets), -1) 46 | matching_bracket = next( 47 | (bracket_pair[1] for bracket_pair in brackets 48 | if bracket_pair[0] == text[index]), None) 49 | return index, matching_bracket 50 | 51 | while text: 52 | if not is_bracket_open: 53 | bracket_index, matching_bracket = find_first_bracket() 54 | else: 55 | # Looking for the matching bracket allows us to better handle 56 | # some rare cases with nested brackets. 57 | bracket_index = text.find(matching_bracket) if matching_bracket is not None else -1 58 | 59 | if bracket_index != 0: # Found a token before the bracket 60 | self._tokenize_by_preidentified( 61 | text[:bracket_index] if bracket_index != -1 else text, 62 | enclosed=is_bracket_open 63 | ) 64 | 65 | if bracket_index != -1: # Found bracket 66 | self._add_token( 67 | TokenCategory.BRACKET, text[bracket_index], enclosed=True) 68 | is_bracket_open = not is_bracket_open 69 | text = text[bracket_index+1:] 70 | else: # Reached the end 71 | text = '' 72 | 73 | def _tokenize_by_preidentified(self, text, enclosed): 74 | preidentified_tokens = keyword_manager.peek(self.elements, text) 75 | 76 | last_token_end_pos = 0 77 | for token_begin_pos, token_end_pos in preidentified_tokens: 78 | if last_token_end_pos != token_begin_pos: 79 | # Tokenize the text between the preidentified tokens 80 | self._tokenize_by_delimiters( 81 | text[last_token_end_pos:token_begin_pos], enclosed) 82 | self._add_token(TokenCategory.IDENTIFIER, text[token_begin_pos:token_end_pos], enclosed) 83 | last_token_end_pos = token_end_pos 84 | 85 | if last_token_end_pos != len(text): 86 | # Tokenize the text after the preidentified tokens (or all the text 87 | # if there was no preidentified tokens) 88 | self._tokenize_by_delimiters(text[last_token_end_pos:], enclosed) 89 | 90 | def _tokenize_by_delimiters(self, text, enclosed): 91 | delimiters = ''.join( 92 | ['\\' + d for d in self.options['allowed_delimiters']]) 93 | pattern = '([{0}])'.format(delimiters) 94 | splited_text = re.split(pattern, text) 95 | 96 | for sub_text in splited_text: 97 | if sub_text: 98 | if sub_text in self.options['allowed_delimiters']: 99 | self._add_token( 100 | TokenCategory.DELIMITER, sub_text, enclosed) 101 | else: 102 | self._add_token(TokenCategory.UNKNOWN, sub_text, enclosed) 103 | 104 | self._validate_delimiter_tokens() 105 | 106 | def _validate_delimiter_tokens(self): 107 | def find_previous_valid_token(token): 108 | return self.tokens.find_previous(token, TokenFlags.VALID) 109 | 110 | def find_next_valid_token(token): 111 | return self.tokens.find_next(token, TokenFlags.VALID) 112 | 113 | def is_delimiter_token(token): 114 | return token is not None and \ 115 | token.category == TokenCategory.DELIMITER 116 | 117 | def is_unknown_token(token): 118 | return token is not None and \ 119 | token.category == TokenCategory.UNKNOWN 120 | 121 | def is_single_character_token(token): 122 | return is_unknown_token(token) and len(token.content) == 1 and \ 123 | token.content != '-' 124 | 125 | def append_token_to(token, append_to): 126 | append_to.content += token.content 127 | token.category = TokenCategory.INVALID 128 | 129 | for token in self.tokens.get_list(): 130 | if token.category != TokenCategory.DELIMITER: 131 | continue 132 | 133 | delimiter = token.content 134 | prev_token = find_previous_valid_token(token) 135 | next_token = find_next_valid_token(token) 136 | 137 | # Check for single-character tokens to prevent splitting group 138 | # names, keywords, episode number, etc. 139 | if delimiter != ' ' and delimiter != '_': 140 | if is_single_character_token(prev_token): 141 | append_token_to(token, prev_token) 142 | while is_unknown_token(next_token): 143 | append_token_to(next_token, prev_token) 144 | next_token = find_next_valid_token(next_token) 145 | if is_delimiter_token(next_token) and \ 146 | next_token.content == delimiter: 147 | append_token_to(next_token, prev_token) 148 | next_token = find_next_valid_token(next_token) 149 | continue 150 | if is_single_character_token(next_token): 151 | append_token_to(token, prev_token) 152 | append_token_to(next_token, prev_token) 153 | continue 154 | 155 | # Check for adjacent delimiters 156 | if is_unknown_token(prev_token) and is_delimiter_token(next_token): 157 | next_delimiter = next_token.content 158 | if delimiter != next_delimiter and delimiter != ',': 159 | if next_delimiter == ' ' or next_delimiter == '_': 160 | append_token_to(token, prev_token) 161 | 162 | elif is_delimiter_token(prev_token) and \ 163 | is_delimiter_token(next_token): 164 | prev_delimiter = prev_token.content 165 | next_delimiter = next_token.content 166 | if prev_delimiter == next_delimiter and \ 167 | prev_delimiter != delimiter: 168 | token.category = TokenCategory.UNKNOWN # e.g. "&" in "_&_" 169 | 170 | # Check for other special cases 171 | if delimiter == '&' or delimiter == '+': 172 | if is_unknown_token(prev_token) and \ 173 | is_unknown_token(next_token): 174 | if prev_token.content.isdigit() and \ 175 | next_token.content.isdigit(): 176 | append_token_to(token, prev_token) 177 | append_token_to(next_token, prev_token) # e.g. "01+02" 178 | 179 | self.tokens.update([ 180 | token for token in self.tokens.get_list() 181 | if token.category != TokenCategory.INVALID 182 | ]) 183 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | enum34;python_version<'3.4' -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | 3 | flake8==4.0.1 4 | twine==4.0.1 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from setuptools import setup 3 | import sys 4 | 5 | 6 | LONG_DESCRIPTION = Path('README.rst').read_text() 7 | REQUIRED_PACKAGES = [] 8 | if sys.version_info < (3, 4): 9 | REQUIRED_PACKAGES.append('enum34') 10 | 11 | setup( 12 | name='anitopy', 13 | packages=['anitopy'], 14 | version='2.2.0rc2', 15 | description='An anime video filename parser', 16 | long_description=LONG_DESCRIPTION, 17 | long_description_content_type='text/x-rst', 18 | author='Igor Cescon de Moura', 19 | author_email='igorcesconm@gmail.com', 20 | url='https://github.com/igorcmoura/anitopy', 21 | python_requires='>=2.7', 22 | install_requires=REQUIRED_PACKAGES, 23 | classifiers=[ 24 | 'Intended Audience :: Developers', 25 | 'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)', 26 | 'Programming Language :: Python', 27 | 'Programming Language :: Python :: 2.7', 28 | 'Programming Language :: Python :: 3', 29 | 'Programming Language :: Python :: 3.3', 30 | 'Programming Language :: Python :: 3.4', 31 | 'Programming Language :: Python :: 3.5', 32 | 'Programming Language :: Python :: 3.6', 33 | 'Programming Language :: Python :: 3.7', 34 | 'Programming Language :: Python :: 3.8', 35 | 'Programming Language :: Python :: 3.9', 36 | ] 37 | ) 38 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igorcmoura/anitopy/1a151905087c99e60bc6915fcd86c2e7fdd247e0/tests/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igorcmoura/anitopy/1a151905087c99e60bc6915fcd86c2e7fdd247e0/tests/fixtures/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/failing_table.py: -------------------------------------------------------------------------------- 1 | failing_table = [ 2 | # Tests took from anitomy on 2022-10-23 3 | [ 4 | '[BakaWolf-m.3.3.w] Special A 01 (H.264) [C83164B9].mkv', 5 | None, 6 | { 7 | 'anime_title': 'Special A', 8 | 'episode_number': '01', 9 | 'file_checksum': 'C83164B9', 10 | 'file_extension': 'mkv', 11 | 'file_name': '[BakaWolf-m.3.3.w] Special A 01 (H.264) [C83164B9].mkv', # noqa E501 12 | 'id': 3470, 13 | 'release_group': 'BakaWolf-m.3.3.w', 14 | 'video_term': 'H.264' 15 | } 16 | ], [ 17 | '[Juuni.Kokki]-(Les.12.Royaumes)-[Ep.24]-[x264+OGG]-[JAP+FR+Sub.FR]-[Chap]-[AzF].mkv', # noqa E501 18 | None, 19 | { 20 | 'anime_title': 'Juuni Kokki', 21 | 'audio_term': 'OGG', 22 | 'episode_number': '24', 23 | 'file_extension': 'mkv', 24 | 'file_name': '[Juuni.Kokki]-(Les.12.Royaumes)-[Ep.24]-[x264+OGG]-[JAP+FR+Sub.FR]-[Chap]-[AzF].mkv', # noqa E501 25 | 'id': 153, 26 | 'language': 'JAP', 27 | 'subtitles': 'Sub', 28 | 'video_term': 'x264' 29 | } 30 | ], [ 31 | 'Code_Geass_R2_TV_[20_of_25]_[ru_jp]_[HDTV]_[Varies_&_Cuba77_&_AnimeReactor_RU].mkv', # noqa E501 32 | None, 33 | { 34 | 'anime_title': 'Code Geass R2 TV', 35 | 'anime_type': 'TV', 36 | 'episode_number': '20', 37 | 'file_extension': 'mkv', 38 | 'file_name': 'Code_Geass_R2_TV_[20_of_25]_[ru_jp]_[HDTV]_[Varies_&_Cuba77_&_AnimeReactor_RU].mkv', # noqa E501 39 | 'id': 2904, 40 | 'release_group': 'Varies & Cuba77 & AnimeReactor RU', 41 | 'source': 'HDTV' 42 | } 43 | ], [ 44 | 'Noein_[01_of_24]_[ru_jp]_[bodlerov_&_torrents_ru].mkv', 45 | None, 46 | { 47 | 'anime_title': 'Noein', 48 | 'episode_number': '01', 49 | 'file_extension': 'mkv', 50 | 'file_name': 'Noein_[01_of_24]_[ru_jp]_[bodlerov_&_torrents_ru].mkv', # noqa E501 51 | 'id': 584, 52 | 'release_group': 'bodlerov & torrents ru' 53 | } 54 | ], [ 55 | '[Ayu]_Kiddy_Grade_2_-_Pilot_[H264_AC3][650B731B].mkv', 56 | None, 57 | { 58 | 'anime_title': 'Kiddy Grade 2', 59 | 'audio_term': 'AC3', 60 | 'episode_title': 'Pilot', 61 | 'file_checksum': '650B731B', 62 | 'file_extension': 'mkv', 63 | 'file_name': '[Ayu]_Kiddy_Grade_2_-_Pilot_[H264_AC3][650B731B].mkv', # noqa E501 64 | 'release_group': 'Ayu', 65 | 'video_term': 'H264' 66 | } 67 | ], [ 68 | '[Keroro].148.[Xvid.mp3].[FE68D5F1].avi', 69 | None, 70 | { 71 | 'anime_title': 'Keroro', 72 | 'audio_term': 'mp3', 73 | 'episode_number': '148', 74 | 'file_checksum': 'FE68D5F1', 75 | 'file_extension': 'avi', 76 | 'file_name': '[Keroro].148.[Xvid.mp3].[FE68D5F1].avi', 77 | 'id': 516, 78 | 'video_term': 'Xvid' 79 | } 80 | ], [ 81 | 'Macross Frontier - Sayonara no Tsubasa (Central Anime, 720p) [46B35E25].mkv', # noqa E501 82 | None, 83 | { 84 | 'anime_title': 'Macross Frontier - Sayonara no Tsubasa', 85 | 'file_checksum': '46B35E25', 86 | 'file_extension': 'mkv', 87 | 'file_name': 'Macross Frontier - Sayonara no Tsubasa (Central Anime, 720p) [46B35E25].mkv', # noqa E501 88 | 'id': 7222, 89 | 'release_group': 'Central Anime', 90 | 'video_resolution': '720p' 91 | } 92 | ], [ 93 | '[Nubles] Space Battleship Yamato 2199 (2012) episode 18 (720p 8 bit AAC)[BA70BA9C]', # noqa E501 94 | None, 95 | { 96 | 'anime_year': '2012', 97 | 'anime_title': 'Space Battleship Yamato 2199', 98 | 'audio_term': 'AAC', 99 | 'episode_number': '18', 100 | 'file_checksum': 'BA70BA9C', 101 | 'file_name': '[Nubles] Space Battleship Yamato 2199 (2012) episode 18 (720p 8 bit AAC)[BA70BA9C]', # noqa E501 102 | 'id': 12029, 103 | 'release_group': 'Nubles', 104 | 'video_resolution': '720p', 105 | 'video_term': '8 bit' 106 | } 107 | ], [ 108 | '[Nubles] Space Battleship Yamato 2199 (2012) episode 18 (720p 10 bit AAC)[1F56D642]', # noqa E501 109 | None, 110 | { 111 | 'anime_year': '2012', 112 | 'anime_title': 'Space Battleship Yamato 2199', 113 | 'audio_term': 'AAC', 114 | 'episode_number': '18', 115 | 'file_checksum': '1F56D642', 116 | 'file_name': '[Nubles] Space Battleship Yamato 2199 (2012) episode 18 (720p 10 bit AAC)[1F56D642]', # noqa E501 117 | 'id': 12029, 118 | 'release_group': 'Nubles', 119 | 'video_resolution': '720p', 120 | 'video_term': '10 bit' 121 | } 122 | ], [ 123 | '[Hien] Kotoura-san - Special Short Anime \'Haruka\'s Room\' - 01 [BD 1080p H.264 10-bit AAC][6B6BE015].mkv', # noqa E501 124 | None, 125 | { 126 | 'anime_title': 'Kotoura-san - Special Short Anime \'Haruka\'s Room\'', # noqa E501 127 | 'audio_term': 'AAC', 128 | 'episode_number': '01', 129 | 'file_checksum': '6B6BE015', 130 | 'file_extension': 'mkv', 131 | 'file_name': '[Hien] Kotoura-san - Special Short Anime \'Haruka\'s Room\' - 01 [BD 1080p H.264 10-bit AAC][6B6BE015].mkv', # noqa E501 132 | 'id': 16636, 133 | 'release_group': 'Hien', 134 | 'source': 'BD', 135 | 'video_resolution': '1080p', 136 | 'video_term': [ 137 | 'H.264', 138 | '10-bit' 139 | ] 140 | } 141 | ], [ 142 | 'Aim_For_The_Top!_Gunbuster-ep1.BD(H264.FLAC.10bit)[KAA][69ECCDCF].mkv', # noqa E501 143 | None, 144 | { 145 | 'anime_title': 'Aim For The Top! Gunbuster', 146 | 'audio_term': 'FLAC', 147 | 'episode_number': '1', 148 | 'file_checksum': '69ECCDCF', 149 | 'file_extension': 'mkv', 150 | 'file_name': 'Aim_For_The_Top!_Gunbuster-ep1.BD(H264.FLAC.10bit)[KAA][69ECCDCF].mkv', # noqa E501 151 | 'id': 949, 152 | 'release_group': 'KAA', 153 | 'source': 'BD', 154 | 'video_term': [ 155 | 'H264', 156 | '10bit' 157 | ] 158 | } 159 | ], [ 160 | '[Mobile Suit Gundam Seed Destiny HD REMASTER][07][Big5][720p][AVC_AAC][encoded by SEED].mp4', # noqa E501 161 | None, 162 | { 163 | 'anime_title': 'Mobile Suit Gundam Seed Destiny', 164 | 'audio_term': 'AAC', 165 | 'episode_number': '07', 166 | 'file_extension': 'mp4', 167 | 'file_name': '[Mobile Suit Gundam Seed Destiny HD REMASTER][07][Big5][720p][AVC_AAC][encoded by SEED].mp4', # noqa E501 168 | 'id': 94, 169 | 'other': 'REMASTER', 170 | 'subtitles': 'Big5', 171 | 'release_group': 'SEED', 172 | 'video_resolution': '720p', 173 | 'video_term': [ 174 | 'HD', 175 | 'AVC' 176 | ] 177 | } 178 | ], [ 179 | '「K」 Image Blu-ray WHITE & BLACK - Main (BD 1280x720 AVC AAC).mp4', 180 | None, 181 | { 182 | 'anime_title': '「K」', 183 | 'audio_term': 'AAC', 184 | 'file_extension': 'mp4', 185 | 'file_name': '「K」 Image Blu-ray WHITE & BLACK - Main (BD 1280x720 AVC AAC).mp4', # noqa E501 186 | 'id': 14467, 187 | 'source': [ 188 | 'Blu-ray', 189 | 'BD' 190 | ], 191 | 'video_resolution': '1280x720', 192 | 'video_term': 'AVC' 193 | } 194 | ], [ 195 | '[[Zero-Raws] Shingeki no Kyojin - 05 (MBS 1280x720 x264 AAC).mp4', 196 | None, 197 | { 198 | 'anime_title': 'Shingeki no Kyojin', 199 | 'audio_term': 'AAC', 200 | 'episode_number': '05', 201 | 'file_extension': 'mp4', 202 | 'file_name': '[[Zero-Raws] Shingeki no Kyojin - 05 (MBS 1280x720 x264 AAC).mp4', # noqa E501 203 | 'id': 16498, 204 | 'release_group': 'Zero-Raws', 205 | 'video_resolution': '1280x720', 206 | 'video_term': 'x264' 207 | } 208 | ], [ 209 | '[TV-J] Kidou Senshi Gundam UC Unicorn - episode.02 [BD 1920x1080 h264+AAC(5.1ch JP+EN) +Sub(JP-EN-SP-FR-CH) Chap].mp4', # noqa E501 210 | None, 211 | { 212 | 'anime_title': 'Kidou Senshi Gundam UC Unicorn', 213 | 'audio_term': [ 214 | 'AAC', 215 | '5.1ch' 216 | ], 217 | 'episode_number': '02', 218 | 'file_extension': 'mp4', 219 | 'file_name': '[TV-J] Kidou Senshi Gundam UC Unicorn - episode.02 [BD 1920x1080 h264+AAC(5.1ch JP+EN) +Sub(JP-EN-SP-FR-CH) Chap].mp4', # noqa E501 220 | 'id': 6336, 221 | 'release_group': 'TV-J', 222 | 'source': 'BD', 223 | 'video_resolution': '1920x1080', 224 | 'video_term': 'h264' 225 | } 226 | ], [ 227 | '[Zero-Raws] Shingeki no Kyojin - 25 END (MBS 1280x720 x264 AAC).mp4', 228 | None, 229 | { 230 | 'anime_title': 'Shingeki no Kyojin', 231 | 'audio_term': 'AAC', 232 | 'episode_number': '25', 233 | 'file_extension': 'mp4', 234 | 'file_name': '[Zero-Raws] Shingeki no Kyojin - 25 END (MBS 1280x720 x264 AAC).mp4', # noqa E501 235 | 'id': 16498, 236 | 'release_group': 'Zero-Raws', 237 | 'release_information': 'END', 238 | 'video_resolution': '1280x720', 239 | 'video_term': 'x264' 240 | } 241 | ], [ 242 | 'Evangelion Shin Gekijouban Q (BDrip 1920x1080 x264 FLACx2 5.1ch)-ank.mkv', # noqa E501 243 | None, 244 | { 245 | 'anime_title': 'Evangelion Shin Gekijouban Q', 246 | 'anime_type': 'Gekijouban', 247 | 'audio_term': [ 248 | 'FLACx2', 249 | '5.1ch' 250 | ], 251 | 'file_extension': 'mkv', 252 | 'file_name': 'Evangelion Shin Gekijouban Q (BDrip 1920x1080 x264 FLACx2 5.1ch)-ank.mkv', # noqa E501 253 | 'id': 3785, 254 | 'release_group': 'ank', 255 | 'source': 'BDrip', 256 | 'video_resolution': '1920x1080', 257 | 'video_term': 'x264' 258 | } 259 | ], [ 260 | '【MMZYSUB】★【Golden Time】[24(END)][GB][720P_MP4]', 261 | None, 262 | { 263 | 'anime_title': 'Golden Time', 264 | 'episode_number': '24', 265 | 'file_name': '【MMZYSUB】★【Golden Time】[24(END)][GB][720P_MP4]', 266 | 'id': 17895, 267 | 'release_group': 'MMZYSUB', 268 | 'release_information': 'END', 269 | 'video_resolution': '720P', 270 | 'video_term': 'MP4' 271 | } 272 | ], [ 273 | '[AoJiaoZero][Mangaka-san to Assistant-san to the Animation] 02 [BIG][X264_AAC][720P].mp4', # noqa E501 274 | None, 275 | { 276 | 'anime_title': 'Mangaka-san to Assistant-san to the Animation', 277 | 'audio_term': 'AAC', 278 | 'episode_number': '02', 279 | 'file_extension': 'mp4', 280 | 'file_name': '[AoJiaoZero][Mangaka-san to Assistant-san to the Animation] 02 [BIG][X264_AAC][720P].mp4', # noqa E501 281 | 'id': 21863, 282 | 'release_group': 'AoJiaoZero', 283 | 'video_resolution': '720P', 284 | 'video_term': 'X264' 285 | } 286 | ], [ 287 | '[Asenshi] Rozen Maiden 3 - PV [CA57F300].mkv', 288 | None, 289 | { 290 | 'anime_title': 'Rozen Maiden 3', 291 | 'anime_type': 'PV', 292 | 'file_checksum': 'CA57F300', 293 | 'file_extension': 'mkv', 294 | 'file_name': '[Asenshi] Rozen Maiden 3 - PV [CA57F300].mkv', 295 | 'id': 18041, 296 | 'release_group': 'Asenshi' 297 | } 298 | ], [ 299 | '__BLUE DROP 10 (1).avi', 300 | None, 301 | { 302 | 'anime_title': 'BLUE DROP', 303 | 'episode_number': '10', 304 | 'file_extension': 'avi', 305 | 'file_name': '__BLUE DROP 10 (1).avi', 306 | 'id': 2964 307 | } 308 | ], [ 309 | '37 [Ruberia]_Death_Note_-_37v2_[FINAL]_[XviD][6FA7D273].avi', 310 | None, 311 | { 312 | 'anime_title': 'Death Note', 313 | 'episode_number': '37', 314 | 'file_checksum': '6FA7D273', 315 | 'file_extension': 'avi', 316 | 'file_name': '37 [Ruberia]_Death_Note_-_37v2_[FINAL]_[XviD][6FA7D273].avi', # noqa E501 317 | 'id': 1535, 318 | 'release_group': 'Ruberia', 319 | 'release_information': 'FINAL', 320 | 'release_version': '2', 321 | 'video_term': 'XviD' 322 | } 323 | ], [ 324 | '[UTW]_Accel_World_-_EX01_[BD][h264-720p_AAC][3E56EE18].mkv', 325 | None, 326 | { 327 | 'anime_title': 'Accel World - EX', 328 | 'audio_term': 'AAC', 329 | 'episode_number': '01', 330 | 'file_checksum': '3E56EE18', 331 | 'file_extension': 'mkv', 332 | 'file_name': '[UTW]_Accel_World_-_EX01_[BD][h264-720p_AAC][3E56EE18].mkv', # noqa E501 333 | 'id': 13939, 334 | 'release_group': 'UTW', 335 | 'source': 'BD', 336 | 'video_resolution': '720p', 337 | 'video_term': 'h264' 338 | } 339 | ], [ 340 | '[Urusai]_Bokura_Ga_Ita_01_[DVD_h264_AC3]_[BFCE1627][Fixed].mkv', 341 | None, 342 | { 343 | 'anime_title': 'Bokura Ga Ita', 344 | 'audio_term': 'AC3', 345 | 'episode_number': '01', 346 | 'file_checksum': 'BFCE1627', 347 | 'file_extension': 'mkv', 348 | 'file_name': '[Urusai]_Bokura_Ga_Ita_01_[DVD_h264_AC3]_[BFCE1627][Fixed].mkv', # noqa E501 349 | 'id': 1222, 350 | 'release_group': 'Urusai', 351 | 'source': 'DVD', 352 | 'video_term': 'h264' 353 | } 354 | ], [ 355 | '01 - Land of Visible Pain.mkv', 356 | None, 357 | { 358 | 'episode_number': '01', 359 | 'episode_title': 'Land of Visible Pain', 360 | 'file_extension': 'mkv', 361 | 'file_name': '01 - Land of Visible Pain.mkv' 362 | } 363 | ], [ 364 | 'The iDOLM@STER 765 Pro to Iu Monogatari.mkv', 365 | None, 366 | { 367 | 'anime_title': 'The iDOLM@STER 765 Pro to Iu Monogatari', 368 | 'file_extension': 'mkv', 369 | 'file_name': 'The iDOLM@STER 765 Pro to Iu Monogatari.mkv', 370 | 'id': 11889 371 | } 372 | ], [ 373 | '[SpoonSubs]_Hidamari_Sketch_x365_-_04.1_(DVD)[B6CE8458].mkv', 374 | None, 375 | { 376 | 'anime_title': 'Hidamari Sketch x365', 377 | 'episode_number': '04.1', 378 | 'file_checksum': 'B6CE8458', 379 | 'file_extension': 'mkv', 380 | 'file_name': '[SpoonSubs]_Hidamari_Sketch_x365_-_04.1_(DVD)[B6CE8458].mkv', # noqa E501 381 | 'id': 3604, 382 | 'release_group': 'SpoonSubs', 383 | 'source': 'DVD' 384 | } 385 | ], [ 386 | 'Ep. 01 - The Boy in the Iceberg', 387 | None, 388 | { 389 | 'episode_number': '01', 390 | 'episode_title': 'The Boy in the Iceberg', 391 | 'file_name': 'Ep. 01 - The Boy in the Iceberg' 392 | } 393 | ], [ 394 | '[B-G_&_m.3.3.w]_Myself_Yourself_12.DVD(H.264_DD2.0)_[CB2B37F1].mkv', 395 | None, 396 | { 397 | 'anime_title': 'Myself Yourself', 398 | 'audio_term': 'DD2.0', 399 | 'episode_number': '12', 400 | 'file_checksum': 'CB2B37F1', 401 | 'file_extension': 'mkv', 402 | 'file_name': '[B-G_&_m.3.3.w]_Myself_Yourself_12.DVD(H.264_DD2.0)_[CB2B37F1].mkv', # noqa E501 403 | 'id': 2926, 404 | 'release_group': 'B-G_&_m.3.3.w', 405 | 'source': 'DVD', 406 | 'video_term': 'H.264' 407 | } 408 | ], [ 409 | 'The.Animatrix.08.A.Detective.Story.720p.BluRay.DTS.x264-ESiR.mkv', 410 | None, 411 | { 412 | 'anime_title': 'The Animatrix', 413 | 'audio_term': 'DTS', 414 | 'episode_number': '08', 415 | 'episode_title': 'A Detective Story', 416 | 'file_extension': 'mkv', 417 | 'file_name': 'The.Animatrix.08.A.Detective.Story.720p.BluRay.DTS.x264-ESiR.mkv', # noqa E501 418 | 'id': 1303, 419 | 'release_group': 'ESiR', 420 | 'source': 'BluRay', 421 | 'video_resolution': '720p', 422 | 'video_term': 'x264' 423 | } 424 | ], [ 425 | 'Neko no Ongaeshi - [HQR.remux-DualAudio][NTV.1280x692.h264](0CDC2145).mkv', # noqa E501 426 | None, 427 | { 428 | 'anime_title': 'Neko no Ongaeshi', 429 | 'audio_term': 'DualAudio', 430 | 'file_checksum': '0CDC2145', 431 | 'file_extension': 'mkv', 432 | 'file_name': 'Neko no Ongaeshi - [HQR.remux-DualAudio][NTV.1280x692.h264](0CDC2145).mkv', # noqa E501 433 | 'id': 597, 434 | 'video_resolution': '1280x692', 435 | 'video_term': 'h264' 436 | } 437 | ], [ 438 | '[ReDone] Memories Off 3.5 - 04 (DVD 10-bit).mkv', 439 | None, 440 | { 441 | 'anime_title': 'Memories Off 3.5', 442 | 'episode_number': '04', 443 | 'file_extension': 'mkv', 444 | 'file_name': '[ReDone] Memories Off 3.5 - 04 (DVD 10-bit).mkv', 445 | 'id': 363, 446 | 'release_group': 'ReDone', 447 | 'source': 'DVD', 448 | 'video_term': '10-bit' 449 | } 450 | ], [ 451 | 'Byousoku 5 Centimeter [Blu-Ray][1920x1080 H.264][2.0ch AAC][SOFTSUBS]', # noqa E501 452 | None, 453 | { 454 | 'anime_title': 'Byousoku 5 Centimeter', 455 | 'audio_term': [ 456 | '2.0ch', 457 | 'AAC' 458 | ], 459 | 'file_name': 'Byousoku 5 Centimeter [Blu-Ray][1920x1080 H.264][2.0ch AAC][SOFTSUBS]', # noqa E501 460 | 'id': 1689, 461 | 'source': 'Blu-Ray', 462 | 'subtitles': 'SOFTSUBS', 463 | 'video_resolution': '1920x1080', 464 | 'video_term': 'H.264' 465 | } 466 | ], [ 467 | 'Dragon.Ball.KAI.-.01.-.1080p.BluRay.x264.DHD.mkv', 468 | None, 469 | { 470 | 'anime_title': 'Dragon Ball KAI', 471 | 'episode_number': '01', 472 | 'file_extension': 'mkv', 473 | 'file_name': 'Dragon.Ball.KAI.-.01.-.1080p.BluRay.x264.DHD.mkv', 474 | 'id': 6033, 475 | 'source': 'BluRay', 476 | 'video_resolution': '1080p', 477 | 'video_term': 'x264' 478 | } 479 | ], [ 480 | '[EveTaku] AKB0048 Vol.03 - Making of Kibou-ni-Tsuite Music Video (BDRip 1080i H.264-Hi10P FLAC)[C09462E2]', # noqa E501 481 | None, 482 | { 483 | 'anime_title': 'AKB0048', 484 | 'audio_term': 'FLAC', 485 | 'file_checksum': 'C09462E2', 486 | 'file_name': '[EveTaku] AKB0048 Vol.03 - Making of Kibou-ni-Tsuite Music Video (BDRip 1080i H.264-Hi10P FLAC)[C09462E2]', # noqa E501 487 | 'id': 12149, 488 | 'release_group': 'EveTaku', 489 | 'source': 'BDRip', 490 | 'video_term': [ 491 | 'H.264', 492 | 'Hi10P' 493 | ], 494 | 'video_resolution': '1080i', 495 | 'volume_number': '03' 496 | } 497 | ], 498 | ] 499 | -------------------------------------------------------------------------------- /tests/fixtures/table.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | table = [ 6 | # Tests took from anitomy on 2022-10-23 7 | [ 8 | '[TaigaSubs]_Toradora!_(2008)_-_01v2_-_Tiger_and_Dragon_[1280x720_H.264_FLAC][1234ABCD].mkv', # noqa E501 9 | None, 10 | { 11 | 'anime_title': 'Toradora!', 12 | 'anime_year': '2008', 13 | 'audio_term': 'FLAC', 14 | 'episode_number': '01', 15 | 'episode_title': 'Tiger and Dragon', 16 | 'file_checksum': '1234ABCD', 17 | 'file_extension': 'mkv', 18 | 'file_name': '[TaigaSubs]_Toradora!_(2008)_-_01v2_-_Tiger_and_Dragon_[1280x720_H.264_FLAC][1234ABCD].mkv', # noqa E501 19 | 'id': 4224, 20 | 'release_group': 'TaigaSubs', 21 | 'release_version': '2', 22 | 'video_resolution': '1280x720', 23 | 'video_term': 'H.264' 24 | } 25 | ], [ 26 | '[ANBU]_Princess_Lover!_-_01_[2048A39A].mkv', 27 | None, 28 | { 29 | 'anime_title': 'Princess Lover!', 30 | 'episode_number': '01', 31 | 'file_checksum': '2048A39A', 32 | 'file_extension': 'mkv', 33 | 'file_name': '[ANBU]_Princess_Lover!_-_01_[2048A39A].mkv', 34 | 'id': 6201, 35 | 'release_group': 'ANBU' 36 | } 37 | ], [ 38 | '[ANBU-Menclave]_Canaan_-_01_[1024x576_H.264_AAC][12F00E89].mkv', 39 | None, 40 | { 41 | 'anime_title': 'Canaan', 42 | 'audio_term': 'AAC', 43 | 'episode_number': '01', 44 | 'file_checksum': '12F00E89', 45 | 'file_extension': 'mkv', 46 | 'file_name': '[ANBU-Menclave]_Canaan_-_01_[1024x576_H.264_AAC][12F00E89].mkv', # noqa E501 47 | 'id': 5356, 48 | 'release_group': 'ANBU-Menclave', 49 | 'video_resolution': '1024x576', 50 | 'video_term': 'H.264' 51 | } 52 | ], [ 53 | '[ANBU-umai]_Haiyoru!_Nyaru-Ani_[596DD8E6].mkv', 54 | None, 55 | { 56 | 'anime_title': 'Haiyoru! Nyaru-Ani', 57 | 'file_checksum': '596DD8E6', 58 | 'file_extension': 'mkv', 59 | 'file_name': '[ANBU-umai]_Haiyoru!_Nyaru-Ani_[596DD8E6].mkv', 60 | 'id': 7596, 61 | 'release_group': 'ANBU-umai' 62 | } 63 | ], [ 64 | '[chibi-Doki] Seikon no Qwaser - 13v0 (Uncensored Director\'s Cut) [988DB090].mkv', # noqa E501 65 | None, 66 | { 67 | 'anime_title': 'Seikon no Qwaser', 68 | 'episode_number': '13', 69 | 'file_checksum': '988DB090', 70 | 'file_extension': 'mkv', 71 | 'file_name': '[chibi-Doki] Seikon no Qwaser - 13v0 (Uncensored Director\'s Cut) [988DB090].mkv', # noqa E501 72 | 'id': 6500, 73 | 'other': 'Uncensored', 74 | 'release_group': 'chibi-Doki', 75 | 'release_version': '0' 76 | } 77 | ], [ 78 | '[Chihiro]_Kono_Aozora_ni_Yakusoku_Wo_10_v2_[DVD][h264][C83D206B].mkv', 79 | None, 80 | { 81 | 'anime_title': 'Kono Aozora ni Yakusoku Wo', 82 | 'episode_number': '10', 83 | 'file_checksum': 'C83D206B', 84 | 'file_extension': 'mkv', 85 | 'file_name': '[Chihiro]_Kono_Aozora_ni_Yakusoku_Wo_10_v2_[DVD][h264][C83D206B].mkv', # noqa E501 86 | 'id': 2155, 87 | 'release_group': 'Chihiro', 88 | 'release_version': '2', 89 | 'source': 'DVD', 90 | 'video_term': 'h264' 91 | } 92 | ], [ 93 | '[Coalgirls]_Toradora_ED2_(704x480_DVD_AAC)_[3B65D1E6].mkv', 94 | None, 95 | { 96 | 'anime_title': 'Toradora ED', 97 | 'anime_type': 'ED', 98 | 'audio_term': 'AAC', 99 | 'episode_number': '2', 100 | 'file_checksum': '3B65D1E6', 101 | 'file_extension': 'mkv', 102 | 'file_name': '[Coalgirls]_Toradora_ED2_(704x480_DVD_AAC)_[3B65D1E6].mkv', # noqa E501 103 | 'id': 4224, 104 | 'release_group': 'Coalgirls', 105 | 'source': 'DVD', 106 | 'video_resolution': '704x480' 107 | } 108 | ], [ 109 | '[Conclave-Mendoi]_Mobile_Suit_Gundam_00_S2_-_01v2_[1280x720_H.264_AAC][4863FBE8].mkv', # noqa E501 110 | None, 111 | { 112 | 'anime_title': 'Mobile Suit Gundam 00', 113 | 'audio_term': 'AAC', 114 | 'anime_season': '2', 115 | 'episode_number': '01', 116 | 'file_checksum': '4863FBE8', 117 | 'file_extension': 'mkv', 118 | 'file_name': '[Conclave-Mendoi]_Mobile_Suit_Gundam_00_S2_-_01v2_[1280x720_H.264_AAC][4863FBE8].mkv', # noqa E501 119 | 'id': 3927, 120 | 'release_group': 'Conclave-Mendoi', 121 | 'release_version': '2', 122 | 'video_resolution': '1280x720', 123 | 'video_term': 'H.264' 124 | } 125 | ], [ 126 | '[DB]_Bleach_225_[C63D149C].avi', 127 | None, 128 | { 129 | 'anime_title': 'Bleach', 130 | 'episode_number': '225', 131 | 'file_checksum': 'C63D149C', 132 | 'file_extension': 'avi', 133 | 'file_name': '[DB]_Bleach_225_[C63D149C].avi', 134 | 'id': 269, 135 | 'release_group': 'DB' 136 | } 137 | ], [ 138 | '[Frostii]_Nodame_Cantabile_Finale_-_00_[73AD0735].mkv', 139 | None, 140 | { 141 | 'anime_title': 'Nodame Cantabile Finale', 142 | 'episode_number': '00', 143 | 'file_checksum': '73AD0735', 144 | 'file_extension': 'mkv', 145 | 'file_name': '[Frostii]_Nodame_Cantabile_Finale_-_00_[73AD0735].mkv', # noqa E501 146 | 'id': 5690, 147 | 'release_group': 'Frostii' 148 | } 149 | ], [ 150 | '[Hard-Boiled FS]FullMetalAlchemist_09.rmvb', 151 | None, 152 | { 153 | 'anime_title': 'FullMetalAlchemist', 154 | 'episode_number': '09', 155 | 'file_extension': 'rmvb', 156 | 'file_name': '[Hard-Boiled FS]FullMetalAlchemist_09.rmvb', 157 | 'id': 121, 158 | 'release_group': 'Hard-Boiled FS' 159 | } 160 | ], [ 161 | '[HorribleSubs] Tower of Druaga - Sword of Uruk - 04 [480p].mkv', 162 | None, 163 | { 164 | 'anime_title': 'Tower of Druaga - Sword of Uruk', 165 | 'episode_number': '04', 166 | 'file_extension': 'mkv', 167 | 'file_name': '[HorribleSubs] Tower of Druaga - Sword of Uruk - 04 [480p].mkv', # noqa E501 168 | 'id': 4726, 169 | 'release_group': 'HorribleSubs', 170 | 'video_resolution': '480p' 171 | } 172 | ], [ 173 | '[KAF-TEAM]_One_Piece_Movie_9_vostfr_HD.avi', 174 | None, 175 | { 176 | 'anime_title': 'One Piece Movie 9', 177 | 'anime_type': 'Movie', 178 | 'file_extension': 'avi', 179 | 'file_name': '[KAF-TEAM]_One_Piece_Movie_9_vostfr_HD.avi', 180 | 'id': 3848, 181 | 'language': 'vostfr', 182 | 'release_group': 'KAF-TEAM', 183 | 'video_term': 'HD' 184 | } 185 | ], [ 186 | '[kito].Nazca.episode.01.DVDRip.[x264.He-aac.{Jpn}+Sub{Fr}].mkv', 187 | None, 188 | { 189 | 'anime_title': 'Nazca', 190 | 'episode_number': '01', 191 | 'file_extension': 'mkv', 192 | 'file_name': '[kito].Nazca.episode.01.DVDRip.[x264.He-aac.{Jpn}+Sub{Fr}].mkv', # noqa E501 193 | 'id': 1775, 194 | 'release_group': 'kito', 195 | 'source': 'DVDRip', 196 | 'video_term': 'x264' 197 | } 198 | ], [ 199 | '[Lambda-Delta]_Umineko_no_Naku_Koro_ni_-_11_[848x480_H.264_AAC][943106AD].mkv', # noqa E501 200 | None, 201 | { 202 | 'anime_title': 'Umineko no Naku Koro ni', 203 | 'audio_term': 'AAC', 204 | 'episode_number': '11', 205 | 'file_checksum': '943106AD', 206 | 'file_extension': 'mkv', 207 | 'file_name': '[Lambda-Delta]_Umineko_no_Naku_Koro_ni_-_11_[848x480_H.264_AAC][943106AD].mkv', # noqa E501 208 | 'id': 4896, 209 | 'release_group': 'Lambda-Delta', 210 | 'video_resolution': '848x480', 211 | 'video_term': 'H.264' 212 | } 213 | ], [ 214 | '[SS]_Kemono_no_Souja_Erin_-_12_(1280x720_h264)_[0F5F884F].mkv', 215 | None, 216 | { 217 | 'anime_title': 'Kemono no Souja Erin', 218 | 'episode_number': '12', 219 | 'file_checksum': '0F5F884F', 220 | 'file_extension': 'mkv', 221 | 'file_name': '[SS]_Kemono_no_Souja_Erin_-_12_(1280x720_h264)_[0F5F884F].mkv', # noqa E501 222 | 'id': 5420, 223 | 'release_group': 'SS', 224 | 'video_resolution': '1280x720', 225 | 'video_term': 'h264' 226 | } 227 | ], [ 228 | '[Taka]_Fullmetal_Alchemist_(2009)_04_[720p][40F2A957].mp4', 229 | None, 230 | { 231 | 'anime_title': 'Fullmetal Alchemist', 232 | 'anime_year': '2009', 233 | 'episode_number': '04', 234 | 'file_checksum': '40F2A957', 235 | 'file_extension': 'mp4', 236 | 'file_name': '[Taka]_Fullmetal_Alchemist_(2009)_04_[720p][40F2A957].mp4', # noqa E501 237 | 'id': 5114, 238 | 'release_group': 'Taka', 239 | 'video_resolution': '720p' 240 | } 241 | ], [ 242 | '[UTW-TMD]_Summer_Wars_[BD][h264-720p][TrueHD5.1][9F311DAB].mkv', 243 | None, 244 | { 245 | 'anime_title': 'Summer Wars', 246 | 'audio_term': 'TrueHD5.1', 247 | 'file_checksum': '9F311DAB', 248 | 'file_extension': 'mkv', 249 | 'file_name': '[UTW-TMD]_Summer_Wars_[BD][h264-720p][TrueHD5.1][9F311DAB].mkv', # noqa E501 250 | 'id': 5681, 251 | 'release_group': 'UTW-TMD', 252 | 'source': 'BD', 253 | 'video_resolution': '720p', 254 | 'video_term': 'h264' 255 | } 256 | ], [ 257 | '[ValdikSS]_First_Squad_The_Morment_Of_Truth_[720x576_h264_dvdscr_eng_hardsub].mkv', # noqa E501 258 | None, 259 | { 260 | 'anime_title': 'First Squad The Morment Of Truth', 261 | 'file_extension': 'mkv', 262 | 'file_name': '[ValdikSS]_First_Squad_The_Morment_Of_Truth_[720x576_h264_dvdscr_eng_hardsub].mkv', # noqa E501 263 | 'id': 5178, 264 | 'language': 'eng', 265 | 'release_group': 'ValdikSS', 266 | 'subtitles': 'hardsub', 267 | 'video_resolution': '720x576', 268 | 'video_term': 'h264' 269 | } 270 | ], [ 271 | 'Evangelion_1.11_You_Are_(Not)_Alone_(2009)_[1080p,BluRay,x264,DTS-ES]_-_THORA.mkv', # noqa E501 272 | None, 273 | { 274 | 'anime_title': 'Evangelion 1.11 You Are (Not) Alone', 275 | 'anime_year': '2009', 276 | 'audio_term': 'DTS-ES', 277 | 'file_extension': 'mkv', 278 | 'file_name': 'Evangelion_1.11_You_Are_(Not)_Alone_(2009)_[1080p,BluRay,x264,DTS-ES]_-_THORA.mkv', # noqa E501 279 | 'id': 2759, 280 | 'release_group': 'THORA', 281 | 'source': 'BluRay', 282 | 'video_resolution': '1080p', 283 | 'video_term': 'x264' 284 | } 285 | ], [ 286 | 'Evangelion_1.11_You_Are_(Not)_Alone_[1080p,BluRay,x264,DTS-ES]_-_THORA.mkv', # noqa E501 287 | None, 288 | { 289 | 'anime_title': 'Evangelion 1.11 You Are (Not) Alone', 290 | 'audio_term': 'DTS-ES', 291 | 'file_extension': 'mkv', 292 | 'file_name': 'Evangelion_1.11_You_Are_(Not)_Alone_[1080p,BluRay,x264,DTS-ES]_-_THORA.mkv', # noqa E501 293 | 'id': 2759, 294 | 'release_group': 'THORA', 295 | 'source': 'BluRay', 296 | 'video_resolution': '1080p', 297 | 'video_term': 'x264' 298 | } 299 | ], [ 300 | 'Eve no Jikan 2 [88F4F7F0].mkv', 301 | None, 302 | { 303 | 'anime_title': 'Eve no Jikan', 304 | 'episode_number': '2', 305 | 'file_checksum': '88F4F7F0', 306 | 'file_extension': 'mkv', 307 | 'file_name': 'Eve no Jikan 2 [88F4F7F0].mkv', 308 | 'id': 3167 309 | } 310 | ], [ 311 | 'Gin\'iro_no_Kami_no_Agito_(2006)_[1080p,BluRay,x264,DTS]_-_THORA.mkv', 312 | None, 313 | { 314 | 'anime_title': 'Gin\'iro no Kami no Agito', 315 | 'anime_year': '2006', 316 | 'audio_term': 'DTS', 317 | 'file_extension': 'mkv', 318 | 'file_name': 'Gin\'iro_no_Kami_no_Agito_(2006)_[1080p,BluRay,x264,DTS]_-_THORA.mkv', # noqa E501 319 | 'id': 1140, 320 | 'release_group': 'THORA', 321 | 'source': 'BluRay', 322 | 'video_resolution': '1080p', 323 | 'video_term': 'x264' 324 | } 325 | ], [ 326 | 'Magical Girl Lyrical Nanoha A\'s - 01.DVD[H264.AAC][DGz][7A8A7769].mkv', # noqa E501 327 | None, 328 | { 329 | 'anime_title': 'Magical Girl Lyrical Nanoha A\'s', 330 | 'audio_term': 'AAC', 331 | 'episode_number': '01', 332 | 'file_checksum': '7A8A7769', 333 | 'file_extension': 'mkv', 334 | 'file_name': 'Magical Girl Lyrical Nanoha A\'s - 01.DVD[H264.AAC][DGz][7A8A7769].mkv', # noqa E501 335 | 'id': 77, 336 | 'release_group': 'DGz', 337 | 'source': 'DVD', 338 | 'video_term': 'H264' 339 | } 340 | ], [ 341 | 'Mobile_Suit_Gundam_00_Season_2_Ep07_A_Reunion_and_a_Parting_[1080p,BluRay,x264]_-_THORA.mkv', # noqa E501 342 | None, 343 | { 344 | 'anime_season': '2', 345 | 'anime_title': 'Mobile Suit Gundam 00', 346 | 'episode_number': '07', 347 | 'episode_title': 'A Reunion and a Parting', 348 | 'file_extension': 'mkv', 349 | 'file_name': 'Mobile_Suit_Gundam_00_Season_2_Ep07_A_Reunion_and_a_Parting_[1080p,BluRay,x264]_-_THORA.mkv', # noqa E501 350 | 'id': 3927, 351 | 'release_group': 'THORA', 352 | 'source': 'BluRay', 353 | 'video_resolution': '1080p', 354 | 'video_term': 'x264' 355 | } 356 | ], [ 357 | 'ponyo_on_the_cliff_by_the_sea[h264.dts][niizk].mkv', 358 | None, 359 | { 360 | 'anime_title': 'ponyo on the cliff by the sea', 361 | 'audio_term': 'dts', 362 | 'file_extension': 'mkv', 363 | 'file_name': 'ponyo_on_the_cliff_by_the_sea[h264.dts][niizk].mkv', 364 | 'id': 2890, 365 | 'release_group': 'niizk', 366 | 'video_term': 'h264' 367 | } 368 | ], [ 369 | '[Seto_Otaku]_AIKa_ZERO_OVA_-_01_[BD][1920x1080_H264-Flac][6730D40A].mkv', # noqa E501 370 | None, 371 | { 372 | 'anime_title': 'AIKa ZERO OVA', 373 | 'anime_type': 'OVA', 374 | 'audio_term': 'Flac', 375 | 'episode_number': '01', 376 | 'file_checksum': '6730D40A', 377 | 'file_extension': 'mkv', 378 | 'file_name': '[Seto_Otaku]_AIKa_ZERO_OVA_-_01_[BD][1920x1080_H264-Flac][6730D40A].mkv', # noqa E501 379 | 'id': 6443, 380 | 'release_group': 'Seto_Otaku', 381 | 'source': 'BD', 382 | 'video_resolution': '1920x1080', 383 | 'video_term': 'H264' 384 | } 385 | ], [ 386 | '[a4e]R.O.D_the_TV_01[divx5.2.1].mkv', 387 | None, 388 | { 389 | 'anime_title': 'R.O.D the TV', 390 | 'anime_type': 'TV', 391 | 'episode_number': '01', 392 | 'file_extension': 'mkv', 393 | 'file_name': '[a4e]R.O.D_the_TV_01[divx5.2.1].mkv', 394 | 'id': 209, 395 | 'release_group': 'a4e' 396 | } 397 | ], [ 398 | 'Ghost_in_the_Shell_Stand_Alone_Complex_2nd_GIG_Ep05v2_EXCAVATION_[720p,HDTV,x264,AAC_5.1]_-_THORA.mkv', # noqa E501 399 | None, 400 | { 401 | 'anime_title': 'Ghost in the Shell Stand Alone Complex 2nd GIG', 402 | 'audio_term': [ 403 | 'AAC', 404 | '5.1' 405 | ], 406 | 'episode_number': '05', 407 | 'episode_title': 'EXCAVATION', 408 | 'file_extension': 'mkv', 409 | 'file_name': 'Ghost_in_the_Shell_Stand_Alone_Complex_2nd_GIG_Ep05v2_EXCAVATION_[720p,HDTV,x264,AAC_5.1]_-_THORA.mkv', # noqa E501 410 | 'id': 801, 411 | 'release_group': 'THORA', 412 | 'release_version': '2', 413 | 'source': 'HDTV', 414 | 'video_resolution': '720p', 415 | 'video_term': 'x264' 416 | } 417 | ], [ 418 | 'Ghost_in_the_Shell_Stand_Alone_Complex_2nd_GIG_Ep06_Pu239_[720p,HDTV,x264,AAC_5.1]_-_THORA.mkv', # noqa E501 419 | None, 420 | { 421 | 'anime_title': 'Ghost in the Shell Stand Alone Complex 2nd GIG', 422 | 'audio_term': [ 423 | 'AAC', 424 | '5.1' 425 | ], 426 | 'episode_number': '06', 427 | 'episode_title': 'Pu239', 428 | 'file_extension': 'mkv', 429 | 'file_name': 'Ghost_in_the_Shell_Stand_Alone_Complex_2nd_GIG_Ep06_Pu239_[720p,HDTV,x264,AAC_5.1]_-_THORA.mkv', # noqa E501 430 | 'id': 801, 431 | 'release_group': 'THORA', 432 | 'source': 'HDTV', 433 | 'video_resolution': '720p', 434 | 'video_term': 'x264' 435 | } 436 | ], [ 437 | 'Fate_Stay_Night_Ep05_The_Two_Magi_Part1_[720p,BluRay,x264]_-_THORA.mkv', # noqa E501 438 | None, 439 | { 440 | 'anime_title': 'Fate Stay Night', 441 | 'episode_number': '05', 442 | 'episode_title': 'The Two Magi Part1', 443 | 'file_extension': 'mkv', 444 | 'file_name': 'Fate_Stay_Night_Ep05_The_Two_Magi_Part1_[720p,BluRay,x264]_-_THORA.mkv', # noqa E501 445 | 'id': 356, 446 | 'release_group': 'THORA', 447 | 'source': 'BluRay', 448 | 'video_resolution': '720p', 449 | 'video_term': 'x264' 450 | } 451 | ], [ 452 | '[RaX]Mezzo(DSA)_-_05_-_[x264_ogg]_[585d9971].mkv', 453 | None, 454 | { 455 | 'anime_title': 'Mezzo(DSA)', 456 | 'audio_term': 'ogg', 457 | 'episode_number': '05', 458 | 'file_checksum': '585d9971', 459 | 'file_extension': 'mkv', 460 | 'file_name': '[RaX]Mezzo(DSA)_-_05_-_[x264_ogg]_[585d9971].mkv', 461 | 'id': 222, 462 | 'release_group': 'RaX', 463 | 'video_term': 'x264' 464 | } 465 | ], [ 466 | '[AKH-SWE]_Fullmetal_Alchemist_(2009)_02v2_[H.264.AAC][7B2C5E8B].mkv', 467 | None, 468 | { 469 | 'anime_title': 'Fullmetal Alchemist', 470 | 'anime_year': '2009', 471 | 'audio_term': 'AAC', 472 | 'episode_number': '02', 473 | 'file_checksum': '7B2C5E8B', 474 | 'file_extension': 'mkv', 475 | 'file_name': '[AKH-SWE]_Fullmetal_Alchemist_(2009)_02v2_[H.264.AAC][7B2C5E8B].mkv', # noqa E501 476 | 'id': 5114, 477 | 'release_group': 'AKH-SWE', 478 | 'release_version': '2', 479 | 'video_term': 'H.264' 480 | } 481 | ], [ 482 | '[FuktLogik][Sayonara_Zetsubou_Sensei][01][DVDRip][x264_AC3].mkv', 483 | None, 484 | { 485 | 'anime_title': 'Sayonara Zetsubou Sensei', 486 | 'audio_term': 'AC3', 487 | 'episode_number': '01', 488 | 'file_extension': 'mkv', 489 | 'file_name': '[FuktLogik][Sayonara_Zetsubou_Sensei][01][DVDRip][x264_AC3].mkv', # noqa E501 490 | 'id': 2605, 491 | 'release_group': 'FuktLogik', 492 | 'source': 'DVDRip', 493 | 'video_term': 'x264' 494 | } 495 | ], [ 496 | '[Darksoul-Subs] Tatakau Shisho - The Book of Bantorra [848x480 XVID_MP3].mkv', # noqa E501 497 | None, 498 | { 499 | 'anime_title': 'Tatakau Shisho - The Book of Bantorra', 500 | 'audio_term': 'MP3', 501 | 'file_extension': 'mkv', 502 | 'file_name': '[Darksoul-Subs] Tatakau Shisho - The Book of Bantorra [848x480 XVID_MP3].mkv', # noqa E501 503 | 'id': 6758, 504 | 'release_group': 'Darksoul-Subs', 505 | 'video_resolution': '848x480', 506 | 'video_term': 'XVID' 507 | } 508 | ], [ 509 | '[ACX]Neon_Genesis_Evangelion_-_Platinum_-_06_-_Showdown_in_Tokyo_3_[SaintDeath]_[CBDB8577].mkv', # noqa E501 510 | None, 511 | { 512 | 'anime_title': 'Neon Genesis Evangelion - Platinum', 513 | 'episode_number': '06', 514 | 'episode_title': 'Showdown in Tokyo 3', 515 | 'file_checksum': 'CBDB8577', 516 | 'file_extension': 'mkv', 517 | 'file_name': '[ACX]Neon_Genesis_Evangelion_-_Platinum_-_06_-_Showdown_in_Tokyo_3_[SaintDeath]_[CBDB8577].mkv', # noqa E501 518 | 'id': 30, 519 | 'release_group': 'ACX' 520 | } 521 | ], [ 522 | '[Himatsubushi]_Sora_no_Woto_-_01_-_H264_-_720p_-_E83AD672.mkv', 523 | None, 524 | { 525 | 'anime_title': 'Sora no Woto', 526 | 'episode_number': '01', 527 | 'file_checksum': 'E83AD672', 528 | 'file_extension': 'mkv', 529 | 'file_name': '[Himatsubushi]_Sora_no_Woto_-_01_-_H264_-_720p_-_E83AD672.mkv', # noqa E501 530 | 'id': 6802, 531 | 'release_group': 'Himatsubushi', 532 | 'video_resolution': '720p', 533 | 'video_term': 'H264' 534 | } 535 | ], [ 536 | '[EroGaKi-Team]_Nurse_Witch_Komugi-chan_Magikarte_02.5_[902BB314].mkv', 537 | None, 538 | { 539 | 'anime_title': 'Nurse Witch Komugi-chan Magikarte', 540 | 'episode_number': '02.5', 541 | 'file_checksum': '902BB314', 542 | 'file_extension': 'mkv', 543 | 'file_name': '[EroGaKi-Team]_Nurse_Witch_Komugi-chan_Magikarte_02.5_[902BB314].mkv', # noqa E501 544 | 'id': 920, 545 | 'release_group': 'EroGaKi-Team' 546 | } 547 | ], [ 548 | 'Ookiku Furikabutte S2 - 09 (Central Anime) [BD841253].mkv', 549 | None, 550 | { 551 | 'anime_title': 'Ookiku Furikabutte', 552 | 'anime_season': '2', 553 | 'episode_number': '09', 554 | 'file_checksum': 'BD841253', 555 | 'file_extension': 'mkv', 556 | 'file_name': 'Ookiku Furikabutte S2 - 09 (Central Anime) [BD841253].mkv', # noqa E501 557 | 'id': 7720, 558 | 'release_group': 'Central Anime' 559 | } 560 | ], [ 561 | '[HorribleSubs] HEROMAN - 10_(XviD_AnimeSenshi).mkv', 562 | None, 563 | { 564 | 'anime_title': 'HEROMAN', 565 | 'episode_number': '10', 566 | 'file_extension': 'mkv', 567 | 'file_name': '[HorribleSubs] HEROMAN - 10_(XviD_AnimeSenshi).mkv', 568 | 'id': 4334, 569 | 'release_group': 'HorribleSubs', 570 | 'video_term': 'XviD' 571 | } 572 | ], [ 573 | 'Detective Conan - 316-317 [DCTP][2411959B].mkv', 574 | None, 575 | { 576 | 'anime_title': 'Detective Conan', 577 | 'episode_number': [ 578 | '316', 579 | '317' 580 | ], 581 | 'file_checksum': '2411959B', 582 | 'file_extension': 'mkv', 583 | 'file_name': 'Detective Conan - 316-317 [DCTP][2411959B].mkv', 584 | 'id': 235, 585 | 'release_group': 'DCTP' 586 | } 587 | ], [ 588 | '[N LogN Fansubs] Angel Beats (9).mkv', 589 | None, 590 | { 591 | 'anime_title': 'Angel Beats', 592 | 'episode_number': '9', 593 | 'file_extension': 'mkv', 594 | 'file_name': '[N LogN Fansubs] Angel Beats (9).mkv', 595 | 'id': 6547, 596 | 'release_group': 'N LogN Fansubs' 597 | } 598 | ], [ 599 | 'To_Aru_Kagaku_no_Railgun_13-15_[BD_1080p][AtsA]', 600 | None, 601 | { 602 | 'anime_title': 'To Aru Kagaku no Railgun', 603 | 'episode_number': [ 604 | '13', 605 | '15' 606 | ], 607 | 'file_name': 'To_Aru_Kagaku_no_Railgun_13-15_[BD_1080p][AtsA]', 608 | 'id': 6213, 609 | 'release_group': 'AtsA', 610 | 'source': 'BD', 611 | 'video_resolution': '1080p' 612 | } 613 | ], [ 614 | 'Juuousei_-_01_[Black_Sheep][HDTV_H264_AAC][803DA487].mkv', 615 | None, 616 | { 617 | 'anime_title': 'Juuousei', 618 | 'audio_term': 'AAC', 619 | 'episode_number': '01', 620 | 'file_checksum': '803DA487', 621 | 'file_extension': 'mkv', 622 | 'file_name': 'Juuousei_-_01_[Black_Sheep][HDTV_H264_AAC][803DA487].mkv', # noqa E501 623 | 'id': 953, 624 | 'release_group': 'Black_Sheep', 625 | 'source': 'HDTV', 626 | 'video_term': 'H264' 627 | } 628 | ], [ 629 | '[RNA]_Sakura_Taisen_New_York_NY_Ep_2_[1590D378].avi', 630 | None, 631 | { 632 | 'anime_title': 'Sakura Taisen New York NY', 633 | 'episode_number': '2', 634 | 'file_checksum': '1590D378', 635 | 'file_extension': 'avi', 636 | 'file_name': '[RNA]_Sakura_Taisen_New_York_NY_Ep_2_[1590D378].avi', 637 | 'id': 2168, 638 | 'release_group': 'RNA' 639 | } 640 | ], [ 641 | 'Hayate no Gotoku 2nd Season 24 (Blu-Ray 1080p) [Chihiro]', 642 | None, 643 | { 644 | 'anime_season': '2', 645 | 'anime_title': 'Hayate no Gotoku', 646 | 'episode_number': '24', 647 | 'file_name': 'Hayate no Gotoku 2nd Season 24 (Blu-Ray 1080p) [Chihiro]', # noqa E501 648 | 'id': 4192, 649 | 'release_group': 'Chihiro', 650 | 'source': 'Blu-Ray', 651 | 'video_resolution': '1080p' 652 | } 653 | ], [ 654 | '[BluDragon] Blue Submarine No.6 (DVD, R2, Dual Audio) V3', 655 | None, 656 | { 657 | 'anime_title': 'Blue Submarine No.6', 658 | 'audio_term': 'Dual Audio', 659 | 'file_name': '[BluDragon] Blue Submarine No.6 (DVD, R2, Dual Audio) V3', # noqa E501 660 | 'id': 1051, 661 | 'release_group': 'BluDragon', 662 | 'release_version': '3', 663 | 'source': 'DVD' 664 | } 665 | ], [ 666 | 'Chrono Crusade ep. 1-5', 667 | None, 668 | { 669 | 'anime_title': 'Chrono Crusade', 670 | 'episode_number': [ 671 | '1', 672 | '5' 673 | ], 674 | 'file_name': 'Chrono Crusade ep. 1-5', 675 | 'id': 60 676 | } 677 | ], [ 678 | '[gg]_Kimi_ni_Todoke_2nd_Season_-_00_[BF735BC4].mkv', 679 | None, 680 | { 681 | 'anime_season': '2', 682 | 'anime_title': 'Kimi ni Todoke', 683 | 'episode_number': '00', 684 | 'file_checksum': 'BF735BC4', 685 | 'file_extension': 'mkv', 686 | 'file_name': '[gg]_Kimi_ni_Todoke_2nd_Season_-_00_[BF735BC4].mkv', 687 | 'id': 9656, 688 | 'release_group': 'gg' 689 | } 690 | ], [ 691 | 'K-ON!_Ep03_Training!_[1080p,BluRay,x264]_-_THORA.mkv', 692 | None, 693 | { 694 | 'anime_title': 'K-ON!', 695 | 'episode_number': '03', 696 | 'episode_title': 'Training!', 697 | 'file_extension': 'mkv', 698 | 'file_name': 'K-ON!_Ep03_Training!_[1080p,BluRay,x264]_-_THORA.mkv', # noqa E501 699 | 'id': 5680, 700 | 'release_group': 'THORA', 701 | 'source': 'BluRay', 702 | 'video_resolution': '1080p', 703 | 'video_term': 'x264' 704 | } 705 | ], [ 706 | 'K-ON!!_Ep08_Career_Plan!_[1080p,BluRay,x264]_-_THORA.mkv', 707 | None, 708 | { 709 | 'anime_title': 'K-ON!!', 710 | 'episode_number': '08', 711 | 'episode_title': 'Career Plan!', 712 | 'file_extension': 'mkv', 713 | 'file_name': 'K-ON!!_Ep08_Career_Plan!_[1080p,BluRay,x264]_-_THORA.mkv', # noqa E501 714 | 'id': 7791, 715 | 'release_group': 'THORA', 716 | 'source': 'BluRay', 717 | 'video_resolution': '1080p', 718 | 'video_term': 'x264' 719 | } 720 | ], [ 721 | '[SFW]_Queen\'s_Blade_S2', 722 | None, 723 | { 724 | 'anime_title': 'Queen\'s Blade', 725 | 'anime_season': '2', 726 | 'file_name': '[SFW]_Queen\'s_Blade_S2', 727 | 'id': 6633, 728 | 'release_group': 'SFW' 729 | } 730 | ], [ 731 | 'Evangelion_1.0_You_Are_[Not]_Alone_(1080p)_[@Home]', 732 | None, 733 | { 734 | 'anime_title': 'Evangelion 1.0 You Are [Not] Alone', 735 | 'file_name': 'Evangelion_1.0_You_Are_[Not]_Alone_(1080p)_[@Home]', 736 | 'id': 2759, 737 | 'release_group': '@Home', 738 | 'video_resolution': '1080p' 739 | } 740 | ], [ 741 | '[Ayako]_Infinite_Stratos_-_IS_-_01v2_[XVID][400p][29675B71].avi', 742 | None, 743 | { 744 | 'anime_title': 'Infinite Stratos - IS', 745 | 'episode_number': '01', 746 | 'file_checksum': '29675B71', 747 | 'file_extension': 'avi', 748 | 'file_name': '[Ayako]_Infinite_Stratos_-_IS_-_01v2_[XVID][400p][29675B71].avi', # noqa E501 749 | 'id': 9041, 750 | 'release_group': 'Ayako', 751 | 'release_version': '2', 752 | 'video_resolution': '400p', 753 | 'video_term': 'XVID' 754 | } 755 | ], [ 756 | '[E-HARO Raws] Kore wa Zombie desu ka - 03 (TV 1280x720 h264 AAC) [888E4991].mkv', # noqa E501 757 | None, 758 | { 759 | 'anime_title': 'Kore wa Zombie desu ka', 760 | 'anime_type': 'TV', 761 | 'audio_term': 'AAC', 762 | 'episode_number': '03', 763 | 'file_checksum': '888E4991', 764 | 'file_extension': 'mkv', 765 | 'file_name': '[E-HARO Raws] Kore wa Zombie desu ka - 03 (TV 1280x720 h264 AAC) [888E4991].mkv', # noqa E501 766 | 'id': 8841, 767 | 'release_group': 'E-HARO Raws', 768 | 'video_resolution': '1280x720', 769 | 'video_term': 'h264' 770 | } 771 | ], [ 772 | '[Edomae Subs] Kore wa Zombie desu ka Episode 2.mkv', 773 | None, 774 | { 775 | 'anime_title': 'Kore wa Zombie desu ka', 776 | 'episode_number': '2', 777 | 'file_extension': 'mkv', 778 | 'file_name': '[Edomae Subs] Kore wa Zombie desu ka Episode 2.mkv', 779 | 'id': 8841, 780 | 'release_group': 'Edomae Subs' 781 | } 782 | ], [ 783 | 'Juuni.Kokki.Ep.5.avi', 784 | None, 785 | { 786 | 'anime_title': 'Juuni Kokki', 787 | 'episode_number': '5', 788 | 'file_extension': 'avi', 789 | 'file_name': 'Juuni.Kokki.Ep.5.avi', 790 | 'id': 153 791 | } 792 | ], [ 793 | 'Juuni Kokki Ep.5.avi', 794 | None, 795 | { 796 | 'anime_title': 'Juuni Kokki', 797 | 'episode_number': '5', 798 | 'file_extension': 'avi', 799 | 'file_name': 'Juuni Kokki Ep.5.avi', 800 | 'id': 153 801 | } 802 | ], [ 803 | '5_centimeters_per_second[1904x1072.h264.flac][niizk].mkv', 804 | None, 805 | { 806 | 'anime_title': '5 centimeters per second', 807 | 'audio_term': 'flac', 808 | 'file_extension': 'mkv', 809 | 'file_name': '5_centimeters_per_second[1904x1072.h264.flac][niizk].mkv', # noqa E501 810 | 'id': 1689, 811 | 'release_group': 'niizk', 812 | 'video_resolution': '1904x1072', 813 | 'video_term': 'h264' 814 | } 815 | ], [ 816 | '[Yoroshiku]_009-1_-_02_(H264)_[36D2444D].mkv', 817 | None, 818 | { 819 | 'anime_title': '009-1', 820 | 'episode_number': '02', 821 | 'file_checksum': '36D2444D', 822 | 'file_extension': 'mkv', 823 | 'file_name': '[Yoroshiku]_009-1_-_02_(H264)_[36D2444D].mkv', 824 | 'id': 1583, 825 | 'release_group': 'Yoroshiku', 826 | 'video_term': 'H264' 827 | } 828 | ], [ 829 | 'After War Gundam X - 1x03 - My Mount is Fierce!.mkv', 830 | None, 831 | { 832 | 'anime_season': '1', 833 | 'anime_title': 'After War Gundam X', 834 | 'episode_number': '03', 835 | 'episode_title': 'My Mount is Fierce!', 836 | 'file_extension': 'mkv', 837 | 'file_name': 'After War Gundam X - 1x03 - My Mount is Fierce!.mkv', 838 | 'id': 92 839 | } 840 | ], [ 841 | '[HorribleSubs] The World God Only Knows 2 - 03 [720p].mkv', 842 | None, 843 | { 844 | 'anime_title': 'The World God Only Knows 2', 845 | 'episode_number': '03', 846 | 'file_extension': 'mkv', 847 | 'file_name': '[HorribleSubs] The World God Only Knows 2 - 03 [720p].mkv', # noqa E501 848 | 'id': 10080, 849 | 'release_group': 'HorribleSubs', 850 | 'video_resolution': '720p' 851 | } 852 | ], [ 853 | '[FFF] Red Data Girl - 10v0 [29EA865B].mkv', 854 | None, 855 | { 856 | 'anime_title': 'Red Data Girl', 857 | 'episode_number': '10', 858 | 'file_checksum': '29EA865B', 859 | 'file_extension': 'mkv', 860 | 'file_name': '[FFF] Red Data Girl - 10v0 [29EA865B].mkv', 861 | 'id': 14921, 862 | 'release_group': 'FFF', 863 | 'release_version': '0' 864 | } 865 | ], [ 866 | '[CMS] Magical☆Star Kanon 100% OVA[DVD][E9F43685].mkv', 867 | None, 868 | { 869 | 'anime_title': 'Magical☆Star Kanon 100% OVA', 870 | 'anime_type': 'OVA', 871 | 'file_checksum': 'E9F43685', 872 | 'file_extension': 'mkv', 873 | 'file_name': '[CMS] Magical☆Star Kanon 100% OVA[DVD][E9F43685].mkv', # noqa E501 874 | 'id': 17725, 875 | 'release_group': 'CMS', 876 | 'source': 'DVD' 877 | } 878 | ], [ 879 | '[Doremi].Ro-Kyu-Bu!.SS.01.[C1B5CE5D].mkv', 880 | None, 881 | { 882 | 'anime_title': 'Ro-Kyu-Bu! SS', 883 | 'episode_number': '01', 884 | 'file_checksum': 'C1B5CE5D', 885 | 'file_extension': 'mkv', 886 | 'file_name': '[Doremi].Ro-Kyu-Bu!.SS.01.[C1B5CE5D].mkv', 887 | 'id': 16051, 888 | 'release_group': 'Doremi' 889 | } 890 | ], [ 891 | '[Raizel] Persona 4 The Animation Episode 13 - A Stormy Summer Vacation Part 1 [BD_1080p_Dual_Audio_FLAC_Hi10p][8A45634B].mkv', # noqa E501 892 | None, 893 | { 894 | 'anime_title': 'Persona 4 The Animation', 895 | 'audio_term': 'FLAC', 896 | 'episode_number': '13', 897 | 'episode_title': 'A Stormy Summer Vacation Part 1', 898 | 'file_checksum': '8A45634B', 899 | 'file_extension': 'mkv', 900 | 'file_name': '[Raizel] Persona 4 The Animation Episode 13 - A Stormy Summer Vacation Part 1 [BD_1080p_Dual_Audio_FLAC_Hi10p][8A45634B].mkv', # noqa E501 901 | 'id': 10588, 902 | 'release_group': 'Raizel', 903 | 'source': 'BD', 904 | 'video_resolution': '1080p', 905 | 'video_term': 'Hi10p' 906 | } 907 | ], [ 908 | '[R-R] Diebuster.EP1 (720p.Hi10p.AC3) [82E36A36].mkv', 909 | None, 910 | { 911 | 'anime_title': 'Diebuster', 912 | 'audio_term': 'AC3', 913 | 'episode_number': '1', 914 | 'file_checksum': '82E36A36', 915 | 'file_extension': 'mkv', 916 | 'file_name': '[R-R] Diebuster.EP1 (720p.Hi10p.AC3) [82E36A36].mkv', 917 | 'id': 1002, 918 | 'release_group': 'R-R', 919 | 'video_resolution': '720p', 920 | 'video_term': 'Hi10p' 921 | } 922 | ], [ 923 | '[Rakuda].Gift.~eternal.rainbow~.01.dvd.h.264.vorbis.mkv', 924 | None, 925 | { 926 | 'anime_title': 'Gift ~eternal rainbow~', 927 | 'audio_term': 'vorbis', 928 | 'episode_number': '01', 929 | 'file_extension': 'mkv', 930 | 'file_name': '[Rakuda].Gift.~eternal.rainbow~.01.dvd.h.264.vorbis.mkv', # noqa E501 931 | 'id': 1581, 932 | 'release_group': 'Rakuda', 933 | 'source': 'dvd', 934 | 'video_term': 'h.264' 935 | } 936 | ], [ 937 | '[Jumonji-Giri]_[Shinsen-Subs][ASF]_D.C.II_Da_Capo_II_Ep01_(a1fc58a7).mkv', # noqa E501 938 | None, 939 | { 940 | 'anime_title': 'D.C.II Da Capo II', 941 | 'episode_number': '01', 942 | 'file_checksum': 'a1fc58a7', 943 | 'file_extension': 'mkv', 944 | 'file_name': '[Jumonji-Giri]_[Shinsen-Subs][ASF]_D.C.II_Da_Capo_II_Ep01_(a1fc58a7).mkv', # noqa E501 945 | 'id': 2595, 946 | 'release_group': 'Jumonji-Giri' 947 | } 948 | ], [ 949 | '[52wy][SlamDunk][001][Jpn_Chs_Cht][x264_aac][DVDRip][7FE2C873].mkv', 950 | None, 951 | { 952 | 'anime_title': 'SlamDunk', 953 | 'audio_term': 'aac', 954 | 'episode_number': '001', 955 | 'file_checksum': '7FE2C873', 956 | 'file_extension': 'mkv', 957 | 'file_name': '[52wy][SlamDunk][001][Jpn_Chs_Cht][x264_aac][DVDRip][7FE2C873].mkv', # noqa E501 958 | 'id': 170, 959 | 'release_group': '52wy', 960 | 'source': 'DVDRip', 961 | 'video_term': 'x264' 962 | } 963 | ], [ 964 | '[Commie] Last Exile ~Fam, The Silver Wing~ - 13 [AFF9E530].mkv', 965 | None, 966 | { 967 | 'anime_title': 'Last Exile ~Fam, The Silver Wing~', 968 | 'episode_number': '13', 969 | 'file_checksum': 'AFF9E530', 970 | 'file_extension': 'mkv', 971 | 'file_name': '[Commie] Last Exile ~Fam, The Silver Wing~ - 13 [AFF9E530].mkv', # noqa E501 972 | 'id': 10336, 973 | 'release_group': 'Commie' 974 | } 975 | ], [ 976 | '[Hakugetsu&Speed&MGRT][Dragon_Ball_Z_Battle_of_Gods][BDRIP][BIG5][1280x720].mp4', # noqa E501 977 | None, 978 | { 979 | 'anime_title': 'Dragon Ball Z Battle of Gods', 980 | 'file_extension': 'mp4', 981 | 'file_name': '[Hakugetsu&Speed&MGRT][Dragon_Ball_Z_Battle_of_Gods][BDRIP][BIG5][1280x720].mp4', # noqa E501 982 | 'id': 14837, 983 | 'release_group': 'Hakugetsu&Speed&MGRT', 984 | 'source': 'BDRIP', 985 | 'subtitles': 'BIG5', 986 | 'video_resolution': '1280x720' 987 | } 988 | ], [ 989 | '[Hakugetsu&MGRT][Evangelion 3.0 You Can (Not) Redo][480P][V0].mp4', 990 | None, 991 | { 992 | 'anime_title': 'Evangelion 3.0 You Can (Not) Redo', 993 | 'file_extension': 'mp4', 994 | 'file_name': '[Hakugetsu&MGRT][Evangelion 3.0 You Can (Not) Redo][480P][V0].mp4', # noqa E501 995 | 'id': 3785, 996 | 'release_group': 'Hakugetsu&MGRT', 997 | 'release_version': '0', 998 | 'video_resolution': '480P' 999 | } 1000 | ], [ 1001 | '[UTW]_Fate_Zero_-_01_[BD][h264-720p_AC3][02A0491D].mkv', 1002 | None, 1003 | { 1004 | 'anime_title': 'Fate Zero', 1005 | 'audio_term': 'AC3', 1006 | 'episode_number': '01', 1007 | 'file_checksum': '02A0491D', 1008 | 'file_extension': 'mkv', 1009 | 'file_name': '[UTW]_Fate_Zero_-_01_[BD][h264-720p_AC3][02A0491D].mkv', # noqa E501 1010 | 'id': 10087, 1011 | 'release_group': 'UTW', 1012 | 'source': 'BD', 1013 | 'video_resolution': '720p', 1014 | 'video_term': 'h264' 1015 | } 1016 | ], [ 1017 | '[UTW-THORA] Evangelion 3.33 You Can (Not) Redo [BD][1080p,x264,flac][F2060CF5].mkv', # noqa E501 1018 | None, 1019 | { 1020 | 'anime_title': 'Evangelion 3.33 You Can (Not) Redo', 1021 | 'audio_term': 'flac', 1022 | 'file_checksum': 'F2060CF5', 1023 | 'file_extension': 'mkv', 1024 | 'file_name': '[UTW-THORA] Evangelion 3.33 You Can (Not) Redo [BD][1080p,x264,flac][F2060CF5].mkv', # noqa E501 1025 | 'id': 3785, 1026 | 'release_group': 'UTW-THORA', 1027 | 'source': 'BD', 1028 | 'video_resolution': '1080p', 1029 | 'video_term': 'x264' 1030 | } 1031 | ], [ 1032 | 'Bakemonogatari - 01 (BD 1280x720 AVC AACx2).mp4', 1033 | None, 1034 | { 1035 | 'anime_title': 'Bakemonogatari', 1036 | 'audio_term': 'AACx2', 1037 | 'episode_number': '01', 1038 | 'file_extension': 'mp4', 1039 | 'file_name': 'Bakemonogatari - 01 (BD 1280x720 AVC AACx2).mp4', 1040 | 'id': 5081, 1041 | 'source': 'BD', 1042 | 'video_resolution': '1280x720', 1043 | 'video_term': 'AVC' 1044 | } 1045 | ], [ 1046 | 'Evangelion The New Movie Q (BD 1280x720 AVC AACx2 [5.1+2.0]).mp4', 1047 | None, 1048 | { 1049 | 'anime_title': 'Evangelion The New Movie Q', 1050 | 'anime_type': 'Movie', 1051 | 'audio_term': 'AACx2', 1052 | 'file_extension': 'mp4', 1053 | 'file_name': 'Evangelion The New Movie Q (BD 1280x720 AVC AACx2 [5.1+2.0]).mp4', # noqa E501 1054 | 'id': 3785, 1055 | 'source': 'BD', 1056 | 'video_resolution': '1280x720', 1057 | 'video_term': 'AVC' 1058 | } 1059 | ], [ 1060 | 'Howl\'s_Moving_Castle_(2004)_[1080p,BluRay,flac,dts,x264]_-_THORA v2.mkv', # noqa E501 1061 | None, 1062 | { 1063 | 'anime_title': 'Howl\'s Moving Castle', 1064 | 'anime_year': '2004', 1065 | 'audio_term': [ 1066 | 'flac', 1067 | 'dts' 1068 | ], 1069 | 'file_extension': 'mkv', 1070 | 'file_name': 'Howl\'s_Moving_Castle_(2004)_[1080p,BluRay,flac,dts,x264]_-_THORA v2.mkv', # noqa E501 1071 | 'id': 431, 1072 | 'release_group': 'THORA', 1073 | 'release_version': '2', 1074 | 'source': 'BluRay', 1075 | 'video_resolution': '1080p', 1076 | 'video_term': 'x264' 1077 | } 1078 | ], [ 1079 | 'Kotonoha no Niwa (BD 1280x720 AVC AACx3 [5.1+2.0+2.0] Subx3).mp4', 1080 | None, 1081 | { 1082 | 'anime_title': 'Kotonoha no Niwa', 1083 | 'audio_term': 'AACx3', 1084 | 'file_extension': 'mp4', 1085 | 'file_name': 'Kotonoha no Niwa (BD 1280x720 AVC AACx3 [5.1+2.0+2.0] Subx3).mp4', # noqa E501 1086 | 'id': 16782, 1087 | 'source': 'BD', 1088 | 'video_resolution': '1280x720', 1089 | 'video_term': 'AVC' 1090 | } 1091 | ], [ 1092 | 'Queen\'s Blade Utsukushiki Toushi-tachi - OVA_01 (BD 1280x720 AVC AAC).mp4', # noqa E501 1093 | None, 1094 | { 1095 | 'anime_title': 'Queen\'s Blade Utsukushiki Toushi-tachi - OVA', 1096 | 'anime_type': 'OVA', 1097 | 'audio_term': 'AAC', 1098 | 'episode_number': '01', 1099 | 'file_extension': 'mp4', 1100 | 'file_name': 'Queen\'s Blade Utsukushiki Toushi-tachi - OVA_01 (BD 1280x720 AVC AAC).mp4', # noqa E501 1101 | 'id': 8456, 1102 | 'source': 'BD', 1103 | 'video_resolution': '1280x720', 1104 | 'video_term': 'AVC' 1105 | } 1106 | ], [ 1107 | '[FFF] Futsuu no Joshikousei ga [Locodol] Yatte Mita. - 01 [BAD09C76].mkv', # noqa E501 1108 | None, 1109 | { 1110 | 'anime_title': 'Futsuu no Joshikousei ga [Locodol] Yatte Mita.', 1111 | 'episode_number': '01', 1112 | 'file_checksum': 'BAD09C76', 1113 | 'file_extension': 'mkv', 1114 | 'file_name': '[FFF] Futsuu no Joshikousei ga [Locodol] Yatte Mita. - 01 [BAD09C76].mkv', # noqa E501 1115 | 'id': 22189, 1116 | 'release_group': 'FFF' 1117 | } 1118 | ], [ 1119 | '[異域字幕組][漆黑的子彈][Black Bullet][11][1280x720][繁体].mp4', 1120 | None, 1121 | { 1122 | 'anime_title': 'Black Bullet', 1123 | 'episode_number': '11', 1124 | 'file_extension': 'mp4', 1125 | 'file_name': '[異域字幕組][漆黑的子彈][Black Bullet][11][1280x720][繁体].mp4', # noqa E501 1126 | 'id': 20787, 1127 | 'release_group': '異域字幕組', 1128 | 'video_resolution': '1280x720' 1129 | } 1130 | ], [ 1131 | 'Vol.01', 1132 | None, 1133 | { 1134 | 'file_name': 'Vol.01', 1135 | 'volume_number': '01' 1136 | } 1137 | ], [ 1138 | 'Mary Bell (DVD) - 01v2 [h-b].mkv', 1139 | None, 1140 | { 1141 | 'anime_title': 'Mary Bell', 1142 | 'episode_number': '01', 1143 | 'file_extension': 'mkv', 1144 | 'file_name': 'Mary Bell (DVD) - 01v2 [h-b].mkv', 1145 | 'id': 2804, 1146 | 'release_group': 'h-b', 1147 | 'release_version': '2', 1148 | 'source': 'DVD' 1149 | } 1150 | ], [ 1151 | 'Mary Bell (DVD) - 02 [h-b].mkv', 1152 | None, 1153 | { 1154 | 'anime_title': 'Mary Bell', 1155 | 'episode_number': '02', 1156 | 'file_extension': 'mkv', 1157 | 'file_name': 'Mary Bell (DVD) - 02 [h-b].mkv', 1158 | 'id': 2804, 1159 | 'release_group': 'h-b', 1160 | 'source': 'DVD' 1161 | } 1162 | ], [ 1163 | 'Attack on Titan - Episode 3 - A Dim Light Amid Despair / Humanity\'s Comeback, Part 1', # noqa E501 1164 | None, 1165 | { 1166 | 'anime_title': 'Attack on Titan', 1167 | 'episode_number': '3', 1168 | 'episode_title': 'A Dim Light Amid Despair / Humanity\'s Comeback, Part 1', # noqa E501 1169 | 'file_name': 'Attack on Titan - Episode 3 - A Dim Light Amid Despair / Humanity\'s Comeback, Part 1', # noqa E501 1170 | 'id': 16498 1171 | } 1172 | ], [ 1173 | 'The Irregular at Magic High School - S01E01- Enrollment Part I.mkv', 1174 | None, 1175 | { 1176 | 'anime_season': '01', 1177 | 'anime_title': 'The Irregular at Magic High School', 1178 | 'episode_number': '01', 1179 | 'episode_title': 'Enrollment Part I', 1180 | 'file_extension': 'mkv', 1181 | 'file_name': 'The Irregular at Magic High School - S01E01- Enrollment Part I.mkv', # noqa E501 1182 | 'id': 20785 1183 | } 1184 | ], [ 1185 | '[Mezashite] Aikatsu! ‒ 100 [D035A39F].mkv', 1186 | None, 1187 | { 1188 | 'anime_title': 'Aikatsu!', 1189 | 'episode_number': '100', 1190 | 'file_checksum': 'D035A39F', 1191 | 'file_extension': 'mkv', 1192 | 'file_name': '[Mezashite] Aikatsu! ‒ 100 [D035A39F].mkv', 1193 | 'id': 15061, 1194 | 'release_group': 'Mezashite' 1195 | } 1196 | ], [ 1197 | 'DRAMAtical Murder Episode 1 - Data_01_Login', 1198 | { 1199 | 'option_allowed_delimiters': ' ' 1200 | }, 1201 | { 1202 | 'anime_title': 'DRAMAtical Murder', 1203 | 'episode_number': '1', 1204 | 'episode_title': 'Data_01_Login', 1205 | 'file_name': 'DRAMAtical Murder Episode 1 - Data_01_Login', 1206 | 'id': 23333 1207 | } 1208 | ], [ 1209 | '[Triad]_Today_in_Class_5-2_-_04.avi', 1210 | None, 1211 | { 1212 | 'anime_title': 'Today in Class 5-2', 1213 | 'episode_number': '04', 1214 | 'file_extension': 'avi', 1215 | 'file_name': '[Triad]_Today_in_Class_5-2_-_04.avi', 1216 | 'id': 837, 1217 | 'release_group': 'Triad' 1218 | } 1219 | ], [ 1220 | '[HorribleSubs] Tsukimonogatari - (01-04) [1080p].mkv', 1221 | None, 1222 | { 1223 | 'anime_title': 'Tsukimonogatari', 1224 | 'episode_number': [ 1225 | '01', 1226 | '04' 1227 | ], 1228 | 'file_extension': 'mkv', 1229 | 'file_name': '[HorribleSubs] Tsukimonogatari - (01-04) [1080p].mkv', # noqa E501 1230 | 'id': 28025, 1231 | 'release_group': 'HorribleSubs', 1232 | 'video_resolution': '1080p' 1233 | } 1234 | ], [ 1235 | '[Coalgirls]_White_Album_1-13_(1280×720_Blu-Ray_FLAC)', 1236 | None, 1237 | { 1238 | 'anime_title': 'White Album', 1239 | 'audio_term': 'FLAC', 1240 | 'episode_number': [ 1241 | '1', 1242 | '13' 1243 | ], 1244 | 'file_name': '[Coalgirls]_White_Album_1-13_(1280×720_Blu-Ray_FLAC)', # noqa E501 1245 | 'id': 4720, 1246 | 'release_group': 'Coalgirls', 1247 | 'source': 'Blu-Ray', 1248 | 'video_resolution': '1280×720' 1249 | } 1250 | ], [ 1251 | '[Coalgirls]_Bakemonogatari_OP4a_(1280x720_Blu-Ray_FLAC)_[327A2375].mkv', # noqa E501 1252 | None, 1253 | { 1254 | 'anime_title': 'Bakemonogatari OP', 1255 | 'anime_type': 'OP', 1256 | 'audio_term': 'FLAC', 1257 | 'episode_number': '4a', 1258 | 'file_checksum': '327A2375', 1259 | 'file_extension': 'mkv', 1260 | 'file_name': '[Coalgirls]_Bakemonogatari_OP4a_(1280x720_Blu-Ray_FLAC)_[327A2375].mkv', # noqa E501 1261 | 'id': 5081, 1262 | 'release_group': 'Coalgirls', 1263 | 'source': 'Blu-Ray', 1264 | 'video_resolution': '1280x720' 1265 | } 1266 | ], [ 1267 | '[Ruri]No.6 01 [720p][H264][A956075C].mkv', 1268 | None, 1269 | { 1270 | 'anime_title': 'No.6', 1271 | 'episode_number': '01', 1272 | 'file_checksum': 'A956075C', 1273 | 'file_extension': 'mkv', 1274 | 'file_name': '[Ruri]No.6 01 [720p][H264][A956075C].mkv', 1275 | 'id': 10161, 1276 | 'release_group': 'Ruri', 1277 | 'video_resolution': '720p', 1278 | 'video_term': 'H264' 1279 | } 1280 | ], [ 1281 | '[CH] Sword Art Online Extra Edition Dual Audio [BD 480p][10bitH.264+Vorbis]', # noqa E501 1282 | None, 1283 | { 1284 | 'anime_title': 'Sword Art Online Extra Edition', 1285 | 'audio_term': [ 1286 | 'Dual Audio', 1287 | 'Vorbis' 1288 | ], 1289 | 'file_name': '[CH] Sword Art Online Extra Edition Dual Audio [BD 480p][10bitH.264+Vorbis]', # noqa E501 1290 | 'id': 20021, 1291 | 'release_group': 'CH', 1292 | 'source': 'BD', 1293 | 'video_resolution': '480p', 1294 | 'video_term': [ 1295 | 'H.264', 1296 | '10bit' 1297 | ] 1298 | } 1299 | ], [ 1300 | 'EvoBot.[Watakushi]_Akuma_no_Riddle_-_01v2_[720p][69A307A2].mkv', 1301 | { 1302 | 'option_ignored_strings': ['EvoBot.'] 1303 | }, 1304 | { 1305 | 'anime_title': 'Akuma no Riddle', 1306 | 'episode_number': '01', 1307 | 'file_checksum': '69A307A2', 1308 | 'file_extension': 'mkv', 1309 | 'file_name': 'EvoBot.[Watakushi]_Akuma_no_Riddle_-_01v2_[720p][69A307A2].mkv', # noqa E501 1310 | 'id': 19429, 1311 | 'release_group': 'Watakushi', 1312 | 'release_version': '2', 1313 | 'video_resolution': '720p' 1314 | } 1315 | ], [ 1316 | 'Episode 14 Ore no Imouto ga Konnani Kawaii Wake ga Nai. (saison 2) VOSTFR', # noqa E501 1317 | None, 1318 | { 1319 | 'anime_season': '2', 1320 | 'anime_title': 'Ore no Imouto ga Konnani Kawaii Wake ga Nai.', 1321 | 'episode_number': '14', 1322 | 'file_name': 'Episode 14 Ore no Imouto ga Konnani Kawaii Wake ga Nai. (saison 2) VOSTFR', # noqa E501 1323 | 'id': 13659, 1324 | 'language': 'VOSTFR' 1325 | } 1326 | ], [ 1327 | '[Zom-B] Machine-Doll wa Kizutsukanai - 01 - Facing \'\'Cannibal Candy\'\' I (kuroi, FFF remux) [B99C8DED].mkv', # noqa E501 1328 | None, 1329 | { 1330 | 'anime_title': 'Machine-Doll wa Kizutsukanai', 1331 | 'episode_number': '01', 1332 | 'episode_title': 'Facing \'\'Cannibal Candy\'\' I', 1333 | 'file_checksum': 'B99C8DED', 1334 | 'file_extension': 'mkv', 1335 | 'file_name': '[Zom-B] Machine-Doll wa Kizutsukanai - 01 - Facing \'\'Cannibal Candy\'\' I (kuroi, FFF remux) [B99C8DED].mkv', # noqa E501 1336 | 'id': 17247, 1337 | 'release_information': 'remux', 1338 | 'release_group': 'Zom-B' 1339 | } 1340 | ], [ 1341 | '[Yuurisan-Subs]_Darker_than_Black_-_Gemini_of_the_Meteor_-_01v2_[65274FDE].patch.7z', # noqa E501 1342 | None, 1343 | { 1344 | 'anime_title': 'Darker than Black - Gemini of the Meteor', 1345 | 'episode_number': '01', 1346 | 'file_checksum': '65274FDE', 1347 | 'file_extension': '7z', 1348 | 'file_name': '[Yuurisan-Subs]_Darker_than_Black_-_Gemini_of_the_Meteor_-_01v2_[65274FDE].patch.7z', # noqa E501 1349 | 'id': 6573, 1350 | 'release_information': 'patch', 1351 | 'release_group': 'Yuurisan-Subs', 1352 | 'release_version': '2' 1353 | } 1354 | ], [ 1355 | '[Coalgirls]_Fate_Zero_OVA3.5_(1280x720_Blu-ray_FLAC)_[5F5AD026].mkv', 1356 | None, 1357 | { 1358 | 'anime_title': 'Fate Zero OVA', 1359 | 'anime_type': 'OVA', 1360 | 'audio_term': 'FLAC', 1361 | 'episode_number': '3.5', 1362 | 'file_checksum': '5F5AD026', 1363 | 'file_extension': 'mkv', 1364 | 'file_name': '[Coalgirls]_Fate_Zero_OVA3.5_(1280x720_Blu-ray_FLAC)_[5F5AD026].mkv', # noqa E501 1365 | 'id': 10087, 1366 | 'release_group': 'Coalgirls', 1367 | 'source': 'Blu-ray', 1368 | 'video_resolution': '1280x720' 1369 | } 1370 | ], [ 1371 | '[GrimRipper] Koi Kaze [Dual Audio] Ep01 (c13cefe0).mkv', 1372 | None, 1373 | { 1374 | 'anime_title': 'Koi Kaze', 1375 | 'audio_term': 'Dual Audio', 1376 | 'episode_number': '01', 1377 | 'file_checksum': 'c13cefe0', 1378 | 'file_extension': 'mkv', 1379 | 'file_name': '[GrimRipper] Koi Kaze [Dual Audio] Ep01 (c13cefe0).mkv', # noqa E501 1380 | 'id': 634, 1381 | 'release_group': 'GrimRipper' 1382 | } 1383 | ], [ 1384 | '[HorribleSubs] Gintama - 111C [1080p].mkv', 1385 | None, 1386 | { 1387 | 'anime_title': 'Gintama', 1388 | 'episode_number': '111C', 1389 | 'file_extension': 'mkv', 1390 | 'file_name': '[HorribleSubs] Gintama - 111C [1080p].mkv', 1391 | 'id': 918, 1392 | 'release_group': 'HorribleSubs', 1393 | 'video_resolution': '1080p' 1394 | } 1395 | ], [ 1396 | '[Hatsuyuki]_Kuroko_no_Basuke_S3_-_01_(51)_[720p][10bit][619C57A0].mkv', # noqa E501 1397 | None, 1398 | { 1399 | 'anime_title': 'Kuroko no Basuke', 1400 | 'episode_number': '01', 1401 | 'anime_season': '3', 1402 | 'episode_number_alt': '51', 1403 | 'file_checksum': '619C57A0', 1404 | 'file_extension': 'mkv', 1405 | 'file_name': '[Hatsuyuki]_Kuroko_no_Basuke_S3_-_01_(51)_[720p][10bit][619C57A0].mkv', # noqa E501 1406 | 'id': 24415, 1407 | 'release_group': 'Hatsuyuki', 1408 | 'video_resolution': '720p', 1409 | 'video_term': '10bit' 1410 | } 1411 | ], [ 1412 | '[Elysium]Sora.no.Woto.EP07.5(BD.720p.AAC)[C37580F8].mkv', 1413 | None, 1414 | { 1415 | 'anime_title': 'Sora no Woto', 1416 | 'audio_term': 'AAC', 1417 | 'episode_number': '07.5', 1418 | 'file_checksum': 'C37580F8', 1419 | 'file_extension': 'mkv', 1420 | 'file_name': '[Elysium]Sora.no.Woto.EP07.5(BD.720p.AAC)[C37580F8].mkv', # noqa E501 1421 | 'id': 6802, 1422 | 'release_group': 'Elysium', 1423 | 'source': 'BD', 1424 | 'video_resolution': '720p' 1425 | } 1426 | ], [ 1427 | '[Zurako] Sora no Woto - 07.5 - Drinking Party - Fortress Battle (BD 1080p AAC) [F7DF16F7].mkv', # noqa E501 1428 | None, 1429 | { 1430 | 'anime_title': 'Sora no Woto', 1431 | 'audio_term': 'AAC', 1432 | 'episode_number': '07.5', 1433 | 'episode_title': 'Drinking Party - Fortress Battle', 1434 | 'file_checksum': 'F7DF16F7', 1435 | 'file_extension': 'mkv', 1436 | 'file_name': '[Zurako] Sora no Woto - 07.5 - Drinking Party - Fortress Battle (BD 1080p AAC) [F7DF16F7].mkv', # noqa E501 1437 | 'id': 6802, 1438 | 'release_group': 'Zurako', 1439 | 'source': 'BD', 1440 | 'video_resolution': '1080p' 1441 | } 1442 | ], [ 1443 | '[Hiryuu] Maji de Watashi ni Koi Shinasai!! - 02 [720].mkv', 1444 | None, 1445 | { 1446 | 'anime_title': 'Maji de Watashi ni Koi Shinasai!!', 1447 | 'episode_number': '02', 1448 | 'file_extension': 'mkv', 1449 | 'file_name': '[Hiryuu] Maji de Watashi ni Koi Shinasai!! - 02 [720].mkv', # noqa E501 1450 | 'id': 10213, 1451 | 'release_group': 'Hiryuu', 1452 | 'video_resolution': '720' 1453 | } 1454 | ], [ 1455 | '[Kira-Fansub] Uchuu no Stellvia ep 14 (BD H264 1280x960 24fps AAC) [06EE7355].mkv', # noqa E501 1456 | None, 1457 | { 1458 | 'anime_title': 'Uchuu no Stellvia', 1459 | 'audio_term': 'AAC', 1460 | 'episode_number': '14', 1461 | 'file_checksum': '06EE7355', 1462 | 'file_extension': 'mkv', 1463 | 'file_name': '[Kira-Fansub] Uchuu no Stellvia ep 14 (BD H264 1280x960 24fps AAC) [06EE7355].mkv', # noqa E501 1464 | 'id': 113, 1465 | 'release_group': 'Kira-Fansub', 1466 | 'source': 'BD', 1467 | 'video_resolution': '1280x960', 1468 | 'video_term': [ 1469 | 'H264', 1470 | '24fps' 1471 | ] 1472 | } 1473 | ], [ 1474 | '[ANE] Yosuga no Sora - Ep01 Preview (Yorihime ver) [BDRip 1080p x264 FLAC].mkv', # noqa E501 1475 | None, 1476 | { 1477 | 'anime_title': 'Yosuga no Sora', 1478 | 'anime_type': 'Preview', 1479 | 'audio_term': 'FLAC', 1480 | 'episode_number': '01', 1481 | 'file_extension': 'mkv', 1482 | 'file_name': '[ANE] Yosuga no Sora - Ep01 Preview (Yorihime ver) [BDRip 1080p x264 FLAC].mkv', # noqa E501 1483 | 'id': 8861, 1484 | 'release_group': 'ANE', 1485 | 'source': 'BDRip', 1486 | 'video_resolution': '1080p', 1487 | 'video_term': 'x264' 1488 | } 1489 | ], [ 1490 | '[DmonHiro] Oreshura #01v2 - The Start Of High School Life Is A War Zone [BD, 720p] [211375E6].mkv', # noqa E501 1491 | None, 1492 | { 1493 | 'anime_title': 'Oreshura', 1494 | 'episode_number': '01', 1495 | 'episode_title': 'The Start Of High School Life Is A War Zone', 1496 | 'file_extension': 'mkv', 1497 | 'file_checksum': '211375E6', 1498 | 'file_name': '[DmonHiro] Oreshura #01v2 - The Start Of High School Life Is A War Zone [BD, 720p] [211375E6].mkv', # noqa E501 1499 | 'id': 14749, 1500 | 'release_group': 'DmonHiro', 1501 | 'release_version': '2', 1502 | 'source': 'BD', 1503 | 'video_resolution': '720p' 1504 | } 1505 | ], [ 1506 | '[모에-Raws] Abarenbou Rikishi!! Matsutarou #04 (ABC 1280x720 x264 AAC).mp4', # noqa E501 1507 | None, 1508 | { 1509 | 'anime_title': 'Abarenbou Rikishi!! Matsutarou', 1510 | 'audio_term': 'AAC', 1511 | 'episode_number': '04', 1512 | 'file_extension': 'mp4', 1513 | 'file_name': '[모에-Raws] Abarenbou Rikishi!! Matsutarou #04 (ABC 1280x720 x264 AAC).mp4', # noqa E501 1514 | 'id': 22831, 1515 | 'release_group': '모에-Raws', 1516 | 'video_resolution': '1280x720', 1517 | 'video_term': 'x264' 1518 | } 1519 | ], [ 1520 | '[바카-Raws] Nekomonogatari (Black) #1-4 (BS11 1280x720 x264 AAC).mp4', 1521 | None, 1522 | { 1523 | 'anime_title': 'Nekomonogatari (Black)', 1524 | 'audio_term': 'AAC', 1525 | 'episode_number': [ 1526 | '1', 1527 | '4' 1528 | ], 1529 | 'file_extension': 'mp4', 1530 | 'file_name': '[바카-Raws] Nekomonogatari (Black) #1-4 (BS11 1280x720 x264 AAC).mp4', # noqa E501 1531 | 'id': 15689, 1532 | 'release_group': '바카-Raws', 1533 | 'video_resolution': '1280x720', 1534 | 'video_term': 'x264' 1535 | } 1536 | ], [ 1537 | '[NinjaPanda] Tiger & Bunny #01 All\'s well that ends well. (v3, 1080p Hi10P, DA AAC) [4A9AB85F].mkv', # noqa E501 1538 | None, 1539 | { 1540 | 'anime_title': 'Tiger & Bunny', 1541 | 'audio_term': 'AAC', 1542 | 'episode_number': '01', 1543 | 'episode_title': 'All\'s well that ends well.', 1544 | 'file_checksum': '4A9AB85F', 1545 | 'file_extension': 'mkv', 1546 | 'file_name': '[NinjaPanda] Tiger & Bunny #01 All\'s well that ends well. (v3, 1080p Hi10P, DA AAC) [4A9AB85F].mkv', # noqa E501 1547 | 'id': 9941, 1548 | 'release_group': 'NinjaPanda', 1549 | 'release_version': '3', 1550 | 'video_resolution': '1080p', 1551 | 'video_term': 'Hi10P' 1552 | } 1553 | ], [ 1554 | '[FFF] Seirei Tsukai no Blade Dance - SP01 [BD][720p-AAC][F1FF8588].mkv', # noqa E501 1555 | None, 1556 | { 1557 | 'anime_title': 'Seirei Tsukai no Blade Dance - SP', 1558 | 'anime_type': 'SP', 1559 | 'audio_term': 'AAC', 1560 | 'episode_number': '01', 1561 | 'file_checksum': 'F1FF8588', 1562 | 'file_extension': 'mkv', 1563 | 'file_name': '[FFF] Seirei Tsukai no Blade Dance - SP01 [BD][720p-AAC][F1FF8588].mkv', # noqa E501 1564 | 'id': 25285, 1565 | 'release_group': 'FFF', 1566 | 'source': 'BD', 1567 | 'video_resolution': '720p' 1568 | } 1569 | ], [ 1570 | '[SubDESU-H] Swing out Sisters Complete Version (720p x264 8bit AC3) [3ABD57E6].mp4', # noqa E501 1571 | None, 1572 | { 1573 | 'anime_title': 'Swing out Sisters', 1574 | 'audio_term': 'AC3', 1575 | 'file_checksum': '3ABD57E6', 1576 | 'file_extension': 'mp4', 1577 | 'file_name': '[SubDESU-H] Swing out Sisters Complete Version (720p x264 8bit AC3) [3ABD57E6].mp4', # noqa E501 1578 | 'id': 12143, 1579 | 'release_group': 'SubDESU-H', 1580 | 'release_information': 'Complete', 1581 | 'video_resolution': '720p', 1582 | 'video_term': [ 1583 | 'x264', 1584 | '8bit' 1585 | ] 1586 | } 1587 | ], [ 1588 | 'Cyborg 009 (1968) [TSHS] episode 06 [30C15D62].mp4', 1589 | None, 1590 | { 1591 | 'anime_title': 'Cyborg 009', 1592 | 'anime_year': '1968', 1593 | 'episode_number': '06', 1594 | 'file_checksum': '30C15D62', 1595 | 'file_extension': 'mp4', 1596 | 'file_name': 'Cyborg 009 (1968) [TSHS] episode 06 [30C15D62].mp4', 1597 | 'id': 8394, 1598 | 'release_group': 'TSHS' 1599 | } 1600 | ], [ 1601 | '[Hatsuyuki] Dragon Ball Kai (2014) - 002 (100) [1280x720][DD66AFB7].mkv', # noqa E501 1602 | None, 1603 | { 1604 | 'anime_title': 'Dragon Ball Kai', 1605 | 'anime_year': '2014', 1606 | 'episode_number': '002', 1607 | 'episode_number_alt': '100', 1608 | 'file_checksum': 'DD66AFB7', 1609 | 'file_extension': 'mkv', 1610 | 'file_name': '[Hatsuyuki] Dragon Ball Kai (2014) - 002 (100) [1280x720][DD66AFB7].mkv', # noqa E501 1611 | 'id': 22777, 1612 | 'release_group': 'Hatsuyuki', 1613 | 'video_resolution': '1280x720' 1614 | } 1615 | ], [ 1616 | '[Deep] Tegami Bachi (REVERSE) - Letter Bee - 29 (04) [5203142B].mkv', 1617 | None, 1618 | { 1619 | 'anime_title': 'Tegami Bachi (REVERSE) - Letter Bee', 1620 | 'episode_number': '04', 1621 | 'episode_number_alt': '29', 1622 | 'file_checksum': '5203142B', 1623 | 'file_extension': 'mkv', 1624 | 'file_name': '[Deep] Tegami Bachi (REVERSE) - Letter Bee - 29 (04) [5203142B].mkv', # noqa E501 1625 | 'id': 8311, 1626 | 'release_group': 'Deep' 1627 | } 1628 | ], [ 1629 | '[FFF] Love Live! The School Idol Movie - PV [D1A15D2C].mkv', 1630 | None, 1631 | { 1632 | 'anime_title': 'Love Live! The School Idol Movie - PV', 1633 | 'anime_type': [ 1634 | 'Movie', 1635 | 'PV' 1636 | ], 1637 | 'file_checksum': 'D1A15D2C', 1638 | 'file_extension': 'mkv', 1639 | 'file_name': '[FFF] Love Live! The School Idol Movie - PV [D1A15D2C].mkv', # noqa E501 1640 | 'id': 24997, 1641 | 'release_group': 'FFF' 1642 | } 1643 | ], [ 1644 | '[Nishi-Taku] Tamayura ~graduation photo~ Movie Part 1 [BD][720p][98965607].mkv', # noqa E501 1645 | None, 1646 | { 1647 | 'anime_title': 'Tamayura ~graduation photo~ Movie Part 1', 1648 | 'anime_type': 'Movie', 1649 | 'file_checksum': '98965607', 1650 | 'file_extension': 'mkv', 1651 | 'file_name': '[Nishi-Taku] Tamayura ~graduation photo~ Movie Part 1 [BD][720p][98965607].mkv', # noqa E501 1652 | 'id': 25729, 1653 | 'release_group': 'Nishi-Taku', 1654 | 'source': 'BD', 1655 | 'video_resolution': '720p' 1656 | } 1657 | ], [ 1658 | '[LRL] 1001 Nights (1998) [DVD]', 1659 | None, 1660 | { 1661 | 'anime_title': '1001 Nights', 1662 | 'anime_year': '1998', 1663 | 'file_name': '[LRL] 1001 Nights (1998) [DVD]', 1664 | 'id': 3914, 1665 | 'release_group': 'LRL', 1666 | 'source': 'DVD' 1667 | } 1668 | ], [ 1669 | '[TardRaws] 0 [640x360].mkv', 1670 | None, 1671 | { 1672 | 'anime_title': '0', 1673 | 'file_extension': 'mkv', 1674 | 'file_name': '[TardRaws] 0 [640x360].mkv', 1675 | 'id': 20707, 1676 | 'release_group': 'TardRaws', 1677 | 'video_resolution': '640x360' 1678 | } 1679 | ], [ 1680 | '[FB] Crayon Shin-Chan Movie 2 The Secret of Buri Buri Kingdom [DivX5 AC3] 1994 [852X480] V2.avi', # noqa E501 1681 | None, 1682 | { 1683 | 'anime_title': 'Crayon Shin-Chan Movie 2 The Secret of Buri Buri Kingdom', # noqa E501 1684 | 'anime_type': 'Movie', 1685 | 'anime_year': '1994', 1686 | 'audio_term': 'AC3', 1687 | 'file_extension': 'avi', 1688 | 'file_name': '[FB] Crayon Shin-Chan Movie 2 The Secret of Buri Buri Kingdom [DivX5 AC3] 1994 [852X480] V2.avi', # noqa E501 1689 | 'id': 3745, 1690 | 'release_group': 'FB', 1691 | 'release_version': '2', 1692 | 'video_resolution': '852X480', 1693 | 'video_term': 'DivX5' 1694 | } 1695 | ], [ 1696 | '[Hatsuyuki-Kaitou]_Fairy_Tail_2_-_52_(227)_[720p][10bit][9DF6B8D5].mkv', # noqa E501 1697 | None, 1698 | { 1699 | 'anime_title': 'Fairy Tail 2', 1700 | 'episode_number': '52', 1701 | 'episode_number_alt': '227', 1702 | 'file_checksum': '9DF6B8D5', 1703 | 'file_extension': 'mkv', 1704 | 'file_name': '[Hatsuyuki-Kaitou]_Fairy_Tail_2_-_52_(227)_[720p][10bit][9DF6B8D5].mkv', # noqa E501 1705 | 'id': 22043, 1706 | 'release_group': 'Hatsuyuki-Kaitou', 1707 | 'video_resolution': '720p', 1708 | 'video_term': '10bit' 1709 | } 1710 | ], [ 1711 | '[FBI] Baby Princess 3D Paradise Love 01v0 [BD][720p-AAC][457CC066].mkv', # noqa E501 1712 | None, 1713 | { 1714 | 'anime_title': 'Baby Princess 3D Paradise Love', 1715 | 'audio_term': 'AAC', 1716 | 'episode_number': '01', 1717 | 'file_checksum': '457CC066', 1718 | 'file_extension': 'mkv', 1719 | 'file_name': '[FBI] Baby Princess 3D Paradise Love 01v0 [BD][720p-AAC][457CC066].mkv', # noqa E501 1720 | 'id': 10196, 1721 | 'release_group': 'FBI', 1722 | 'release_version': '0', 1723 | 'source': 'BD', 1724 | 'video_resolution': '720p' 1725 | } 1726 | ], [ 1727 | '[Shinsen-Subs]_Macross_Frontier_-_01b_[4D5EC315].avi', 1728 | None, 1729 | { 1730 | 'anime_title': 'Macross Frontier', 1731 | 'episode_number': '01b', 1732 | 'file_checksum': '4D5EC315', 1733 | 'file_extension': 'avi', 1734 | 'file_name': '[Shinsen-Subs]_Macross_Frontier_-_01b_[4D5EC315].avi', # noqa E501 1735 | 'id': 3572, 1736 | 'release_group': 'Shinsen-Subs' 1737 | } 1738 | ], [ 1739 | '[NamaeNai] Hidamari Sketch x365 - 09a (DVD) [49874745].mkv', 1740 | None, 1741 | { 1742 | 'anime_title': 'Hidamari Sketch x365', 1743 | 'episode_number': '09a', 1744 | 'file_checksum': '49874745', 1745 | 'file_extension': 'mkv', 1746 | 'file_name': '[NamaeNai] Hidamari Sketch x365 - 09a (DVD) [49874745].mkv', # noqa E501 1747 | 'id': 3604, 1748 | 'release_group': 'NamaeNai', 1749 | 'source': 'DVD' 1750 | } 1751 | ], [ 1752 | '[KLF]_D.Gray-man_04V2.avi', 1753 | None, 1754 | { 1755 | 'anime_title': 'D.Gray-man', 1756 | 'episode_number': '04', 1757 | 'file_extension': 'avi', 1758 | 'file_name': '[KLF]_D.Gray-man_04V2.avi', 1759 | 'id': 1482, 1760 | 'release_group': 'KLF', 1761 | 'release_version': '2' 1762 | } 1763 | ], [ 1764 | '[FaggotryRaws] Bakuman - 01 (NHK E 848x480).mkv', 1765 | None, 1766 | { 1767 | 'anime_title': 'Bakuman', 1768 | 'episode_number': '01', 1769 | 'file_extension': 'mkv', 1770 | 'file_name': '[FaggotryRaws] Bakuman - 01 (NHK E 848x480).mkv', 1771 | 'id': 7674, 1772 | 'release_group': 'FaggotryRaws', 1773 | 'video_resolution': '848x480' 1774 | } 1775 | ], [ 1776 | '[5F] RWBY 14 Forever Fall Part 2 pt-BR.mp4', 1777 | None, 1778 | { 1779 | 'anime_title': 'RWBY', 1780 | 'episode_number': '14', 1781 | 'episode_title': 'Forever Fall Part 2', 1782 | 'file_extension': 'mp4', 1783 | 'file_name': '[5F] RWBY 14 Forever Fall Part 2 pt-BR.mp4', 1784 | 'id': 0, 1785 | 'language': 'pt-BR', 1786 | 'release_group': '5F' 1787 | } 1788 | ], [ 1789 | '[AnimeRG] Ushio to Tora (TV) - 02 [720p] [Xcelent].mkv', 1790 | None, 1791 | { 1792 | 'anime_title': 'Ushio to Tora (TV)', 1793 | 'anime_type': 'TV', 1794 | 'episode_number': '02', 1795 | 'file_extension': 'mkv', 1796 | 'file_name': '[AnimeRG] Ushio to Tora (TV) - 02 [720p] [Xcelent].mkv', # noqa E501 1797 | 'id': 29854, 1798 | 'release_group': 'AnimeRG', 1799 | 'video_resolution': '720p' 1800 | } 1801 | ], [ 1802 | '[Anime', 1803 | None, 1804 | { 1805 | 'file_name': '[Anime' 1806 | } 1807 | ], [ 1808 | 'Gekkan Shoujo Nozaki-kun [HorribleSubs] (1080p)', 1809 | None, 1810 | { 1811 | 'anime_title': 'Gekkan Shoujo Nozaki-kun', 1812 | 'file_name': 'Gekkan Shoujo Nozaki-kun [HorribleSubs] (1080p)', 1813 | 'id': 23289, 1814 | 'release_group': 'HorribleSubs', 1815 | 'video_resolution': '1080p' 1816 | } 1817 | ], [ 1818 | '[BM&T] Toradora! - 07v2 - Pool Opening [720p Hi10 ] [BD] [8F59F2BA]', 1819 | None, 1820 | { 1821 | 'anime_title': 'Toradora!', 1822 | 'episode_number': '07', 1823 | 'episode_title': 'Pool Opening', 1824 | 'file_checksum': '8F59F2BA', 1825 | 'file_name': '[BM&T] Toradora! - 07v2 - Pool Opening [720p Hi10 ] [BD] [8F59F2BA]', # noqa E501 1826 | 'id': 23289, 1827 | 'release_group': 'BM&T', 1828 | 'release_version': '2', 1829 | 'source': 'BD', 1830 | 'video_term': 'Hi10', 1831 | 'video_resolution': '720p' 1832 | } 1833 | ], [ 1834 | '[DmonHiro] Magi - The Labyrinth Of Magic - Vol.1v2 (BD, 720p)', 1835 | None, 1836 | { 1837 | 'anime_title': 'Magi - The Labyrinth Of Magic', 1838 | 'file_name': '[DmonHiro] Magi - The Labyrinth Of Magic - Vol.1v2 (BD, 720p)', # noqa E501 1839 | 'id': 14513, 1840 | 'release_group': 'DmonHiro', 1841 | 'release_version': '2', 1842 | 'source': 'BD', 1843 | 'video_resolution': '720p', 1844 | 'volume_number': '1' 1845 | } 1846 | ], [ 1847 | '[tlacatlc6] Natsume Yuujinchou Shi Vol. 1v2 & Vol. 2 (BD 1280x720 x264 AAC)', # noqa E501 1848 | None, 1849 | { 1850 | 'anime_title': 'Natsume Yuujinchou Shi', 1851 | 'audio_term': 'AAC', 1852 | 'file_name': '[tlacatlc6] Natsume Yuujinchou Shi Vol. 1v2 & Vol. 2 (BD 1280x720 x264 AAC)', # noqa E501 1853 | 'id': 11665, 1854 | 'release_group': 'tlacatlc6', 1855 | 'release_version': '2', 1856 | 'source': 'BD', 1857 | 'video_term': 'x264', 1858 | 'video_resolution': '1280x720', 1859 | 'volume_number': [ 1860 | '1', 1861 | '2' 1862 | ] 1863 | } 1864 | ], [ 1865 | '[Tsundere] Hyouka - 01v2-04 [BDRip h264 1920x1080 10bit FLAC]', 1866 | None, 1867 | { 1868 | 'anime_title': 'Hyouka', 1869 | 'audio_term': 'FLAC', 1870 | 'episode_number': [ 1871 | '01', 1872 | '04' 1873 | ], 1874 | 'file_name': '[Tsundere] Hyouka - 01v2-04 [BDRip h264 1920x1080 10bit FLAC]', # noqa E501 1875 | 'id': 12189, 1876 | 'release_group': 'Tsundere', 1877 | 'release_version': '2', 1878 | 'source': 'BDRip', 1879 | 'video_resolution': '1920x1080', 1880 | 'video_term': [ 1881 | 'h264', 1882 | '10bit' 1883 | ] 1884 | } 1885 | ], [ 1886 | '[Doki] Nogizaka Haruka no Himitsu - Purezza - 01v2-03v2 (1280x720 h264 AAC)', # noqa E501 1887 | None, 1888 | { 1889 | 'anime_title': 'Nogizaka Haruka no Himitsu - Purezza', 1890 | 'audio_term': 'AAC', 1891 | 'episode_number': [ 1892 | '01', 1893 | '03' 1894 | ], 1895 | 'file_name': '[Doki] Nogizaka Haruka no Himitsu - Purezza - 01v2-03v2 (1280x720 h264 AAC)', # noqa E501 1896 | 'id': 6023, 1897 | 'release_group': 'Doki', 1898 | 'release_version': [ 1899 | '2', 1900 | '2' 1901 | ], 1902 | 'video_resolution': '1280x720', 1903 | 'video_term': 'h264' 1904 | } 1905 | ], [ 1906 | 'Fairy Tail - S06E32 - Tartaros Arc Iron Fist of the Fire Dragon [Episode 83]', # noqa E501 1907 | None, 1908 | { 1909 | 'anime_season': '06', 1910 | 'anime_title': 'Fairy Tail', 1911 | 'episode_number': '32', 1912 | 'episode_number_alt': '83', 1913 | 'episode_title': 'Tartaros Arc Iron Fist of the Fire Dragon', 1914 | 'file_name': 'Fairy Tail - S06E32 - Tartaros Arc Iron Fist of the Fire Dragon [Episode 83]', # noqa E501 1915 | 'id': 6702 1916 | } 1917 | ], [ 1918 | 'Noragami - S02E06 - What Must Be Done [Episode 6]', 1919 | None, 1920 | { 1921 | 'anime_season': '02', 1922 | 'anime_title': 'Noragami', 1923 | 'episode_number': '6', 1924 | 'episode_title': 'What Must Be Done', 1925 | 'file_name': 'Noragami - S02E06 - What Must Be Done [Episode 6]', 1926 | 'id': 30503 1927 | } 1928 | ], [ 1929 | '[Harunatsu] Classroom Crisis - Vol.1 [BD 720p-AAC]', 1930 | None, 1931 | { 1932 | 'anime_title': 'Classroom Crisis', 1933 | 'audio_term': 'AAC', 1934 | 'file_name': '[Harunatsu] Classroom Crisis - Vol.1 [BD 720p-AAC]', 1935 | 'id': 30383, 1936 | 'release_group': 'Harunatsu', 1937 | 'source': 'BD', 1938 | 'video_resolution': '720p', 1939 | 'volume_number': '1' 1940 | } 1941 | ], [ 1942 | '[GS] Classroom Crisis Vol.1&2 (BD 1080p 10bit FLAC)', 1943 | None, 1944 | { 1945 | 'anime_title': 'Classroom Crisis', 1946 | 'audio_term': 'FLAC', 1947 | 'file_name': '[GS] Classroom Crisis Vol.1&2 (BD 1080p 10bit FLAC)', 1948 | 'id': 30383, 1949 | 'release_group': 'GS', 1950 | 'source': 'BD', 1951 | 'video_resolution': '1080p', 1952 | 'video_term': '10bit', 1953 | 'volume_number': [ 1954 | '1', 1955 | '2' 1956 | ] 1957 | } 1958 | ], [ 1959 | '[Infantjedi] Norn9 - Norn + Nonetto - 12', 1960 | None, 1961 | { 1962 | 'anime_title': 'Norn9 - Norn + Nonetto', 1963 | 'episode_number': '12', 1964 | 'file_name': '[Infantjedi] Norn9 - Norn + Nonetto - 12', 1965 | 'id': 31452, 1966 | 'release_group': 'Infantjedi' 1967 | } 1968 | ], [ 1969 | 'Dragon_Ball_Z_Movies_8_&_10_[720p,BluRay,DTS,x264]_-_THORA', 1970 | None, 1971 | { 1972 | 'anime_title': 'Dragon Ball Z Movies', 1973 | 'audio_term': 'DTS', 1974 | 'episode_number': [ 1975 | '8', 1976 | '10' 1977 | ], 1978 | 'file_name': 'Dragon_Ball_Z_Movies_8_&_10_[720p,BluRay,DTS,x264]_-_THORA', # noqa E501 1979 | 'id': [ 1980 | 901, 1981 | 903 1982 | ], 1983 | 'release_group': 'THORA', 1984 | 'source': 'BluRay', 1985 | 'video_resolution': '720p', 1986 | 'video_term': 'x264' 1987 | } 1988 | ], [ 1989 | '[HorribleSubs] Momokuri - 01+02 [720p]', 1990 | None, 1991 | { 1992 | 'anime_title': 'Momokuri', 1993 | 'episode_number': [ 1994 | '01', 1995 | '02' 1996 | ], 1997 | 'file_name': '[HorribleSubs] Momokuri - 01+02 [720p]', 1998 | 'release_group': 'HorribleSubs', 1999 | 'video_resolution': '720p' 2000 | } 2001 | ], [ 2002 | '[(´• ω •`)] Nintama Rantarou - S23E1821 - Buddhist Priest-sama is a Ninja.mkv', 2003 | None, 2004 | { 2005 | "anime_season": "23", 2006 | "anime_title": "Nintama Rantarou", 2007 | "episode_number": "1821", 2008 | "episode_title": "Buddhist Priest-sama is a Ninja", 2009 | "file_extension": "mkv", 2010 | "file_name": "[(´• ω •`)] Nintama Rantarou - S23E1821 - Buddhist Priest-sama is a Ninja.mkv", 2011 | "release_group": "(´• ω •`)" 2012 | } 2013 | ], [ 2014 | '[Judas] Aharen-san wa Hakarenai - S01E06v2.mkv', 2015 | None, 2016 | { 2017 | 'anime_title': 'Aharen-san wa Hakarenai', 2018 | 'anime_season': '01', 2019 | 'episode_number': '06', 2020 | 'release_version': '2', 2021 | 'file_extension': 'mkv', 2022 | 'file_name': '[Judas] Aharen-san wa Hakarenai - S01E06v2.mkv', 2023 | 'release_group': 'Judas' 2024 | } 2025 | ], [ 2026 | '[0x539] Somali and the Forest Spirit - S01E01 (WEB 1080p Hi10P AAC) [BB7C6531].mkv', 2027 | None, 2028 | { 2029 | 'anime_season': '01', 2030 | 'anime_title': 'Somali and the Forest Spirit', 2031 | 'audio_term': 'AAC', 2032 | 'episode_number': '01', 2033 | 'file_checksum': 'BB7C6531', 2034 | 'file_extension': 'mkv', 2035 | 'file_name': '[0x539] Somali and the Forest Spirit - S01E01 (WEB 1080p Hi10P AAC) [BB7C6531].mkv', 2036 | 'release_group': '0x539', 2037 | 'video_resolution': '1080p', 2038 | 'video_term': 'Hi10P' 2039 | } 2040 | ], 2041 | 2042 | ########################################################################### 2043 | # Original tests 2044 | [ 2045 | 'Tsuredure Children Episódio 1 – Confession', 2046 | None, 2047 | { 2048 | 'anime_title': 'Tsuredure Children', 2049 | 'episode_number': '1', 2050 | 'episode_title': 'Confession', 2051 | 'file_name': 'Tsuredure Children Episódio 1 – Confession' 2052 | } 2053 | ], [ 2054 | '[HorribleSubs] Mob Psycho 100 S2 - 07 [1080p].mkv', 2055 | None, 2056 | { 2057 | 'anime_title': 'Mob Psycho 100', 2058 | 'episode_number': '07', 2059 | 'anime_season': '2', 2060 | 'file_name': '[HorribleSubs] Mob Psycho 100 S2 - 07 [1080p].mkv', 2061 | 'file_extension': 'mkv', 2062 | 'release_group': 'HorribleSubs', 2063 | 'video_resolution': '1080p' 2064 | } 2065 | ], [ 2066 | 'One-Punch Man Season 1 OVA [Judas] OVA - 05.mkv', 2067 | None, 2068 | { 2069 | 'anime_title': 'One-Punch Man', 2070 | 'anime_type': ['OVA', 'OVA'], 2071 | 'episode_number': '05', 2072 | 'anime_season': '1', 2073 | 'file_name': 'One-Punch Man Season 1 OVA [Judas] OVA - 05.mkv', 2074 | 'file_extension': 'mkv', 2075 | 'release_group': 'Judas' 2076 | } 2077 | ], [ 2078 | '[Reaktor] Serial Experiments Lain - E01 [1080p][x265][10-bit][Dual-Audio].mkv', # noqa E501 2079 | None, 2080 | { 2081 | 'anime_title': 'Serial Experiments Lain', 2082 | 'audio_term': 'Dual-Audio', 2083 | 'episode_number': '01', 2084 | 'file_extension': 'mkv', 2085 | 'file_name': '[Reaktor] Serial Experiments Lain - E01 [1080p][x265][10-bit][Dual-Audio].mkv', # noqa E501 2086 | 'release_group': 'Reaktor', 2087 | 'video_resolution': '1080p', 2088 | 'video_term': ['x265', '10-bit'] 2089 | } 2090 | ], [ 2091 | '[fong] Lupin III - Kutabare! Nostradamus [BDrip.1080p.10bit.MultiAudio].mkv', # noqa E501 2092 | None, 2093 | { 2094 | 'anime_title': 'Lupin III - Kutabare! Nostradamus', 2095 | 'audio_term': 'MultiAudio', 2096 | 'file_extension': 'mkv', 2097 | 'file_name': '[fong] Lupin III - Kutabare! Nostradamus [BDrip.1080p.10bit.MultiAudio].mkv', # noqa E501 2098 | 'release_group': 'fong', 2099 | 'source': 'BDrip', 2100 | 'video_resolution': '1080p', 2101 | 'video_term': '10bit' 2102 | } 2103 | ], [ 2104 | '[DragsterPS] Devilman Crybaby S01E04 [720p] [Multi-Audio] [Multi-Subs] [24B349D4].mkv', # noqa E501 2105 | None, 2106 | { 2107 | 'anime_season': '01', 2108 | 'anime_title': 'Devilman Crybaby', 2109 | 'audio_term': 'Multi-Audio', 2110 | 'episode_number': '04', 2111 | 'file_checksum': '24B349D4', 2112 | 'file_extension': 'mkv', 2113 | 'file_name': '[DragsterPS] Devilman Crybaby S01E04 [720p] [Multi-Audio] [Multi-Subs] [24B349D4].mkv', # noqa E501 2114 | 'release_group': 'DragsterPS', 2115 | 'subtitles': 'Multi-Subs', 2116 | 'video_resolution': '720p' 2117 | } 2118 | ], [ 2119 | '[Erai-raws] One Piece - 1000 [1080p][Multiple Subtitle][F2AE5FF6].mkv', # noqa E501 2120 | None, 2121 | { 2122 | 'anime_title': 'One Piece', 2123 | 'episode_number': '1000', 2124 | 'file_checksum': 'F2AE5FF6', 2125 | 'file_extension': 'mkv', 2126 | 'file_name': '[Erai-raws] One Piece - 1000 [1080p][Multiple Subtitle][F2AE5FF6].mkv', # noqa E501 2127 | 'release_group': 'Erai-raws', 2128 | 'subtitles': 'Multiple Subtitle', 2129 | 'video_resolution': '1080p' 2130 | } 2131 | ], [ 2132 | '[Judas] One Piece - 1009v2.mkv', 2133 | None, 2134 | { 2135 | 'anime_title': 'One Piece', 2136 | 'episode_number': '1009', 2137 | 'release_version': '2', 2138 | 'file_extension': 'mkv', 2139 | 'file_name': '[Judas] One Piece - 1009v2.mkv', 2140 | 'release_group': 'Judas' 2141 | } 2142 | ], [ 2143 | '[GM-Team][国漫][诛仙][Jade Dynasty][2022][11][HEVC][GB][4K]', 2144 | None, 2145 | { 2146 | 'anime_title': 'Jade Dynasty', 2147 | 'anime_year': '2022', 2148 | 'episode_number': '11', 2149 | 'file_name': '[GM-Team][国漫][诛仙][Jade Dynasty][2022][11][HEVC][GB][4K]', 2150 | 'release_group': 'GM-Team', 2151 | 'video_resolution': '4K', 2152 | 'video_term': 'HEVC' 2153 | } 2154 | ], [ 2155 | "[M-L-Stuffs] Futari wa Precure (Pretty Cure) 01", 2156 | None, 2157 | { 2158 | 'anime_title': 'Futari wa Precure (Pretty Cure)', 2159 | 'episode_number': '01', 2160 | 'file_name': "[M-L-Stuffs] Futari wa Precure (Pretty Cure) 01", 2161 | 'release_group': 'M-L-Stuffs' 2162 | } 2163 | ], 2164 | ] 2165 | -------------------------------------------------------------------------------- /tests/test_anitopy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | from unittest import TestCase 6 | 7 | import anitopy 8 | from tests.fixtures.failing_table import failing_table 9 | from tests.fixtures.table import table 10 | 11 | 12 | class TestAnitopy(TestCase): 13 | def parse_options(self, entry_options): 14 | if entry_options is None: 15 | return {} 16 | 17 | options = {} 18 | for option, value in entry_options.items(): 19 | option_name = option.split('option_')[1] 20 | options[option_name] = value 21 | return options 22 | 23 | def test_table(self): 24 | for index, entry in enumerate(table): 25 | filename = entry[0] 26 | options = self.parse_options(entry[1]) 27 | 28 | elements = anitopy.parse(filename, options=options) 29 | 30 | expected = dict(entry[2]) 31 | if 'id' in expected.keys(): 32 | del expected['id'] 33 | self.assertEqual(expected, elements, 'on entry number %d' % index) 34 | 35 | def test_fails(self): 36 | failed = 0 37 | working_tests = [] 38 | for index, entry in enumerate(failing_table): 39 | filename = entry[0] 40 | options = self.parse_options(entry[1]) 41 | 42 | try: 43 | print('Index %d "%s"' % (index, filename)) 44 | except: # noqa: E722 45 | print(('Index %d "%s"' % (index, filename)).encode("utf-8")) 46 | 47 | elements = anitopy.parse(filename, options=options) 48 | 49 | expected = dict(entry[2]) 50 | if 'id' in expected.keys(): 51 | del expected['id'] 52 | try: 53 | self.assertEqual(expected, elements) 54 | working_tests.append(index) 55 | except AssertionError as err: 56 | failed += 1 57 | print(err) 58 | print('----------------------------------------------------------------------') # noqa E501 59 | 60 | print('\nFailed %d of %d failing cases tests' % ( 61 | failed, len(failing_table))) 62 | if working_tests: 63 | print('There are {} working tests from the failing cases: {}' 64 | .format(len(working_tests), working_tests)) 65 | --------------------------------------------------------------------------------