├── .coveragerc ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── MANIFEST.in ├── README.md ├── RELEASING.md ├── UNLICENSE ├── pytest.ini ├── requirements-dev.txt ├── setup.py ├── tests ├── functional_runner.py ├── helpers.py ├── httpcache │ ├── 0365d0e514a3260eff6fa56d2f4e3d221d8fb71f20e171d05c8489e771dd02e7 │ ├── 0774b76b4bbe942bb29c886072ea3ee15ca5fe59d06fd0ebeef80464d95746aa │ ├── 0939fc03d203db3aef82c95ac8537bfd54ed1eda8cb350a9f1fe859f6cd72fb9 │ ├── 0b1ec7489ea5354497a15d3eee130cb37e8817872e634ba36c4a96d4ba10745a │ ├── 17be4ec7fbbfa90e3aba86ba7cfcfe981dd450cf464c14d5363d9b446b890a07 │ ├── 19b1ca273ef7294dc13469f41e29683e8a4aabe96913c7269a711bafb31f72f1 │ ├── 19c631c68ee9147bacb68c112e4ba7812607d80398dbfee2963ebd5fd30cf64b │ ├── 1a8316738ed28a9bbe32d269d9f6366a9633aa0516042b51eef62462f49aaf5f │ ├── 1c092e14e75ff046b2cb88006eb72d76452aec03c9dddfc59f0da44c19961bca │ ├── 24c6372c6dc3e761aa752fae971b417e4349bb6650a1603872bb5f301a729957 │ ├── 26443651ad88ed46defce0f3a7f3a17ca4d5eeca0301786ec6313a8a8c3b2e00 │ ├── 26567eefbd6faf9e898765f5bad6268706094ca25746142f790af73d33f4d0de │ ├── 26b722ac2c2384fccaa8ac50269b0cafb89dcc0e7c41ff651378fe6ae32df2c2 │ ├── 26f6319dd5397394e111735ef3a27a45008cf64f871d74ece2d03fa64d44d143 │ ├── 28d0cde4eca2d207513f8bdcf65ec5e5e471be176b75567993b3b1ced41ec7a9 │ ├── 2956bb3d537d72413f15792f69967c1aeb0d22f93eac0ee2c3e56737648346fe │ ├── 29adb55c9884ee5222c9ecf51edf0df83b5e4a97c1341c23c3b5ad0662770772 │ ├── 2b54cddf424144c1a5a635d80ddf36519dadf03fbc84a9717604247012695685 │ ├── 2b6f8593c212b626714e1d155cb0352d188b07165d30f747c1bbc7e5a94e6c4a │ ├── 2d7fb223303105a50e59776842ffe95cc44187627e2c55c266823e48540b2451 │ ├── 2fea195f905dd40e625db94bdb7a34b0238360e1d80785e5359050a335c26669 │ ├── 374beefab949ff34a1d1392225cf7084b73fdde44fcbcc4d81d322c573575443 │ ├── 376f777caac26b557bfc81adf35b0e42d9df70fc98aae5bdbcba0b525cac5c52 │ ├── 381bc0c47cf105a4f65c8ec733db9760bb853f1c3cf38bbba58a593bfad6d81b │ ├── 3ae5d9999bc32bc133d6cb342bd00c64ed921e0dd116fb847ac742a2855b674b │ ├── 3bcd3884c4ebfe4084e802157801a063bd3122fe17623d35b6f80aa4da8358c5 │ ├── 3f1cd0c8ed94701d658e4af8dbb795665bf21d4a5a9f08faae44629f4a7fca7d │ ├── 409edd232e2c12ee61953fe3161938a21046926e27e5787065e1c24fc74817d6 │ ├── 418224939ef1716ccee804d12f1a3b731304bce684a9ac331a809617afa4a1b7 │ ├── 42ae05b54a74957f496dd1c72bf90ceb1b956517695f9ffb35eafcfcd16c9b15 │ ├── 46de8d8920e6cc49a590764a902065548080bb34e6025454f36a433a690c7bb2 │ ├── 4d001018f1a474826c8c29b96608689bba4b72e8ff6a3584bcf30cba5e42b524 │ ├── 4d2c98cc0c7eadc98a0bb5745c1dd501164714ca896eccc89f57b77f9ec24e19 │ ├── 4d8f37567cca22a9b6c5869b4794083ffafb001b77461a21fb00b1eb5e5c7e68 │ ├── 4e1f77bb0f359b6546a9f794618cd165e1eb1b438d7359d9d9e3bbdeb7f86690 │ ├── 4f43f35247445e44cc2cd623b595da442ce9696a0cd6e40e1e857337935078a0 │ ├── 536480a297c5e739d080360543e077a2acda2a0d507d7f43df673ed3a4fcdf4e │ ├── 55474e9f790f2ea7275aad21543941a4d919e33a45e7188851921ef16bd01ec5 │ ├── 558436374ae0973c21747db12d1ecd1094bba0fc9252afaf79a700063f324757 │ ├── 5ba778581d38aa49351ab40941ba3ceddcd7f504c8b5475b72f5765bd4200a26 │ ├── 5c3986cf8b8c955221f4bd9c2ef5c7d6944417cc7185931088c29dc0ff3d7f74 │ ├── 5c98039e813b59ea8829b2704b4aaa4da0af0b2e253eb1e63a1501ecc47a250d │ ├── 5dd49fce20e25ee8dafea20ac051a7112106445ed471658318f2e1a434420d4d │ ├── 5fd59565fadbe6394dfb40fa498cee8dbd296b93f35afb8aa05b4662f9dafcec │ ├── 63b55211570099e0789f51a9a6ef085f2eec0dc6c44d2cc0a59bf1f89e8d6910 │ ├── 65da57c45d52a1eead05c858687c4fabf81fa118ac997380ae45a17facc499a0 │ ├── 6a6f4ff6442c50b20bad2fd73d65c6268d32b38220568a447321dc5b18fc8b72 │ ├── 6d4cf6ea06bdc7a7270bd1844ee0f173ebdf2ae3cff58783dc625d637ae94dc8 │ ├── 74c1a28dbdd194fe95acc40f8f71743a654343316d68db5fb3731cd19e9a431e │ ├── 75547d38a729f10580c16081c8855de4bb40d02666cd32f6a8972aa434235fb9 │ ├── 75e34c34723ad651fb641be58721bd354b8f339bb96d93c1f6d1c83a40b66d22 │ ├── 76e7c59a9880ea21deb5ad5eba78ff924f64aef01662796aa06a410004b24f96 │ ├── 7ca193e39cc8b1505a024ac4e8066164caafb9f297feeda833acf46d6cd38cc7 │ ├── 7e5ab06171c96080e8ce8560a58ad2e7a7a8fc95b3093e33220087b44bb06d60 │ ├── 7f323cf5ae6f8ce98ed071a3096e7481e5934b370ba785ed89d85727052e9572 │ ├── 898662e651ebd17cdfc26aa3c9af2c7c82c64a4c02228aada48d7a311eb0c122 │ ├── 89f91219996603ff2b9e530420baf591471dfba5915fef2840904617694c8b7f │ ├── 8bc618662072aeae643e2dd72119bfb83d9fdce27d35c11fd01aef3d45edba5c │ ├── 8d5e9df72d499faba74ed29989220f02d10db6604b5e1c853b0f65f1850b0407 │ ├── 91d1e6550aafe12c21e5dbec04239b1fc4fbb7f4acd61ff85ba13c1051148a4a │ ├── 94284ea094b150e600e4db9bdbee28e810082351a8dde440f44e2bbb2e269c44 │ ├── 954dbfd278fe22fdbb9e6048f89724601e465e2c69c528d6dba08b37424c959b │ ├── 96cfc406025ecd0e0859f7f3fec70cfae392e44e4d54d4f5c6dd7960b0b10095 │ ├── 97e19da6eeec8344f9f067f07e267a4288450f41f24d3d1916b9a8f884a5f1cc │ ├── 9da0e86bbe54faf436b18e1aa9fdf9885fc323a88610b77dd14b777699e8a389 │ ├── 9df508fb547ee94990dfa38b1d5afccde71bce59fb1f78db59b3913760d034d3 │ ├── 9e1e4d37834dc12c287fbfe81a12da3dd6a4230bb730950b01730c9b5561835c │ ├── 9efad0ef561e1f8445b553c6e6f8e8769654c6eb7b779198d0b3a126df269a03 │ ├── 9f2550b71171d99736cc3d6481529d6571ba4f36538a9a0cc2294f1a309753da │ ├── a0d4f649dc2841a656f6229cdc57af39b7a7c8493fc2739ad3d58f80f9aa1051 │ ├── a335a3713346e2cfe934550999147f795309342d06839f4c2b4f3eef44359a66 │ ├── a530f622b739baa9eece084c0287e60678bb56f3b8d7131b0636987af9ba9982 │ ├── ab8362fd06c3e41c414e4e5057268b9f4418746ebfcd857f1770bf7ec1973d8d │ ├── b14353790e336eb95ea5ce21b2cdc0d100b921f7c64a85b5ee74e082737f7d05 │ ├── b1e98c05abd019630dd9cfcea654df9f8dd625aea7685feb8703c5ab45ec454c │ ├── b269f35a0295bf2fc16110147372f0dfc1a00a8a3a0445a51067ab37d3173f3e │ ├── b4822134ab18acca34bf060fb285423bc882fc6844619e2b37784462e77dc7ce │ ├── b7565e3856127cbf5ac65406f43036b7d27398aa2cc7c74ee5c29c4b00dd3861 │ ├── b8db48d5ae45cb1acec4253ce05b9ec44aac6852a867fb4510efa4593e934c37 │ ├── bb03e50999258768f938b5eec267b6811acdfa72979db35cffdf358acd255300 │ ├── bd79acb788ac6d429eac632ce9ee4cfc540d38db69134f4353df3e6da7e28b90 │ ├── bd8fa3a1b728c133437cc0c7e0ac2161667d326630775e547e05922ba8bde745 │ ├── c19a35ad2661c58a272bdb5d0f78eab17fe75c8d55f21be7b2648209cefd9292 │ ├── c26b427022fde68252cdab75ba2262d0ef439073af2c9e8a9e8878fdd722891d │ ├── c42a6ed1cfe63440384872837c2619b1022f23382e67c4d0be8389c42c49ee5c │ ├── c693a49540fdf86104d0cea6d233b6e689d8c2e99529b2a819365c623cf551ad │ ├── c7ff254f38c54ecfc0460ba54ea5ac569740d9f88f157f96be9bbc75a7e1dd43 │ ├── cd7b24a702aaf5e260133896e7b2777746bf83a477bfc7afc56d079f9b8204f5 │ ├── cda2efd6e84d746454fcc08d558ccf0349e84e0ca6d206a84b0d722f805f8088 │ ├── cdc28d5a38ad2cd008e01f315a0dde695057f9565271f5afacacc0efebc770d7 │ ├── cee312137b5a516672e54de590ac768d4b0b0c7338028c72b32dcbc2bcb24add │ ├── d544603bb2a9e717b1164080237e2965ce53bbf9783c65fece77895004b895b6 │ ├── d5bc81d09b5367e75bbf157ccc4edcfd75712f9add2ef61a3181b602be44a10a │ ├── da34fec696da99892a9dbd2910c2aa4bf39de13fd53e2b669e19195a3664b468 │ ├── e2c15b01e8736f14d20f4f187d6855bba8f09bb8092cfee74719ea7eea273723 │ ├── ea8d5b29d3521138cd9eefc5357f19a237a30bfcb6d93c6daf9604fc9e0ba9ad │ ├── ecbb72dbba362eb83644f9b6754295e25b161a994240dce923888a99c2718606 │ ├── ed66cf25e2fbba5595506d9f9eb10b52db7e469fd13b79752c0ce065678ee33e │ ├── ed8a31158a000b41735b079e1a75f5f0022ca6737a01313ff05974237b54b13e │ ├── f14b73cec71a82fe0f31aa2d8bba8e422e3a50011b1b90e8728f5e632d020869 │ ├── f24ed01d674c77fb302289a36c99cac160007a4116e034418f1fb2beaad6a495 │ ├── f5b84280fe0914746d26e57e7592ea4f516be3f3995f01ee9d9d73b647e721c5 │ ├── f689d0763a7d0c1818e781c5e61f505311f30e87b6d622ed7e073e81f346e146 │ ├── f707c0955a69c7b7d8ece7c64b259c25d882d97671f3f24941a29092aa44453c │ ├── fa75d727d82b502d666f51b3c36f680656997df68b3b1da161ad7bae108c892c │ ├── fb67e232491aa926a94199b3fddf479c8f0953c4354546dea4056b146a46d143 │ ├── fde23201de1905da6f20b4253e364a4b055e0ee33d8fdca5b322efe92404c52f │ └── febe8f53c32ecc0ac28ca03538f119fefee32327d7f1410c7edb472b8d2633ad ├── test_absolute_number_ambiguity.py ├── test_anime_filenames.py ├── test_configfunctional.py ├── test_custom_replacement.py ├── test_datestamp_episode.py ├── test_extension_pattern.py ├── test_filename_blacklist.py ├── test_fileparse_api.py ├── test_files.py ├── test_force_series.py ├── test_functional.py ├── test_invalid_files.py ├── test_limit_by_extension.py ├── test_movingfiles.py ├── test_multiepisode_filenames.py ├── test_name_generation.py ├── test_no_series_in_filename.py ├── test_override_seriesname.py ├── test_parsing.py ├── test_safefilename.py ├── test_series_replacement.py └── test_system.py └── tvnamer ├── __init__.py ├── __main__.py ├── _titlecase.py ├── cliarg_parser.py ├── config.py ├── config_defaults.py ├── data.py ├── files.py ├── main.py ├── test_cache.py ├── tvnamer_exceptions.py └── utils.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch=True 3 | parallel=True 4 | source=tvnamer 5 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | build: 9 | 10 | strategy: 11 | matrix: 12 | python-version: ['3.5', '3.6', '3.7', '3.8', '3.9'] 13 | os: 14 | - ubuntu-latest 15 | - macOS-latest 16 | # - windows-latest 17 | 18 | name: Test on ${{ matrix.python-version }} and ${{ matrix.os }} 19 | runs-on: ${{ matrix.os }} 20 | 21 | steps: 22 | - uses: actions/checkout@v1 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v2 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dev deps 28 | run: | 29 | pip install codecov 30 | pip install -r requirements-dev.txt 31 | python setup.py develop 32 | - name: Run tests 33 | run: python -m pytest 34 | - name: Upload test results 35 | run: codecov 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | tvnamer.egg-info/* 4 | htmlcov/* 5 | .pytest_cache/* 6 | .coverage 7 | .coverage.* 8 | /.mypy_cache 9 | /.eggs 10 | /dist 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 3.5 4 | - 3.6 5 | - 3.7 6 | - 3.8 7 | 8 | install: 9 | - pip install codecov 10 | - pip install -r requirements-dev.txt 11 | - python setup.py develop 12 | 13 | script: 14 | - python -m pytest 15 | 16 | after_success: 17 | - codecov 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | # `4.0` - unreleased 4 | - Dropped support for EOL Python 2.7. 5 | - Fix `TypeError: '<' not supported between instances of 'int' and 'str'` error when using series replacements with show-ID 6 | ([Issue #150](https://github.com/dbr/tvnamer/issues/150)) 7 | - No longer supported filename patterns. These are ambigious and cause incorrect matches with things like `H.264` in filenames 8 | - `show.123.avi` (previously parsed as episode 23 of season 1) 9 | - `show.0123.avi` 10 | ([Issue #140](https://github.com/dbr/tvnamer/issues/140)) 11 | 12 | # `3.0.4` - 2021-05-23 13 | - Fix typo causing series name to not be corrected 14 | ([PR #203](https://github.com/dbr/tvnamer/pull/203)) 15 | 16 | # `3.0.3` - 2021-04-29 17 | - Fix compat with tvdb_api 3.1 - use 'seriesName' instead of long-deprecated 'seriesname' (as per TheTVDB API change in API v2) 18 | ([Issue #201](https://github.com/dbr/tvnamer/issues/201)) 19 | 20 | # `3.0.2` - 2020-11-09 21 | - Backport: Fix `TypeError: '<' not supported between instances of 'int' and 'str'` error when using series replacements with show-ID 22 | ([Issue #150](https://github.com/dbr/tvnamer/issues/150)) 23 | 24 | # `3.0.1` - 2020-09-19 25 | - Backport fix for `tvdb_api_key` config option 26 | ([Issue #187](https://github.com/dbr/tvnamer/issues/187)) 27 | 28 | # `3.0` - 2020-08-03 29 | - Fix `--dry-run` in `--batch` mode 30 | ([PR #173](https://github.com/dbr/tvnamer/pull/173)) 31 | - Config has moved to `~/.config/tvnamer/tvnamer.json` to avoid home-directory clutter (previously located at `~/.tvnamer.json`) 32 | ([PR #175](https://github.com/dbr/tvnamer/pull/175)) 33 | - Files are now moved using `shutil.move` instead of custom logic around `os.rename`, which should make things more robust in situations with unusual partition setups (e.g Docker environments) 34 | ([PR #161](https://github.com/dbr/tvnamer/pull/161)) 35 | - Add command line argument to override language, e.g `tvnamer --lang de [...]` 36 | ([PR #165](https://github.com/dbr/tvnamer/pull/165)) 37 | - Add `tvnamer --version` to display useful debug info 38 | - Can now be run via `python -m tvnamer` as well as the usual `tvnamer` command 39 | - New TheTVDB API key specifically for tvnamer. Users can use their own API key by via the `tvdb_api_key` config option (keys can easily be registered at ) 40 | - Various internal improvements to testing and compatability with later Python 3.x changes 41 | - Dropping explicit support for EOL Python 3.3 and 3.4 (may still work but not tested) 42 | - This is the last major version which will support Python 2.7 43 | 44 | # `2.5` - 2018-08-25 45 | - Began keeping a changelog 46 | - Added `--force-rename` and `--force-move` arguments 47 | ([PR #133](https://github.com/dbr/tvnamer/pull/133)) 48 | - Added `skip_behavior` option 49 | ([PR #111](https://github.com/dbr/tvnamer/pull/111)) 50 | - Added `--dry-run` argument 51 | ([PR #130](https://github.com/dbr/tvnamer/pull/130)) 52 | - Fix `normalize_unicode_filenames` in Python 3 53 | ([Issue #134](https://github.com/dbr/tvnamer/issues/134)) 54 | - Dropped support for Python 2.6. `tvnamer==2.4` is last version to 55 | support Python 2.6 56 | - Added support for Python 3.6 and 3.7 57 | - Fix search by air-date when episode had special episodes aired on same day 58 | ([PR #97](https://github.com/dbr/tvnamer/pull/97)) 59 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Pull requests are welcomed. It is worthwhile opening an issue first to discuss any larger changes to make sure they fit with the project. 4 | 5 | 6 | ## Installing development version 7 | 8 | If you wish to install the latest (non-stable) development version from source, download the latest version of the code, either from or by running: 9 | 10 | git clone git://github.com/dbr/tvnamer.git 11 | 12 | ..then `cd` into the directory, and run: 13 | 14 | python setup.py install 15 | 16 | You may wish to do this via virtualenv to avoid cluttering your global install 17 | 18 | Example terminal session: 19 | 20 | $ virtualenv tvn-env 21 | [...] 22 | Installing setuptools, pip, wheel...done. 23 | $ source tvn-env/bin/activate 24 | (tvn-env) $ git clone git://github.com/dbr/tvnamer.git 25 | Cloning into 'tvnamer'... 26 | [...] 27 | (tvn-env) $ cd tvnamer/ 28 | (tvn-env) $ python setup.py develop 29 | [...] 30 | (tvn-env) $ tvnamer --help 31 | [...] 32 | (tvn-env) $ deactivate 33 | $ tvnamer 34 | -bash: tvnamer: command not found 35 | 36 | 37 | ## Development setup 38 | 39 | First set up a virtual environment (e.g using `mkvirtualenv tvn` then `workon tvn` if you are using [`virtualenvwrapper`](https://pypi.org/project/virtualenvwrapper/)) 40 | 41 | In this env, install the development tools: 42 | 43 | pip install -r requirements-dev.txt 44 | 45 | This installs things like pytest and the coverage module. 46 | 47 | Then to execute the test-suite just run 48 | 49 | pytest 50 | 51 | This outputs the results of the test-run and a summary of test coverage. To generate the full coverage report run: 52 | 53 | coverage html 54 | 55 | Then look at `htmlcov/index.html` in a browser. 56 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include UNLICENSE 2 | include readme.md 3 | include tests/*.py 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `tvnamer` 2 | 3 | [![PyPI][pypi-img]][pypi-link] [![Build Status][build-img]][build-link] [![codecov][coverage-img]][coverage-link] 4 | 5 | `tvnamer` is a utility to rename files from `some.show.s01e03.blah.abc.avi` to `Some Show - [01x03] - The Episode Name.avi` (by retrieving the episode name using data from [`tvdb_api`](http://github.com/dbr/tvdb_api)) 6 | 7 | It supports Python 3.5 onwards. The last version of tvnamer to support Python 2.7 was `tvnamer==3` 8 | 9 | TV information is provided by TheTVDB.com, but we are not endorsed or certified by TheTVDB.com or its affiliates. 10 | 11 | [build-link]: https://travis-ci.org/dbr/tvnamer 12 | [build-img]: https://github.com/dbr/tvnamer/workflows/CI/badge.svg 13 | [pypi-link]: https://pypi.org/project/tvnamer/ 14 | [pypi-img]: https://img.shields.io/pypi/v/tvnamer 15 | [coverage-link]: https://codecov.io/gh/dbr/tvnamer 16 | [coverage-img]: https://codecov.io/gh/dbr/tvnamer/branch/master/graph/badge.svg 17 | 18 | ## Installing 19 | 20 | The "official" way to install `tvnamer` is via `pip`: 21 | 22 | pip install tvnamer 23 | 24 | This installs the `tvnamer` command-line tool and the requirements from [the `tvnamer` package on PyPI](https://pypi.python.org/pypi/tvnamer/) 25 | 26 | Alternatively, the community have packaged tvnamer in various platform/distro specific package managers, including: 27 | 28 | 1. [Homebrew for OS X](https://formulae.brew.sh/formula/tvnamer) - `brew install tvnamer` 29 | 2. [Debian](https://tracker.debian.org/pkg/tvnamer) - `apt-get install tvnamer` 30 | 3. [FreeBSD/DragonFlyBSD/MacPorts](https://www.freshports.org/multimedia/py-tvnamer) - `pkg install py36-tvnamer` etc 31 | 4. [Nix package manager](https://github.com/NixOS/nixpkgs/blob/master/pkgs/development/python-modules/tvnamer/default.nix) - `nix-env -iA nixpkgs.python37Packages.tvnamer` 32 | 33 | Although not recommended for general use, see [`CONTRIBUTING.md`](./CONTRIBUTING.md) for details on installing the unstable development version. 34 | 35 | ## Features 36 | 37 | - Interactive command line based interface, with a non-interactive "batch" mode for automation. 38 | - Comprehensive set of file-name matching patterns 39 | - Handles non-ASCII names 40 | - Support for anime filenames, such as `[Shinsen-Subs] Beet - 19 [24DAB497].mkv` 41 | - Support for multi-episode files, such as `scrubs.s01e23e24.avi` 42 | - Extensive configuration options (via a JSON config file) and command-line arguments, including output name customising, series name replacements 43 | - Support for moving files to specific location after renaming (`/media/tv/{series name}/season {seasonnumber}/` for example) 44 | 45 | ## Bugs? 46 | 47 | Please file issues on tvnamer's [Github Issues page](http://github.com/dbr/tvnamer/issues) 48 | 49 | Please make tickets for any possible bugs or feature requests, or if you discover a filename format that tvnamer cannot parse (as long as a reasonably common format, and has enough information to be parsed!), or if you are struggling with the a custom configuration (please state your desired filename output, and what problems you are encountering) 50 | 51 | ## Basic usage 52 | 53 | From the command line, simply run: 54 | 55 | tvnamer the.file.s01e01.avi 56 | 57 | For example: 58 | 59 | $ tvnamer brass.eye.s01e01.avi 60 | #################### 61 | # Starting tvnamer 62 | # Found 1 episodes 63 | # Processing brass.eye.s01e01.avi 64 | TVDB Search Results: 65 | 1 -> Brass Eye [en] # http://thetvdb.com/?tab=series&id=70679&lid=7 66 | Automatically selecting only result 67 | #################### 68 | # Old filename: brass.eye.s01e01.avi 69 | # New filename: Brass Eye - [01x01] - Animals.avi 70 | Rename? 71 | ([y]/n/a/q) 72 | 73 | Enter `y` then press `return` and the file will be renamed to "Brass Eye - [01x01] - Animals.avi". You can also simply press `return` to select the default option, denoted by the surrounding `[]` 74 | 75 | If there are multiple shows with the same (or similar) names or languages, you will be asked to select the correct one - "Lost" is a good example of this: 76 | 77 | $ tvnamer lost.s01e01.avi 78 | #################### 79 | # Starting tvnamer 80 | # Found 1 episodes 81 | # Processing lost.s01e01.avi 82 | TVDB Search Results: 83 | 1 -> Lost [en] # http://thetvdb.com/?tab=series&id=73739&lid=7 84 | 2 -> Lost [sv] # http://thetvdb.com/?tab=series&id=73739&lid=8 85 | 3 -> Lost [no] # http://thetvdb.com/?tab=series&id=73739&lid=9 86 | 4 -> Lost [fi] # http://thetvdb.com/?tab=series&id=73739&lid=11 87 | 5 -> Lost [nl] # http://thetvdb.com/?tab=series&id=73739&lid=13 88 | 6 -> Lost [de] # http://thetvdb.com/?tab=series&id=73739&lid=14 89 | Enter choice (first number, ? for help): 90 | 91 | To select the first result, enter `1` then `return`, to select the second enter `2` and so on. The link after `#` goes to the relevant [thetvdb.com][tvdb] page, which will contain information and images to help you select the correct series. 92 | 93 | You can rename multiple files, or an entire directory by using the files or directories as arguments: 94 | 95 | $ tvnamer file1.avi file2.avi etc 96 | $ tvnamer . 97 | $ tvnamer /path/to/my/folder/ 98 | $ tvnamer ./folder/1/ ./folder/2/ 99 | 100 | You can skip a specific file by entering `n` (no). If you enter `a` (always) `tvnamer` will rename the remaining files automatically. The suggested use of this is check the first few episodes are named correctly, then use `a` to rename the rest. 101 | 102 | Note, tvnamer will only descend one level into directories unless the `-r` (or `--recursive`) flag is specified. For example, if you have the following directory structure: 103 | 104 | dir1/ 105 | file1.avi 106 | dir2/ 107 | file2.avi 108 | file3.avi 109 | 110 | ..then running `tvnamer dir1/` will only rename `file1.avi`, ignoring `dir2/` and its contents. 111 | 112 | If you wish to rename all files (file1, file2 and file3), you would run: 113 | 114 | tvnamer --recursive dir1/ 115 | 116 | ## Command line arguments 117 | 118 | There are various flags you can use with `tvnamer`, run.. 119 | 120 | tvnamer --help 121 | 122 | ..to see them, and a short description of each. 123 | 124 | The most useful are most likely `--batch`, `--selectfirst` and `--always`: 125 | 126 | `--selectfirst` will select the first series the search found, but will not automatically rename any episodes. 127 | 128 | `--always` will ask you select the correct series, then automatically rename all files. 129 | 130 | `--batch` will not prompt you for anything. It automatically selects the first series search result, and automatically rename all files (identical to using both `--selectfirst` and `--always`). Use carefully! 131 | 132 | `--series-id` will allow you to use a specific ID from theTVdb. This can help with name detection issues. 133 | 134 | ## Configs 135 | 136 | One of the largest improvements in tvnamer v2 is the ability to have custom configuration. This allows you to customise behaviour without modifying the code (as was necessary with tvnamer v1). 137 | 138 | To write the default JSON configuration file, which is a good starting point for your modifications, simply run: 139 | 140 | tvnamer --save=./mytvnamerconfig.json 141 | 142 | To use your custom configuration, you must either specify the location using `tvnamer --config=/path/to/mytvnamerconfig.json` or place the file at `~/.config/tvnamer/tvnamer.json` 143 | 144 | **Important:** If tvnamer's default settings change and your saved config contains the old settings, you may experience strange behaviour or bugs (the config may contain a buggy `filename_patterns` regex, for example). It is recommended you remove config options you are not altering (particularly `filename_patterns`). If you experience any strangeness, try disabling your custom configuration (moving it away from `~/.config/tvnamer/tvnamer.json`) 145 | 146 | If for example you wish to change the default language used to retrieve data, change the option `language` to another two-letter language code, such as `fr` for French. Your config file would look like: 147 | 148 | { 149 | "language": "fr" 150 | } 151 | 152 | If `search_all_languages` is true, tvnamer will return multilingual search results. If false, it will return results only in the preferred language. 153 | 154 | For an always up-to-date description of all config options, see the comments in [`config_defaults.py`](http://github.com/dbr/tvnamer/blob/master/tvnamer/config_defaults.py) 155 | 156 | # Custom output filenames 157 | 158 | If you wish to change the output filename format, there are a bunch of options you can change. 159 | 160 | The most common is an episode with both a season and episode number. There are two patterns, one for when an episode name is found, and one without the episode name: 161 | 162 | - One for a file with an episode name (`filename_with_episode`). Example input: `Scrubs.s01e01.my.ep.name.avi` 163 | - One for a file *without* an episode name (`filename_without_episode`). Example input: `AnUnknownShow.s01e01.avi` 164 | 165 | Next, for episodes without a season number: 166 | 167 | - One for a filename with no season number, and an episode name (`filename_with_episode_no_season`). Example input: `Sid.The.Science.Kid.E11.avi` 168 | - One for a filename with no season number, and no episode name (`filename_without_episode_no_season`). Example input: `AnUnknownShow.E24.avi` 169 | 170 | Date-based episodes (which used aired-date instead of episode numbers): 171 | 172 | - One for date-based episodes (`filename_with_date_and_episode`). Example input: `AnUnknownShow.2000-01-23` 173 | - Date-based episode without epiosde nam (`filename_with_date_without_episode`) 174 | 175 | Finally, anime episodes have the usual with/without episode names, and again with/without the CRC value: 176 | 177 | - `filename_anime_with_episode` - for example, `[SubGrp] SeriesName - 02 - Episode Name [CRC1234].ext` 178 | - `filename_anime_without_episode` - for example, `[SubGrp] SeriesName - 02 [CRC1234].ext` 179 | - `filename_anime_with_episode_without_crc` - for example, `[SubGrp] SeriesName - 02 - Episode Name.ext` 180 | - `filename_anime_without_episode_without_crc` - for example, `[SubGrp] SeriesName - 02.ext` 181 | 182 | This may seem like a lot, but they are mostly the same thing. They all have sensible default values, so you can only change the values you use commonly (say, you could ignore the date-based and anime episodes if you rarely rename such files) 183 | 184 | Say you want the format `Show Name 01x24 Episode Name.avi`, your `filename_with_episode` option would be: 185 | 186 | %(seriesname)s %(seasonno)02dx%(episode)s %(episodename)s%(ext)s 187 | 188 | The formatting language used is Python's string formatting feature, which you can read about in the Python documentation, [6.6.2. String Formatting Operations](http://docs.python.org/library/stdtypes.html#string-formatting). Basically it's just `%()s` and the name element you wish to use between `( )` 189 | 190 | Note `ext` contains the extension separator symbol, usually `.` - for example `.avi` 191 | 192 | Then you need to make a few variants, one without the `episodename` section, and two without the `seasonno` option: 193 | 194 | `filename_with_episode_no_season`: 195 | 196 | %(seriesname)s %(seasonno)02dx%(episode)s %(episodename)s%(ext)s 197 | 198 | `filename_without_episode`: 199 | 200 | %(seriesname)s %(seasonno)02dx%(episode)s%(ext)s 201 | 202 | `filename_without_episode_no_season`: 203 | 204 | %(seriesname)s %(episode)s%(ext)s 205 | 206 | There are yet two more options you may want to change, `episode_single` and `episode_separator` 207 | 208 | `episode_single` is the Python string formatting pattern used to format the episode number. By default it is `%02d` - this simply turns the number `1` to `01`, and keeps `24` as `24` 209 | 210 | If you do not want any padding in your numbers, you could change this to `%d` - this would result in filenames such as `Show - [1x3] - Episode Name.avi` (or `Show 1x3 Episode Name.avi` using your custom name, as described above) 211 | 212 | The `episode_separator` option is for multi-episode files. When multiple episodes are detected in one file (such as `Scrubs.s01e01e02.avi`), this string is used to join the episode numbers together. By default it is `-` which results in filenames such as `Scrubs - [01x01-02] - ... .avi` 213 | 214 | You could change this to `e`, and by altering the `filename_*` options you could create filenames such as.. 215 | 216 | Show - [s01e01e02] - Episode Name.avi 217 | 218 | By default, tvnamer will sanitise files for the current operating system - either POSIX-compatible (OS X, Linux, FreeBSD) or Windows. You can force Windows compatible filenames by setting the option `windows_safe_filenames` to True 219 | 220 | The preferred way to replace spaces with another character is to use the custom replacements feature. For example, to replace spaces with `.` you would use the config: 221 | 222 | { 223 | "output_filename_replacements": [ 224 | {"is_regex": true, 225 | "match": "[ ]", 226 | "replacement": "."} 227 | ] 228 | } 229 | 230 | 231 | You can also remove spaces in characters by adding a space to the option `custom_filename_character_blacklist` and changing the option `replace_blacklisted_characters_with` to `.` 232 | 233 | `normalize_unicode_filenames` attempts to replace Unicode characters with their unaccented ASCII equivalent (`å` becomes `a` etc). Any untranslatable characters are removed. 234 | 235 | `selectfirst` and `always_rename` mirror the command line arguments `--selectfirst` and `--always` - one automatically selects the first series search result, the other always renames files. Setting both to True is equivalent to `--batch`. `recursive` also mirrors the command line argument 236 | 237 | `lowercase_filename` converts the entire filename to lower case. 238 | 239 | 240 | This document does not describe all config options - for a complete list, see the comments in [`config_defaults.py`](http://github.com/dbr/tvnamer/blob/master/tvnamer/config_defaults.py) 241 | 242 | 243 | # Custom filename parsing pattern 244 | 245 | `tvnamer` comes with a set of patterns to parse a majority of common (and many uncommon) TV episode file names. If these don't parse your files, you can write custom patterns. 246 | 247 | The patterns are regular expressions, compiled with the [`re.VERBOSE` flag](http://docs.python.org/library/re.html#re.VERBOSE). Each pattern must contain several named groups. 248 | 249 | Named groups are like regular groups, but the group starts with `?P`. For example: 250 | 251 | (?P.+?) 252 | 253 | All patterns must contain a named group `seriesname` - this is of course the name of the show the filename contains. 254 | 255 | Optionally you can parse a season number using the group `seasonnumber`. If this group is not specified, it will search for the episode(s) in season 1 (following the [thetvdb.com][tvdb] convention) 256 | 257 | You must also match an episode number group. For simple, single episode files use the group `episodenumber` 258 | 259 | If you wish to match multiple episodes in one file, there two options: 260 | 261 | - `episodenumber1` `episodenumber2` etc - match any number of episode numbers (can be non-consecutive), or.. 262 | - Two groups, `episodenumberstart` and `episodenumberend` - you match the first and last numbers in the filename. If the start number is 2, and the end number is 5, the file contains episodes [2, 3, 4, 5]. 263 | 264 | # Regex flags in config files 265 | 266 | Regular expressions are used in several places in the config. It can 267 | be useful to specify flags the "ignore case" flag. This can be done 268 | with the `(?...)` syntax, e.g to replace `and`, `And`, `AND` etc with 269 | `&`: 270 | 271 | { 272 | "input_filename_replacements": [ 273 | {"is_regex": true, 274 | "match": "(?i)and", 275 | "replacement": "&"} 276 | ] 277 | } 278 | 279 | See the other flags 280 | [in the Python `re` docs](http://docs.python.org/2/library/re.html#regular-expression-syntax) 281 | (search for `(?iLmsux)` on the page) 282 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # `tvnamer` release procedure 2 | 3 | 1. Ensure CHANGELOG is up to date 4 | 2. Ensure tests are passing (CI and run again locally) 5 | 3. Verify settings in `setup.py` (e.g supported Python versions) 6 | 4. Bump version in `tvnamer/__init__.py` 7 | 5. Bump version/release date in CHANGELOG 8 | 6. Push changes to git 9 | 7. `python setup.py sdist upload` 10 | 8. Tag change, `git tag -a 0.0etc` 11 | 9. Push tag, `git push --tags` 12 | 10. Verify https://pypi.org/project/tvnamer/ 13 | 11. Verify via virtual env 14 | 15 | mkvirtualenv tvnamertest 16 | pip install tvdb_api 17 | touch scrubs.s01e01.avi 18 | tvnamer scrubs.s01e01.avi 19 | ls Scrubs* 20 | deactivate 21 | rmvirtualenv tvnamertest 22 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2011-2012 Ben Dickson (dbr) 2 | 3 | This is free and unencumbered software released into the public domain. 4 | 5 | Anyone is free to copy, modify, publish, use, compile, sell, or 6 | distribute this software, either in source code form or as a compiled 7 | binary, for any purpose, commercial or non-commercial, and by any 8 | means. 9 | 10 | In jurisdictions that recognize copyright laws, the author or authors 11 | of this software dedicate any and all copyright interest in the 12 | software to the public domain. We make this dedication for the benefit 13 | of the public at large and to the detriment of our heirs and 14 | successors. We intend this dedication to be an overt act of 15 | relinquishment in perpetuity of all present and future rights to this 16 | software under copyright law. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 22 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 23 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | 26 | For more information, please refer to 27 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --cov=tvnamer 3 | 4 | markers = 5 | functional: tests which spawn tvnamer in a new python 6 | 7 | env = 8 | TVNAMER_TEST_MODE=1 9 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest==5.4; python_version > '3.0' 2 | coverage==5 3 | pytest-cov==2.10 4 | pytest-env==0.6 5 | flake8==3.8 6 | pep8-naming==0.10 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Setup tools for tvnamer 2 | """ 3 | 4 | import os 5 | import sys 6 | 7 | # Ensure dir containing script is on PYTHONPATH 8 | sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) 9 | 10 | from tvnamer import __version__ 11 | 12 | 13 | from setuptools import setup 14 | setup( 15 | name = 'tvnamer', 16 | version=__version__, 17 | 18 | author='dbr/Ben', 19 | description='Automatic TV episode namer', 20 | url='http://github.com/dbr/tvnamer', 21 | license='unlicense', 22 | 23 | long_description="""\ 24 | Automatically names downloaded/recorded TV-episodes, by parsing filenames and 25 | retrieving show-names from www.thetvdb.com 26 | 27 | Now deals with files containing multiple: show.name.s01e01e02.avi, anime 28 | files: [SomeGroup] Show Name - 102 [A1B2C3].mkv and better handles files 29 | containing unicode characters. 30 | """, 31 | 32 | packages = ['tvnamer'], 33 | 34 | entry_points = { 35 | 'console_scripts': [ 36 | 'tvnamer = tvnamer.main:main', 37 | ], 38 | }, 39 | 40 | install_requires = ["tvdb_api>=3,<4"], 41 | 42 | classifiers=[ 43 | "Environment :: Console", 44 | "Intended Audience :: End Users/Desktop", 45 | # "License :: Unlicense", 46 | "Natural Language :: English", 47 | "Operating System :: OS Independent", 48 | "Programming Language :: Python", 49 | "Topic :: Multimedia", 50 | "Topic :: Utilities", 51 | "Programming Language :: Python", 52 | "Programming Language :: Python :: 3.5", 53 | "Programming Language :: Python :: 3.6", 54 | "Programming Language :: Python :: 3.7", 55 | "Programming Language :: Python :: 3.8", 56 | ], 57 | ) 58 | -------------------------------------------------------------------------------- /tests/functional_runner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Functional-test runner for use in other tests 4 | 5 | Useful functions are run_tvnamer and verify_out_data. 6 | 7 | Simple example test: 8 | 9 | out_data = run_tvnamer( 10 | with_files = ['scrubs.s01e01.avi'], 11 | with_config = None, 12 | with_input = "1\ny\n") 13 | 14 | expected_files = ['Scrubs - [01x01] - My First Day.avi'] 15 | 16 | verify_out_data(out_data, expected_files) 17 | 18 | This runs tvnamer with no custom config (can be a string). It then 19 | sends "1[return]y[return]" to the console UI, and verifies the file was 20 | created correctly, in a way that nosetest displays useful info when an 21 | expected file is not found. 22 | """ 23 | 24 | import os 25 | import sys 26 | import shutil 27 | import tempfile 28 | import subprocess 29 | import unicodedata 30 | 31 | import coverage 32 | 33 | try: 34 | # os.path.relpath was added in 2.6, use custom implimentation if not found 35 | relpath = os.path.relpath 36 | except AttributeError: 37 | 38 | def relpath(path, start=None): 39 | """Return a relative version of a path""" 40 | 41 | if start is None: 42 | start = os.getcwd() 43 | 44 | start_list = os.path.abspath(start).split(os.path.sep) 45 | path_list = os.path.abspath(path).split(os.path.sep) 46 | 47 | # Work out how much of the filepath is shared by start and path. 48 | i = len(os.path.commonprefix([start_list, path_list])) 49 | 50 | rel_list = [os.path.pardir] * (len(start_list)-i) + path_list[i:] 51 | if not rel_list: 52 | return os.getcwd() 53 | return os.path.join(*rel_list) 54 | 55 | 56 | def make_temp_config(config): 57 | """Creates a temporary file containing the supplied config (string) 58 | """ 59 | (fhandle, fname) = tempfile.mkstemp() 60 | f = open(fname, 'w+') 61 | f.write(config) 62 | f.close() 63 | 64 | return fname 65 | 66 | 67 | def make_temp_dir(): 68 | """Creates a temp folder and returns the path 69 | """ 70 | return tempfile.mkdtemp() 71 | 72 | 73 | def make_dummy_files(files, location): 74 | """Creates dummy files at location. 75 | """ 76 | dummies = [] 77 | for f in files: 78 | # Removing leading slash to prevent files being created outside 79 | # location. This is necessary because.. 80 | # os.path.join('tempdir', '/otherpath/example.avi) 81 | # ..will return '/otherpath/example.avi' 82 | if f.startswith("/"): 83 | f = f.replace("/", "", 1) 84 | 85 | floc = os.path.join(location, f) 86 | 87 | dirnames, _ = os.path.split(floc) 88 | os.makedirs(dirnames, exist_ok=True) 89 | 90 | open(floc, "w").close() 91 | dummies.append(floc) 92 | 93 | return dummies 94 | 95 | 96 | def clear_temp_dir(location): 97 | """Removes file or directory at specified location 98 | """ 99 | print("Clearing %s" % location) 100 | shutil.rmtree(location) 101 | 102 | 103 | def run_tvnamer(with_files, with_flags = None, with_input = "", with_config = None, run_on_directory = False, with_coverage = True): 104 | """Runs tvnamer on list of file-names in with_files. 105 | with_files is a list of strings. 106 | with_flags is a list of command line arguments to pass to tvnamer. 107 | with_input is the sent to tvnamer's stdin 108 | with_config is a string containing the tvnamer to run tvnamer with. 109 | 110 | Returns a dict with stdout, stderr and a list of files created 111 | """ 112 | # Create dummy files (config and episodes) 113 | episodes_location = make_temp_dir() 114 | dummy_files = make_dummy_files(with_files, episodes_location) 115 | 116 | if with_config is not None: 117 | configfname = make_temp_config(with_config) 118 | conf_args = ['-c', configfname] 119 | else: 120 | conf_args = [] 121 | 122 | if with_flags is None: 123 | with_flags = [] 124 | 125 | if run_on_directory: 126 | files = [episodes_location] 127 | else: 128 | files = dummy_files 129 | 130 | # Copy sys.path to PYTHONPATH so same modules are available as in 131 | # test environmen 132 | env = os.environ.copy() 133 | env['PYTHONPATH'] = ":".join(sys.path) 134 | if with_coverage: 135 | # Get path to .coveragerc in parent dir 136 | covconf = os.path.abspath( 137 | os.path.join( 138 | os.path.dirname(os.path.abspath(__file__)), 139 | "..", 140 | ".coveragerc")) 141 | # Set env vars to flag to tvnamer/__init__.py to configure coverage.py 142 | env['COVERAGE_PROCESS_START'] = covconf 143 | env['TVNAMER_COVERAGE_SUBPROCESS'] = '' 144 | 145 | # Construct command 146 | cmd = [sys.executable, "-m", "tvnamer"] + conf_args + with_flags + files 147 | 148 | print("Running command:") 149 | print(" ".join(cmd)) 150 | 151 | proc = subprocess.Popen( 152 | cmd, 153 | stdout = subprocess.PIPE, 154 | stderr = subprocess.STDOUT, # All stderr to stdout 155 | stdin = subprocess.PIPE, 156 | env=env) 157 | 158 | proc.stdin.write(with_input.encode("utf-8")) 159 | output, _ = proc.communicate() 160 | 161 | output = output.decode("utf-8") 162 | 163 | created_files = [] 164 | 165 | for walkroot, walkdirs, walkfiles in os.walk(episodes_location): 166 | curlist = [ 167 | os.path.join( 168 | walkroot, 169 | unicodedata.normalize('NFKD', name)) # Make unicode stuff consistent 170 | for name in walkfiles] 171 | 172 | # Remove episodes_location from start of path 173 | curlist = [relpath(x, episodes_location) for x in curlist] 174 | 175 | created_files.extend(curlist) 176 | 177 | # Clean up dummy files and config 178 | clear_temp_dir(episodes_location) 179 | if with_config is not None: 180 | os.unlink(configfname) 181 | 182 | return { 183 | 'output': output, 184 | 'files': created_files, 185 | 'returncode': proc.returncode} 186 | 187 | 188 | def verify_out_data(out_data, expected_files, expected_returncode = 0): 189 | """Verifies the out_data from run_tvnamer contains the expected files. 190 | 191 | Prints the stdout/stderr/files, then asserts all files exist. 192 | If an assertion fails, nosetest will handily print the stdout/etc. 193 | """ 194 | 195 | print("Return code: %d" % out_data['returncode']) 196 | 197 | print("Expected files:", expected_files) 198 | print("Got files: ", [x for x in out_data['files']]) 199 | 200 | print("\n" + "*" * 20 + "\n") 201 | print("output:\n") 202 | print(out_data['output']) 203 | 204 | # Check number of files 205 | if len(expected_files) != len(out_data['files']): 206 | raise AssertionError("Expected %d files, but got %d" % ( 207 | len(expected_files), 208 | len(out_data['files']))) 209 | 210 | # Check all files were created 211 | for cur in expected_files: 212 | if cur not in out_data['files']: 213 | raise AssertionError("File named %r not created" % (cur)) 214 | 215 | # Check exit code is zero 216 | if out_data['returncode'] != expected_returncode: 217 | raise AssertionError("Exit code was %d, not %d" % (out_data['returncode'], expected_returncode)) 218 | -------------------------------------------------------------------------------- /tests/helpers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Helper functions for use in tests 4 | """ 5 | 6 | import os 7 | import functools 8 | 9 | 10 | def assertEquals(a, b): 11 | assert a == b, "Error, %r not equal to %r" % (a, b) 12 | 13 | 14 | def assertType(obj, type): 15 | assert isinstance(obj, type), "Expecting %s, got %r" % ( 16 | type(obj), 17 | type) 18 | 19 | 20 | def expected_failure(test): 21 | """Used as a decorator on a test function. Skips the test if it 22 | fails, or fails the test if it passes (so the decorator can be 23 | removed) 24 | 25 | Kind of like the SkipTest nose plugin, but avoids tests being 26 | skipped quietly if they are fixed "accidentally" 27 | 28 | http://stackoverflow.com/q/9613932/745 29 | """ 30 | 31 | @functools.wraps(test) 32 | def inner(*args, **kwargs): 33 | try: 34 | test(*args, **kwargs) 35 | except AssertionError: 36 | from nose.plugins.skip import SkipTest 37 | raise SkipTest("Expected failure failed, as expected") 38 | else: 39 | raise AssertionError('Failure expected') 40 | 41 | return inner 42 | 43 | 44 | def attr(name): 45 | import pytest 46 | return getattr(pytest.mark, name) 47 | -------------------------------------------------------------------------------- /tests/httpcache/0365d0e514a3260eff6fa56d2f4e3d221d8fb71f20e171d05c8489e771dd02e7: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/0365d0e514a3260eff6fa56d2f4e3d221d8fb71f20e171d05c8489e771dd02e7 -------------------------------------------------------------------------------- /tests/httpcache/0774b76b4bbe942bb29c886072ea3ee15ca5fe59d06fd0ebeef80464d95746aa: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/0774b76b4bbe942bb29c886072ea3ee15ca5fe59d06fd0ebeef80464d95746aa -------------------------------------------------------------------------------- /tests/httpcache/0939fc03d203db3aef82c95ac8537bfd54ed1eda8cb350a9f1fe859f6cd72fb9: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/0939fc03d203db3aef82c95ac8537bfd54ed1eda8cb350a9f1fe859f6cd72fb9 -------------------------------------------------------------------------------- /tests/httpcache/0b1ec7489ea5354497a15d3eee130cb37e8817872e634ba36c4a96d4ba10745a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/0b1ec7489ea5354497a15d3eee130cb37e8817872e634ba36c4a96d4ba10745a -------------------------------------------------------------------------------- /tests/httpcache/17be4ec7fbbfa90e3aba86ba7cfcfe981dd450cf464c14d5363d9b446b890a07: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/17be4ec7fbbfa90e3aba86ba7cfcfe981dd450cf464c14d5363d9b446b890a07 -------------------------------------------------------------------------------- /tests/httpcache/19b1ca273ef7294dc13469f41e29683e8a4aabe96913c7269a711bafb31f72f1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/19b1ca273ef7294dc13469f41e29683e8a4aabe96913c7269a711bafb31f72f1 -------------------------------------------------------------------------------- /tests/httpcache/19c631c68ee9147bacb68c112e4ba7812607d80398dbfee2963ebd5fd30cf64b: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/19c631c68ee9147bacb68c112e4ba7812607d80398dbfee2963ebd5fd30cf64b -------------------------------------------------------------------------------- /tests/httpcache/1a8316738ed28a9bbe32d269d9f6366a9633aa0516042b51eef62462f49aaf5f: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/1a8316738ed28a9bbe32d269d9f6366a9633aa0516042b51eef62462f49aaf5f -------------------------------------------------------------------------------- /tests/httpcache/1c092e14e75ff046b2cb88006eb72d76452aec03c9dddfc59f0da44c19961bca: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/1c092e14e75ff046b2cb88006eb72d76452aec03c9dddfc59f0da44c19961bca -------------------------------------------------------------------------------- /tests/httpcache/24c6372c6dc3e761aa752fae971b417e4349bb6650a1603872bb5f301a729957: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/24c6372c6dc3e761aa752fae971b417e4349bb6650a1603872bb5f301a729957 -------------------------------------------------------------------------------- /tests/httpcache/26443651ad88ed46defce0f3a7f3a17ca4d5eeca0301786ec6313a8a8c3b2e00: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/26443651ad88ed46defce0f3a7f3a17ca4d5eeca0301786ec6313a8a8c3b2e00 -------------------------------------------------------------------------------- /tests/httpcache/26567eefbd6faf9e898765f5bad6268706094ca25746142f790af73d33f4d0de: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/26567eefbd6faf9e898765f5bad6268706094ca25746142f790af73d33f4d0de -------------------------------------------------------------------------------- /tests/httpcache/26b722ac2c2384fccaa8ac50269b0cafb89dcc0e7c41ff651378fe6ae32df2c2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/26b722ac2c2384fccaa8ac50269b0cafb89dcc0e7c41ff651378fe6ae32df2c2 -------------------------------------------------------------------------------- /tests/httpcache/26f6319dd5397394e111735ef3a27a45008cf64f871d74ece2d03fa64d44d143: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/26f6319dd5397394e111735ef3a27a45008cf64f871d74ece2d03fa64d44d143 -------------------------------------------------------------------------------- /tests/httpcache/28d0cde4eca2d207513f8bdcf65ec5e5e471be176b75567993b3b1ced41ec7a9: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/28d0cde4eca2d207513f8bdcf65ec5e5e471be176b75567993b3b1ced41ec7a9 -------------------------------------------------------------------------------- /tests/httpcache/2956bb3d537d72413f15792f69967c1aeb0d22f93eac0ee2c3e56737648346fe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/2956bb3d537d72413f15792f69967c1aeb0d22f93eac0ee2c3e56737648346fe -------------------------------------------------------------------------------- /tests/httpcache/29adb55c9884ee5222c9ecf51edf0df83b5e4a97c1341c23c3b5ad0662770772: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/29adb55c9884ee5222c9ecf51edf0df83b5e4a97c1341c23c3b5ad0662770772 -------------------------------------------------------------------------------- /tests/httpcache/2b54cddf424144c1a5a635d80ddf36519dadf03fbc84a9717604247012695685: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/2b54cddf424144c1a5a635d80ddf36519dadf03fbc84a9717604247012695685 -------------------------------------------------------------------------------- /tests/httpcache/2b6f8593c212b626714e1d155cb0352d188b07165d30f747c1bbc7e5a94e6c4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/2b6f8593c212b626714e1d155cb0352d188b07165d30f747c1bbc7e5a94e6c4a -------------------------------------------------------------------------------- /tests/httpcache/2d7fb223303105a50e59776842ffe95cc44187627e2c55c266823e48540b2451: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/2d7fb223303105a50e59776842ffe95cc44187627e2c55c266823e48540b2451 -------------------------------------------------------------------------------- /tests/httpcache/2fea195f905dd40e625db94bdb7a34b0238360e1d80785e5359050a335c26669: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/2fea195f905dd40e625db94bdb7a34b0238360e1d80785e5359050a335c26669 -------------------------------------------------------------------------------- /tests/httpcache/374beefab949ff34a1d1392225cf7084b73fdde44fcbcc4d81d322c573575443: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/374beefab949ff34a1d1392225cf7084b73fdde44fcbcc4d81d322c573575443 -------------------------------------------------------------------------------- /tests/httpcache/376f777caac26b557bfc81adf35b0e42d9df70fc98aae5bdbcba0b525cac5c52: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/376f777caac26b557bfc81adf35b0e42d9df70fc98aae5bdbcba0b525cac5c52 -------------------------------------------------------------------------------- /tests/httpcache/381bc0c47cf105a4f65c8ec733db9760bb853f1c3cf38bbba58a593bfad6d81b: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/381bc0c47cf105a4f65c8ec733db9760bb853f1c3cf38bbba58a593bfad6d81b -------------------------------------------------------------------------------- /tests/httpcache/3ae5d9999bc32bc133d6cb342bd00c64ed921e0dd116fb847ac742a2855b674b: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/3ae5d9999bc32bc133d6cb342bd00c64ed921e0dd116fb847ac742a2855b674b -------------------------------------------------------------------------------- /tests/httpcache/3bcd3884c4ebfe4084e802157801a063bd3122fe17623d35b6f80aa4da8358c5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/3bcd3884c4ebfe4084e802157801a063bd3122fe17623d35b6f80aa4da8358c5 -------------------------------------------------------------------------------- /tests/httpcache/3f1cd0c8ed94701d658e4af8dbb795665bf21d4a5a9f08faae44629f4a7fca7d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/3f1cd0c8ed94701d658e4af8dbb795665bf21d4a5a9f08faae44629f4a7fca7d -------------------------------------------------------------------------------- /tests/httpcache/409edd232e2c12ee61953fe3161938a21046926e27e5787065e1c24fc74817d6: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/409edd232e2c12ee61953fe3161938a21046926e27e5787065e1c24fc74817d6 -------------------------------------------------------------------------------- /tests/httpcache/418224939ef1716ccee804d12f1a3b731304bce684a9ac331a809617afa4a1b7: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/418224939ef1716ccee804d12f1a3b731304bce684a9ac331a809617afa4a1b7 -------------------------------------------------------------------------------- /tests/httpcache/42ae05b54a74957f496dd1c72bf90ceb1b956517695f9ffb35eafcfcd16c9b15: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/42ae05b54a74957f496dd1c72bf90ceb1b956517695f9ffb35eafcfcd16c9b15 -------------------------------------------------------------------------------- /tests/httpcache/46de8d8920e6cc49a590764a902065548080bb34e6025454f36a433a690c7bb2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/46de8d8920e6cc49a590764a902065548080bb34e6025454f36a433a690c7bb2 -------------------------------------------------------------------------------- /tests/httpcache/4d001018f1a474826c8c29b96608689bba4b72e8ff6a3584bcf30cba5e42b524: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/4d001018f1a474826c8c29b96608689bba4b72e8ff6a3584bcf30cba5e42b524 -------------------------------------------------------------------------------- /tests/httpcache/4d2c98cc0c7eadc98a0bb5745c1dd501164714ca896eccc89f57b77f9ec24e19: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/4d2c98cc0c7eadc98a0bb5745c1dd501164714ca896eccc89f57b77f9ec24e19 -------------------------------------------------------------------------------- /tests/httpcache/4d8f37567cca22a9b6c5869b4794083ffafb001b77461a21fb00b1eb5e5c7e68: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/4d8f37567cca22a9b6c5869b4794083ffafb001b77461a21fb00b1eb5e5c7e68 -------------------------------------------------------------------------------- /tests/httpcache/4e1f77bb0f359b6546a9f794618cd165e1eb1b438d7359d9d9e3bbdeb7f86690: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/4e1f77bb0f359b6546a9f794618cd165e1eb1b438d7359d9d9e3bbdeb7f86690 -------------------------------------------------------------------------------- /tests/httpcache/4f43f35247445e44cc2cd623b595da442ce9696a0cd6e40e1e857337935078a0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/4f43f35247445e44cc2cd623b595da442ce9696a0cd6e40e1e857337935078a0 -------------------------------------------------------------------------------- /tests/httpcache/536480a297c5e739d080360543e077a2acda2a0d507d7f43df673ed3a4fcdf4e: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/536480a297c5e739d080360543e077a2acda2a0d507d7f43df673ed3a4fcdf4e -------------------------------------------------------------------------------- /tests/httpcache/55474e9f790f2ea7275aad21543941a4d919e33a45e7188851921ef16bd01ec5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/55474e9f790f2ea7275aad21543941a4d919e33a45e7188851921ef16bd01ec5 -------------------------------------------------------------------------------- /tests/httpcache/558436374ae0973c21747db12d1ecd1094bba0fc9252afaf79a700063f324757: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/558436374ae0973c21747db12d1ecd1094bba0fc9252afaf79a700063f324757 -------------------------------------------------------------------------------- /tests/httpcache/5ba778581d38aa49351ab40941ba3ceddcd7f504c8b5475b72f5765bd4200a26: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/5ba778581d38aa49351ab40941ba3ceddcd7f504c8b5475b72f5765bd4200a26 -------------------------------------------------------------------------------- /tests/httpcache/5c3986cf8b8c955221f4bd9c2ef5c7d6944417cc7185931088c29dc0ff3d7f74: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/5c3986cf8b8c955221f4bd9c2ef5c7d6944417cc7185931088c29dc0ff3d7f74 -------------------------------------------------------------------------------- /tests/httpcache/5c98039e813b59ea8829b2704b4aaa4da0af0b2e253eb1e63a1501ecc47a250d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/5c98039e813b59ea8829b2704b4aaa4da0af0b2e253eb1e63a1501ecc47a250d -------------------------------------------------------------------------------- /tests/httpcache/5dd49fce20e25ee8dafea20ac051a7112106445ed471658318f2e1a434420d4d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/5dd49fce20e25ee8dafea20ac051a7112106445ed471658318f2e1a434420d4d -------------------------------------------------------------------------------- /tests/httpcache/5fd59565fadbe6394dfb40fa498cee8dbd296b93f35afb8aa05b4662f9dafcec: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/5fd59565fadbe6394dfb40fa498cee8dbd296b93f35afb8aa05b4662f9dafcec -------------------------------------------------------------------------------- /tests/httpcache/63b55211570099e0789f51a9a6ef085f2eec0dc6c44d2cc0a59bf1f89e8d6910: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/63b55211570099e0789f51a9a6ef085f2eec0dc6c44d2cc0a59bf1f89e8d6910 -------------------------------------------------------------------------------- /tests/httpcache/65da57c45d52a1eead05c858687c4fabf81fa118ac997380ae45a17facc499a0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/65da57c45d52a1eead05c858687c4fabf81fa118ac997380ae45a17facc499a0 -------------------------------------------------------------------------------- /tests/httpcache/6a6f4ff6442c50b20bad2fd73d65c6268d32b38220568a447321dc5b18fc8b72: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/6a6f4ff6442c50b20bad2fd73d65c6268d32b38220568a447321dc5b18fc8b72 -------------------------------------------------------------------------------- /tests/httpcache/6d4cf6ea06bdc7a7270bd1844ee0f173ebdf2ae3cff58783dc625d637ae94dc8: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/6d4cf6ea06bdc7a7270bd1844ee0f173ebdf2ae3cff58783dc625d637ae94dc8 -------------------------------------------------------------------------------- /tests/httpcache/74c1a28dbdd194fe95acc40f8f71743a654343316d68db5fb3731cd19e9a431e: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/74c1a28dbdd194fe95acc40f8f71743a654343316d68db5fb3731cd19e9a431e -------------------------------------------------------------------------------- /tests/httpcache/75547d38a729f10580c16081c8855de4bb40d02666cd32f6a8972aa434235fb9: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/75547d38a729f10580c16081c8855de4bb40d02666cd32f6a8972aa434235fb9 -------------------------------------------------------------------------------- /tests/httpcache/75e34c34723ad651fb641be58721bd354b8f339bb96d93c1f6d1c83a40b66d22: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/75e34c34723ad651fb641be58721bd354b8f339bb96d93c1f6d1c83a40b66d22 -------------------------------------------------------------------------------- /tests/httpcache/76e7c59a9880ea21deb5ad5eba78ff924f64aef01662796aa06a410004b24f96: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/76e7c59a9880ea21deb5ad5eba78ff924f64aef01662796aa06a410004b24f96 -------------------------------------------------------------------------------- /tests/httpcache/7ca193e39cc8b1505a024ac4e8066164caafb9f297feeda833acf46d6cd38cc7: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/7ca193e39cc8b1505a024ac4e8066164caafb9f297feeda833acf46d6cd38cc7 -------------------------------------------------------------------------------- /tests/httpcache/7e5ab06171c96080e8ce8560a58ad2e7a7a8fc95b3093e33220087b44bb06d60: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/7e5ab06171c96080e8ce8560a58ad2e7a7a8fc95b3093e33220087b44bb06d60 -------------------------------------------------------------------------------- /tests/httpcache/7f323cf5ae6f8ce98ed071a3096e7481e5934b370ba785ed89d85727052e9572: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/7f323cf5ae6f8ce98ed071a3096e7481e5934b370ba785ed89d85727052e9572 -------------------------------------------------------------------------------- /tests/httpcache/898662e651ebd17cdfc26aa3c9af2c7c82c64a4c02228aada48d7a311eb0c122: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/898662e651ebd17cdfc26aa3c9af2c7c82c64a4c02228aada48d7a311eb0c122 -------------------------------------------------------------------------------- /tests/httpcache/89f91219996603ff2b9e530420baf591471dfba5915fef2840904617694c8b7f: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/89f91219996603ff2b9e530420baf591471dfba5915fef2840904617694c8b7f -------------------------------------------------------------------------------- /tests/httpcache/8bc618662072aeae643e2dd72119bfb83d9fdce27d35c11fd01aef3d45edba5c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/8bc618662072aeae643e2dd72119bfb83d9fdce27d35c11fd01aef3d45edba5c -------------------------------------------------------------------------------- /tests/httpcache/8d5e9df72d499faba74ed29989220f02d10db6604b5e1c853b0f65f1850b0407: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/8d5e9df72d499faba74ed29989220f02d10db6604b5e1c853b0f65f1850b0407 -------------------------------------------------------------------------------- /tests/httpcache/91d1e6550aafe12c21e5dbec04239b1fc4fbb7f4acd61ff85ba13c1051148a4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/91d1e6550aafe12c21e5dbec04239b1fc4fbb7f4acd61ff85ba13c1051148a4a -------------------------------------------------------------------------------- /tests/httpcache/94284ea094b150e600e4db9bdbee28e810082351a8dde440f44e2bbb2e269c44: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/94284ea094b150e600e4db9bdbee28e810082351a8dde440f44e2bbb2e269c44 -------------------------------------------------------------------------------- /tests/httpcache/954dbfd278fe22fdbb9e6048f89724601e465e2c69c528d6dba08b37424c959b: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/954dbfd278fe22fdbb9e6048f89724601e465e2c69c528d6dba08b37424c959b -------------------------------------------------------------------------------- /tests/httpcache/96cfc406025ecd0e0859f7f3fec70cfae392e44e4d54d4f5c6dd7960b0b10095: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/96cfc406025ecd0e0859f7f3fec70cfae392e44e4d54d4f5c6dd7960b0b10095 -------------------------------------------------------------------------------- /tests/httpcache/97e19da6eeec8344f9f067f07e267a4288450f41f24d3d1916b9a8f884a5f1cc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/97e19da6eeec8344f9f067f07e267a4288450f41f24d3d1916b9a8f884a5f1cc -------------------------------------------------------------------------------- /tests/httpcache/9da0e86bbe54faf436b18e1aa9fdf9885fc323a88610b77dd14b777699e8a389: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/9da0e86bbe54faf436b18e1aa9fdf9885fc323a88610b77dd14b777699e8a389 -------------------------------------------------------------------------------- /tests/httpcache/9df508fb547ee94990dfa38b1d5afccde71bce59fb1f78db59b3913760d034d3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/9df508fb547ee94990dfa38b1d5afccde71bce59fb1f78db59b3913760d034d3 -------------------------------------------------------------------------------- /tests/httpcache/9e1e4d37834dc12c287fbfe81a12da3dd6a4230bb730950b01730c9b5561835c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/9e1e4d37834dc12c287fbfe81a12da3dd6a4230bb730950b01730c9b5561835c -------------------------------------------------------------------------------- /tests/httpcache/9efad0ef561e1f8445b553c6e6f8e8769654c6eb7b779198d0b3a126df269a03: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/9efad0ef561e1f8445b553c6e6f8e8769654c6eb7b779198d0b3a126df269a03 -------------------------------------------------------------------------------- /tests/httpcache/9f2550b71171d99736cc3d6481529d6571ba4f36538a9a0cc2294f1a309753da: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/9f2550b71171d99736cc3d6481529d6571ba4f36538a9a0cc2294f1a309753da -------------------------------------------------------------------------------- /tests/httpcache/a0d4f649dc2841a656f6229cdc57af39b7a7c8493fc2739ad3d58f80f9aa1051: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/a0d4f649dc2841a656f6229cdc57af39b7a7c8493fc2739ad3d58f80f9aa1051 -------------------------------------------------------------------------------- /tests/httpcache/a335a3713346e2cfe934550999147f795309342d06839f4c2b4f3eef44359a66: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/a335a3713346e2cfe934550999147f795309342d06839f4c2b4f3eef44359a66 -------------------------------------------------------------------------------- /tests/httpcache/a530f622b739baa9eece084c0287e60678bb56f3b8d7131b0636987af9ba9982: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/a530f622b739baa9eece084c0287e60678bb56f3b8d7131b0636987af9ba9982 -------------------------------------------------------------------------------- /tests/httpcache/ab8362fd06c3e41c414e4e5057268b9f4418746ebfcd857f1770bf7ec1973d8d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/ab8362fd06c3e41c414e4e5057268b9f4418746ebfcd857f1770bf7ec1973d8d -------------------------------------------------------------------------------- /tests/httpcache/b14353790e336eb95ea5ce21b2cdc0d100b921f7c64a85b5ee74e082737f7d05: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/b14353790e336eb95ea5ce21b2cdc0d100b921f7c64a85b5ee74e082737f7d05 -------------------------------------------------------------------------------- /tests/httpcache/b1e98c05abd019630dd9cfcea654df9f8dd625aea7685feb8703c5ab45ec454c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/b1e98c05abd019630dd9cfcea654df9f8dd625aea7685feb8703c5ab45ec454c -------------------------------------------------------------------------------- /tests/httpcache/b269f35a0295bf2fc16110147372f0dfc1a00a8a3a0445a51067ab37d3173f3e: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/b269f35a0295bf2fc16110147372f0dfc1a00a8a3a0445a51067ab37d3173f3e -------------------------------------------------------------------------------- /tests/httpcache/b4822134ab18acca34bf060fb285423bc882fc6844619e2b37784462e77dc7ce: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/b4822134ab18acca34bf060fb285423bc882fc6844619e2b37784462e77dc7ce -------------------------------------------------------------------------------- /tests/httpcache/b7565e3856127cbf5ac65406f43036b7d27398aa2cc7c74ee5c29c4b00dd3861: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/b7565e3856127cbf5ac65406f43036b7d27398aa2cc7c74ee5c29c4b00dd3861 -------------------------------------------------------------------------------- /tests/httpcache/b8db48d5ae45cb1acec4253ce05b9ec44aac6852a867fb4510efa4593e934c37: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/b8db48d5ae45cb1acec4253ce05b9ec44aac6852a867fb4510efa4593e934c37 -------------------------------------------------------------------------------- /tests/httpcache/bb03e50999258768f938b5eec267b6811acdfa72979db35cffdf358acd255300: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/bb03e50999258768f938b5eec267b6811acdfa72979db35cffdf358acd255300 -------------------------------------------------------------------------------- /tests/httpcache/bd79acb788ac6d429eac632ce9ee4cfc540d38db69134f4353df3e6da7e28b90: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/bd79acb788ac6d429eac632ce9ee4cfc540d38db69134f4353df3e6da7e28b90 -------------------------------------------------------------------------------- /tests/httpcache/bd8fa3a1b728c133437cc0c7e0ac2161667d326630775e547e05922ba8bde745: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/bd8fa3a1b728c133437cc0c7e0ac2161667d326630775e547e05922ba8bde745 -------------------------------------------------------------------------------- /tests/httpcache/c19a35ad2661c58a272bdb5d0f78eab17fe75c8d55f21be7b2648209cefd9292: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/c19a35ad2661c58a272bdb5d0f78eab17fe75c8d55f21be7b2648209cefd9292 -------------------------------------------------------------------------------- /tests/httpcache/c26b427022fde68252cdab75ba2262d0ef439073af2c9e8a9e8878fdd722891d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/c26b427022fde68252cdab75ba2262d0ef439073af2c9e8a9e8878fdd722891d -------------------------------------------------------------------------------- /tests/httpcache/c42a6ed1cfe63440384872837c2619b1022f23382e67c4d0be8389c42c49ee5c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/c42a6ed1cfe63440384872837c2619b1022f23382e67c4d0be8389c42c49ee5c -------------------------------------------------------------------------------- /tests/httpcache/c693a49540fdf86104d0cea6d233b6e689d8c2e99529b2a819365c623cf551ad: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/c693a49540fdf86104d0cea6d233b6e689d8c2e99529b2a819365c623cf551ad -------------------------------------------------------------------------------- /tests/httpcache/c7ff254f38c54ecfc0460ba54ea5ac569740d9f88f157f96be9bbc75a7e1dd43: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/c7ff254f38c54ecfc0460ba54ea5ac569740d9f88f157f96be9bbc75a7e1dd43 -------------------------------------------------------------------------------- /tests/httpcache/cd7b24a702aaf5e260133896e7b2777746bf83a477bfc7afc56d079f9b8204f5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/cd7b24a702aaf5e260133896e7b2777746bf83a477bfc7afc56d079f9b8204f5 -------------------------------------------------------------------------------- /tests/httpcache/cda2efd6e84d746454fcc08d558ccf0349e84e0ca6d206a84b0d722f805f8088: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/cda2efd6e84d746454fcc08d558ccf0349e84e0ca6d206a84b0d722f805f8088 -------------------------------------------------------------------------------- /tests/httpcache/cdc28d5a38ad2cd008e01f315a0dde695057f9565271f5afacacc0efebc770d7: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/cdc28d5a38ad2cd008e01f315a0dde695057f9565271f5afacacc0efebc770d7 -------------------------------------------------------------------------------- /tests/httpcache/cee312137b5a516672e54de590ac768d4b0b0c7338028c72b32dcbc2bcb24add: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/cee312137b5a516672e54de590ac768d4b0b0c7338028c72b32dcbc2bcb24add -------------------------------------------------------------------------------- /tests/httpcache/d544603bb2a9e717b1164080237e2965ce53bbf9783c65fece77895004b895b6: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/d544603bb2a9e717b1164080237e2965ce53bbf9783c65fece77895004b895b6 -------------------------------------------------------------------------------- /tests/httpcache/d5bc81d09b5367e75bbf157ccc4edcfd75712f9add2ef61a3181b602be44a10a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/d5bc81d09b5367e75bbf157ccc4edcfd75712f9add2ef61a3181b602be44a10a -------------------------------------------------------------------------------- /tests/httpcache/da34fec696da99892a9dbd2910c2aa4bf39de13fd53e2b669e19195a3664b468: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/da34fec696da99892a9dbd2910c2aa4bf39de13fd53e2b669e19195a3664b468 -------------------------------------------------------------------------------- /tests/httpcache/e2c15b01e8736f14d20f4f187d6855bba8f09bb8092cfee74719ea7eea273723: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/e2c15b01e8736f14d20f4f187d6855bba8f09bb8092cfee74719ea7eea273723 -------------------------------------------------------------------------------- /tests/httpcache/ea8d5b29d3521138cd9eefc5357f19a237a30bfcb6d93c6daf9604fc9e0ba9ad: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/ea8d5b29d3521138cd9eefc5357f19a237a30bfcb6d93c6daf9604fc9e0ba9ad -------------------------------------------------------------------------------- /tests/httpcache/ecbb72dbba362eb83644f9b6754295e25b161a994240dce923888a99c2718606: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/ecbb72dbba362eb83644f9b6754295e25b161a994240dce923888a99c2718606 -------------------------------------------------------------------------------- /tests/httpcache/ed66cf25e2fbba5595506d9f9eb10b52db7e469fd13b79752c0ce065678ee33e: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/ed66cf25e2fbba5595506d9f9eb10b52db7e469fd13b79752c0ce065678ee33e -------------------------------------------------------------------------------- /tests/httpcache/ed8a31158a000b41735b079e1a75f5f0022ca6737a01313ff05974237b54b13e: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/ed8a31158a000b41735b079e1a75f5f0022ca6737a01313ff05974237b54b13e -------------------------------------------------------------------------------- /tests/httpcache/f14b73cec71a82fe0f31aa2d8bba8e422e3a50011b1b90e8728f5e632d020869: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/f14b73cec71a82fe0f31aa2d8bba8e422e3a50011b1b90e8728f5e632d020869 -------------------------------------------------------------------------------- /tests/httpcache/f24ed01d674c77fb302289a36c99cac160007a4116e034418f1fb2beaad6a495: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/f24ed01d674c77fb302289a36c99cac160007a4116e034418f1fb2beaad6a495 -------------------------------------------------------------------------------- /tests/httpcache/f5b84280fe0914746d26e57e7592ea4f516be3f3995f01ee9d9d73b647e721c5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/f5b84280fe0914746d26e57e7592ea4f516be3f3995f01ee9d9d73b647e721c5 -------------------------------------------------------------------------------- /tests/httpcache/f689d0763a7d0c1818e781c5e61f505311f30e87b6d622ed7e073e81f346e146: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/f689d0763a7d0c1818e781c5e61f505311f30e87b6d622ed7e073e81f346e146 -------------------------------------------------------------------------------- /tests/httpcache/f707c0955a69c7b7d8ece7c64b259c25d882d97671f3f24941a29092aa44453c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/f707c0955a69c7b7d8ece7c64b259c25d882d97671f3f24941a29092aa44453c -------------------------------------------------------------------------------- /tests/httpcache/fa75d727d82b502d666f51b3c36f680656997df68b3b1da161ad7bae108c892c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/fa75d727d82b502d666f51b3c36f680656997df68b3b1da161ad7bae108c892c -------------------------------------------------------------------------------- /tests/httpcache/fb67e232491aa926a94199b3fddf479c8f0953c4354546dea4056b146a46d143: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/fb67e232491aa926a94199b3fddf479c8f0953c4354546dea4056b146a46d143 -------------------------------------------------------------------------------- /tests/httpcache/fde23201de1905da6f20b4253e364a4b055e0ee33d8fdca5b322efe92404c52f: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/fde23201de1905da6f20b4253e364a4b055e0ee33d8fdca5b322efe92404c52f -------------------------------------------------------------------------------- /tests/httpcache/febe8f53c32ecc0ac28ca03538f119fefee32327d7f1410c7edb472b8d2633ad: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbr/tvnamer/2e1e969542d65c5cac2dad9b3cd20dce4f53b6a8/tests/httpcache/febe8f53c32ecc0ac28ca03538f119fefee32327d7f1410c7edb472b8d2633ad -------------------------------------------------------------------------------- /tests/test_absolute_number_ambiguity.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Test ability to set the series name by series id 4 | """ 5 | 6 | from functional_runner import run_tvnamer, verify_out_data 7 | from helpers import attr 8 | 9 | 10 | @attr("functional") 11 | def test_ambiguity_fix(): 12 | """Test amiguous eisode number fix 13 | """ 14 | 15 | conf = """ 16 | {"always_rename": true, 17 | "select_first": true} 18 | """ 19 | 20 | out_data = run_tvnamer( 21 | with_files = ['[ANBU-AonE]_Naruto_43_[3811CBB5].avi'], 22 | with_config = conf, 23 | with_flags = [], 24 | with_input = "") 25 | 26 | expected_files = ['[ANBU-AonE] Naruto - 43 - Killer Kunoichi and a Shaky Shikamaru [3811CBB5].avi'] 27 | 28 | verify_out_data(out_data, expected_files) 29 | -------------------------------------------------------------------------------- /tests/test_anime_filenames.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Tests anime filename output 4 | """ 5 | 6 | from functional_runner import run_tvnamer, verify_out_data 7 | from helpers import attr 8 | 9 | 10 | @attr("functional") 11 | def test_group(): 12 | """Anime filename [#100] 13 | """ 14 | out_data = run_tvnamer( 15 | with_files = ['[Some Group] Scrubs - 01 [A1B2C3].avi'], 16 | with_config = """ 17 | { 18 | "always_rename": true, 19 | "select_first": true, 20 | 21 | "filename_anime_with_episode": "[%(group)s] %(seriesname)s - %(episodenumber)s - %(episodename)s [%(crc)s]%(ext)s" 22 | } 23 | """) 24 | 25 | expected_files = ['[Some Group] Scrubs - 01 - My First Day [A1B2C3].avi'] 26 | 27 | verify_out_data(out_data, expected_files) 28 | 29 | 30 | @attr("functional") 31 | def test_group_no_epname(): 32 | """Anime filename, on episode with no name [#100] 33 | """ 34 | out_data = run_tvnamer( 35 | with_files = ['[Some Group] Somefakeseries - 01 [A1B2C3].avi'], 36 | with_config = """ 37 | { 38 | "always_rename": true, 39 | "select_first": true, 40 | 41 | "filename_anime_without_episode": "[%(group)s] %(seriesname)s - %(episodenumber)s [%(crc)s]%(ext)s" 42 | } 43 | """) 44 | 45 | expected_files = ['[Some Group] Somefakeseries - 01 [A1B2C3].avi'] 46 | 47 | verify_out_data(out_data, expected_files) 48 | -------------------------------------------------------------------------------- /tests/test_configfunctional.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Tests various configs load correctly 4 | """ 5 | 6 | from functional_runner import run_tvnamer, verify_out_data 7 | from helpers import attr 8 | import pytest 9 | 10 | 11 | @attr("functional") 12 | def test_batchconfig(): 13 | """Test configured batch mode works 14 | """ 15 | 16 | conf = """ 17 | {"always_rename": true, 18 | "select_first": true} 19 | """ 20 | 21 | out_data = run_tvnamer( 22 | with_files = ['scrubs.s01e01.avi'], 23 | with_config = conf, 24 | with_input = "") 25 | 26 | expected_files = ['Scrubs - [01x01] - My First Day.avi'] 27 | 28 | verify_out_data(out_data, expected_files) 29 | 30 | 31 | @attr("functional") 32 | def test_never_skip_file(): 33 | """Test default of skipping file on error 34 | """ 35 | 36 | conf = """ 37 | {"batch": true} 38 | """ 39 | 40 | out_data = run_tvnamer( 41 | with_files = ['scrubs.s01e01.avi', 'a.nonsense.fake.episode.s01e01.avi'], 42 | with_config = conf, 43 | with_input = "") 44 | 45 | expected_files = ['Scrubs - [01x01] - My First Day.avi', 'a.nonsense.fake.episode.s01e01.avi'] 46 | 47 | verify_out_data(out_data, expected_files) 48 | 49 | 50 | @attr("functional") 51 | def test_never_skip_file(): 52 | """Test for using skip_file_on_error to always do best-effort rename of file without TVDB data 53 | """ 54 | 55 | conf = """ 56 | {"batch": true, 57 | "skip_file_on_error": false} 58 | """ 59 | 60 | out_data = run_tvnamer( 61 | with_files = ['scrubs.s01e01.avi', 'a.nonsense.fake.episode.s01e01.avi'], 62 | with_config = conf, 63 | with_input = "") 64 | 65 | expected_files = ['Scrubs - [01x01] - My First Day.avi', 'a nonsense fake episode - [01x01].avi'] 66 | 67 | verify_out_data(out_data, expected_files) 68 | 69 | 70 | @attr("functional") 71 | def test_skip_behaviour_warn(): 72 | """skip_behaviour:warn should keep renaming other files 73 | """ 74 | 75 | conf = """ 76 | {"batch": true, 77 | "skip_behaviour": "warn"} 78 | """ 79 | 80 | out_data = run_tvnamer( 81 | with_files = ['scrubs.s01e01.avi', 'a.nonsense.fake.episode.s01e01.avi'], 82 | with_config = conf, 83 | with_input = "") 84 | 85 | expected_files = ['Scrubs - [01x01] - My First Day.avi', 'a.nonsense.fake.episode.s01e01.avi'] 86 | 87 | verify_out_data(out_data, expected_files) 88 | 89 | 90 | @attr("functional") 91 | def test_skip_behaviour_error(): 92 | """An error with skip_behaviour 'exit' should end process 93 | """ 94 | 95 | conf = """ 96 | {"batch": true, 97 | "skip_behaviour": "exit"} 98 | """ 99 | 100 | out_data = run_tvnamer( 101 | with_files = ['scrubs.s01e01.avi', 'a.nonsense.fake.episode.s01e01.avi'], 102 | with_config = conf, 103 | with_input = "") 104 | 105 | # Files are processed alphabetically, so file starting wiht S is never touched 106 | expected_files = ['scrubs.s01e01.avi', 'a.nonsense.fake.episode.s01e01.avi'] 107 | 108 | verify_out_data(out_data, expected_files, expected_returncode=2) 109 | 110 | 111 | out_data = run_tvnamer( 112 | with_files = ['scrubs.s01e01.avi', 'z.fake.episode.s01e01.avi'], 113 | with_config = conf, 114 | with_input = "") 115 | 116 | # Files are processed alphabetically, so file starting wiht S is never touched 117 | expected_files = ['Scrubs - [01x01] - My First Day.avi', 'z.fake.episode.s01e01.avi'] 118 | 119 | verify_out_data(out_data, expected_files, expected_returncode=2) 120 | 121 | 122 | @attr("functional") 123 | def test_lowercase_names(): 124 | """Test setting "lowercase_filename" config option 125 | """ 126 | 127 | conf = """ 128 | {"lowercase_filename": true, 129 | "always_rename": true, 130 | "select_first": true} 131 | """ 132 | 133 | out_data = run_tvnamer( 134 | with_files = ['scrubs.s01e01.avi'], 135 | with_config = conf, 136 | with_input = "") 137 | 138 | expected_files = ['scrubs - [01x01] - my first day.avi'] 139 | 140 | verify_out_data(out_data, expected_files) 141 | 142 | 143 | @attr("functional") 144 | def test_replace_with_underscore(): 145 | """Test custom blacklist to replace " " with "_" 146 | """ 147 | 148 | conf = """ 149 | {"custom_filename_character_blacklist": " ", 150 | "replace_blacklisted_characters_with": "_", 151 | "always_rename": true, 152 | "select_first": true} 153 | """ 154 | 155 | out_data = run_tvnamer( 156 | with_files = ['scrubs.s01e01.avi'], 157 | with_config = conf, 158 | with_input = "") 159 | 160 | expected_files = ['Scrubs_-_[01x01]_-_My_First_Day.avi'] 161 | 162 | verify_out_data(out_data, expected_files) 163 | 164 | 165 | @attr("functional") 166 | @pytest.mark.xfail 167 | def test_abs_epnmber(): 168 | """Ensure the absolute episode number is available for custom 169 | filenames in config 170 | """ 171 | 172 | 173 | conf = """ 174 | {"filename_with_episode": "%(seriesname)s - %(absoluteepisode)s%(ext)s", 175 | "always_rename": true, 176 | "select_first": true} 177 | """ 178 | 179 | out_data = run_tvnamer( 180 | with_files = ['scrubs.s01e01.avi'], 181 | with_config = conf, 182 | with_input = "") 183 | 184 | expected_files = ['Scrubs - 01.avi'] 185 | 186 | verify_out_data(out_data, expected_files) 187 | 188 | 189 | @attr("functional") 190 | def test_resolve_absoloute_episode(): 191 | """Test resolving by absolute episode number 192 | """ 193 | 194 | conf = """ 195 | {"always_rename": true, 196 | "select_first": true} 197 | """ 198 | 199 | out_data = run_tvnamer( 200 | with_files = ['[Bleachverse]_BLEACH_310.avi'], 201 | with_config = conf, 202 | with_flags=["--series-id=74796"], # Forcing series as `Bleach Kai` currently appears above Bleach in search results 203 | with_input = "") 204 | 205 | expected_files = ['[Bleachverse] Bleach - 310 - Ichigo\'s Resolution.avi'] 206 | 207 | verify_out_data(out_data, expected_files) 208 | 209 | print("Checking output files are re-parsable") 210 | out_data = run_tvnamer( 211 | with_files = expected_files, 212 | with_config = conf, 213 | with_input = "") 214 | 215 | expected_files = ['[Bleachverse] Bleach - 310 - Ichigo\'s Resolution.avi'] 216 | 217 | verify_out_data(out_data, expected_files) 218 | 219 | 220 | @attr("functional") 221 | def test_valid_extension_recursive(): 222 | """When using valid_extensions in a custom config file, recursive search doesn't work. Github issue #36 223 | """ 224 | 225 | conf = """ 226 | {"always_rename": true, 227 | "select_first": true, 228 | "valid_extensions": ["avi","mp4","m4v","wmv","mkv","mov","srt"], 229 | "recursive": true} 230 | """ 231 | 232 | out_data = run_tvnamer( 233 | with_files = ['nested/dir/scrubs.s01e01.avi'], 234 | with_config = conf, 235 | with_input = "", 236 | run_on_directory = True) 237 | 238 | expected_files = ['nested/dir/Scrubs - [01x01] - My First Day.avi'] 239 | 240 | verify_out_data(out_data, expected_files) 241 | 242 | 243 | @attr("functional") 244 | def test_replace_ands(): 245 | """Test replace "and" "&" 246 | """ 247 | 248 | conf = r""" 249 | {"always_rename": true, 250 | "select_first": true, 251 | "input_filename_replacements": [ 252 | {"is_regex": true, 253 | "match": "(\\Wand\\W| & )", 254 | "replacement": " "} 255 | ] 256 | } 257 | """ 258 | 259 | out_data = run_tvnamer( 260 | with_files = ['Brothers.and.Sisters.S05E16.HDTV.XviD-LOL.avi'], 261 | with_config = conf, 262 | with_input = "", 263 | run_on_directory = True) 264 | 265 | expected_files = ['Brothers & Sisters - [05x16] - Home Is Where The Fort Is.avi'] 266 | 267 | verify_out_data(out_data, expected_files) 268 | 269 | 270 | @attr("functional") 271 | def test_replace_ands_in_output_also(): 272 | """Test replace "and" "&" for search, and replace & in output filename 273 | """ 274 | 275 | conf = r""" 276 | {"always_rename": true, 277 | "select_first": true, 278 | "input_filename_replacements": [ 279 | {"is_regex": true, 280 | "match": "(\\Wand\\W| & )", 281 | "replacement": " "} 282 | ], 283 | "output_filename_replacements": [ 284 | {"is_regex": true, 285 | "match": " & ", 286 | "replacement": " and "} 287 | ] 288 | } 289 | """ 290 | 291 | out_data = run_tvnamer( 292 | with_files = ['Brothers.and.Sisters.S05E16.HDTV.XviD-LOL.avi'], 293 | with_config = conf, 294 | with_input = "", 295 | run_on_directory = True) 296 | 297 | expected_files = ['Brothers and Sisters - [05x16] - Home Is Where The Fort Is.avi'] 298 | 299 | verify_out_data(out_data, expected_files) 300 | 301 | 302 | @attr("functional") 303 | def test_force_overwrite_enabled(): 304 | """Tests forcefully overwritting existing filenames 305 | """ 306 | 307 | conf = r""" 308 | {"always_rename": true, 309 | "select_first": true, 310 | "overwrite_destination_on_rename": true 311 | } 312 | """ 313 | 314 | out_data = run_tvnamer( 315 | with_files = ['scrubs.s01e01.avi', 'Scrubs - [01x01] - My First Day.avi'], 316 | with_config = conf, 317 | with_input = "", 318 | run_on_directory = True) 319 | 320 | expected_files = ['Scrubs - [01x01] - My First Day.avi'] 321 | 322 | verify_out_data(out_data, expected_files) 323 | 324 | 325 | @attr("functional") 326 | def test_force_overwrite_disabled(): 327 | """Explicitly disabling forceful-overwrite 328 | """ 329 | 330 | conf = r""" 331 | {"always_rename": true, 332 | "select_first": true, 333 | "overwrite_destination_on_rename": false 334 | } 335 | """ 336 | 337 | out_data = run_tvnamer( 338 | with_files = ['Scrubs - [01x01] - My First Day.avi', 'scrubs - [01x01].avi'], 339 | with_config = conf, 340 | with_input = "", 341 | run_on_directory = True) 342 | 343 | expected_files = ['Scrubs - [01x01] - My First Day.avi', 'scrubs - [01x01].avi'] 344 | 345 | verify_out_data(out_data, expected_files) 346 | 347 | 348 | @attr("functional") 349 | def test_force_overwrite_default(): 350 | """Forceful-overwrite should be disabled by default 351 | """ 352 | 353 | conf = r""" 354 | {"always_rename": true, 355 | "select_first": true 356 | } 357 | """ 358 | 359 | out_data = run_tvnamer( 360 | with_files = ['Scrubs - [01x01] - My First Day.avi', 'scrubs - [01x01].avi'], 361 | with_config = conf, 362 | with_input = "", 363 | run_on_directory = True) 364 | 365 | expected_files = ['Scrubs - [01x01] - My First Day.avi', 'scrubs - [01x01].avi'] 366 | 367 | verify_out_data(out_data, expected_files) 368 | 369 | 370 | @attr("functional") 371 | def test_titlecase(): 372 | """Tests Title Case Option To Make Episodes Like This 373 | """ 374 | 375 | conf = r""" 376 | {"always_rename": true, 377 | "select_first": true, 378 | "skip_file_on_error": false, 379 | "titlecase_filename": true 380 | } 381 | """ 382 | 383 | out_data = run_tvnamer( 384 | with_files = ['this.is.a.fake.episode.s01e01.avi'], 385 | with_config = conf, 386 | with_input = "", 387 | run_on_directory = True) 388 | 389 | expected_files = ['This Is a Fake Episode - [01x01].avi'] 390 | 391 | verify_out_data(out_data, expected_files) 392 | 393 | 394 | @attr("functional") 395 | def test_unicode_normalization(): 396 | conf = r""" 397 | {"always_rename": true, 398 | "select_first": true, 399 | "skip_file_on_error": false, 400 | "normalize_unicode_filenames": true 401 | } 402 | """ 403 | 404 | out_data = run_tvnamer( 405 | with_files = ["Carniv\xe0le 1x11 - The Day of the Dead.avi"], 406 | with_config = conf, 407 | with_input = "", 408 | run_on_directory = True) 409 | 410 | expected_files = ["Carnivale - [01x11] - The Day of the Dead.avi"] 411 | 412 | verify_out_data(out_data, expected_files) 413 | 414 | 415 | @attr("functional") 416 | def test_own_api_key(): 417 | """Check overriding API key works 418 | """ 419 | 420 | conf = r""" 421 | {"always_rename": true, 422 | "select_first": true, 423 | "tvdb_api_key": "xxxxxxxxx", 424 | "skip_behaviour": "error 425 | } 426 | """ 427 | 428 | out_data = run_tvnamer( 429 | with_files = ['scrubs.s01e01.avi'], 430 | with_config = conf, 431 | with_input = "", 432 | with_flags=['-vvv'], 433 | run_on_directory = True) 434 | 435 | expected_files = ['scrubs.s01e01.avi'] 436 | 437 | verify_out_data(out_data, expected_files, expected_returncode=1) 438 | 439 | 440 | @attr("functional") 441 | def test_dry_run_basic(): 442 | """Test dry run mode 443 | """ 444 | 445 | conf = """ 446 | {"batch": true, 447 | "dry_run": true} 448 | """ 449 | 450 | out_data = run_tvnamer( 451 | with_files = ['scrubs.s01e01.avi'], 452 | with_config = conf, 453 | with_input = "") 454 | 455 | expected_files = ['scrubs.s01e01.avi',] 456 | 457 | verify_out_data(out_data, expected_files) 458 | -------------------------------------------------------------------------------- /tests/test_custom_replacement.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Tests custom replacements on input/output files 4 | """ 5 | 6 | from functional_runner import run_tvnamer, verify_out_data 7 | from helpers import attr 8 | 9 | 10 | @attr("functional") 11 | def test_simple_input_replacements(): 12 | """Tests replacing strings in input files 13 | """ 14 | out_data = run_tvnamer( 15 | with_files = ['scruuuuuubs.s01e01.avi'], 16 | with_config = """ 17 | { 18 | "input_filename_replacements": [ 19 | {"is_regex": false, 20 | "match": "uuuuuu", 21 | "replacement": "u"} 22 | ], 23 | "always_rename": true, 24 | "select_first": true 25 | } 26 | """) 27 | 28 | expected_files = ['Scrubs - [01x01] - My First Day.avi'] 29 | 30 | verify_out_data(out_data, expected_files) 31 | 32 | 33 | @attr("functional") 34 | def test_simple_output_replacements(): 35 | """Tests replacing strings in input files 36 | """ 37 | out_data = run_tvnamer( 38 | with_files = ['scrubs.s01e01.avi'], 39 | with_config = """ 40 | { 41 | "output_filename_replacements": [ 42 | {"is_regex": false, 43 | "match": "u", 44 | "replacement": "v"} 45 | ], 46 | "always_rename": true, 47 | "select_first": true 48 | } 49 | """) 50 | 51 | expected_files = ['Scrvbs - [01x01] - My First Day.avi'] 52 | 53 | verify_out_data(out_data, expected_files) 54 | 55 | 56 | @attr("functional") 57 | def test_regex_input_replacements(): 58 | """Tests regex replacement in input files 59 | """ 60 | out_data = run_tvnamer( 61 | with_files = ['scruuuuuubs.s01e01.avi'], 62 | with_config = """ 63 | { 64 | "input_filename_replacements": [ 65 | {"is_regex": true, 66 | "match": "[u]+", 67 | "replacement": "u"} 68 | ], 69 | "always_rename": true, 70 | "select_first": true 71 | } 72 | """) 73 | 74 | expected_files = ['Scrubs - [01x01] - My First Day.avi'] 75 | 76 | verify_out_data(out_data, expected_files) 77 | 78 | 79 | @attr("functional") 80 | def test_regex_output_replacements(): 81 | """Tests regex replacement in output files 82 | """ 83 | out_data = run_tvnamer( 84 | with_files = ['scrubs.s01e01.avi'], 85 | with_config = """ 86 | { 87 | "output_filename_replacements": [ 88 | {"is_regex": true, 89 | "match": "[ua]+", 90 | "replacement": "v"} 91 | ], 92 | "always_rename": true, 93 | "select_first": true 94 | } 95 | """) 96 | 97 | expected_files = ['Scrvbs - [01x01] - My First Dvy.avi'] 98 | 99 | verify_out_data(out_data, expected_files) 100 | 101 | 102 | @attr("functional") 103 | def test_replacing_spaces(): 104 | """Tests more practical use of replacements, removing spaces 105 | """ 106 | out_data = run_tvnamer( 107 | with_files = ['scrubs.s01e01.avi'], 108 | with_config = """ 109 | { 110 | "output_filename_replacements": [ 111 | {"is_regex": true, 112 | "match": "[ ]", 113 | "replacement": "."} 114 | ], 115 | "always_rename": true, 116 | "select_first": true 117 | } 118 | """) 119 | 120 | expected_files = ['Scrubs.-.[01x01].-.My.First.Day.avi'] 121 | 122 | verify_out_data(out_data, expected_files) 123 | 124 | 125 | @attr("functional") 126 | def test_replacing_ands(): 127 | """Tests removind "and" and "&" from input files 128 | """ 129 | out_data = run_tvnamer( 130 | with_files = ['Law & Order UK s01e01.avi'], 131 | with_config = """ 132 | { 133 | "input_filename_replacements": [ 134 | {"is_regex": true, 135 | "match": "( and | & )", 136 | "replacement": " "} 137 | ], 138 | "output_filename_replacements": [ 139 | {"is_regex": false, 140 | "match": " & ", 141 | "replacement": " and "} 142 | ], 143 | "custom_filename_character_blacklist": ":", 144 | "always_rename": true, 145 | "select_first": true 146 | } 147 | """) 148 | 149 | expected_files = ['Law and Order_ UK - [01x01] - Care.avi'] 150 | 151 | verify_out_data(out_data, expected_files) 152 | 153 | 154 | @attr("functional") 155 | def test_multiple_replacements(): 156 | """Tests multiple replacements on one file 157 | """ 158 | out_data = run_tvnamer( 159 | with_files = ['scrubs.s01e01.avi'], 160 | with_config = """ 161 | { 162 | "output_filename_replacements": [ 163 | {"is_regex": true, 164 | "match": "[ua]+", 165 | "replacement": "v"}, 166 | {"is_regex": false, 167 | "match": "v", 168 | "replacement": "_"} 169 | ], 170 | "always_rename": true, 171 | "select_first": true 172 | } 173 | """) 174 | 175 | expected_files = ['Scr_bs - [01x01] - My First D_y.avi'] 176 | 177 | verify_out_data(out_data, expected_files) 178 | 179 | 180 | @attr("functional") 181 | def test_fullpath_replacements(): 182 | """Tests replacing strings in output path 183 | """ 184 | out_data = run_tvnamer( 185 | with_files = ['scrubs.s01e01.avi'], 186 | with_config = """ 187 | { 188 | "move_files_enable": true, 189 | "move_files_destination": "%(seriesname)s", 190 | "move_files_fullpath_replacements": [ 191 | {"is_regex": true, 192 | "match": "Scr.*?s", 193 | "replacement": "A Test"} 194 | ], 195 | "always_rename": true, 196 | "select_first": true 197 | } 198 | """) 199 | 200 | expected_files = ['A Test/A Test - [01x01] - My First Day.avi'] 201 | 202 | verify_out_data(out_data, expected_files) 203 | 204 | 205 | @attr("functional") 206 | def test_restoring_dot(): 207 | """Test replace the parsed "Tosh 0" with "Tosh.0" 208 | """ 209 | out_data = run_tvnamer( 210 | with_files = ['tosh.0.s03.e02.avi'], 211 | with_config = """ 212 | { 213 | "input_filename_replacements": [ 214 | {"is_regex": false, 215 | "match": "tosh.0", 216 | "replacement": "tosh0"} 217 | ], 218 | "always_rename": true, 219 | "select_first": true 220 | } 221 | """) 222 | 223 | expected_files = ['Tosh.0 - [03x02] - Brian Atene.avi'] 224 | 225 | verify_out_data(out_data, expected_files) 226 | 227 | 228 | @attr("functional") 229 | def test_replacement_order(): 230 | """Ensure output replacements happen before the valid filename function is run 231 | """ 232 | out_data = run_tvnamer( 233 | with_files = ['24.s03.e02.avi'], 234 | with_config = """ 235 | { 236 | "output_filename_replacements": [ 237 | {"is_regex": false, 238 | "match": ":", 239 | "replacement": "-"} 240 | ], 241 | "always_rename": true, 242 | "select_first": true 243 | } 244 | """) 245 | 246 | expected_files = ['24 - [03x02] - Day 3- 2-00 P.M. - 3-00 P.M..avi'] 247 | 248 | verify_out_data(out_data, expected_files) 249 | 250 | 251 | @attr("functional") 252 | def test_replacement_preserve_extension(): 253 | """Ensure with_extension replacement option defaults to preserving extension 254 | """ 255 | out_data = run_tvnamer( 256 | with_files = ['scrubs.s01e01.avi'], 257 | with_config = """ 258 | { 259 | "output_filename_replacements": [ 260 | {"is_regex": false, 261 | "match": "avi", 262 | "replacement": "ohnobroken"} 263 | ], 264 | "always_rename": true, 265 | "select_first": true 266 | } 267 | """) 268 | 269 | expected_files = ['Scrubs - [01x01] - My First Day.avi'] 270 | 271 | verify_out_data(out_data, expected_files) 272 | 273 | 274 | @attr("functional") 275 | def test_replacement_including_extension(): 276 | """Option to allow replacement search/replace to include file extension 277 | """ 278 | out_data = run_tvnamer( 279 | with_files = ['scrubs.s01e01.avi'], 280 | with_config = """ 281 | { 282 | "output_filename_replacements": [ 283 | {"is_regex": false, 284 | "with_extension": true, 285 | "match": "Day.avi", 286 | "replacement": "Day.nl.avi"} 287 | ], 288 | "always_rename": true, 289 | "select_first": true 290 | } 291 | """) 292 | 293 | expected_files = ['Scrubs - [01x01] - My First Day.nl.avi'] 294 | 295 | verify_out_data(out_data, expected_files) 296 | -------------------------------------------------------------------------------- /tests/test_datestamp_episode.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Tests episodes based on dates, not season/episode numbers 4 | """ 5 | 6 | from functional_runner import run_tvnamer, verify_out_data 7 | from helpers import attr 8 | import pytest 9 | 10 | 11 | @attr("functional") 12 | def test_issue_56_dated_episode(): 13 | """Season and episode should set correctly for date-parsed episodes 14 | """ 15 | 16 | conf = """ 17 | {"batch": true, 18 | "select_first": true, 19 | "filename_with_episode": "%(seriesname)s %(date)s - %(episodename)s%(ext)s"} 20 | """ 21 | 22 | out_data = run_tvnamer( 23 | with_files = ['tonight.show.conan.2009.06.05.hdtv.blah.avi'], 24 | with_config = conf) 25 | 26 | expected_files = ['The Tonight Show with Conan O\'Brien - [2009-06-05] - Ryan Seacrest, Patton Oswalt, Chickenfoot.avi'] 27 | 28 | verify_out_data(out_data, expected_files) 29 | 30 | 31 | @attr("functional") 32 | @pytest.mark.xfail(reason="dependant on episode objects all having consistent data - issue #125") 33 | def test_date_in_s01e01_out(): 34 | """File with date-stamp, outputs s01e01-ish name 35 | """ 36 | 37 | conf = """ 38 | {"always_rename": true, 39 | "select_first": true, 40 | "filename_with_date_and_episode": "%(seriesname)s - [%(seasonnumber)02dx%(episode)s] - %(episodename)s%(ext)s"} 41 | """ 42 | 43 | out_data = run_tvnamer( 44 | with_files = ['scrubs.2001.10.02.avi'], 45 | with_config = conf, 46 | with_input = "") 47 | 48 | expected_files = ['Scrubs - [01x01] - My First Day.avi'] 49 | 50 | verify_out_data(out_data, expected_files) 51 | 52 | 53 | def test_issue_31_twochar_year(): 54 | """Fix for parsing rather ambigious dd.mm.yy being parsed as "0011" 55 | """ 56 | 57 | from tvnamer.files import intepret_year 58 | 59 | assert intepret_year("99") == 1999 60 | assert intepret_year("79") == 1979 61 | 62 | assert intepret_year("00") == 2000 63 | assert intepret_year("20") == 2020 64 | -------------------------------------------------------------------------------- /tests/test_extension_pattern.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Tests multi-episode filename generation 4 | """ 5 | 6 | from functional_runner import run_tvnamer, verify_out_data 7 | from helpers import attr 8 | 9 | 10 | @attr("functional") 11 | def test_extension_pattern_default(): 12 | """Test default extension handling, no language codes 13 | """ 14 | 15 | conf = r""" 16 | {"extension_pattern": "(\\.[a-zA-Z0-9]+)$", 17 | "batch": true, 18 | "valid_extensions": ["avi", "srt"]} 19 | """ 20 | 21 | input_files = [ 22 | "scrubs.s01e01.hdtv.fake.avi", 23 | "scrubs.s01e01.hdtv.fake.srt", 24 | "my.name.is.earl.s01e01.fake.avi", 25 | "my.name.is.earl.s01e01.some.other.fake.eng.srt", 26 | ] 27 | expected_files = [ 28 | "Scrubs - [01x01] - My First Day.avi", 29 | "Scrubs - [01x01] - My First Day.srt", 30 | "My Name Is Earl - [01x01] - Pilot.avi", 31 | "My Name Is Earl - [01x01] - Pilot.srt", 32 | ] 33 | 34 | out_data = run_tvnamer( 35 | with_files = input_files, 36 | with_config = conf, 37 | with_input = "") 38 | 39 | verify_out_data(out_data, expected_files) 40 | 41 | @attr("functional") 42 | def test_extension_pattern_custom(): 43 | """Test custom extension pattern, multiple language codes 44 | """ 45 | 46 | conf = r""" 47 | {"extension_pattern": "((\\.|-)(eng|cze|EN|CZ)(?=\\.(sub|srt)))?(\\.[a-zA-Z0-9]+)$", 48 | "batch": true, 49 | "valid_extensions": ["avi", "srt"]} 50 | """ 51 | 52 | input_files = [ 53 | "scrubs.s01e01.hdtv.fake.avi", 54 | "scrubs.s01e01.hdtv.fake.srt", 55 | "scrubs.s01e01.hdtv.fake-CZ.srt", 56 | "scrubs.s01e01.hdtv.fake-EN.srt", 57 | "my.name.is.earl.s01e01.fake.avi", 58 | "my.name.is.earl.s01e01.some.other.fake.eng.srt", 59 | "my.name.is.earl.s01e01.fake.cze.srt", 60 | ] 61 | expected_files = [ 62 | "Scrubs - [01x01] - My First Day.avi", 63 | "Scrubs - [01x01] - My First Day.srt", 64 | "Scrubs - [01x01] - My First Day-CZ.srt", 65 | "Scrubs - [01x01] - My First Day-EN.srt", 66 | "My Name Is Earl - [01x01] - Pilot.avi", 67 | "My Name Is Earl - [01x01] - Pilot.eng.srt", 68 | "My Name Is Earl - [01x01] - Pilot.cze.srt", 69 | ] 70 | 71 | out_data = run_tvnamer( 72 | with_files = input_files, 73 | with_config = conf, 74 | with_input = "") 75 | 76 | verify_out_data(out_data, expected_files) 77 | -------------------------------------------------------------------------------- /tests/test_filename_blacklist.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Tests ignoreing files by regexp (e.g. all files with "sample" in the name) 4 | """ 5 | 6 | from functional_runner import run_tvnamer, verify_out_data 7 | from helpers import attr 8 | 9 | 10 | @attr("functional") 11 | def test_no_blacklist(): 12 | """Tests empty list of filename regexps is parsed as expected 13 | """ 14 | 15 | conf = """ 16 | {"always_rename": true, 17 | "select_first": true, 18 | "filename_blacklist": []} 19 | """ 20 | 21 | out_data = run_tvnamer( 22 | with_files = ['scrubs.s01e01.avi', 'scrubs.s01e02.avi'], 23 | with_config = conf) 24 | 25 | expected_files = [ 26 | 'Scrubs - [01x01] - My First Day.avi', 27 | 'Scrubs - [01x02] - My Mentor.avi'] 28 | 29 | verify_out_data(out_data, expected_files) 30 | 31 | 32 | @attr("functional") 33 | def test_partial_blacklist_using_simple_match(): 34 | """Tests single match of filename blacklist using a simple match 35 | """ 36 | 37 | conf = """ 38 | {"always_rename": true, 39 | "select_first": true, 40 | "filename_blacklist": [ 41 | {"is_regex": false, 42 | "match": "s02e01"} 43 | ] 44 | } 45 | """ 46 | 47 | out_data = run_tvnamer( 48 | with_files = ['scrubs.s01e01.avi', 'scrubs.s02e01.avi', 'scrubs.s02e02.avi'], 49 | with_config = conf) 50 | 51 | expected_files = [ 52 | 'Scrubs - [01x01] - My First Day.avi', 53 | 'scrubs.s02e01.avi', 54 | 'Scrubs - [02x02] - My Nightingale.avi'] 55 | 56 | verify_out_data(out_data, expected_files) 57 | 58 | 59 | @attr("functional") 60 | def test_partial_blacklist_using_regex(): 61 | """Tests single match of filename blacklist using a regex match 62 | """ 63 | 64 | conf = """ 65 | {"always_rename": true, 66 | "select_first": true, 67 | "filename_blacklist": [ 68 | {"is_regex": true, 69 | "match": ".*s02e01.*"} 70 | ] 71 | } 72 | """ 73 | 74 | out_data = run_tvnamer( 75 | with_files = ['scrubs.s01e01.avi', 'scrubs.s02e01.avi', 'scrubs.s02e02.avi'], 76 | with_config = conf) 77 | 78 | expected_files = [ 79 | 'Scrubs - [01x01] - My First Day.avi', 80 | 'scrubs.s02e01.avi', 81 | 'Scrubs - [02x02] - My Nightingale.avi'] 82 | 83 | verify_out_data(out_data, expected_files) 84 | 85 | 86 | @attr("functional") 87 | def test_partial_blacklist_using_mix(): 88 | """Tests single match of filename blacklist using a mix of regex and simple match 89 | """ 90 | 91 | conf = """ 92 | {"always_rename": true, 93 | "select_first": true, 94 | "filename_blacklist": [ 95 | {"is_regex": true, 96 | "match": ".*s02e01.*"}, 97 | {"is_regex": false, 98 | "match": "s02e02"} 99 | ] 100 | } 101 | """ 102 | 103 | out_data = run_tvnamer( 104 | with_files = ['scrubs.s01e01.avi', 'scrubs.s02e01.avi', 'scrubs.s02e02.avi'], 105 | with_config = conf) 106 | 107 | expected_files = [ 108 | 'Scrubs - [01x01] - My First Day.avi', 109 | 'scrubs.s02e01.avi', 110 | 'scrubs.s02e02.avi'] 111 | 112 | verify_out_data(out_data, expected_files) 113 | 114 | 115 | @attr("functional") 116 | def test_full_blacklist(): 117 | """Tests complete blacklist of all filenames with a regex 118 | """ 119 | 120 | conf = """ 121 | {"always_rename": true, 122 | "select_first": true, 123 | "filename_blacklist": [ 124 | {"is_regex": true, 125 | "match": ".*"} 126 | ] 127 | } 128 | """ 129 | 130 | out_data = run_tvnamer( 131 | with_files = ['scrubs.s01e01.avi', 'scrubs.s02e01.avi', 'scrubs.s02e02.avi'], 132 | with_config = conf) 133 | 134 | expected_files = ['scrubs.s01e01.avi', 'scrubs.s02e01.avi', 'scrubs.s02e02.avi'] 135 | 136 | verify_out_data(out_data, expected_files, expected_returncode = 2) 137 | 138 | 139 | @attr("functional") 140 | def test_dotfiles(): 141 | """Tests blacklisting filename beginning with "." 142 | """ 143 | 144 | conf = """ 145 | {"always_rename": true, 146 | "select_first": true, 147 | "filename_blacklist": [ 148 | {"is_regex": true, 149 | "match": "^\\\\..*"} 150 | ] 151 | } 152 | """ 153 | 154 | out_data = run_tvnamer( 155 | with_files = ['.scrubs.s01e01.avi', 'scrubs.s02e02.avi'], 156 | with_config = conf) 157 | 158 | expected_files = ['.scrubs.s01e01.avi', 'Scrubs - [02x02] - My Nightingale.avi'] 159 | 160 | verify_out_data(out_data, expected_files, expected_returncode = 0) 161 | 162 | 163 | @attr("functional") 164 | def test_blacklist_fullpath(): 165 | """Blacklist against full path 166 | """ 167 | 168 | conf = """ 169 | {"always_rename": true, 170 | "select_first": true, 171 | "filename_blacklist": [ 172 | {"is_regex": true, 173 | "full_path": true, 174 | "match": ".*/subdir/.*"} 175 | ] 176 | } 177 | """ 178 | 179 | out_data = run_tvnamer( 180 | with_files = ['subdir/scrubs.s01e01.avi'], 181 | with_config = conf) 182 | 183 | expected_files = ['subdir/scrubs.s01e01.avi'] 184 | 185 | verify_out_data(out_data, expected_files, expected_returncode = 2) 186 | 187 | 188 | @attr("functional") 189 | def test_blacklist_exclude_extension(): 190 | """Blacklist against full path 191 | """ 192 | 193 | conf = """ 194 | {"always_rename": true, 195 | "select_first": true, 196 | "filename_blacklist": [ 197 | {"is_regex": true, 198 | "full_path": true, 199 | "exclude_extension": true, 200 | "match": "\\\\.avi"} 201 | ] 202 | } 203 | """ 204 | 205 | out_data = run_tvnamer( 206 | with_files = ['scrubs.s01e01.avi'], 207 | with_config = conf) 208 | 209 | expected_files = ['Scrubs - [01x01] - My First Day.avi'] 210 | 211 | verify_out_data(out_data, expected_files, expected_returncode = 0) 212 | 213 | 214 | @attr("functional") 215 | def test_simple_blacklist(): 216 | """Blacklist with simple strings 217 | """ 218 | 219 | conf = """ 220 | {"always_rename": true, 221 | "select_first": true, 222 | "filename_blacklist": [ 223 | "scrubs.s02e01.avi" 224 | ] 225 | } 226 | """ 227 | 228 | out_data = run_tvnamer( 229 | with_files = ['scrubs.s01e01.avi', 'scrubs.s02e01.avi', 'scrubs.s02e02.avi'], 230 | with_config = conf) 231 | 232 | expected_files = [ 233 | 'Scrubs - [01x01] - My First Day.avi', 234 | 'scrubs.s02e01.avi', 235 | 'Scrubs - [02x02] - My Nightingale.avi'] 236 | 237 | verify_out_data(out_data, expected_files) 238 | 239 | 240 | @attr("functional") 241 | def test_simple_blacklist_mixed(): 242 | """Blacklist with simple strings, mixed with the more complex dict 243 | option (which allows regexs and matching against extension) 244 | """ 245 | 246 | conf = """ 247 | {"always_rename": true, 248 | "select_first": true, 249 | "filename_blacklist": [ 250 | "scrubs.s02e01.avi", 251 | {"is_regex": true, 252 | "match": ".*s\\\\d+e02.*"} 253 | ] 254 | } 255 | """ 256 | 257 | out_data = run_tvnamer( 258 | with_files = ['scrubs.s01e01.avi', 'scrubs.s02e01.avi', 'scrubs.s02e02.avi'], 259 | with_config = conf) 260 | 261 | expected_files = [ 262 | 'Scrubs - [01x01] - My First Day.avi', 263 | 'scrubs.s02e01.avi', 264 | 'scrubs.s02e02.avi'] 265 | 266 | verify_out_data(out_data, expected_files) 267 | -------------------------------------------------------------------------------- /tests/test_fileparse_api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Tests the FileParser API 4 | """ 5 | 6 | from tvnamer.files import FileParser 7 | from tvnamer.data import EpisodeInfo, DatedEpisodeInfo, NoSeasonEpisodeInfo 8 | from helpers import assertType, assertEquals 9 | 10 | 11 | def test_episodeinfo(): 12 | """Parsing a s01e01 episode should return EpisodeInfo class 13 | """ 14 | p = FileParser("scrubs.s01e01.avi").parse() 15 | assertType(p, EpisodeInfo) 16 | 17 | 18 | def test_datedepisodeinfo(): 19 | """Parsing a 2009.06.05 episode should return DatedEpisodeInfo class 20 | """ 21 | p = FileParser("scrubs.2009.06.05.avi").parse() 22 | assertType(p, DatedEpisodeInfo) 23 | 24 | 25 | def test_noseasonepisodeinfo(): 26 | """Parsing a e23 episode should return NoSeasonEpisodeInfo class 27 | """ 28 | p = FileParser("scrubs - e23.avi").parse() 29 | assertType(p, NoSeasonEpisodeInfo) 30 | 31 | 32 | def test_episodeinfo_naming(): 33 | """Parsing a s01e01 episode should return EpisodeInfo class 34 | """ 35 | p = FileParser("scrubs.s01e01.avi").parse() 36 | assertType(p, EpisodeInfo) 37 | assertEquals(p.generate_filename(), "scrubs - [01x01].avi") 38 | 39 | 40 | def test_datedepisodeinfo_naming(): 41 | """Parsing a 2009.06.05 episode should return DatedEpisodeInfo class 42 | """ 43 | p = FileParser("scrubs.2009.06.05.avi").parse() 44 | assertType(p, DatedEpisodeInfo) 45 | assertEquals(p.generate_filename(), "scrubs - [2009-06-05].avi") 46 | 47 | 48 | def test_noseasonepisodeinfo_naming(): 49 | """Parsing a e23 episode should return NoSeasonEpisodeInfo class 50 | """ 51 | p = FileParser("scrubs - e23.avi").parse() 52 | assertType(p, NoSeasonEpisodeInfo) 53 | assertEquals(p.generate_filename(), "scrubs - [23].avi") 54 | -------------------------------------------------------------------------------- /tests/test_files.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Test file names for tvnamer 4 | """ 5 | 6 | import datetime 7 | 8 | 9 | files = {} 10 | 11 | files['default_format'] = [ 12 | {'input': 'Scrubs - [04x19] - My Best Laid Plans', 13 | 'parsedseriesname': 'Scrubs', 14 | 'correctedseriesname': 'Scrubs', 15 | 'seasonnumber': 4, 'episodenumbers': [19], 16 | 'episodenames': ['My Best Laid Plans']}, 17 | 18 | {'input': 'Scrubs - [02x11]', 19 | 'parsedseriesname': 'Scrubs', 20 | 'correctedseriesname': 'Scrubs', 21 | 'seasonnumber': 2, 'episodenumbers': [11], 22 | 'episodenames': ['My Sex Buddy']}, 23 | 24 | {'input': 'Scrubs - [04X19] - My Best Laid Plans', 25 | 'parsedseriesname': 'Scrubs', 26 | 'correctedseriesname': 'Scrubs', 27 | 'seasonnumber': 4, 'episodenumbers': [19], 28 | 'episodenames': ['My Best Laid Plans']}, 29 | ] 30 | 31 | files['s01e01_format'] = [ 32 | {'input': 'scrubs.s01e01', 33 | 'parsedseriesname': 'scrubs', 34 | 'correctedseriesname': 'Scrubs', 35 | 'seasonnumber': 1, 'episodenumbers': [1], 36 | 'episodenames': ['My First Day']}, 37 | 38 | {'input': 'my.name.is.earl.s01e01', 39 | 'parsedseriesname': 'my name is earl', 40 | 'correctedseriesname': 'My Name Is Earl', 41 | 'seasonnumber': 1, 'episodenumbers': [1], 42 | 'episodenames': ['Pilot']}, 43 | 44 | {'input': 'scrubs.s01e24.blah.fake', 45 | 'parsedseriesname': 'scrubs', 46 | 'correctedseriesname': 'Scrubs', 47 | 'seasonnumber': 1, 'episodenumbers': [24], 48 | 'episodenames': ['My Last Day']}, 49 | 50 | {'input': 'dexter.s04e05.720p.blah', 51 | 'parsedseriesname': 'dexter', 52 | 'correctedseriesname': 'Dexter', 53 | 'seasonnumber': 4, 'episodenumbers': [5], 54 | 'episodenames': ['Dirty Harry']}, 55 | 56 | {'input': 'QI.S04E01.2006-09-29.blah', 57 | 'parsedseriesname': 'QI', 58 | 'correctedseriesname': 'QI', 59 | 'seasonnumber': 4, 'episodenumbers': [1], 60 | 'episodenames': ['Danger']}, 61 | 62 | {'input': 'The Wire s05e10 30.mp4', 63 | 'parsedseriesname': 'The Wire', 64 | 'correctedseriesname': 'The Wire', 65 | 'seasonnumber': 5, 'episodenumbers': [10], 66 | 'episodenames': ['-30-']}, 67 | 68 | {'input': 'Arrested Development - S2 E 02 - Dummy Ep Name.blah', 69 | 'parsedseriesname': 'Arrested Development', 70 | 'correctedseriesname': 'Arrested Development', 71 | 'seasonnumber': 2, 'episodenumbers': [2], 72 | 'episodenames': ['The One Where They Build a House']}, 73 | 74 | {'input': 'Next Stop Discovery - s2001e02 - Arkawa line.avi', 75 | 'parsedseriesname': 'Next Stop Discovery', 76 | 'correctedseriesname': 'Next Stop, Discovery', 77 | 'seasonnumber': 2001, 'episodenumbers': [2], 78 | 'episodenames': ['Arakawa line']}, 79 | 80 | {'input': 'next.stop.discovery.s2001e02.arkawa.line.avi', 81 | 'parsedseriesname': 'next stop discovery', 82 | 'correctedseriesname': 'Next Stop, Discovery', 83 | 'seasonnumber': 2001, 'episodenumbers': [2], 84 | 'episodenames': ['Arakawa line']}, 85 | 86 | {'input': 'next stop discovery - [2001x03] - Total Isolation.avi', 87 | 'parsedseriesname': 'next stop discovery', 88 | 'correctedseriesname': 'Next Stop, Discovery', 89 | 'seasonnumber': 2001, 'episodenumbers': [3], 90 | 'episodenames': ['Spring winds in the Bay City']}, 91 | 92 | {'input': 'Scrubs 1x01-720p.avi', 93 | 'parsedseriesname': 'Scrubs', 94 | 'correctedseriesname': 'Scrubs', 95 | 'seasonnumber': 1, 'episodenumbers': [1], 96 | 'episodenames': ['My First Day']}, 97 | 98 | {'input': 'Scrubs - [s01e01].avi', 99 | 'parsedseriesname': 'Scrubs', 100 | 'correctedseriesname': 'Scrubs', 101 | 'seasonnumber': 1, 'episodenumbers': [1], 102 | 'episodenames': ['My First Day']}, 103 | 104 | {'input': 'Scrubs - [01.01].avi', 105 | 'parsedseriesname': 'Scrubs', 106 | 'correctedseriesname': 'Scrubs', 107 | 'seasonnumber': 1, 'episodenumbers': [1], 108 | 'episodenames': ['My First Day']}, 109 | 110 | {'input': '30 Rock [2.10] Episode 210.avi', 111 | 'parsedseriesname': '30 Rock', 112 | 'correctedseriesname': '30 Rock', 113 | 'seasonnumber': 2, 'episodenumbers': [10], 114 | 'episodenames': ['Episode 210']}, 115 | 116 | {'input': 'scrubs.s01_e01.avi', 117 | 'parsedseriesname': 'Scrubs', 118 | 'correctedseriesname': 'Scrubs', 119 | 'seasonnumber': 1, 'episodenumbers': [1], 120 | 'episodenames': ['My First Day']}, 121 | 122 | {'input': 'scrubs - s01 - e02 - something.avi', 123 | 'parsedseriesname': 'Scrubs', 124 | 'correctedseriesname': 'Scrubs', 125 | 'seasonnumber': 1, 'episodenumbers': [2], 126 | 'episodenames': ['My Mentor']}, 127 | ] 128 | 129 | files['misc'] = [ 130 | {'input': 'Six.Feet.Under.S0201.test_testing-yay', 131 | 'parsedseriesname': 'Six Feet Under', 132 | 'correctedseriesname': 'Six Feet Under', 133 | 'seasonnumber': 2, 'episodenumbers': [1], 134 | 'episodenames': ['In the Game']}, 135 | 136 | {'input': 'Sid.The.Science.Kid.E11.The.Itchy.Tag.WS.ABC.DeF-HIJK', 137 | 'parsedseriesname': 'Sid The Science Kid', 138 | 'correctedseriesname': 'Sid the Science Kid', 139 | 'seasonnumber': None, 'episodenumbers': [11], 140 | 'episodenames': ['The Itchy Tag']}, 141 | 142 | {'input': 'Total Access - [01x01]', 143 | 'parsedseriesname': 'total access', 144 | 'correctedseriesname': 'Total Access 24/7', 145 | 'seasonnumber': 1, 'episodenumbers': [1], 146 | 'episodenames': ['Episode #1']}, 147 | 148 | {'input': 'Scrubs - Episode 2 [S 1 - Ep 002] - Fri 24 Jan 2001 [KCRT].avi', 149 | 'parsedseriesname': 'Scrubs', 150 | 'correctedseriesname': 'Scrubs', 151 | 'seasonnumber': 1, 'episodenumbers': [2], 152 | 'episodenames': ['My Mentor']}, 153 | 154 | {'input': 'Scrubs Season 01 Episode 01 - The Series Title.avi', 155 | 'parsedseriesname': 'Scrubs', 156 | 'correctedseriesname': 'Scrubs', 157 | 'seasonnumber': 1, 'episodenumbers': [1], 158 | 'episodenames': ['My First Day']}, 159 | ] 160 | 161 | files['multiple_episodes'] = [ 162 | {'input': 'Scrubs - [01x01-02-03]', 163 | 'parsedseriesname': 'Scrubs', 164 | 'correctedseriesname': 'Scrubs', 165 | 'seasonnumber': 1, 'episodenumbers': [1, 2, 3], 166 | 'episodenames': ['My First Day', 'My Mentor', 'My Best Friend\'s Mistake']}, 167 | 168 | {'input': 'scrubs.s01e23e24', 169 | 'parsedseriesname': 'scrubs', 170 | 'correctedseriesname': 'Scrubs', 171 | 'seasonnumber': 1, 'episodenumbers': [23, 24], 172 | 'episodenames': ['My Hero', 'My Last Day']}, 173 | 174 | {'input': 'scrubs.01x23x24', 175 | 'parsedseriesname': 'scrubs', 176 | 'correctedseriesname': 'Scrubs', 177 | 'seasonnumber': 1, 'episodenumbers': [23, 24], 178 | 'episodenames': ['My Hero', 'My Last Day']}, 179 | 180 | {'input': 'scrubs.01x23-24', 181 | 'parsedseriesname': 'scrubs', 182 | 'correctedseriesname': 'Scrubs', 183 | 'seasonnumber': 1, 'episodenumbers': [23, 24], 184 | 'episodenames': ['My Hero', 'My Last Day']}, 185 | 186 | {'input': 'Stargate SG-1 - [01x01-02]', 187 | 'parsedseriesname': 'Stargate SG-1', 188 | 'correctedseriesname': 'Stargate SG-1', 189 | 'seasonnumber': 1, 'episodenumbers': [1, 2], 190 | 'episodenames': ['Children of the Gods (1)', 'Children of the Gods (2)']}, 191 | 192 | {'input': '[Lunar] Victory Gundam - 11-12 [B937F496]', 193 | 'parsedseriesname': 'Victory Gundam', 194 | 'correctedseriesname': 'Mobile Suit Victory Gundam', 195 | 'seasonnumber': None, 'episodenumbers': [11, 12], 196 | 'episodenames': ['The Shrike Team Bulwark', 'Smash the Guillotine']}, 197 | 198 | {'input': 'scrubs.s01e01e02e03', 199 | 'parsedseriesname': 'scrubs', 200 | 'correctedseriesname': 'Scrubs', 201 | 'seasonnumber': 1, 'episodenumbers': [1, 2, 3], 202 | 'episodenames': ['My First Day', 'My Mentor', 'My Best Friend\'s Mistake']}, 203 | 204 | {'input': 'Scrubs - [02x01-03]', 205 | 'parsedseriesname': 'scrubs', 206 | 'correctedseriesname': 'Scrubs', 207 | 'seasonnumber': 2, 'episodenumbers': [1, 2, 3], 208 | 'episodenames': ['My Overkill', 'My Nightingale', 'My Case Study']}, 209 | 210 | {'input': 'Scrubs - [02x01+02]', 211 | 'parsedseriesname': 'scrubs', 212 | 'correctedseriesname': 'Scrubs', 213 | 'seasonnumber': 2, 'episodenumbers': [1, 2], 214 | 'episodenames': ['My Overkill', 'My Nightingale']}, 215 | 216 | {'input': 'Scrubs 2x01+02', 217 | 'parsedseriesname': 'scrubs', 218 | 'correctedseriesname': 'Scrubs', 219 | 'seasonnumber': 2, 'episodenumbers': [1, 2], 220 | 'episodenames': ['My Overkill', 'My Nightingale']}, 221 | 222 | {'input': 'Flight.of.the.Conchords.S01E01-02.An.Ep.name.avi', 223 | 'parsedseriesname': 'Flight of the Conchords', 224 | 'correctedseriesname': 'Flight of the Conchords', 225 | 'seasonnumber': 1, 'episodenumbers': [1, 2], 226 | 'episodenames': ['Sally', 'Bret Gives Up the Dream']}, 227 | 228 | {'input': 'Flight.of.the.Conchords.S01E02e01.An.Ep.name.avi', 229 | 'parsedseriesname': 'Flight of the Conchords', 230 | 'correctedseriesname': 'Flight of the Conchords', 231 | 'seasonnumber': 1, 'episodenumbers': [1, 2], 232 | 'episodenames': ['Sally', 'Bret Gives Up the Dream']}, 233 | 234 | {'input': 'Scrubs s01e22 s01e23 s01e24.avi', 235 | 'parsedseriesname': 'Scrubs', 236 | 'correctedseriesname': 'Scrubs', 237 | 'seasonnumber': 1, 'episodenumbers': [22, 23, 24], 238 | 'episodenames': ['My Occurrence', 'My Hero', 'My Last Day']}, 239 | 240 | {'input': 'Scrubs s01e22 s01e23.avi', 241 | 'parsedseriesname': 'Scrubs', 242 | 'correctedseriesname': 'Scrubs', 243 | 'seasonnumber': 1, 'episodenumbers': [22, 23], 244 | 'episodenames': ['My Occurrence', 'My Hero']}, 245 | 246 | {'input': 'Scrubs - 01x22 01x23.avi', 247 | 'parsedseriesname': 'Scrubs', 248 | 'correctedseriesname': 'Scrubs', 249 | 'seasonnumber': 1, 'episodenumbers': [22, 23], 250 | 'episodenames': ['My Occurrence', 'My Hero']}, 251 | 252 | {'input': 'Scrubs.01x22.01x23.avi', 253 | 'parsedseriesname': 'Scrubs', 254 | 'correctedseriesname': 'Scrubs', 255 | 'seasonnumber': 1, 'episodenumbers': [22, 23], 256 | 'episodenames': ['My Occurrence', 'My Hero']}, 257 | 258 | {'input': 'Scrubs 1x22 1x23.avi', 259 | 'parsedseriesname': 'Scrubs', 260 | 'correctedseriesname': 'Scrubs', 261 | 'seasonnumber': 1, 'episodenumbers': [22, 23], 262 | 'episodenames': ['My Occurrence', 'My Hero']}, 263 | 264 | {'input': 'Scrubs.S01E01-E04.avi', 265 | 'parsedseriesname': 'Scrubs', 266 | 'correctedseriesname': 'Scrubs', 267 | 'seasonnumber': 1, 'episodenumbers': [1, 2, 3, 4], 268 | 'episodenames': ['My First Day', 'My Mentor', 'My Best Friend\'s Mistake', 'My Old Lady']}, 269 | 270 | ] 271 | 272 | files['unicode'] = [ 273 | {'input': 'Carniv\xe0le 1x11 - The Day of the Dead', 274 | 'parsedseriesname': 'Carniv\xe0le', 275 | 'correctedseriesname': 'Carniv\xe0le', 276 | 'seasonnumber': 1, 'episodenumbers': [11], 277 | 'episodenames': ['The Day of the Dead']}, 278 | 279 | {'input': 'The Big Bang Theory - S02E07 - The Panty Pi\xf1ata Polarization.avi', 280 | 'parsedseriesname': 'The Big Bang Theory', 281 | 'correctedseriesname': 'The Big Bang Theory', 282 | 'seasonnumber': 2, 'episodenumbers': [7], 283 | 'episodenames': ['The Panty Pi\xf1ata Polarization']}, 284 | 285 | {'input': 'NCIS - 1x16.avi', 286 | 'parsedseriesname': 'NCIS', 287 | 'correctedseriesname': 'NCIS', 288 | 'seasonnumber': 1, 'episodenumbers': [16], 289 | 'episodenames': ['B\xeate Noire']}, 290 | ] 291 | 292 | files['anime'] = [ 293 | {'input': '[Eclipse] Fullmetal Alchemist Brotherhood - 02 (1280x720 h264) [8452C4BF].mkv', 294 | 'parsedseriesname': 'Fullmetal Alchemist Brotherhood', 295 | 'correctedseriesname': 'Fullmetal Alchemist: Brotherhood', 296 | 'seasonnumber': None, 'episodenumbers': [2], 297 | 'episodenames': ['The First Day']}, 298 | 299 | {'input': '[Shinsen-Subs] Armored Trooper Votoms - 01 [9E3F1D1C].mkv', 300 | 'parsedseriesname': 'armored trooper votoms', 301 | 'correctedseriesname': 'Armored Trooper VOTOMS', 302 | 'seasonnumber': None, 'episodenumbers': [1], 303 | 'episodenames': ['War\'s End']}, 304 | 305 | {'input': '[AG-SHS]Victory_Gundam-03_DVD[FC6E3A6F].mkv', 306 | 'parsedseriesname': 'victory gundam', 307 | 'correctedseriesname': 'Mobile Suit Victory Gundam', 308 | 'seasonnumber': None, 'episodenumbers': [3], 309 | 'episodenames': ['\xdcso\'s Battle']}, 310 | 311 | {'input': '[YuS-SHS]Gintama-24(H264)_[52CA4F8B].mkv', 312 | 'parsedseriesname': 'gintama', 313 | 'correctedseriesname': 'Gintama', 314 | 'seasonnumber': None, 'episodenumbers': [24], 315 | 'episodenames': ['Cute Faces Are Always Hiding Something']}, 316 | 317 | {'input': '[Shinsen-Subs] True Mazinger - 07 [848x480 H.264 Vorbis][787D0074].mkv', 318 | 'parsedseriesname': 'True Mazinger', 319 | 'correctedseriesname': 'Mazinger Edition Z: The Impact!', 320 | 'seasonnumber': None, 'episodenumbers': [7], 321 | 'episodenames': ['Legend! The Mechanical Beasts of Bardos!']}, 322 | 323 | {'input': '[BSS]_Tokyo_Magnitude_8.0_-_02_[0E5C4A40].mkv', 324 | 'parsedseriesname': 'tokyo magnitude 8.0', 325 | 'correctedseriesname': 'Tokyo Magnitude 8.0', 326 | 'seasonnumber': None, 'episodenumbers': [2], 327 | 'episodenames': ['Broken World']}, 328 | ] 329 | 330 | files['date_based'] = [ 331 | {'input': 'Scrubs.2001-10-02.avi', 332 | 'parsedseriesname': 'Scrubs', 333 | 'correctedseriesname': 'Scrubs', 334 | 'episodenumbers': [datetime.date(2001, 10, 2)], 335 | 'episodenames': ['My First Day']}, 336 | 337 | {'input': 'Scrubs - 2001-10-02 - Old Episode Title.avi', 338 | 'parsedseriesname': 'Scrubs', 339 | 'correctedseriesname': 'Scrubs', 340 | 'episodenumbers': [datetime.date(2001, 10, 2)], 341 | 'episodenames': ['My First Day']}, 342 | 343 | {'input': 'Scrubs - 2001.10.02 - Old Episode Title.avi', 344 | 'parsedseriesname': 'Scrubs', 345 | 'correctedseriesname': 'Scrubs', 346 | 'episodenumbers': [datetime.date(2001, 10, 2)], 347 | 'episodenames': ['My First Day']}, 348 | 349 | {'input': 'yes.we.canberra.2010.08.18.pdtv.xvid', 350 | 'parsedseriesname': 'yes we canberra', 351 | 'correctedseriesname': 'Yes We Canberra', 352 | 'episodenumbers': [datetime.date(2010, 8, 18)], 353 | 'episodenames': ['Episode 4']}, 354 | ] 355 | 356 | files['x_of_x'] = [ 357 | {'input': 'Scrubs.1of5.avi', 358 | 'parsedseriesname': 'Scrubs', 359 | 'correctedseriesname': 'Scrubs', 360 | 'seasonnumber': None, 'episodenumbers': [1], 361 | 'episodenames': ['My First Day']}, 362 | 363 | {'input': 'Scrubs part 1.avi', 364 | 'parsedseriesname': 'Scrubs', 365 | 'correctedseriesname': 'Scrubs', 366 | 'seasonnumber': None, 'episodenumbers': [1], 367 | 'episodenames': ['My First Day']}, 368 | 369 | {'input': 'Scrubs part 1 of 10.avi', # only one episode, as it's not "1 to 10" 370 | 'parsedseriesname': 'Scrubs', 371 | 'correctedseriesname': 'Scrubs', 372 | 'seasonnumber': None, 'episodenumbers': [1], 373 | 'episodenames': ['My First Day']}, 374 | 375 | {'input': 'Scrubs part 1 and part 2.avi', 376 | 'parsedseriesname': 'Scrubs', 377 | 'correctedseriesname': 'Scrubs', 378 | 'seasonnumber': None, 'episodenumbers': [1, 2], 379 | 'episodenames': ['My First Day', 'My Mentor']}, 380 | 381 | {'input': 'Scrubs part 1 to part 3.avi', 382 | 'parsedseriesname': 'Scrubs', 383 | 'correctedseriesname': 'Scrubs', 384 | 'seasonnumber': None, 'episodenumbers': [1, 2, 3], 385 | 'episodenames': ['My First Day', 'My Mentor', 'My Best Friend\'s Mistake']}, 386 | 387 | {'input': 'Scrubs part 1 to 4.avi', 388 | 'parsedseriesname': 'Scrubs', 389 | 'correctedseriesname': 'Scrubs', 390 | 'seasonnumber': None, 'episodenumbers': [1, 2, 3, 4], 391 | 'episodenames': ['My First Day', 'My Mentor', 'My Best Friend\'s Mistake', 'My Old Lady']}, 392 | 393 | ] 394 | 395 | 396 | files['no_series_name'] = [ 397 | {'input': 's01e01.avi', 398 | 'force_name': 'Scrubs', 399 | 'parsedseriesname': None, 400 | 'correctedseriesname': 'Scrubs', 401 | 'seasonnumber': 1, 'episodenumbers': [1], 402 | 'episodenames': ['My First Day']}, 403 | 404 | {'input': '[01x01].avi', 405 | 'force_name': 'Scrubs', 406 | 'parsedseriesname': None, 407 | 'correctedseriesname': 'Scrubs', 408 | 'seasonnumber': 1, 'episodenumbers': [1], 409 | 'episodenames': ['My First Day']}, 410 | ] 411 | 412 | 413 | def test_verify_test_data_sanity(): 414 | """Checks all test data is consistent. 415 | 416 | Keys within each test category must be consistent, but keys can vary 417 | category to category. E.g date-based episodes do not have a season number 418 | """ 419 | from helpers import assertEquals 420 | 421 | for test_category, testcases in files.items(): 422 | keys = [ctest.keys() for ctest in testcases] 423 | for k1 in keys: 424 | for k2 in keys: 425 | assertEquals(sorted(k1), sorted(k2)) 426 | -------------------------------------------------------------------------------- /tests/test_force_series.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Test ability to set the series name by series id 4 | """ 5 | 6 | from functional_runner import run_tvnamer, verify_out_data 7 | from helpers import attr 8 | 9 | 10 | @attr("functional") 11 | def test_series_id(): 12 | """Test --series-id argument 13 | """ 14 | 15 | conf = """ 16 | {"always_rename": true, 17 | "select_first": true} 18 | """ 19 | 20 | out_data = run_tvnamer( 21 | with_files = ['whatever.s01e01.avi'], 22 | with_config = conf, 23 | with_flags = ["--series-id", '76156'], 24 | with_input = "") 25 | 26 | expected_files = ['Scrubs - [01x01] - My First Day.avi'] 27 | 28 | verify_out_data(out_data, expected_files) 29 | 30 | 31 | @attr("functional") 32 | def test_series_id_with_nameless_series(): 33 | """Test --series-id argument with '6x17.etc.avi' type filename 34 | """ 35 | 36 | conf = """ 37 | {"always_rename": true, 38 | "select_first": true} 39 | """ 40 | 41 | out_data = run_tvnamer( 42 | with_files = ['s01e01.avi'], 43 | with_config = conf, 44 | with_flags = ["--series-id", '76156'], 45 | with_input = "") 46 | 47 | expected_files = ['Scrubs - [01x01] - My First Day.avi'] 48 | 49 | verify_out_data(out_data, expected_files) 50 | -------------------------------------------------------------------------------- /tests/test_functional.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Functional tests for tvnamer tests 4 | """ 5 | 6 | import os 7 | from functional_runner import run_tvnamer, verify_out_data 8 | from helpers import attr 9 | import pytest 10 | 11 | 12 | @attr("functional") 13 | def test_simple_single_file(): 14 | """Test most simple usage 15 | """ 16 | 17 | out_data = run_tvnamer( 18 | with_files = ['scrubs.s01e01.avi'], 19 | with_input = "1\ny\n") 20 | 21 | expected_files = ['Scrubs - [01x01] - My First Day.avi'] 22 | 23 | verify_out_data(out_data, expected_files) 24 | 25 | 26 | @attr("functional") 27 | def test_simple_multiple_files(): 28 | """Tests simple interactive usage with multiple files 29 | """ 30 | 31 | input_files = [ 32 | 'scrubs.s01e01.hdtv.fake.avi', 33 | 'my.name.is.earl.s01e01.fake.avi', 34 | 'a.nonsensical.fake.show.s12e24.fake.avi', 35 | 'total.access.s01e01.avi'] 36 | 37 | expected_files = [ 38 | 'Scrubs - [01x01] - My First Day.avi', 39 | 'My Name Is Earl - [01x01] - Pilot.avi', 40 | 'a nonsensical fake show - [12x24].avi', 41 | 'Total Access 24_7 - [01x01] - Episode #1.avi'] 42 | 43 | out_data = run_tvnamer( 44 | with_files = input_files, 45 | with_input = "y\n1\ny\n1\ny\n1\ny\ny\n") 46 | 47 | verify_out_data(out_data, expected_files) 48 | 49 | 50 | @attr("functional") 51 | def test_simple_batch_functionality(): 52 | """Tests renaming single files at a time, in batch mode 53 | """ 54 | 55 | tests = [ 56 | {'in':'scrubs.s01e01.hdtv.fake.avi', 57 | 'expected':'Scrubs - [01x01] - My First Day.avi'}, 58 | {'in':'my.name.is.earl.s01e01.fake.avi', 59 | 'expected':'My Name Is Earl - [01x01] - Pilot.avi'}, 60 | {'in':'a.fake.show.s12e24.fake.avi', 61 | 'expected':'a.fake.show.s12e24.fake.avi'}, 62 | {'in': 'total.access.s01e01.avi', 63 | 'expected': 'Total Access 24_7 - [01x01] - Episode #1.avi'}, 64 | ] 65 | 66 | for curtest in tests: 67 | 68 | print("Expecting %r to turn into %r" % ( 69 | curtest['in'], curtest['expected'])) 70 | out_data = run_tvnamer( 71 | with_files = [curtest['in'], ], 72 | with_flags = ['--batch'], 73 | ) 74 | verify_out_data(out_data, [curtest['expected'], ]) 75 | 76 | 77 | @attr("functional") 78 | def test_interactive_always_option(): 79 | """Tests the "a" always rename option in interactive UI 80 | """ 81 | 82 | input_files = [ 83 | 'scrubs.s01e01.hdtv.fake.avi', 84 | 'my.name.is.earl.s01e01.fake.avi', 85 | 'a.nonsensical.fake.show.s12e24.fake.avi', 86 | 'total.access.s01e01.avi'] 87 | 88 | expected_files = [ 89 | 'Scrubs - [01x01] - My First Day.avi', 90 | 'My Name Is Earl - [01x01] - Pilot.avi', 91 | 'a nonsensical fake show - [12x24].avi', 92 | 'Total Access 24_7 - [01x01] - Episode #1.avi'] 93 | 94 | out_data = run_tvnamer( 95 | with_files = input_files, 96 | with_flags = ["--selectfirst"], 97 | with_input = "a\n") 98 | 99 | verify_out_data(out_data, expected_files) 100 | 101 | 102 | @attr("functional") 103 | @pytest.mark.skipif(os.getenv("TRAVIS", "false")=="true", reason="Test fails for some reason on Travis-CI") 104 | def test_unicode_in_inputname(): 105 | """Tests parsing a file with unicode in the input filename 106 | """ 107 | 108 | import os, sys 109 | if os.getenv("TRAVIS", "false") == "true" and sys.version_info[0:2] == (2.6): 110 | from nose.plugins.skip import SkipTest 111 | raise SkipTest("Ignoring test which triggers bizarre bug in nosetests, in python 2.6, only on travis.") 112 | 113 | 114 | input_files = [ 115 | 'The Big Bang Theory - S02E07 - The Panty Pin\u0303ata Polarization.avi'] 116 | 117 | expected_files = [ 118 | 'The Big Bang Theory - [02x07] - The Panty Pin\u0303ata Polarization.avi'] 119 | 120 | out_data = run_tvnamer( 121 | with_files = input_files, 122 | with_flags = ["--batch"]) 123 | 124 | verify_out_data(out_data, expected_files) 125 | 126 | 127 | @attr("functional") 128 | def test_unicode_in_search_results(): 129 | """Show with unicode in search results 130 | """ 131 | input_files = [ 132 | 'psych.s04e11.avi'] 133 | 134 | expected_files = [ 135 | 'Psych - [04x11] - Thrill Seekers and Hell-Raisers.avi'] 136 | 137 | out_data = run_tvnamer( 138 | with_files = input_files, 139 | with_input = '1\ny\n') 140 | 141 | verify_out_data(out_data, expected_files) 142 | 143 | 144 | @attr("functional") 145 | def test_renaming_always_doesnt_overwrite(): 146 | """If trying to rename a file that exists, should not create new file 147 | """ 148 | input_files = [ 149 | 'Scrubs.s01e01.avi', 150 | 'Scrubs - [01x01] - My First Day.avi'] 151 | 152 | expected_files = [ 153 | 'Scrubs.s01e01.avi', 154 | 'Scrubs - [01x01] - My First Day.avi'] 155 | 156 | out_data = run_tvnamer( 157 | with_files = input_files, 158 | with_flags = ['--batch']) 159 | 160 | verify_out_data(out_data, expected_files) 161 | 162 | 163 | @attr("functional") 164 | @pytest.mark.skipif(os.getenv("TRAVIS", "false")=="true", reason="Test fails for some reason on Travis-CI") 165 | @pytest.mark.skipif(os.getenv("CI", "false")=="true", reason="Test fails for some reason on GH Actions") 166 | def test_not_overwritting_unicode_filename(): 167 | """Test no error occurs when warning about a unicode filename being overwritten 168 | """ 169 | input_files = [ 170 | 'The Big Bang Theory - S02E07.avi', 171 | 'The Big Bang Theory - [02x07] - The Panty Pin\u0303ata Polarization.avi'] 172 | 173 | expected_files = [ 174 | 'The Big Bang Theory - S02E07.avi', 175 | 'The Big Bang Theory - [02x07] - The Panty Pin\u0303ata Polarization.avi'] 176 | 177 | out_data = run_tvnamer( 178 | with_files = input_files, 179 | with_flags = ['--batch']) 180 | 181 | verify_out_data(out_data, expected_files) 182 | 183 | 184 | @attr("functional") 185 | def test_not_recursive(): 186 | """Tests the nested files aren't found when not recursive 187 | """ 188 | input_files = [ 189 | 'Scrubs.s01e01.avi', 190 | 'nested/subdir/Scrubs.s01e02.avi'] 191 | 192 | expected_files = [ 193 | 'Scrubs - [01x01] - My First Day.avi', 194 | 'nested/subdir/Scrubs.s01e02.avi'] 195 | 196 | out_data = run_tvnamer( 197 | with_files = input_files, 198 | with_flags = ['--not-recursive', '--batch'], 199 | run_on_directory = True) 200 | 201 | verify_out_data(out_data, expected_files) 202 | 203 | 204 | @attr("functional") 205 | def test_correct_filename(): 206 | """If the filename is already correct, don't prompt 207 | """ 208 | 209 | out_data = run_tvnamer( 210 | with_files = ['Scrubs - [01x01] - My First Day.avi'], 211 | with_input = "1\ny\n") 212 | 213 | expected_files = ['Scrubs - [01x01] - My First Day.avi'] 214 | 215 | verify_out_data(out_data, expected_files) 216 | 217 | 218 | @attr("functional") 219 | def test_filename_already_exists(): 220 | """Don't overwrite 221 | """ 222 | 223 | out_data = run_tvnamer( 224 | with_files = ['Scrubs - [01x01] - My First Day.avi', 'Scrubs.s01e01.avi'], 225 | with_input = "1\ny\n") 226 | 227 | expected_files = ['Scrubs - [01x01] - My First Day.avi', 'Scrubs.s01e01.avi'] 228 | 229 | verify_out_data(out_data, expected_files) 230 | 231 | 232 | @attr("functional") 233 | def test_no_seasonnumber(): 234 | """Test episode with no series number 235 | """ 236 | 237 | out_data = run_tvnamer( 238 | with_files = ['scrubs.e01.avi'], 239 | with_flags = ['--batch']) 240 | 241 | expected_files = ['Scrubs - [01] - My First Day.avi'] 242 | 243 | verify_out_data(out_data, expected_files) 244 | 245 | 246 | @attr("functional") 247 | def test_skipping_after_replacements(): 248 | """When custom-replacement is specified, should still skip file if name is correct 249 | """ 250 | 251 | conf = """ 252 | {"select_first": true, 253 | "input_filename_replacements": [ 254 | {"is_regex": false, 255 | "match": "v", 256 | "replacement": "u"} 257 | ], 258 | "output_filename_replacements": [ 259 | {"is_regex": false, 260 | "match": "u", 261 | "replacement": "v"} 262 | ] 263 | } 264 | """ 265 | 266 | out_data = run_tvnamer( 267 | with_files = ['Scrvbs - [01x01] - My First Day.avi'], 268 | with_config = conf, 269 | with_input = "") 270 | 271 | expected_files = ['Scrvbs - [01x01] - My First Day.avi'] 272 | 273 | verify_out_data(out_data, expected_files) 274 | 275 | @attr("functional") 276 | def test_dvd_order(): 277 | """Tests TvDB dvd order naming 278 | """ 279 | 280 | input_files = [ 281 | 'batman the animated series s01e01.xvid'] 282 | 283 | expected_files = [ 284 | 'Batman - The Animated Series - [01x01] - On Leather Wings.xvid'] 285 | 286 | conf = r""" 287 | { 288 | "output_filename_replacements": [ 289 | {"is_regex": true, 290 | "match": ": ", 291 | "replacement": " - "} 292 | ] 293 | } 294 | """ 295 | 296 | out_data = run_tvnamer( 297 | with_files = input_files, 298 | with_flags = ["--order", 'dvd'], 299 | with_input = "1\ny\n", 300 | with_config = conf) 301 | 302 | verify_out_data(out_data, expected_files) 303 | 304 | 305 | @attr("functional") 306 | def test_show_version(): 307 | """Tests the --version arg 308 | """ 309 | 310 | input_files = ['scrubs.s01e01.avi'] 311 | # Shouldn't touch files 312 | expected_files = ['scrubs.s01e01.avi'] 313 | 314 | out_data = run_tvnamer( 315 | with_files = input_files, 316 | with_flags = ["--version"]) 317 | 318 | print(out_data['output']) 319 | import tvnamer 320 | assert "%s" % (tvnamer.__version__,) in out_data['output'] 321 | -------------------------------------------------------------------------------- /tests/test_invalid_files.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Ensure that invalid files (non-episodes) are not renamed 4 | """ 5 | 6 | from functional_runner import run_tvnamer, verify_out_data 7 | from helpers import attr 8 | 9 | 10 | @attr("functional") 11 | def test_simple_single_file(): 12 | """Boring example 13 | """ 14 | 15 | out_data = run_tvnamer( 16 | with_files = ['Some File.avi'], 17 | with_flags = ["--batch"]) 18 | 19 | expected_files = ['Some File.avi'] 20 | 21 | verify_out_data(out_data, expected_files, expected_returncode = 2) 22 | 23 | 24 | @attr("functional") 25 | def test_no_series_name(): 26 | """File without series name should be skipped (unless '--name=MySeries' arg is supplied) 27 | """ 28 | 29 | out_data = run_tvnamer( 30 | with_files = ['s01e01 Some File.avi'], 31 | with_flags = ["--batch"]) 32 | 33 | expected_files = ['s01e01 Some File.avi'] 34 | 35 | verify_out_data(out_data, expected_files, expected_returncode = 2) 36 | 37 | 38 | @attr("functional") 39 | def test_ambigious(): 40 | invalid_files = [ 41 | 'show.123.avi', # Maybe s01e23 but too ambigious. #140 42 | 'Scrubs.0101.avi', # Ambigious. #140 43 | ] 44 | 45 | for f in invalid_files: 46 | out_data = run_tvnamer( 47 | with_files = [f], 48 | with_flags = ["--batch"]) 49 | 50 | expected_files = [f] 51 | 52 | verify_out_data(out_data, expected_files, expected_returncode = 2) 53 | -------------------------------------------------------------------------------- /tests/test_limit_by_extension.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Tests the valid_extensions config option 4 | """ 5 | 6 | from functional_runner import run_tvnamer, verify_out_data 7 | from helpers import attr 8 | 9 | 10 | @attr("functional") 11 | def test_no_extensions(): 12 | """Tests empty list of extensions is parsed as expected 13 | """ 14 | 15 | conf = """ 16 | {"always_rename": true, 17 | "select_first": true, 18 | "valid_extensions": []} 19 | """ 20 | 21 | out_data = run_tvnamer( 22 | with_files = ['scrubs.s01e01.avi', 'scrubs.s01e02.mkv'], 23 | with_config = conf) 24 | 25 | expected_files = [ 26 | 'Scrubs - [01x01] - My First Day.avi', 27 | 'Scrubs - [01x02] - My Mentor.mkv'] 28 | 29 | verify_out_data(out_data, expected_files) 30 | 31 | 32 | @attr("functional") 33 | def test_single_extensions(): 34 | """Tests one valid extension with multiple files 35 | """ 36 | 37 | conf = """ 38 | {"always_rename": true, 39 | "select_first": true, 40 | "valid_extensions": ["mkv"]} 41 | """ 42 | 43 | out_data = run_tvnamer( 44 | with_files = ['scrubs.s01e01.avi', 'scrubs.s01e02.mkv'], 45 | with_config = conf) 46 | 47 | expected_files = [ 48 | 'scrubs.s01e01.avi', 49 | 'Scrubs - [01x02] - My Mentor.mkv'] 50 | 51 | verify_out_data(out_data, expected_files) 52 | 53 | 54 | @attr("functional") 55 | def test_single_extension_with_subdirs(): 56 | """Tests one valid extension recursing into sub-dirs 57 | """ 58 | 59 | conf = """ 60 | {"always_rename": true, 61 | "select_first": true, 62 | "valid_extensions": ["avi"], 63 | "recursive": true} 64 | """ 65 | 66 | out_data = run_tvnamer( 67 | with_files = ['scrubs.s01e01.avi', 'testdir/scrubs.s01e02.mkv', 'testdir/scrubs.s01e04.avi'], 68 | with_config = conf, 69 | run_on_directory = True) 70 | 71 | expected_files = [ 72 | 'Scrubs - [01x01] - My First Day.avi', 73 | 'testdir/scrubs.s01e02.mkv', 74 | 'testdir/Scrubs - [01x04] - My Old Lady.avi'] 75 | 76 | verify_out_data(out_data, expected_files) 77 | -------------------------------------------------------------------------------- /tests/test_movingfiles.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Tests moving renamed files 4 | """ 5 | 6 | from functional_runner import run_tvnamer, verify_out_data 7 | from helpers import attr 8 | 9 | 10 | @attr("functional") 11 | def test_simple_realtive_move(): 12 | """Move file to simple relative static dir 13 | """ 14 | 15 | conf = """ 16 | {"move_files_enable": true, 17 | "move_files_destination": "test/", 18 | "batch": true} 19 | """ 20 | 21 | out_data = run_tvnamer( 22 | with_files = ['scrubs.s01e01.avi'], 23 | with_config = conf, 24 | with_input = "") 25 | 26 | expected_files = ['test/Scrubs - [01x01] - My First Day.avi'] 27 | 28 | verify_out_data(out_data, expected_files) 29 | 30 | 31 | @attr("functional") 32 | def test_dynamic_destination(): 33 | """Move file to simple relative static dir 34 | """ 35 | 36 | conf = """ 37 | {"move_files_enable": true, 38 | "move_files_destination": "tv/%(seriesname)s/season %(seasonnumber)d/", 39 | "batch": true} 40 | """ 41 | 42 | out_data = run_tvnamer( 43 | with_files = ['scrubs.s01e01.avi'], 44 | with_config = conf) 45 | 46 | expected_files = ['tv/Scrubs/season 1/Scrubs - [01x01] - My First Day.avi'] 47 | 48 | verify_out_data(out_data, expected_files) 49 | 50 | 51 | @attr("functional") 52 | def test_cli_destination(): 53 | """Tests specifying the destination via command line argument 54 | """ 55 | 56 | out_data = run_tvnamer( 57 | with_files = ['scrubs.s01e01.avi'], 58 | with_flags = ['--batch', '--move', '--movedestination=season %(seasonnumber)d/']) 59 | 60 | expected_files = ['season 1/Scrubs - [01x01] - My First Day.avi'] 61 | 62 | verify_out_data(out_data, expected_files) 63 | 64 | 65 | @attr("functional") 66 | def test_move_interactive_allyes(): 67 | """Tests interactive UI for moving all files 68 | """ 69 | 70 | conf = """ 71 | {"move_files_enable": true, 72 | "move_files_destination": "test", 73 | "select_first": true} 74 | """ 75 | 76 | out_data = run_tvnamer( 77 | with_files = ['scrubs.s01e01.avi', 'scrubs.s01e02.avi'], 78 | with_config = conf, 79 | with_input = "y\ny\ny\ny\n") 80 | 81 | expected_files = ['test/Scrubs - [01x01] - My First Day.avi', 82 | 'test/Scrubs - [01x02] - My Mentor.avi'] 83 | 84 | verify_out_data(out_data, expected_files) 85 | 86 | 87 | @attr("functional") 88 | def test_move_interactive_allno(): 89 | """Tests interactive UI allows not moving any files 90 | """ 91 | 92 | conf = """ 93 | {"move_files_enable": true, 94 | "move_files_destination": "test", 95 | "select_first": true} 96 | """ 97 | 98 | out_data = run_tvnamer( 99 | with_files = ['scrubs.s01e01.avi', 'scrubs.s01e02.avi'], 100 | with_config = conf, 101 | with_input = "y\nn\ny\nn\n") 102 | 103 | expected_files = ['Scrubs - [01x01] - My First Day.avi', 104 | 'Scrubs - [01x02] - My Mentor.avi'] 105 | 106 | verify_out_data(out_data, expected_files) 107 | 108 | 109 | @attr("functional") 110 | def test_move_interactive_somefiles(): 111 | """Tests interactive UI allows not renaming some files, renaming/moving others 112 | 113 | Rename and move first file, don't rename second file (so no move), and 114 | rename but do not move last file (Input is: y/y, n, y/n) 115 | """ 116 | 117 | conf = """ 118 | {"move_files_enable": true, 119 | "move_files_destination": "test", 120 | "select_first": true} 121 | """ 122 | 123 | out_data = run_tvnamer( 124 | with_files = ['scrubs.s01e01.avi', 'scrubs.s01e02.avi', 'scrubs.s01e03.avi'], 125 | with_config = conf, 126 | with_input = "y\ny\nn\ny\nn\n") 127 | 128 | expected_files = ['test/Scrubs - [01x01] - My First Day.avi', 129 | 'scrubs.s01e02.avi', 130 | 'Scrubs - [01x03] - My Best Friend\'s Mistake.avi'] 131 | 132 | verify_out_data(out_data, expected_files) 133 | 134 | 135 | @attr("functional") 136 | def test_with_invalid_seriesname(): 137 | """Tests series name containing invalid filename characters 138 | """ 139 | 140 | conf = """ 141 | {"move_files_enable": true, 142 | "move_files_destination": "%(seriesname)s", 143 | "batch": true, 144 | "windows_safe_filenames": true} 145 | """ 146 | 147 | out_data = run_tvnamer( 148 | with_files = ['csi.cyber.s01e03.avi'], 149 | with_config = conf) 150 | 151 | expected_files = ['CSI_ Cyber/CSI_ Cyber - [01x03] - Killer En Route.avi'] 152 | 153 | verify_out_data(out_data, expected_files) 154 | 155 | 156 | @attr("functional") 157 | def test_with_invalid_seriesname_test2(): 158 | """Another test for series name containing invalid filename characters 159 | """ 160 | 161 | conf = """ 162 | {"move_files_enable": true, 163 | "move_files_destination": "%(seriesname)s", 164 | "batch": true, 165 | "move_files_fullpath_replacements": [ 166 | {"is_regex": true, 167 | "match": "CSI_ Miami", 168 | "replacement": "CSI"}], 169 | "windows_safe_filenames": true} 170 | """ 171 | 172 | out_data = run_tvnamer( 173 | with_files = ['csi.miami.s01e01.avi'], 174 | with_config = conf) 175 | 176 | expected_files = ['CSI/CSI - [01x01] - Golden Parachute.avi'] 177 | 178 | verify_out_data(out_data, expected_files) 179 | 180 | 181 | @attr("functional") 182 | def test_move_files_lowercase_destination(): 183 | """Test move_files_lowercase_destination configuration option. 184 | """ 185 | 186 | conf = """ 187 | {"move_files_enable": true, 188 | "move_files_destination": "Test/This/%(seriesname)s/S%(seasonnumber)02d", 189 | "move_files_lowercase_destination": true, 190 | "batch": true} 191 | """ 192 | 193 | out_data = run_tvnamer( 194 | with_files = ['scrubs.s01e01.This.Is.a.Test.avi'], 195 | with_config = conf, 196 | with_input = "") 197 | 198 | expected_files = ['Test/This/scrubs/S01/Scrubs - [01x01] - My First Day.avi'] 199 | 200 | verify_out_data(out_data, expected_files) 201 | 202 | 203 | @attr("functional") 204 | def test_move_date_based_episode(): 205 | """Moving a date-base episode (lighthouse ticket #56) 206 | """ 207 | 208 | conf = """ 209 | {"move_files_enable": true, 210 | "move_files_destination_date": "Test/%(seriesname)s/%(year)s/%(month)s/%(day)s", 211 | "move_files_lowercase_destination": true, 212 | "batch": true} 213 | """ 214 | 215 | out_data = run_tvnamer( 216 | with_files = ['The Colbert Report - 2011-09-28 Ken Burns.avi'], 217 | with_config = conf, 218 | with_input = "") 219 | 220 | expected_files = ['Test/The Colbert Report/2011/9/28/The Colbert Report - [2011-09-28] - Ken Burns.avi'] 221 | 222 | verify_out_data(out_data, expected_files) 223 | 224 | 225 | @attr("functional") 226 | def test_move_files_full_filepath_simple(): 227 | """Moving file destination including a fixed filename 228 | """ 229 | 230 | conf = """ 231 | {"move_files_enable": true, 232 | "move_files_destination": "TestDir/%(seriesname)s/season %(seasonnumber)02d/%(episodenumbers)s/SpecificName.avi", 233 | "move_files_destination_is_filepath": true, 234 | "batch": true} 235 | """ 236 | 237 | out_data = run_tvnamer( 238 | with_files = ['scrubs.s01e02.avi'], 239 | with_config = conf, 240 | with_input = "") 241 | 242 | expected_files = ['TestDir/Scrubs/season 01/02/SpecificName.avi'] 243 | 244 | verify_out_data(out_data, expected_files) 245 | 246 | 247 | @attr("functional") 248 | def test_move_files_full_filepath_with_origfilename(): 249 | """Moving file destination including a filename 250 | """ 251 | 252 | conf = """ 253 | {"move_files_enable": true, 254 | "move_files_destination": "TestDir/%(seriesname)s/season %(seasonnumber)02d/%(episodenumbers)s/%(originalfilename)s", 255 | "move_files_destination_is_filepath": true, 256 | "batch": true} 257 | """ 258 | 259 | out_data = run_tvnamer( 260 | with_files = ['scrubs.s01e01.avi', 'scrubs.s01e02.avi'], 261 | with_config = conf, 262 | with_input = "") 263 | 264 | expected_files = [ 265 | 'TestDir/Scrubs/season 01/01/scrubs.s01e01.avi', 266 | 'TestDir/Scrubs/season 01/02/scrubs.s01e02.avi'] 267 | 268 | verify_out_data(out_data, expected_files) 269 | 270 | 271 | @attr("functional") 272 | def test_move_with_correct_name(): 273 | """Files with correct name should still be moved 274 | """ 275 | 276 | conf = """ 277 | {"move_files_enable": true, 278 | "move_files_destination": "SubDir", 279 | "batch": true} 280 | """ 281 | 282 | out_data = run_tvnamer( 283 | with_files = ['Scrubs - [01x02] - My Mentor.avi'], 284 | with_config = conf, 285 | with_input = "y\n") 286 | 287 | expected_files = ['SubDir/Scrubs - [01x02] - My Mentor.avi'] 288 | 289 | verify_out_data(out_data, expected_files) 290 | 291 | 292 | @attr("functional") 293 | def test_move_no_season(): 294 | """Files with no season number should moveable [#94] 295 | """ 296 | 297 | conf = """ 298 | {"move_files_enable": true, 299 | "move_files_destination": "SubDir", 300 | "batch": true} 301 | """ 302 | 303 | out_data = run_tvnamer( 304 | with_files = ['Scrubs - [02] - My Mentor.avi'], 305 | with_config = conf, 306 | with_input = "y\n") 307 | 308 | expected_files = ['SubDir/Scrubs - [02] - My Mentor.avi'] 309 | 310 | verify_out_data(out_data, expected_files) 311 | 312 | 313 | @attr("functional") 314 | def test_move_files_only(): 315 | """With parameter move_files_only set to true files should be moved and not renamed 316 | """ 317 | 318 | conf = """ 319 | {"move_files_only": true, 320 | "move_files_enable": true, 321 | "move_files_destination": "tv/%(seriesname)s/season %(seasonnumber)d/", 322 | "batch": true} 323 | """ 324 | 325 | out_data = run_tvnamer( 326 | with_files = ['scrubs.s01e01.avi'], 327 | with_config = conf) 328 | 329 | expected_files = ['tv/Scrubs/season 1/scrubs.s01e01.avi'] 330 | 331 | verify_out_data(out_data, expected_files) 332 | 333 | 334 | @attr("functional") 335 | def test_forcefully_moving_enabled(): 336 | """Forcefully moving files, overwriting destination 337 | """ 338 | 339 | conf = """ 340 | {"move_files_enable": true, 341 | "move_files_destination": "tv/%(seriesname)s/season %(seasonnumber)d/", 342 | "batch": true, 343 | "overwrite_destination_on_move": true} 344 | """ 345 | 346 | out_data = run_tvnamer( 347 | with_files = ['scrubs.s01e01.avi', 'Scrubs - [01x01] - My First Day.avi'], 348 | with_config = conf) 349 | 350 | expected_files = ['tv/Scrubs/season 1/Scrubs - [01x01] - My First Day.avi'] 351 | 352 | verify_out_data(out_data, expected_files) 353 | 354 | 355 | @attr("functional") 356 | def test_forcefully_moving_disabled(): 357 | """Explicitly disable forcefully moving files 358 | """ 359 | 360 | conf = """ 361 | {"move_files_enable": true, 362 | "move_files_destination": "tv/%(seriesname)s/season %(seasonnumber)d/", 363 | "batch": true, 364 | "overwrite_destination_on_move": false} 365 | """ 366 | 367 | out_data = run_tvnamer( 368 | with_files = ['scrubs.s01e01.avi', 'scrubs - [01x01].avi'], 369 | with_config = conf) 370 | 371 | expected_files = [ 372 | 'Scrubs - [01x01] - My First Day.avi', 373 | 'tv/Scrubs/season 1/Scrubs - [01x01] - My First Day.avi'] 374 | 375 | verify_out_data(out_data, expected_files) 376 | 377 | 378 | @attr("functional") 379 | def test_forcefully_moving_default(): 380 | """Ensure default is not overwrite destination 381 | """ 382 | 383 | conf = """ 384 | {"move_files_enable": true, 385 | "move_files_destination": "tv/%(seriesname)s/season %(seasonnumber)d/", 386 | "batch": true} 387 | """ 388 | 389 | out_data = run_tvnamer( 390 | with_files = ['scrubs.s01e01.avi', 'scrubs - [01x01].avi'], 391 | with_config = conf) 392 | 393 | expected_files = [ 394 | 'Scrubs - [01x01] - My First Day.avi', 395 | 'tv/Scrubs/season 1/Scrubs - [01x01] - My First Day.avi'] 396 | 397 | verify_out_data(out_data, expected_files) 398 | -------------------------------------------------------------------------------- /tests/test_multiepisode_filenames.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Tests multi-episode filename generation 4 | """ 5 | 6 | from functional_runner import run_tvnamer, verify_out_data 7 | from helpers import attr 8 | 9 | 10 | @attr("functional") 11 | def test_multiep_different_names(): 12 | """Default config - two different names are joined with 'multiep_join_name_with', 'multiep_format' doesn't matter 13 | """ 14 | 15 | conf = """ 16 | { 17 | "output_filename_replacements": [ 18 | {"is_regex": false, 19 | "match": ":", 20 | "replacement": " -"} 21 | ], 22 | 23 | "multiep_join_name_with": ", ", 24 | "batch": true, 25 | "multiep_format": "%(foo)s"} 26 | """ 27 | 28 | out_data = run_tvnamer( 29 | with_files = ["star.trek.enterprise.s01e03e04.avi"], 30 | with_config = conf, 31 | with_input = "") 32 | 33 | expected_files = ['Star Trek - Enterprise - [01x03-04] - Fight or Flight, Strange New World.avi'] 34 | 35 | verify_out_data(out_data, expected_files) 36 | 37 | @attr("functional") 38 | def test_multiep_same_names(): 39 | """Default config - same names, format according to 'multiep_format', 'multiep_join_name_with' doesn't matter 40 | """ 41 | 42 | conf = """ 43 | { 44 | "output_filename_replacements": [ 45 | {"is_regex": false, 46 | "match": ":", 47 | "replacement": " -"} 48 | ], 49 | "multiep_join_name_with": ", ", 50 | "batch": true, 51 | "multiep_format": "%(epname)s (%(episodemin)s-%(episodemax)s)"} 52 | """ 53 | 54 | out_data = run_tvnamer( 55 | with_files = ["star.trek.enterprise.s01e01e02.avi"], 56 | with_config = conf, 57 | with_input = "") 58 | 59 | expected_files = ['Star Trek - Enterprise - [01x01-02] - Broken Bow (1-2).avi'] 60 | 61 | verify_out_data(out_data, expected_files) 62 | 63 | @attr("functional") 64 | def test_multiep_same_names_without_number(): 65 | """Default config - same names, ensure that missing number doesn't matter 66 | """ 67 | 68 | conf = """ 69 | { 70 | "output_filename_replacements": [ 71 | {"is_regex": false, 72 | "match": ":", 73 | "replacement": " -"} 74 | ], 75 | 76 | "multiep_join_name_with": ", ", 77 | "batch": true, 78 | "multiep_format": "%(epname)s (Parts %(episodemin)s-%(episodemax)s)"} 79 | """ 80 | 81 | out_data = run_tvnamer( 82 | with_files = ["star.trek.deep.space.nine.s01e01e02.avi"], 83 | with_config = conf, 84 | with_input = "") 85 | 86 | expected_files = ['Star Trek - Deep Space Nine - [01x01-02] - Emissary (Parts 1-2).avi'] 87 | 88 | verify_out_data(out_data, expected_files) 89 | -------------------------------------------------------------------------------- /tests/test_name_generation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Test tvnamer's EpisodeInfo file name generation 4 | """ 5 | 6 | import os 7 | import datetime 8 | from typing import Any 9 | 10 | from helpers import assertEquals 11 | 12 | from tvnamer.main import TVNAMER_API_KEY 13 | from tvnamer.data import (EpisodeInfo, DatedEpisodeInfo, NoSeasonEpisodeInfo) 14 | from test_files import files 15 | 16 | from tvdb_api import Tvdb 17 | 18 | 19 | def verify_name_gen(curtest, tvdb_instance): 20 | # type: (Any, Tvdb) -> None 21 | if "seasonnumber" in curtest: 22 | ep = EpisodeInfo( 23 | seriesname = curtest['parsedseriesname'], 24 | seasonnumber = curtest['seasonnumber'], 25 | episodenumbers = curtest['episodenumbers']) 26 | elif any([isinstance(x, datetime.date) for x in curtest['episodenumbers']]): 27 | ep = DatedEpisodeInfo( 28 | seriesname = curtest['parsedseriesname'], 29 | episodenumbers = curtest['episodenumbers']) 30 | else: 31 | ep = NoSeasonEpisodeInfo( 32 | seriesname = curtest['parsedseriesname'], 33 | episodenumbers = curtest['episodenumbers']) 34 | 35 | ep.populate_from_tvdb(tvdb_instance, force_name = curtest.get("force_name")) 36 | 37 | assert ep.seriesname is not None, "Corrected series name was none" 38 | assert ep.episodename is not None, "Episode name was None" 39 | 40 | assertEquals(ep.seriesname, curtest['correctedseriesname']) 41 | assertEquals(ep.episodename, curtest['episodenames']) 42 | 43 | 44 | def test_name_generation_on_testfiles(): 45 | # type: () -> None 46 | 47 | # Test data stores episode names in English, language= is normally set 48 | # via the configuration, same with search_all_languages. 49 | 50 | tvdb_instance = Tvdb(search_all_languages=True, cache=True, language='en', apikey=TVNAMER_API_KEY) 51 | for category, testcases in files.items(): 52 | for curtest in testcases: 53 | verify_name_gen(curtest, tvdb_instance) 54 | 55 | 56 | def test_single_episode(): 57 | # type: () -> None 58 | """Simple episode name, with show/season/episode/name/filename 59 | """ 60 | 61 | ep = EpisodeInfo( 62 | seriesname = 'Scrubs', 63 | seasonnumber = 1, 64 | episodenumbers = [2], 65 | episodename = ['My Mentor'], 66 | filename = 'scrubs.example.file.avi') 67 | 68 | assertEquals( 69 | ep.generate_filename(), 70 | 'Scrubs - [01x02] - My Mentor.avi') 71 | 72 | 73 | def test_multi_episodes_continuous(): 74 | # type: () -> None 75 | """A two-part episode should not have the episode name repeated 76 | """ 77 | ep = EpisodeInfo( 78 | seriesname = 'Stargate SG-1', 79 | seasonnumber = 1, 80 | episodenumbers = [1, 2], 81 | episodename = [ 82 | 'Children of the Gods (1)', 83 | 'Children of the Gods (2)'], 84 | filename = 'stargate.example.file.avi') 85 | 86 | assertEquals( 87 | ep.generate_filename(), 88 | 'Stargate SG-1 - [01x01-02] - Children of the Gods (1-2).avi') 89 | 90 | 91 | def test_episode_numeric_title(): 92 | # type: () -> None 93 | """An episode with a name starting with a number should not be 94 | detected as a range 95 | """ 96 | 97 | ep = EpisodeInfo( 98 | seriesname = 'Star Trek TNG', 99 | seasonnumber = 1, 100 | episodenumbers = [15], 101 | episodename = [ 102 | '11001001' 103 | ], 104 | filename = 'STTNG-S01E15-11001001.avi') 105 | 106 | assertEquals( 107 | ep.generate_filename(), 108 | 'Star Trek TNG - [01x15] - 11001001.avi') 109 | 110 | 111 | def test_multi_episodes_seperate(): 112 | # type: () -> None 113 | """File with two episodes, but with different names 114 | """ 115 | ep = EpisodeInfo( 116 | seriesname = 'Stargate SG-1', 117 | seasonnumber = 1, 118 | episodenumbers = [2, 3], 119 | episodename = [ 120 | 'Children of the Gods (2)', 121 | 'The Enemy Within'], 122 | filename = 'stargate.example.file.avi') 123 | 124 | assertEquals( 125 | ep.generate_filename(), 126 | 'Stargate SG-1 - [01x02-03] - Children of the Gods (2), The Enemy Within.avi') 127 | 128 | 129 | def test_simple_no_ext(): 130 | # type: () -> None 131 | """Simple episode with out extension 132 | """ 133 | ep = EpisodeInfo( 134 | seriesname = 'Scrubs', 135 | seasonnumber = 1, 136 | episodenumbers = [2], 137 | episodename = ['My Mentor'], 138 | filename = None) 139 | 140 | assertEquals( 141 | ep.generate_filename(), 142 | 'Scrubs - [01x02] - My Mentor') 143 | 144 | 145 | def test_no_name(): 146 | # type: () -> None 147 | """Episode without a name 148 | """ 149 | ep = EpisodeInfo( 150 | seriesname = 'Scrubs', 151 | seasonnumber = 1, 152 | episodenumbers = [2], 153 | episodename = None, 154 | filename = 'scrubs.example.file.avi') 155 | 156 | assertEquals( 157 | ep.generate_filename(), 158 | 'Scrubs - [01x02].avi') 159 | 160 | 161 | def test_episode_no_name_no_ext(): 162 | # type: () -> None 163 | """EpisodeInfo with no name or extension 164 | """ 165 | ep = EpisodeInfo( 166 | seriesname = 'Scrubs', 167 | seasonnumber = 1, 168 | episodenumbers = [2], 169 | episodename = None, 170 | filename = None) 171 | 172 | assertEquals( 173 | ep.generate_filename(), 174 | 'Scrubs - [01x02]') 175 | 176 | 177 | def test_noseason_no_name_no_ext(): 178 | # type: () -> None 179 | """NoSeasonEpisodeInfo with no name or extension 180 | """ 181 | ep = NoSeasonEpisodeInfo( 182 | seriesname = 'Scrubs', 183 | episodenumbers = [2], 184 | episodename = None, 185 | filename = None) 186 | 187 | assertEquals( 188 | ep.generate_filename(), 189 | 'Scrubs - [02]') 190 | 191 | 192 | def test_datedepisode_no_name_no_ext(): 193 | # type: () -> None 194 | """DatedEpisodeInfo with no name or extension 195 | """ 196 | ep = DatedEpisodeInfo( 197 | seriesname = 'Scrubs', 198 | episodenumbers = [datetime.date(2010, 11, 23)], 199 | episodename = None, 200 | filename = None) 201 | 202 | assertEquals( 203 | ep.generate_filename(), 204 | 'Scrubs - [2010-11-23]') 205 | 206 | 207 | def test_no_series_number(): 208 | # type: () -> None 209 | """Episode without season number 210 | """ 211 | ep = NoSeasonEpisodeInfo( 212 | seriesname = 'Scrubs', 213 | episodenumbers = [2], 214 | episodename = ['My Mentor'], 215 | filename = None) 216 | 217 | assertEquals( 218 | ep.generate_filename(), 219 | 'Scrubs - [02] - My Mentor') 220 | 221 | 222 | def test_episode_number_formatting(): 223 | # type: () -> None 224 | from tvnamer.data import format_episode_name 225 | fmt = "%(epname)s (%(episodemin)s-%(episodemax)s)" 226 | joiner = ", " 227 | 228 | # Simple cases 229 | assert format_episode_name(['A test'], joiner, fmt) == 'A test' 230 | assert format_episode_name(['A test (1)'], joiner, fmt) == 'A test (1)' 231 | assert format_episode_name(['A test (1)', 'A test (2)'], joiner, fmt) == 'A test (1-2)' 232 | assert format_episode_name(['A test (1)', 'A test (2)', 'A test (3)'], joiner, fmt) == 'A test (1-3)' 233 | 234 | # Inconsistent episode names 235 | assert format_episode_name(['A test (1)', "Weirdness (2)"], joiner, fmt) == 'A test (1), Weirdness (2)' 236 | 237 | # Skip incomplete sequences 238 | assert format_episode_name(['A test (1)', 'A test (8)'], joiner, fmt) == 'A test (1), A test (8)' 239 | 240 | # Skip if numbers are duplicated 241 | assert format_episode_name(['A test (1)', 'A test (1)'], joiner, fmt) == 'A test (1), A test (1)' 242 | 243 | # First episode can miss name 244 | assert format_episode_name(['A test', 'A test (2)', 'A test (3)'], joiner, fmt) == 'A test (1-3)' 245 | 246 | # Different format options 247 | assert format_episode_name(['Yep', 'Thing'], "!", fmt) == 'Yep!Thing' 248 | assert format_episode_name(['A test (1)', 'A test (2)', 'A test (3)'], ",", "%(epname)s (%(episodemin)s to %(episodemax)s)") == 'A test (1 to 3)' 249 | -------------------------------------------------------------------------------- /tests/test_no_series_in_filename.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Ensure that invalid files (non-episodes) are not renamed 4 | """ 5 | 6 | from functional_runner import run_tvnamer, verify_out_data 7 | from helpers import attr 8 | 9 | 10 | @attr("functional") 11 | def test_simple_single_file(): 12 | """Files without series name should be skipped, unless --name=MySeries is specified 13 | """ 14 | 15 | out_data = run_tvnamer( 16 | with_files = ['S01E02 - Some File.avi'], 17 | with_flags = ["--batch"]) 18 | 19 | expected_files = ['S01E02 - Some File.avi'] 20 | 21 | verify_out_data(out_data, expected_files, expected_returncode = 2) 22 | 23 | 24 | @attr("functional") 25 | def test_simple_single_file_with_forced_seriesnames(): 26 | """Specifying 's01e01.avi' should parse when --name=SeriesName arg is specified 27 | """ 28 | 29 | out_data = run_tvnamer( 30 | with_files = ['S01E02 - Some File.avi'], 31 | with_flags = ["--batch", '--name', 'Scrubs']) 32 | 33 | expected_files = ['Scrubs - [01x02] - My Mentor.avi'] 34 | 35 | verify_out_data(out_data, expected_files) 36 | 37 | 38 | @attr("functional") 39 | def test_name_arg_skips_replacements(): 40 | """Should not apply input_filename_replacements to --name=SeriesName arg value 41 | """ 42 | 43 | conf = r""" 44 | {"always_rename": true, 45 | "select_first": true, 46 | 47 | "force_name": "Scrubs", 48 | 49 | "input_filename_replacements": [ 50 | {"is_regex": true, 51 | "match": "Scrubs", 52 | "replacement": "Blahblahblah"} 53 | ] 54 | } 55 | """ 56 | 57 | out_data = run_tvnamer( 58 | with_files = ['S01E02 - Some File.avi'], 59 | with_config = conf) 60 | 61 | expected_files = ['Scrubs - [01x02] - My Mentor.avi'] 62 | 63 | verify_out_data(out_data, expected_files) 64 | 65 | 66 | @attr("functional") 67 | def test_replacements_applied_before_force_name(): 68 | """input_filename_replacements apply to filename, before --name=SeriesName takes effect 69 | """ 70 | 71 | conf = r""" 72 | {"always_rename": true, 73 | "select_first": true, 74 | 75 | "force_name": "Scrubs", 76 | 77 | "input_filename_replacements": [ 78 | {"is_regex": true, 79 | "match": "S01E02 - ", 80 | "replacement": ""} 81 | ] 82 | } 83 | """ 84 | 85 | out_data = run_tvnamer( 86 | with_files = ['S01E02 - Some File.avi'], 87 | with_config = conf) 88 | 89 | expected_files = ['S01E02 - Some File.avi'] 90 | 91 | verify_out_data(out_data, expected_files, expected_returncode = 2) 92 | -------------------------------------------------------------------------------- /tests/test_override_seriesname.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Test ability to override the series name 4 | """ 5 | 6 | from functional_runner import run_tvnamer, verify_out_data 7 | from helpers import attr 8 | 9 | 10 | @attr("functional") 11 | def test_temp_override(): 12 | """Test --name argument 13 | """ 14 | 15 | conf = """ 16 | {"always_rename": true, 17 | "select_first": true} 18 | """ 19 | 20 | out_data = run_tvnamer( 21 | with_files = ['scrubs.s01e01.avi'], 22 | with_config = conf, 23 | with_flags = ["--name", "lost"], 24 | with_input = "") 25 | 26 | expected_files = ['Lost - [01x01] - Pilot (1).avi'] 27 | 28 | verify_out_data(out_data, expected_files) 29 | -------------------------------------------------------------------------------- /tests/test_parsing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Test tvnamer's filename parser 4 | """ 5 | 6 | from helpers import assertEquals 7 | 8 | from tvnamer.files import FileParser 9 | from tvnamer.data import (DatedEpisodeInfo, NoSeasonEpisodeInfo) 10 | 11 | from test_files import files 12 | 13 | 14 | def test_autogen_names(): 15 | """Tests set of standard filename formats with various data 16 | """ 17 | 18 | """Mostly based on scene naming standards: 19 | http://tvunderground.org.ru/forum/index.php?showtopic=8488 20 | 21 | %(seriesname)s becomes the seriesname, 22 | %(seasno)s becomes the season number, 23 | %(epno)s becomes the episode number. 24 | 25 | Each is string-formatted with seasons from 0 to 10, and ep 0 to 10 26 | """ 27 | 28 | name_formats = [ 29 | '%(seriesname)s.s%(seasno)de%(epno)d.dsr.nf.avi', # seriesname.s01e02.dsr.nf.avi 30 | '%(seriesname)s.S%(seasno)dE%(epno)d.PROPER.dsr.nf.avi', # seriesname.S01E02.PROPER.dsr.nf.avi 31 | '%(seriesname)s.s%(seasno)d.e%(epno)d.avi', # seriesname.s01.e02.avi 32 | '%(seriesname)s-s%(seasno)de%(epno)d.avi', # seriesname-s01e02.avi 33 | '%(seriesname)s-s%(seasno)de%(epno)d.the.wrong.ep.name.avi', # seriesname-s01e02.the.wrong.ep.name.avi 34 | '%(seriesname)s - [%(seasno)dx%(epno)d].avi', # seriesname - [01x02].avi 35 | '%(seriesname)s - [%(seasno)dx0%(epno)d].avi', # seriesname - [01x002].avi 36 | '%(seriesname)s-[%(seasno)dx%(epno)d].avi', # seriesname-[01x02].avi 37 | '%(seriesname)s [%(seasno)dx%(epno)d].avi', # seriesname [01x02].avi 38 | '%(seriesname)s [%(seasno)dx%(epno)d] the wrong ep name.avi', # seriesname [01x02] epname.avi 39 | '%(seriesname)s [%(seasno)dx%(epno)d] - the wrong ep name.avi', # seriesname [01x02] - the wrong ep name.avi 40 | '%(seriesname)s - [%(seasno)dx%(epno)d] - the wrong ep name.avi', # seriesname - [01x02] - the wrong ep name.avi 41 | '%(seriesname)s.%(seasno)dx%(epno)d.The_Wrong_ep_name.avi', # seriesname.01x02.epname.avi 42 | '%(seriesname)s_s%(seasno)de%(epno)d_The_Wrong_ep_na-me.avi', # seriesname_s1e02_epname.avi 43 | '%(seriesname)s - s%(seasno)de%(epno)d - dsr.nf.avi', # seriesname - s01e02 - dsr.nf.avi 44 | '%(seriesname)s - s%(seasno)de%(epno)d - the wrong ep name.avi', # seriesname - s01e02 - the wrong ep name.avi 45 | '%(seriesname)s - s%(seasno)de%(epno)d - the wrong ep name.avi', # seriesname - s01e02 - the_wrong_ep_name!.avi 46 | ] 47 | 48 | test_data = [ 49 | {'name': 'test_name_parser_unicode', 50 | 'description': 'Tests parsing show containing unicode characters', 51 | 'name_data': {'seriesname': 'T\xc3\xacnh Ng\xc6\xb0\xe1\xbb\x9di Hi\xe1\xbb\x87n \xc4\x90\xe1\xba\xa1i'}}, 52 | 53 | {'name': 'test_name_parser_basic', 54 | 'description': 'Tests most basic filename (simple seriesname)', 55 | 'name_data': {'seriesname': 'series name'}}, 56 | 57 | {'name': 'test_name_parser_showdashname', 58 | 'description': 'Tests with dash in seriesname', 59 | 'name_data': {'seriesname': 'S-how name'}}, 60 | 61 | {'name': 'test_name_parser_exclaim', 62 | 'description': 'Tests parsing show with exclamation mark', 63 | 'name_data': {'seriesname': 'Show name!'}}, 64 | 65 | {'name': 'test_name_parser_shownumeric', 66 | 'description': 'Tests with numeric show name', 67 | 'name_data': {'seriesname': '123'}}, 68 | 69 | {'name': 'test_name_parser_shownumericspaces', 70 | 'description': 'Tests with numeric show name, with spaces', 71 | 'name_data': {'seriesname': '123 2008'}}, 72 | ] 73 | 74 | for cdata in test_data: 75 | # Make new wrapped function 76 | def cur_test(): 77 | for seas in range(1, 11): 78 | for ep in range(1, 11): 79 | 80 | name_data = cdata['name_data'] 81 | 82 | name_data['seasno'] = seas 83 | name_data['epno'] = ep 84 | 85 | names = [x % name_data for x in name_formats] 86 | 87 | for cur in names: 88 | p = FileParser(cur).parse() 89 | 90 | assertEquals(p.episodenumbers, [name_data['epno']]) 91 | assertEquals(p.seriesname, name_data['seriesname']) 92 | # Only EpisodeInfo has seasonnumber 93 | if not isinstance(p, (DatedEpisodeInfo, NoSeasonEpisodeInfo)): 94 | assertEquals(p.seasonnumber, name_data['seasno']) 95 | #end cur_test 96 | 97 | print("Testing: %s" % cdata['description']) 98 | cur_test() 99 | 100 | 101 | def check_case(curtest): 102 | """Runs test case, used by test_parsing_generator 103 | """ 104 | parser = FileParser(curtest['input']) 105 | theep = parser.parse() 106 | 107 | if theep.seriesname is None and curtest['parsedseriesname'] is None: 108 | pass # allow for None seriesname 109 | else: 110 | assert theep.seriesname.lower() == curtest['parsedseriesname'].lower(), "%s == %s" % ( 111 | theep.seriesname.lower(), 112 | curtest['parsedseriesname'].lower()) 113 | 114 | assertEquals(theep.episodenumbers, curtest['episodenumbers']) 115 | if not isinstance(theep, (DatedEpisodeInfo, NoSeasonEpisodeInfo)): 116 | assertEquals(theep.seasonnumber, curtest['seasonnumber']) 117 | 118 | 119 | def test_parsing_generator(): 120 | """Generates test for each test case in test_files.py 121 | """ 122 | for category, testcases in files.items(): 123 | for curtest in testcases: 124 | check_case(curtest) 125 | 126 | 127 | if __name__ == '__main__': 128 | import nose 129 | nose.main() 130 | -------------------------------------------------------------------------------- /tests/test_safefilename.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Test the function to create safe filenames 4 | """ 5 | 6 | import platform 7 | 8 | from helpers import assertEquals 9 | 10 | from tvnamer.utils import make_valid_filename 11 | 12 | 13 | def test_basic(): 14 | """Test make_valid_filename does not mess up simple filenames 15 | """ 16 | assertEquals(make_valid_filename("test.avi"), "test.avi") 17 | assertEquals(make_valid_filename("Test File.avi"), "Test File.avi") 18 | assertEquals(make_valid_filename("Test"), "Test") 19 | 20 | 21 | def test_dirseperators(): 22 | """Tests make_valid_filename removes directory separators 23 | """ 24 | assertEquals(make_valid_filename("Test/File.avi"), "Test_File.avi") 25 | assertEquals(make_valid_filename("Test/File"), "Test_File") 26 | 27 | 28 | def test_windowsfilenames(): 29 | """Tests make_valid_filename windows_safe flag makes Windows-safe filenames 30 | """ 31 | assertEquals(make_valid_filename("Test/File.avi", windows_safe = True), "Test_File.avi") 32 | assertEquals(make_valid_filename("\\/:*?|\"", windows_safe = True), "______Evil___") 33 | assertEquals(make_valid_filename("COM2.txt", windows_safe = True), "_COM2.txt") 34 | assertEquals(make_valid_filename("COM2", windows_safe = True), "_COM2") 35 | 36 | 37 | def test_dotfilenames(): 38 | """Tests make_valid_filename on filenames only consisting of . 39 | """ 40 | assertEquals(make_valid_filename("."), "_.") 41 | assertEquals(make_valid_filename(".."), "_..") 42 | assertEquals(make_valid_filename("..."), "_...") 43 | assertEquals(make_valid_filename(".test.rc"), "_.test.rc") 44 | 45 | 46 | def test_customblacklist(): 47 | """Test make_valid_filename custom_blacklist feature 48 | """ 49 | assertEquals(make_valid_filename("Test.avi", custom_blacklist="e"), "T_st.avi") 50 | 51 | 52 | def test_replacewith(): 53 | """Tests replacing blacklisted character with custom characters 54 | """ 55 | assertEquals(make_valid_filename("My Test File.avi", custom_blacklist=" ", replace_with="."), "My.Test.File.avi") 56 | 57 | 58 | def _test_truncation(max_len, windows_safe): 59 | """Tests truncation works correctly. 60 | Called with different parameters for both Windows and Darwin/Linux. 61 | """ 62 | assertEquals(make_valid_filename("a" * 300, windows_safe = windows_safe), "a" * max_len) 63 | assertEquals(make_valid_filename("a" * 255 + ".avi", windows_safe = windows_safe), "a" * (max_len-4) + ".avi") 64 | assertEquals(make_valid_filename("a" * 251 + "b" * 10 + ".avi", windows_safe = windows_safe), "a" * (max_len-4) + ".avi") 65 | assertEquals(make_valid_filename("test." + "a" * 255, windows_safe = windows_safe), "test." + "a" * (max_len-5)) 66 | 67 | 68 | def test_truncation_darwinlinux(): 69 | """Tests make_valid_filename truncates filenames to valid length 70 | """ 71 | 72 | if platform.system() not in ['Darwin', 'Linux']: 73 | import nose 74 | raise nose.SkipTest("Test only valid on Darwin and Linux platform") 75 | 76 | _test_truncation(254, windows_safe = False) 77 | 78 | 79 | def test_truncation_windows(): 80 | """Tests truncate works on Windows (using windows_safe=True) 81 | """ 82 | _test_truncation(max_len = 254, windows_safe = True) 83 | -------------------------------------------------------------------------------- /tests/test_series_replacement.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Tests custom replacements on input/output files 4 | """ 5 | 6 | from functional_runner import run_tvnamer, verify_out_data 7 | from helpers import attr 8 | 9 | 10 | @attr("functional") 11 | def test_replace_input(): 12 | """Tests replacing strings in input files 13 | """ 14 | out_data = run_tvnamer( 15 | with_files = ['scruuuuuubs.s01e01.avi'], 16 | with_config = """ 17 | { 18 | "input_series_replacements": { 19 | "scru*bs": "scrubs"}, 20 | "always_rename": true, 21 | "select_first": true 22 | } 23 | """) 24 | 25 | expected_files = ['Scrubs - [01x01] - My First Day.avi'] 26 | 27 | verify_out_data(out_data, expected_files) 28 | 29 | 30 | @attr("functional") 31 | def test_replace_input_with_id(): 32 | """Map from a series name to a numberic TVDB ID 33 | """ 34 | 35 | out_data = run_tvnamer( 36 | with_files = ['seriesnamegoeshere.s01e01.avi'], 37 | with_config = """ 38 | { 39 | "input_series_replacements": { 40 | "seriesnamegoeshere": 76156}, 41 | "always_rename": true, 42 | "select_first": true 43 | } 44 | """) 45 | 46 | expected_files = ['Scrubs - [01x01] - My First Day.avi'] 47 | 48 | verify_out_data(out_data, expected_files) 49 | 50 | 51 | @attr("functional") 52 | def test_replace_output(): 53 | """Tests replacing strings in input files 54 | """ 55 | out_data = run_tvnamer( 56 | with_files = ['Scrubs.s01e01.avi'], 57 | with_config = """ 58 | { 59 | "output_series_replacements": { 60 | "Scrubs": "Replacement Series Name"}, 61 | "always_rename": true, 62 | "select_first": true 63 | } 64 | """) 65 | 66 | expected_files = ['Replacement Series Name - [01x01] - My First Day.avi'] 67 | 68 | verify_out_data(out_data, expected_files) 69 | 70 | 71 | @attr("functional") 72 | def test_replacements_mulitple_Files(): 73 | """Test for https://github.com/dbr/tvnamer/issues/150 - need to test replacement with multiple files specified 74 | """ 75 | 76 | conf = r""" 77 | {"always_rename": true, 78 | "select_first": true, 79 | "skip_file_on_error": false, 80 | "input_series_replacements": { 81 | "Example": 153021 82 | } 83 | } 84 | """ 85 | 86 | out_data = run_tvnamer( 87 | with_files = ['Example.s01e01.avi', 'Scrubs.s01e01.avi'], 88 | with_config = conf, 89 | with_input = "", 90 | run_on_directory = True) 91 | 92 | expected_files = ['The Walking Dead - [01x01] - Days Gone Bye.avi', 'Scrubs - [01x01] - My First Day.avi'] 93 | 94 | verify_out_data(out_data, expected_files) 95 | -------------------------------------------------------------------------------- /tests/test_system.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Tests the current system for things that might cause problems 4 | """ 5 | 6 | import os 7 | 8 | 9 | def test_nosavedconfig(): 10 | """A config at ~/.tvnamer.json could cause problems with some tests 11 | """ 12 | assert not os.path.isfile(os.path.expanduser("~/.tvnamer.json")), "~/.tvnamer.json exists, which could cause problems with some tests" 13 | assert not os.path.isfile(os.path.expanduser("~/.config/tvnamer/tvnamer.json")), "~/.config/tvnamer/tvnamer.json exists, which could cause problems with some tests" 14 | -------------------------------------------------------------------------------- /tvnamer/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """tvnamer - Automagical TV episode renamer 4 | 5 | Uses data from www.thetvdb.com (via tvdb_api) to rename TV episode files from 6 | "some.show.name.s01e01.blah.avi" to "Some Show Name - [01x01] - The First.avi" 7 | """ 8 | 9 | __version__ = "4.0-dev" 10 | __author__ = "dbr/Ben" 11 | 12 | import os 13 | 14 | if ( 15 | "TVNAMER_COVERAGE_SUBPROCESS" in os.environ 16 | and "COVERAGE_PROCESS_START" in os.environ 17 | ): 18 | # Hackery for coverage testing in functional tests, based on 19 | # https://coverage.readthedocs.io/en/coverage-5.1/subprocess.html#configuring-python-for-sub-process-coverage 20 | import coverage 21 | 22 | coverage.process_startup() 23 | -------------------------------------------------------------------------------- /tvnamer/__main__.py: -------------------------------------------------------------------------------- 1 | """Allows executing tvnamer like so: 2 | 3 | python -m tvnamer show.s01e01.avi etc 4 | """ 5 | 6 | from .main import main 7 | 8 | if __name__ == "__main__": 9 | main() 10 | -------------------------------------------------------------------------------- /tvnamer/_titlecase.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # fmt: off 5 | 6 | """ 7 | Original Perl version by: John Gruber http://daringfireball.net/ 10 May 2008 8 | Python version by Stuart Colville http://muffinresearch.co.uk 9 | License: http://www.opensource.org/licenses/mit-license.php 10 | """ 11 | 12 | import re 13 | 14 | __all__ = ['titlecase'] 15 | __version__ = '0.5.2' 16 | 17 | SMALL = 'a|an|and|as|at|but|by|en|for|if|in|of|on|or|the|to|v\\.?|via|vs\\.?' 18 | PUNCT = r"""!"#$%&'‘()*+,\-./:;?@[\\\]_`{|}~""" 19 | 20 | SMALL_WORDS = re.compile(r'^(%s)$' % SMALL, re.I) 21 | INLINE_PERIOD = re.compile(r'[a-z][.][a-z]', re.I) 22 | UC_ELSEWHERE = re.compile(r'[%s]*?[a-zA-Z]+[A-Z]+?' % PUNCT) 23 | CAPFIRST = re.compile(r"^[%s]*?([A-Za-z])" % PUNCT) 24 | SMALL_FIRST = re.compile(r'^([%s]*)(%s)\b' % (PUNCT, SMALL), re.I) 25 | SMALL_LAST = re.compile(r'\b(%s)[%s]?$' % (SMALL, PUNCT), re.I) 26 | SUBPHRASE = re.compile(r'([:.;?!][ ])(%s)' % SMALL) 27 | APOS_SECOND = re.compile(r"^[dol]{1}['‘]{1}[a-z]+$", re.I) 28 | ALL_CAPS = re.compile(r'^[A-Z\s%s]+$' % PUNCT) 29 | UC_INITIALS = re.compile(r"^(?:[A-Z]{1}\.{1}|[A-Z]{1}\.{1}[A-Z]{1})+$") 30 | MAC_MC = re.compile(r"^([Mm]a?c)(\w+)") 31 | 32 | 33 | def titlecase(text): 34 | """ 35 | Titlecases input text 36 | 37 | This filter changes all words to Title Caps, and attempts to be clever 38 | about *un*capitalizing SMALL words like a/an/the in the input. 39 | 40 | The list of "SMALL words" which are not capped comes from 41 | the New York Times Manual of Style, plus 'vs' and 'v'. 42 | 43 | """ 44 | 45 | lines = re.split('[\r\n]+', text) 46 | processed = [] 47 | for line in lines: 48 | all_caps = ALL_CAPS.match(line) 49 | words = re.split('[\t ]', line) 50 | tc_line = [] 51 | for word in words: 52 | if all_caps: 53 | if UC_INITIALS.match(word): 54 | tc_line.append(word) 55 | continue 56 | else: 57 | word = word.lower() 58 | 59 | if APOS_SECOND.match(word): 60 | word = word.replace(word[0], word[0].upper()) 61 | word = word.replace(word[2], word[2].upper()) 62 | tc_line.append(word) 63 | continue 64 | if INLINE_PERIOD.search(word) or UC_ELSEWHERE.match(word): 65 | tc_line.append(word) 66 | continue 67 | if SMALL_WORDS.match(word): 68 | tc_line.append(word.lower()) 69 | continue 70 | 71 | match = MAC_MC.match(word) 72 | if match: 73 | tc_line.append("%s%s" % (match.group(1).capitalize(), 74 | match.group(2).capitalize())) 75 | continue 76 | 77 | if "/" in word and "//" not in word: 78 | slashed = [] 79 | for item in word.split('/'): 80 | slashed.append(CAPFIRST.sub(lambda m: m.group(0).upper(), item)) 81 | tc_line.append("/".join(slashed)) 82 | continue 83 | 84 | hyphenated = [] 85 | for item in word.split('-'): 86 | hyphenated.append(CAPFIRST.sub(lambda m: m.group(0).upper(), item)) 87 | tc_line.append("-".join(hyphenated)) 88 | 89 | result = " ".join(tc_line) 90 | 91 | result = SMALL_FIRST.sub(lambda m: '%s%s' % ( 92 | m.group(1), 93 | m.group(2).capitalize() 94 | ), result) 95 | 96 | result = SMALL_LAST.sub(lambda m: m.group(0).capitalize(), result) 97 | 98 | result = SUBPHRASE.sub(lambda m: '%s%s' % ( 99 | m.group(1), 100 | m.group(2).capitalize() 101 | ), result) 102 | 103 | processed.append(result) 104 | 105 | return "\n".join(processed) 106 | -------------------------------------------------------------------------------- /tvnamer/cliarg_parser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Constructs command line argument parser for tvnamer 4 | """ 5 | 6 | import optparse 7 | from typing import Any, TYPE_CHECKING 8 | 9 | if TYPE_CHECKING: 10 | from .config_defaults import TypedDefaults 11 | 12 | 13 | class Group(object): 14 | """Simple helper context manager to add a group to an OptionParser 15 | """ 16 | 17 | def __init__(self, parser, name): 18 | # type: (optparse.OptionParser, str) -> None 19 | self.parser = parser 20 | self.name = name 21 | self.group = optparse.OptionGroup(self.parser, name) 22 | 23 | def __enter__(self): 24 | # type: () -> optparse.OptionGroup 25 | return self.group 26 | 27 | def __exit__(self, *k, **kw): 28 | # type: (Any, Any) -> None 29 | self.parser.add_option_group(self.group) 30 | 31 | 32 | def get_cli_parser(defaults): 33 | # type: (TypedDefaults) -> optparse.OptionParser 34 | parser = optparse.OptionParser( 35 | usage="%prog [options] ", add_help_option=False 36 | ) 37 | 38 | parser.set_defaults(**defaults) 39 | 40 | # fmt: off 41 | 42 | # Console output 43 | with Group(parser, "Console output") as g: 44 | g.add_option("-v", "--verbose", action="store_true", dest="verbose", help="show debugging info") 45 | g.add_option("-q", "--not-verbose", action="store_false", dest="verbose", help="no verbose output (useful to override 'verbose':true in config file)") 46 | g.add_option("--dry-run", action="store_true", dest="dry_run", help="Only tell what script is going to do") 47 | 48 | # Batch options 49 | with Group(parser, "Batch options") as g: 50 | g.add_option("-a", "--always", action="store_true", dest="always_rename", help="Always renames files (but prompt for correct series)") 51 | g.add_option("--not-always", action="store_true", dest="always_rename", help="Overrides --always") 52 | 53 | g.add_option("-f", "--selectfirst", action="store_true", dest="select_first", help="Select first series search result automatically") 54 | g.add_option("--not-selectfirst", action="store_false", dest="select_first", help="Overrides --selectfirst") 55 | 56 | g.add_option("-b", "--batch", action="store_true", dest="batch", help="Rename without human intervention, same as --always and --selectfirst combined") 57 | g.add_option("--not-batch", action="store_false", dest="batch", help="Overrides --batch") 58 | 59 | # Config options 60 | with Group(parser, "Config options") as g: 61 | g.add_option("-c", "--config", action="store", dest="loadconfig", help="Load config from this file") 62 | g.add_option("-s", "--save", action="store", dest="saveconfig", help="Save configuration to this file and exit") 63 | g.add_option("-p", "--preview-config", action="store_true", dest="showconfig", help="Show current config values and exit") 64 | 65 | # Override values 66 | with Group(parser, "Override values") as g: 67 | g.add_option("-n", "--name", action="store", dest="force_name", help="override the parsed series name with this (applies to all files)") 68 | g.add_option("--series-id", action="store", dest="series_id", help="explicitly set the show id for TVdb to use (applies to all files)") 69 | g.add_option("--order", action="store", dest="order", help="set the TvDB episode order ('aired' [default] or 'dvd')") 70 | g.add_option("-l", "--lang", action="store", dest="language", help="set the language used to retrieve data") 71 | 72 | # Misc 73 | with Group(parser, "Misc") as g: 74 | g.add_option("-r", "--recursive", action="store_true", dest="recursive", help="Descend more than one level directories supplied as arguments") 75 | g.add_option("--not-recursive", action="store_false", dest="recursive", help="Only descend one level into directories") 76 | 77 | g.add_option("-m", "--move", action="store_true", dest="move_files_enable", help="Move files to destination specified in config or with --movedestination argument") 78 | g.add_option("--not-move", action="store_false", dest="move_files_enable", help="Files will remain in current directory") 79 | 80 | g.add_option("--force-move", action="store_true", dest="overwrite_destination_on_move", help="Force move and potentially overwrite existing files in destination folder") 81 | g.add_option("--force-rename", action="store_true", dest="overwrite_destination_on_rename", help="Force rename source file") 82 | 83 | g.add_option("-d", "--movedestination", action="store", dest="move_files_destination", help="Destination to move files to. Variables: %(seriesname)s %(seasonnumber)d %(episodenumbers)s") 84 | 85 | g.add_option("-h", "--help", action="help", help="show this help message and exit") 86 | g.add_option("--version", action="store_true", dest="show_version", help="show verison number and exit") 87 | 88 | return parser 89 | -------------------------------------------------------------------------------- /tvnamer/config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Holds Config singleton 4 | """ 5 | 6 | from typing import TYPE_CHECKING 7 | if TYPE_CHECKING: 8 | from .config_defaults import TypedDefaults 9 | 10 | from tvnamer.config_defaults import defaults 11 | 12 | Config = dict(defaults) # type: TypedDefaults 13 | -------------------------------------------------------------------------------- /tvnamer/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Main tvnamer utility functionality 4 | """ 5 | 6 | import os 7 | import sys 8 | import logging 9 | import warnings 10 | 11 | try: 12 | import readline 13 | except ImportError: 14 | pass 15 | 16 | import json 17 | 18 | import tvdb_api 19 | from typing import List, Union, Optional 20 | 21 | from tvnamer import cliarg_parser, __version__ 22 | from tvnamer.config_defaults import defaults 23 | from tvnamer.config import Config 24 | from .files import FileFinder, FileParser, Renamer, _apply_replacements_input 25 | from .utils import ( 26 | warn, 27 | format_episode_numbers, 28 | make_valid_filename, 29 | ) 30 | from tvnamer.data import ( 31 | BaseInfo, 32 | EpisodeInfo, 33 | DatedEpisodeInfo, 34 | NoSeasonEpisodeInfo, 35 | ) 36 | 37 | from tvnamer.tvnamer_exceptions import ( 38 | ShowNotFound, 39 | SeasonNotFound, 40 | EpisodeNotFound, 41 | EpisodeNameNotFound, 42 | UserAbort, 43 | InvalidPath, 44 | NoValidFilesFoundError, 45 | SkipBehaviourAbort, 46 | InvalidFilename, 47 | DataRetrievalError, 48 | ) 49 | 50 | 51 | LOG = logging.getLogger(__name__) 52 | 53 | 54 | # Key for use in tvnamer only - other keys can easily be registered at https://thetvdb.com/api-information 55 | TVNAMER_API_KEY = "fb51f9b848ffac9750bada89ecba0225" 56 | 57 | 58 | def get_move_destination(episode): 59 | # type: (BaseInfo) -> str 60 | """Constructs the location to move/copy the file 61 | """ 62 | 63 | # TODO: Write functional test to ensure this valid'ifying works 64 | def wrap_validfname(fname): 65 | # type: (str) -> str 66 | """Wrap the make_valid_filename function as it's called twice 67 | and this is slightly long.. 68 | """ 69 | if Config["move_files_lowercase_destination"]: 70 | fname = fname.lower() 71 | return make_valid_filename( 72 | fname, 73 | windows_safe=Config["windows_safe_filenames"], 74 | custom_blacklist=Config["custom_filename_character_blacklist"], 75 | replace_with=Config["replace_invalid_characters_with"], 76 | ) 77 | 78 | # Calls make_valid_filename on series name, as it must valid for a filename 79 | if isinstance(episode, DatedEpisodeInfo): 80 | dest_dir = Config["move_files_destination_date"] % { 81 | "seriesname": make_valid_filename(episode.seriesname), 82 | "year": episode.episodenumbers[0].year, 83 | "month": episode.episodenumbers[0].month, 84 | "day": episode.episodenumbers[0].day, 85 | "originalfilename": episode.originalfilename, 86 | } 87 | elif isinstance(episode, NoSeasonEpisodeInfo): 88 | dest_dir = Config["move_files_destination"] % { 89 | "seriesname": wrap_validfname(episode.seriesname), 90 | "episodenumbers": wrap_validfname( 91 | format_episode_numbers(episode.episodenumbers) 92 | ), 93 | "originalfilename": episode.originalfilename, 94 | } 95 | elif isinstance(episode, EpisodeInfo): 96 | dest_dir = Config["move_files_destination"] % { 97 | "seriesname": wrap_validfname(episode.seriesname), 98 | "seasonnumber": episode.seasonnumber, 99 | "episodenumbers": wrap_validfname( 100 | format_episode_numbers(episode.episodenumbers) 101 | ), 102 | "originalfilename": episode.originalfilename, 103 | } 104 | else: 105 | raise RuntimeError("Unhandled episode subtype of %s" % type(episode)) 106 | 107 | return dest_dir 108 | 109 | 110 | def do_rename_file(cnamer, new_name): 111 | # type: (Renamer, str) -> None 112 | """Renames the file. cnamer should be Renamer instance, 113 | new_name should be string containing new filename. 114 | """ 115 | try: 116 | cnamer.new_path( 117 | new_fullpath=new_name, 118 | force=Config["overwrite_destination_on_rename"], 119 | leave_symlink=Config["leave_symlink"], 120 | ) 121 | except OSError as e: 122 | if Config["skip_behaviour"] == "exit": 123 | warn("Exiting due to error: %s" % e) 124 | raise SkipBehaviourAbort() 125 | warn("Skipping file due to error: %s" % e) 126 | 127 | 128 | def do_move_file(cnamer, dest_dir=None, dest_filepath=None, get_path_preview=False): 129 | # type: (Renamer, Optional[str], Optional[str], bool) -> Optional[str] 130 | """Moves file to dest_dir, or to dest_filepath 131 | """ 132 | 133 | if (dest_dir, dest_filepath).count(None) != 1: 134 | raise ValueError("Specify only dest_dir or dest_filepath") 135 | 136 | if not Config["move_files_enable"]: 137 | raise ValueError("move_files feature is disabled but do_move_file was called") 138 | 139 | if Config["move_files_destination"] is None: 140 | raise ValueError( 141 | "Config value for move_files_destination cannot be None if move_files_enabled is True" 142 | ) 143 | 144 | try: 145 | return cnamer.new_path( 146 | new_path=dest_dir, 147 | new_fullpath=dest_filepath, 148 | always_move=Config["always_move"], 149 | leave_symlink=Config["leave_symlink"], 150 | get_path_preview=get_path_preview, 151 | force=Config["overwrite_destination_on_move"], 152 | ) 153 | 154 | except OSError as e: 155 | if Config["skip_behaviour"] == "exit": 156 | warn("Exiting due to error: %s" % e) 157 | raise SkipBehaviourAbort() 158 | warn("Skipping file due to error: %s" % e) 159 | return None 160 | 161 | 162 | def confirm(question, options, default="y"): 163 | # type: (str, List[str], str) -> str 164 | """Takes a question (string), list of options and a default value (used 165 | when user simply hits enter). 166 | Asks until valid option is entered. 167 | """ 168 | # Highlight default option with [ ] 169 | options_chunks = [] 170 | for x in options: 171 | if x == default: 172 | x = "[%s]" % x 173 | if x != "": 174 | options_chunks.append(x) 175 | options_str = "/".join(options_chunks) 176 | 177 | while True: 178 | print(question) 179 | print("(%s) " % (options_str), end="") 180 | try: 181 | ans = input().strip() 182 | except KeyboardInterrupt as errormsg: 183 | print("\n", errormsg) 184 | raise UserAbort(errormsg) 185 | 186 | if ans in options: 187 | return ans 188 | elif ans == "": 189 | return default 190 | 191 | 192 | def process_file(tvdb_instance, episode): 193 | # type: (tvdb_api.Tvdb, BaseInfo) -> None 194 | """Gets episode name, prompts user for input 195 | """ 196 | print("#" * 20) 197 | print("# Processing file: %s" % episode.fullfilename) 198 | 199 | if len(Config["input_filename_replacements"]) > 0: 200 | replaced = _apply_replacements_input(episode.fullfilename) 201 | print("# With custom replacements: %s" % (replaced)) 202 | 203 | # Use force_name option. Done after input_filename_replacements so 204 | # it can be used to skip the replacements easily 205 | if Config["force_name"] is not None: 206 | episode.seriesname = Config["force_name"] 207 | 208 | print("# Detected series: %s (%s)" % (episode.seriesname, episode.number_string())) 209 | 210 | try: 211 | episode.populate_from_tvdb( 212 | tvdb_instance, 213 | force_name=Config["force_name"], 214 | series_id=Config["series_id"], 215 | ) 216 | except (DataRetrievalError, ShowNotFound) as errormsg: 217 | if Config["always_rename"] and Config["skip_file_on_error"] is True: 218 | if Config["skip_behaviour"] == "exit": 219 | warn("Exiting due to error: %s" % errormsg) 220 | raise SkipBehaviourAbort() 221 | warn("Skipping file due to error: %s" % errormsg) 222 | return 223 | else: 224 | warn("%s" % (errormsg)) 225 | except (SeasonNotFound, EpisodeNotFound, EpisodeNameNotFound) as errormsg: 226 | # Show was found, so use corrected series name 227 | if Config["always_rename"] and Config["skip_file_on_error"]: 228 | if Config["skip_behaviour"] == "exit": 229 | warn("Exiting due to error: %s" % errormsg) 230 | raise SkipBehaviourAbort() 231 | warn("Skipping file due to error: %s" % errormsg) 232 | return 233 | 234 | warn("%s" % (errormsg)) 235 | 236 | cnamer = Renamer(episode.fullpath) 237 | 238 | should_rename = False 239 | 240 | if Config["move_files_only"]: 241 | 242 | new_name = episode.fullfilename 243 | should_rename = True 244 | 245 | else: 246 | new_name = episode.generate_filename() 247 | if new_name == episode.fullfilename: 248 | print("#" * 20) 249 | print("Existing filename is correct: %s" % episode.fullfilename) 250 | print("#" * 20) 251 | 252 | should_rename = True 253 | 254 | else: 255 | print("#" * 20) 256 | print("Old filename: %s" % episode.fullfilename) 257 | 258 | if len(Config["output_filename_replacements"]) > 0: 259 | # Show filename without replacements 260 | print( 261 | "Before custom output replacements: %s" 262 | % (episode.generate_filename(preview_orig_filename=True)) 263 | ) 264 | 265 | print("New filename: %s" % new_name) 266 | 267 | if Config["dry_run"]: 268 | print("%s will be renamed to %s" % (episode.fullfilename, new_name)) 269 | if Config["move_files_enable"]: 270 | print( 271 | "%s will be moved to %s" 272 | % (new_name, get_move_destination(episode)) 273 | ) 274 | return 275 | elif Config["always_rename"]: 276 | do_rename_file(cnamer, new_name) 277 | if Config["move_files_enable"]: 278 | if Config["move_files_destination_is_filepath"]: 279 | do_move_file( 280 | cnamer=cnamer, dest_filepath=get_move_destination(episode) 281 | ) 282 | else: 283 | do_move_file(cnamer=cnamer, dest_dir=get_move_destination(episode)) 284 | return 285 | 286 | ans = confirm("Rename?", options=["y", "n", "a", "q"], default="y") 287 | 288 | if ans == "a": 289 | print("Always renaming") 290 | Config["always_rename"] = True 291 | should_rename = True 292 | elif ans == "q": 293 | print("Quitting") 294 | raise UserAbort("User exited with q") 295 | elif ans == "y": 296 | print("Renaming") 297 | should_rename = True 298 | elif ans == "n": 299 | print("Skipping") 300 | else: 301 | print("Invalid input, skipping") 302 | 303 | if should_rename: 304 | do_rename_file(cnamer, new_name) 305 | 306 | if should_rename and Config["move_files_enable"]: 307 | new_path = get_move_destination(episode) 308 | if Config["dry_run"]: 309 | print("%s will be moved to %s" % (new_name, get_move_destination(episode))) 310 | return 311 | 312 | if Config["move_files_destination_is_filepath"]: 313 | do_move_file(cnamer=cnamer, dest_filepath=new_path, get_path_preview=True) 314 | else: 315 | do_move_file(cnamer=cnamer, dest_dir=new_path, get_path_preview=True) 316 | 317 | if not Config["batch"] and Config["move_files_confirmation"]: 318 | ans = confirm("Move file?", options=["y", "n", "q"], default="y") 319 | else: 320 | ans = "y" 321 | 322 | if ans == "y": 323 | print("Moving file") 324 | do_move_file(cnamer, new_path) 325 | elif ans == "q": 326 | print("Quitting") 327 | raise UserAbort("user exited with q") 328 | 329 | 330 | def find_files(paths): 331 | # type: (List[str]) -> List[str] 332 | """Takes an array of paths, returns all files found 333 | """ 334 | valid_files = [] 335 | 336 | for cfile in paths: 337 | cur = FileFinder( 338 | cfile, 339 | with_extension=Config["valid_extensions"], 340 | filename_blacklist=Config["filename_blacklist"], 341 | recursive=Config["recursive"], 342 | ) 343 | 344 | try: 345 | valid_files.extend(cur.find_files()) 346 | except InvalidPath: 347 | warn("Invalid path: %s" % cfile) 348 | 349 | if len(valid_files) == 0: 350 | raise NoValidFilesFoundError() 351 | 352 | # Remove duplicate files (all paths from FileFinder are absolute) 353 | valid_files = list(set(valid_files)) 354 | 355 | return valid_files 356 | 357 | 358 | def tvnamer(paths): 359 | # type: (List[str]) -> None 360 | """Main tvnamer function, takes an array of paths, does stuff. 361 | """ 362 | 363 | print("#" * 20) 364 | print("# Starting tvnamer") 365 | 366 | episodes_found = [] 367 | 368 | for cfile in find_files(paths): 369 | parser = FileParser(cfile) 370 | try: 371 | episode = parser.parse() 372 | except InvalidFilename as e: 373 | warn("Invalid filename: %s" % e) 374 | else: 375 | if ( 376 | episode.seriesname is None 377 | and Config["force_name"] is None 378 | and Config["series_id"] is None 379 | ): 380 | warn( 381 | "Parsed filename did not contain series name (and --name or --series-id not specified), skipping: %s" 382 | % cfile 383 | ) 384 | 385 | else: 386 | episodes_found.append(episode) 387 | 388 | if len(episodes_found) == 0: 389 | raise NoValidFilesFoundError() 390 | 391 | print( 392 | "# Found %d episode" % len(episodes_found) + ("s" * (len(episodes_found) > 1)) 393 | ) 394 | 395 | # Sort episodes by series name, season and episode number 396 | episodes_found.sort(key=lambda x: x.sortable_info()) 397 | 398 | # episode sort order 399 | if Config["order"] == "dvd": 400 | dvdorder = True 401 | else: 402 | dvdorder = False 403 | 404 | if Config["tvdb_api_key"] is not None: 405 | LOG.debug("Using custom API key from config") 406 | api_key = Config["tvdb_api_key"] 407 | else: 408 | LOG.debug("Using tvnamer default API key") 409 | api_key = TVNAMER_API_KEY 410 | 411 | if os.getenv("TVNAMER_TEST_MODE", "0") == "1": 412 | from .test_cache import get_test_cache_session 413 | cache = get_test_cache_session() 414 | else: 415 | cache = True 416 | 417 | tvdb_instance = tvdb_api.Tvdb( 418 | interactive=not Config["select_first"], 419 | search_all_languages=Config["search_all_languages"], 420 | language=Config["language"], 421 | dvdorder=dvdorder, 422 | cache=cache, 423 | apikey=api_key, 424 | ) 425 | 426 | for episode in episodes_found: 427 | process_file(tvdb_instance, episode) 428 | print("") 429 | 430 | print("#" * 20) 431 | print("# Done") 432 | 433 | 434 | def main(): 435 | # type: () -> None 436 | """Parses command line arguments, displays errors from tvnamer in terminal 437 | """ 438 | opter = cliarg_parser.get_cli_parser(defaults) 439 | 440 | opts, args = opter.parse_args() 441 | 442 | if opts.show_version: 443 | print("tvnamer version: %s" % (__version__,)) 444 | print("tvdb_api version: %s" % (tvdb_api.__version__,)) 445 | print("python version: %s" % (sys.version,)) 446 | sys.exit(0) 447 | 448 | if opts.verbose: 449 | logging.basicConfig( 450 | level=logging.DEBUG, 451 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 452 | ) 453 | else: 454 | logging.basicConfig() 455 | 456 | # If a config is specified, load it, update the defaults using the loaded 457 | # values, then reparse the options with the updated defaults. 458 | default_configuration = os.path.expanduser("~/.config/tvnamer/tvnamer.json") 459 | old_default_configuration = os.path.expanduser("~/.tvnamer.json") 460 | 461 | if opts.loadconfig is not None: 462 | # Command line overrides loading ~/.config/tvnamer/tvnamer.json 463 | config_to_load = opts.loadconfig 464 | elif os.path.isfile(default_configuration): 465 | # No --config arg, so load default config if it exists 466 | config_to_load = default_configuration 467 | elif os.path.isfile(old_default_configuration): 468 | # No --config arg and neow defualt config so load old version if it exist 469 | config_to_load = old_default_configuration 470 | else: 471 | # No arg, nothing at default config location, don't load anything 472 | config_to_load = None 473 | 474 | if config_to_load is not None: 475 | LOG.info("Loading config: %s" % (config_to_load)) 476 | if os.path.isfile(old_default_configuration): 477 | LOG.warning("WARNING: you have a config at deprecated ~/.tvnamer.json location.") 478 | LOG.warning("Config must be moved to new location: ~/.config/tvnamer/tvnamer.json") 479 | 480 | try: 481 | loaded_config = json.load(open(os.path.expanduser(config_to_load))) 482 | except ValueError as e: 483 | LOG.error("Error loading config: %s" % e) 484 | opter.exit(1) 485 | else: 486 | # Config loaded, update optparser's defaults and reparse 487 | defaults.update(loaded_config) 488 | opter = cliarg_parser.get_cli_parser(defaults) 489 | opts, args = opter.parse_args() 490 | 491 | # Save config argument 492 | if opts.saveconfig is not None: 493 | LOG.info("Saving config: %s" % (opts.saveconfig)) 494 | config_to_save = dict(opts.__dict__) 495 | del config_to_save["saveconfig"] 496 | del config_to_save["loadconfig"] 497 | del config_to_save["showconfig"] 498 | json.dump( 499 | config_to_save, 500 | open(os.path.expanduser(opts.saveconfig), "w+"), 501 | sort_keys=True, 502 | indent=4, 503 | ) 504 | 505 | opter.exit(0) 506 | 507 | # Show config argument 508 | if opts.showconfig: 509 | print(json.dumps(opts.__dict__, sort_keys=True, indent=2)) 510 | return 511 | 512 | # Process values 513 | if opts.batch: 514 | opts.select_first = True 515 | opts.always_rename = True 516 | 517 | # Update global config object 518 | Config.update(opts.__dict__) 519 | 520 | if Config["move_files_only"] and not Config["move_files_enable"]: 521 | opter.error( 522 | "Parameter move_files_enable cannot be set to false while parameter move_only is set to true." 523 | ) 524 | 525 | if Config["titlecase_filename"] and Config["lowercase_filename"]: 526 | warnings.warn( 527 | "Setting 'lowercase_filename' clobbers 'titlecase_filename' option" 528 | ) 529 | 530 | if len(args) == 0: 531 | opter.error("No filenames or directories supplied") 532 | 533 | try: 534 | tvnamer(paths=sorted(args)) 535 | except NoValidFilesFoundError: 536 | opter.error("No valid files were supplied") 537 | except UserAbort as errormsg: 538 | opter.error(errormsg) 539 | except SkipBehaviourAbort as errormsg: 540 | opter.error(errormsg) 541 | 542 | 543 | if __name__ == "__main__": 544 | main() 545 | -------------------------------------------------------------------------------- /tvnamer/test_cache.py: -------------------------------------------------------------------------------- 1 | # type: ignore # FIXME: lazy 2 | 3 | import os 4 | import types 5 | 6 | import requests_cache.backends # noqa: E402 7 | import requests_cache.backends.base # noqa: E402 8 | 9 | 10 | try: 11 | from collections.abc import MutableMapping 12 | except ImportError: 13 | from collections import MutableMapping 14 | 15 | import pickle 16 | 17 | 18 | # By default tests use persistent (commited to Git) cache. 19 | # Setting this env-var allows the cache to be populated. 20 | # This is necessary if, say, adding new test case or TVDB response changes. 21 | # It is recommended to clear the cache directory before re-populating the cache. 22 | ALLOW_CACHE_WRITE_ENV_VAR = "TVNAMER_TESTS_ALLOW_CACHE_WRITE" 23 | ALLOW_CACHE_WRITE = os.getenv(ALLOW_CACHE_WRITE_ENV_VAR, "0") == "1" 24 | 25 | 26 | class FileCacheDict(MutableMapping): 27 | def __init__(self, base_dir): 28 | self._base_dir = base_dir 29 | 30 | def __getitem__(self, key): 31 | path = os.path.join(self._base_dir, key) 32 | try: 33 | with open(path, "rb") as f: 34 | data = pickle.load(f) 35 | return data 36 | except FileNotFoundError: 37 | if not ALLOW_CACHE_WRITE: 38 | raise RuntimeError("No cache file found %s (and %s is not active)" % (path, ALLOW_CACHE_WRITE_ENV_VAR)) 39 | raise KeyError 40 | 41 | def __setitem__(self, key, item): 42 | if ALLOW_CACHE_WRITE: 43 | path = os.path.join(self._base_dir, key) 44 | with open(path, "wb") as f: 45 | # Dump with protocol 2 to allow Python 2.7 support 46 | f.write(pickle.dumps(item, protocol=2)) 47 | else: 48 | raise RuntimeError( 49 | "Requested uncached URL and $%s not set to 1" % (ALLOW_CACHE_WRITE_ENV_VAR) 50 | ) 51 | 52 | def __delitem__(self, key): 53 | raise RuntimeError("Removing items from test-cache not supported") 54 | 55 | def __len__(self): 56 | raise NotImplementedError() 57 | 58 | def __iter__(self): 59 | raise NotImplementedError() 60 | 61 | def clear(self): 62 | raise NotImplementedError() 63 | 64 | def __str__(self): 65 | return str(dict(self.items())) 66 | 67 | 68 | class FileCache(requests_cache.backends.base.BaseCache): 69 | def __init__(self, _name, fc_base_dir, **options): 70 | super(FileCache, self).__init__(**options) 71 | self.responses = FileCacheDict(base_dir=fc_base_dir) 72 | self.keys_map = FileCacheDict(base_dir=fc_base_dir) 73 | 74 | 75 | requests_cache.backends.registry['tvnamer_file_cache'] = FileCache 76 | 77 | 78 | def get_test_cache_session(): 79 | here = os.path.dirname(os.path.abspath(__file__)) 80 | sess = requests_cache.CachedSession( 81 | backend="tvnamer_file_cache", 82 | fc_base_dir=os.path.join(here, "..", "tests", "httpcache"), 83 | include_get_headers=True, 84 | allowable_codes=(200, 404), 85 | ) 86 | import tvdb_api 87 | sess.cache.create_key = types.MethodType(tvdb_api.create_key, sess.cache) 88 | return sess 89 | -------------------------------------------------------------------------------- /tvnamer/tvnamer_exceptions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Exceptions used through-out tvnamer 4 | """ 5 | 6 | 7 | class BaseTvnamerException(Exception): 8 | """Base exception all tvnamers exceptions inherit from 9 | """ 10 | 11 | pass 12 | 13 | 14 | class SkipBehaviourAbort(BaseTvnamerException): 15 | """Raised when skip_behaviour is set to exit 16 | """ 17 | 18 | pass 19 | 20 | 21 | class InvalidPath(BaseTvnamerException): 22 | """Raised when an argument is a non-existent file or directory path 23 | """ 24 | 25 | pass 26 | 27 | 28 | class NoValidFilesFoundError(BaseTvnamerException): 29 | """Raised when no valid files are found. Effectively exits tvnamer 30 | """ 31 | 32 | pass 33 | 34 | 35 | class InvalidFilename(BaseTvnamerException): 36 | """Raised when a file is parsed, but no episode info can be found 37 | """ 38 | 39 | pass 40 | 41 | 42 | class UserAbort(BaseTvnamerException): 43 | """Base exception for config errors 44 | """ 45 | 46 | pass 47 | 48 | 49 | class BaseConfigError(BaseTvnamerException): 50 | """Base exception for config errors 51 | """ 52 | 53 | pass 54 | 55 | 56 | class ConfigValueError(BaseConfigError): 57 | """Raised if the config file is malformed or unreadable 58 | """ 59 | 60 | pass 61 | 62 | 63 | class DataRetrievalError(BaseTvnamerException): 64 | """Raised when an error (such as a network problem) prevents tvnamer 65 | from being able to retrieve data such as episode name 66 | """ 67 | 68 | 69 | class ShowNotFound(DataRetrievalError): 70 | """Raised when a show cannot be found 71 | """ 72 | 73 | pass 74 | 75 | 76 | class SeasonNotFound(DataRetrievalError): 77 | """Raised when requested season cannot be found 78 | """ 79 | 80 | pass 81 | 82 | 83 | class EpisodeNotFound(DataRetrievalError): 84 | """Raised when episode cannot be found 85 | """ 86 | 87 | pass 88 | 89 | 90 | class EpisodeNameNotFound(DataRetrievalError): 91 | """Raised when the name of the episode cannot be found 92 | """ 93 | 94 | pass 95 | -------------------------------------------------------------------------------- /tvnamer/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Utilities for tvnamer, including filename parsing 4 | """ 5 | 6 | import datetime 7 | import os 8 | import re 9 | import sys 10 | import shutil 11 | import logging 12 | import platform 13 | import errno 14 | 15 | from tvdb_api import Tvdb 16 | 17 | import tvnamer 18 | from tvnamer.config import Config 19 | from tvnamer.tvnamer_exceptions import ( 20 | InvalidPath, 21 | InvalidFilename, 22 | ShowNotFound, 23 | DataRetrievalError, 24 | SeasonNotFound, 25 | EpisodeNotFound, 26 | EpisodeNameNotFound, 27 | ConfigValueError, 28 | UserAbort, 29 | ) 30 | 31 | from typing import Any, Dict, List, Optional, Union, Tuple, Pattern 32 | 33 | 34 | LOG = logging.getLogger(__name__) 35 | 36 | 37 | def warn(text): 38 | # type: (str) -> None 39 | """Displays message to sys.stderr 40 | """ 41 | print(text, file=sys.stderr) 42 | 43 | 44 | def split_extension(filename): 45 | # type: (str) -> Tuple[str, str] 46 | base = re.sub(Config["extension_pattern"], "", filename) 47 | ext = filename.replace(base, "") 48 | return base, ext 49 | 50 | 51 | def _apply_replacements(cfile, replacements): 52 | # type: (str, List) -> str 53 | """Applies custom replacements. 54 | 55 | Argument cfile is string. 56 | 57 | Argument replacements is a list of dicts, with keys "match", 58 | "replacement", and (optional) "is_regex" 59 | """ 60 | for rep in replacements: 61 | if not rep.get('with_extension', False): 62 | # By default, preserve extension 63 | cfile, cext = split_extension(cfile) 64 | else: 65 | cfile = cfile 66 | cext = "" 67 | 68 | if 'is_regex' in rep and rep['is_regex']: 69 | cfile = re.sub(rep['match'], rep['replacement'], cfile) 70 | else: 71 | cfile = cfile.replace(rep['match'], rep['replacement']) 72 | 73 | # Rejoin extension (cext might be empty-string) 74 | cfile = cfile + cext 75 | 76 | return cfile 77 | 78 | 79 | def make_valid_filename( 80 | value, 81 | normalize_unicode=False, 82 | windows_safe=False, 83 | custom_blacklist=None, 84 | replace_with="_", 85 | ): 86 | # type: (str, bool, bool, Optional[str], str) -> str 87 | """ 88 | Takes a string and makes it into a valid filename. 89 | 90 | normalize_unicode replaces accented characters with ASCII equivalent, and 91 | removes characters that cannot be converted sensibly to ASCII. 92 | 93 | windows_safe forces Windows-safe filenames, regardless of current platform 94 | 95 | custom_blacklist specifies additional characters that will removed. This 96 | will not touch the extension separator: 97 | 98 | >>> make_valid_filename("T.est.avi", custom_blacklist=".") 99 | 'T_est.avi' 100 | """ 101 | 102 | if windows_safe: 103 | # Allow user to make Windows-safe filenames, if they so choose 104 | sysname = "Windows" 105 | else: 106 | sysname = platform.system() 107 | 108 | # If the filename starts with a . prepend it with an underscore, so it 109 | # doesn't become hidden. 110 | 111 | # This is done before calling splitext to handle filename of ".", as 112 | # splitext acts differently in python 2.5 and 2.6 - 2.5 returns ('', '.') 113 | # and 2.6 returns ('.', ''), so rather than special case '.', this 114 | # special-cases all files starting with "." equally (since dotfiles have 115 | # no extension) 116 | if value.startswith("."): 117 | value = "_" + value 118 | 119 | # Treat extension seperatly 120 | value, extension = split_extension(value) 121 | 122 | # Remove any null bytes 123 | value = value.replace("\0", "") 124 | 125 | # Blacklist of characters 126 | if sysname == 'Darwin': 127 | # : is technically allowed, but Finder will treat it as / and will 128 | # generally cause weird behaviour, so treat it as invalid. 129 | blacklist = r"/:" 130 | elif sysname in ['Linux', 'FreeBSD']: 131 | blacklist = r"/" 132 | else: 133 | # platform.system docs say it could also return "Windows" or "Java". 134 | # Failsafe and use Windows sanitisation for Java, as it could be any 135 | # operating system. 136 | blacklist = r"\/:*?\"<>|" 137 | 138 | # Append custom blacklisted characters 139 | if custom_blacklist is not None: 140 | blacklist += custom_blacklist 141 | 142 | # Replace every blacklisted character with a underscore 143 | value = re.sub("[%s]" % re.escape(blacklist), replace_with, value) 144 | 145 | # Remove any trailing whitespace 146 | value = value.strip() 147 | 148 | # There are a bunch of filenames that are not allowed on Windows. 149 | # As with character blacklist, treat non Darwin/Linux platforms as Windows 150 | if sysname not in ['Darwin', 'Linux']: 151 | invalid_filenames = [ 152 | "CON", 153 | "PRN", 154 | "AUX", 155 | "NUL", 156 | "COM1", 157 | "COM2", 158 | "COM3", 159 | "COM4", 160 | "COM5", 161 | "COM6", 162 | "COM7", 163 | "COM8", 164 | "COM9", 165 | "LPT1", 166 | "LPT2", 167 | "LPT3", 168 | "LPT4", 169 | "LPT5", 170 | "LPT6", 171 | "LPT7", 172 | "LPT8", 173 | "LPT9", 174 | ] 175 | if value in invalid_filenames: 176 | value = "_" + value 177 | 178 | # Truncate filenames to valid/sane length. 179 | # NTFS is limited to 255 characters, HFS+ and EXT3 don't seem to have 180 | # limits, FAT32 is 254. I doubt anyone will take issue with losing that 181 | # one possible character, and files over 254 are pointlessly unweidly 182 | max_len = 254 183 | 184 | if len(value + extension) > max_len: 185 | if len(extension) > len(value): 186 | # Truncate extension instead of filename, no extension should be 187 | # this long.. 188 | new_length = max_len - len(value) 189 | extension = extension[:new_length] 190 | else: 191 | # File name is longer than extension, truncate filename. 192 | new_length = max_len - len(extension) 193 | value = value[:new_length] 194 | 195 | return value + extension 196 | 197 | 198 | def format_episode_numbers(episodenumbers): 199 | # type: (List[int]) -> str 200 | """Format episode number(s) into string, using configured values 201 | """ 202 | if len(episodenumbers) == 1: 203 | epno = Config['episode_single'] % episodenumbers[0] 204 | else: 205 | epno = Config['episode_separator'].join( 206 | Config['episode_single'] % x for x in episodenumbers 207 | ) 208 | 209 | return epno 210 | --------------------------------------------------------------------------------