pax_global_header00006660000000000000000000000064146514111400014507gustar00rootroot0000000000000052 comment=3c8a318d9a8df26855bdc3a5d23f7fd2b99ade2e syncedlyrics-1.0.1/000077500000000000000000000000001465141114000142215ustar00rootroot00000000000000syncedlyrics-1.0.1/.gitattributes000066400000000000000000000001021465141114000171050ustar00rootroot00000000000000# Auto detect text files and perform LF normalization * text=auto syncedlyrics-1.0.1/.github/000077500000000000000000000000001465141114000155615ustar00rootroot00000000000000syncedlyrics-1.0.1/.github/workflows/000077500000000000000000000000001465141114000176165ustar00rootroot00000000000000syncedlyrics-1.0.1/.github/workflows/pypi-publish.yml000066400000000000000000000022701465141114000227670ustar00rootroot00000000000000name: Publish to PyPI on Version Change on: push: branches: - main jobs: check_version_and_publish: runs-on: ubuntu-latest steps: - name: Checkout Repository uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v3 with: python-version: 3.x - name: Install Poetry run: | pip install poetry poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }} - name: Check for Version Change id: check_version run: | current_version=$(poetry version --short) last_commit_message=$(git log -1 --pretty=%s) if [[ $last_commit_message =~ ^v$current_version ]]; then echo "Version change detected. Proceeding with PyPI publish." echo "version_changed=true" >> $GITHUB_OUTPUT else echo "No version change detected. Skipping PyPI publish." echo "version_changed=false" >> $GITHUB_OUTPUT fi - name: Publish to PyPI if: steps.check_version.outputs.version_changed == 'true' run: | poetry publish -u __token__ -p ${{ secrets.PYPI_TOKEN }} --build syncedlyrics-1.0.1/.github/workflows/tests.yml000066400000000000000000000017141465141114000215060ustar00rootroot00000000000000name: Tests on: schedule: - cron: "26 5 * * *" # Every day, 05:26 UTC push: branches: - main paths: - "syncedlyrics/**" pull_request: branches: - main paths: - "syncedlyrics/**" workflow_dispatch: jobs: provider-tests: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] python-version: ["3.8", "3.12"] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 - name: Installing poetry run: pipx install poetry - uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} cache: "poetry" - name: Inatalling dependencies run: | poetry env use ${{ matrix.python-version }} poetry install - name: Running provider tests run: poetry run python -m pytest -k "not genius and not netease" --durations=0 -vv -s --log-cli-level=DEBUG tests.py syncedlyrics-1.0.1/.gitignore000066400000000000000000000053371465141114000162210ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ .syncedlyrics *.lrcsyncedlyrics-1.0.1/LICENSE000066400000000000000000000020451465141114000152270ustar00rootroot00000000000000MIT License Copyright (c) 2022 Momo Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. syncedlyrics-1.0.1/README.md000066400000000000000000000046751465141114000155140ustar00rootroot00000000000000# syncedlyrics Get an LRC format (synchronized) lyrics for your music. [![Downloads](https://static.pepy.tech/badge/syncedlyrics/month)](https://pepy.tech/project/syncedlyrics) ## Installation ``` pip install syncedlyrics ``` ## Usage ### CLI ``` syncedlyrics "SEARCH_TERM" ``` By default, this will prefer time synced lyrics, but use plaintext lyrics, if no synced lyrics are available. To only allow one type of lyrics specify `--plain-only` or `--synced-only` respectively. #### Available Options | Flag | Description | | --- | --- | | `-o` | Path to save `.lrc` lyrics, default="{search_term}.lrc" | | `-p` | Space-separated list of [providers](#providers) to include in searching | | `-l` | Language code of the translation ([ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes) format) | | `-v` | Use this flag to show the logs | | `--plain-only` | Only look for plain text (not synced) lyrics | | `--synced-only` | Only look for synced lyrics | | `--enhanced` | Searches for an [Enhanced](https://en.wikipedia.org/wiki/LRC_(file_format)#A2_extension:_word_time_tag) (word-level karaoke) format. If it isn't available, search for regular synced lyrics. ### Python ```py # This simple lrc = syncedlyrics.search("[TRACK_NAME] [ARTIST_NAME]") # Or with options: syncedlyrics.search("...", plain_only=True, save_path="{search_term}_1234.lrc", providers=["NetEase"]) # Get a translation along with the original lyrics (separated by `\n`): syncedlyrics.search("...", lang="de") # Get a word-by-word (karaoke) synced-lyrics if available syncedlyrics.search("...", enhanced=True) ``` ## Providers - [Musixmatch](https://www.musixmatch.com/) - ~~[Deezer](https://deezer.com/)~~ (Currently not working anymore) - [Lrclib](https://github.com/tranxuanthang/lrcget/issues/2#issuecomment-1326925928) - [NetEase](https://music.163.com/) - [Megalobiz](https://www.megalobiz.com/) - [Genius](https://genius.com) (For plain format) - ~~[Lyricsify](https://www.lyricsify.com/)~~ (Broken duo to Cloudflare protection) Feel free to suggest more providers or make PRs to fix the broken ones. ## License [MIT](https://github.com/rtcq/syncedlyrics/blob/master/LICENSE) ## Citation If you use this library in your research, you can cite as follows: ``` @misc{syncedlyrics, author = {Momeni, Mohammad}, title = {syncedlyrics}, year = {2022}, publisher = {GitHub}, journal = {GitHub repository}, howpublished = {\url{https://github.com/moehmeni/syncedlyrics}}, } ``` syncedlyrics-1.0.1/poetry.lock000066400000000000000000001232061465141114000164210ustar00rootroot00000000000000# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "beautifulsoup4" version = "4.12.3" description = "Screen-scraping library" optional = false python-versions = ">=3.6.0" files = [ {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, ] [package.dependencies] soupsieve = ">1.2" [package.extras] cchardet = ["cchardet"] chardet = ["chardet"] charset-normalizer = ["charset-normalizer"] html5lib = ["html5lib"] lxml = ["lxml"] [[package]] name = "black" version = "24.4.2" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"}, {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"}, {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"}, {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"}, {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"}, {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"}, {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"}, {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"}, {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"}, {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"}, {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"}, {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"}, {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"}, ] [package.dependencies] click = ">=8.0.0" mypy-extensions = ">=0.4.3" packaging = ">=22.0" pathspec = ">=0.9.0" platformdirs = ">=2" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" version = "2024.2.2" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, ] [[package]] name = "charset-normalizer" version = "3.3.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" files = [ {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] [[package]] name = "click" version = "8.1.7" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" files = [ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, ] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] [[package]] name = "exceptiongroup" version = "1.2.1" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, ] [package.extras] test = ["pytest (>=6)"] [[package]] name = "idna" version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] [[package]] name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] [[package]] name = "mypy-extensions" version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.5" files = [ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] [[package]] name = "packaging" version = "24.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" files = [ {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, ] [[package]] name = "pathspec" version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] [[package]] name = "platformdirs" version = "4.2.1" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ {file = "platformdirs-4.2.1-py3-none-any.whl", hash = "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1"}, {file = "platformdirs-4.2.1.tar.gz", hash = "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf"}, ] [package.extras] docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] type = ["mypy (>=1.8)"] [[package]] name = "pluggy" version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] [[package]] name = "pytest" version = "8.2.0" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ {file = "pytest-8.2.0-py3-none-any.whl", hash = "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233"}, {file = "pytest-8.2.0.tar.gz", hash = "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f"}, ] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=1.5,<2.0" tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "rapidfuzz" version = "3.9.0" description = "rapid fuzzy string matching" optional = false python-versions = ">=3.8" files = [ {file = "rapidfuzz-3.9.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bd375c4830fee11d502dd93ecadef63c137ae88e1aaa29cc15031fa66d1e0abb"}, {file = "rapidfuzz-3.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:55e2c5076f38fc1dbaacb95fa026a3e409eee6ea5ac4016d44fb30e4cad42b20"}, {file = "rapidfuzz-3.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:488f74126904db6b1bea545c2f3567ea882099f4c13f46012fe8f4b990c683df"}, {file = "rapidfuzz-3.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3f2d1ea7cd57dfcd34821e38b4924c80a31bcf8067201b1ab07386996a9faee"}, {file = "rapidfuzz-3.9.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b11e602987bcb4ea22b44178851f27406fca59b0836298d0beb009b504dba266"}, {file = "rapidfuzz-3.9.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3083512e9bf6ed2bb3d25883922974f55e21ae7f8e9f4e298634691ae1aee583"}, {file = "rapidfuzz-3.9.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b33c6d4b3a1190bc0b6c158c3981535f9434e8ed9ffa40cf5586d66c1819fb4b"}, {file = "rapidfuzz-3.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dcb95fde22f98e6d0480db8d6038c45fe2d18a338690e6f9bba9b82323f3469"}, {file = "rapidfuzz-3.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:08d8b49b3a4fb8572e480e73fcddc750da9cbb8696752ee12cca4bf8c8220d52"}, {file = "rapidfuzz-3.9.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e721842e6b601ebbeb8cc5e12c75bbdd1d9e9561ea932f2f844c418c31256e82"}, {file = "rapidfuzz-3.9.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7988363b3a415c5194ce1a68d380629247f8713e669ad81db7548eb156c4f365"}, {file = "rapidfuzz-3.9.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:2d267d4c982ab7d177e994ab1f31b98ff3814f6791b90d35dda38307b9e7c989"}, {file = "rapidfuzz-3.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0bb28ab5300cf974c7eb68ea21125c493e74b35b1129e629533468b2064ae0a2"}, {file = "rapidfuzz-3.9.0-cp310-cp310-win32.whl", hash = "sha256:1b1f74997b6d94d66375479fa55f70b1c18e4d865d7afcd13f0785bfd40a9d3c"}, {file = "rapidfuzz-3.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:c56d2efdfaa1c642029f3a7a5bb76085c5531f7a530777be98232d2ce142553c"}, {file = "rapidfuzz-3.9.0-cp310-cp310-win_arm64.whl", hash = "sha256:6a83128d505cac76ea560bb9afcb3f6986e14e50a6f467db9a31faef4bd9b347"}, {file = "rapidfuzz-3.9.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e2218d62ab63f3c5ad48eced898854d0c2c327a48f0fb02e2288d7e5332a22c8"}, {file = "rapidfuzz-3.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:36bf35df2d6c7d5820da20a6720aee34f67c15cd2daf8cf92e8141995c640c25"}, {file = "rapidfuzz-3.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:905b01a9b633394ff6bb5ebb1c5fd660e0e180c03fcf9d90199cc6ed74b87cf7"}, {file = "rapidfuzz-3.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33cfabcb7fd994938a6a08e641613ce5fe46757832edc789c6a5602e7933d6fa"}, {file = "rapidfuzz-3.9.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1179dcd3d150a67b8a678cd9c84f3baff7413ff13c9e8fe85e52a16c97e24c9b"}, {file = "rapidfuzz-3.9.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47d97e28c42f1efb7781993b67c749223f198f6653ef177a0c8f2b1c516efcaf"}, {file = "rapidfuzz-3.9.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28da953eb2ef9ad527e536022da7afff6ace7126cdd6f3e21ac20f8762e76d2c"}, {file = "rapidfuzz-3.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:182b4e11de928fb4834e8f8b5ecd971b5b10a86fabe8636ab65d3a9b7e0e9ca7"}, {file = "rapidfuzz-3.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c74f2da334ce597f31670db574766ddeaee5d9430c2c00e28d0fbb7f76172036"}, {file = "rapidfuzz-3.9.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:014ac55b03f4074f903248ded181f3000f4cdbd134e6155cbf643f0eceb4f70f"}, {file = "rapidfuzz-3.9.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c4ef34b2ddbf448f1d644b4ec6475df8bbe5b9d0fee173ff2e87322a151663bd"}, {file = "rapidfuzz-3.9.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fc02157f521af15143fae88f92ef3ddcc4e0cff05c40153a9549dc0fbdb9adb3"}, {file = "rapidfuzz-3.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ff08081c49b18ba253a99e6a47f492e6ee8019e19bbb6ddc3ed360cd3ecb2f62"}, {file = "rapidfuzz-3.9.0-cp311-cp311-win32.whl", hash = "sha256:b9bf90b3d96925cbf8ef44e5ee3cf39ef0c422f12d40f7a497e91febec546650"}, {file = "rapidfuzz-3.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:d5d5684f54d82d9b0cf0b2701e55a630527a9c3dd5ddcf7a2e726a475ac238f2"}, {file = "rapidfuzz-3.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:a2de844e0e971d7bd8aa41284627dbeacc90e750b90acfb016836553c7a63192"}, {file = "rapidfuzz-3.9.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f81fe99a69ac8ee3fd905e70c62f3af033901aeb60b69317d1d43d547b46e510"}, {file = "rapidfuzz-3.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:633b9d03fc04abc585c197104b1d0af04b1f1db1abc99f674d871224cd15557a"}, {file = "rapidfuzz-3.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab872cb57ae97c54ba7c71a9e3c9552beb57cb907c789b726895576d1ea9af6f"}, {file = "rapidfuzz-3.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdd8c15c3a14e409507fdf0c0434ec481d85c6cbeec8bdcd342a8cd1eda03825"}, {file = "rapidfuzz-3.9.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2444d8155d9846f206e2079bb355b85f365d9457480b0d71677a112d0a7f7128"}, {file = "rapidfuzz-3.9.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f83bd3d01f04061c3660742dc85143a89d49fd23eb31eccbf60ad56c4b955617"}, {file = "rapidfuzz-3.9.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ca799f882364e69d0872619afb19efa3652b7133c18352e4a3d86a324fb2bb1"}, {file = "rapidfuzz-3.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6993d361f28b9ef5f0fa4e79b8541c2f3507be7471b9f9cb403a255e123b31e1"}, {file = "rapidfuzz-3.9.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:170822a1b1719f02b58e3dce194c8ad7d4c5b39be38c0fdec603bd19c6f9cf81"}, {file = "rapidfuzz-3.9.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:0e86e39c1c1a0816ceda836e6f7bd3743b930cbc51a43a81bb433b552f203f25"}, {file = "rapidfuzz-3.9.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:731269812ea837e0b93d913648e404736407408e33a00b75741e8f27c590caa2"}, {file = "rapidfuzz-3.9.0-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:8e5ff882d3a3d081157ceba7e0ebc7fac775f95b08cbb143accd4cece6043819"}, {file = "rapidfuzz-3.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2003071aa633477a01509890c895f9ef56cf3f2eaa72c7ec0b567f743c1abcba"}, {file = "rapidfuzz-3.9.0-cp312-cp312-win32.whl", hash = "sha256:13857f9070600ea1f940749f123b02d0b027afbaa45e72186df0f278915761d0"}, {file = "rapidfuzz-3.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:134b7098ac109834eeea81424b6822f33c4c52bf80b81508295611e7a21be12a"}, {file = "rapidfuzz-3.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:2a96209f046fe328be30fc43f06e3d4b91f0d5b74e9dcd627dbfd65890fa4a5e"}, {file = "rapidfuzz-3.9.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:544b0bf9d17170720809918e9ccd0d482d4a3a6eca35630d8e1459f737f71755"}, {file = "rapidfuzz-3.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d536f8beb8dd82d6efb20fe9f82c2cfab9ffa0384b5d184327e393a4edde91d"}, {file = "rapidfuzz-3.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:30f7609da871510583f87484a10820b26555a473a90ab356cdda2f3b4456256c"}, {file = "rapidfuzz-3.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f4a2468432a1db491af6f547fad8f6d55fa03e57265c2f20e5eaceb68c7907e"}, {file = "rapidfuzz-3.9.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a7ec4676242c8a430509cff42ce98bca2fbe30188a63d0f60fdcbfd7e84970"}, {file = "rapidfuzz-3.9.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dcb523243e988c849cf81220164ec3bbed378a699e595a8914fffe80596dc49f"}, {file = "rapidfuzz-3.9.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4eea3bf72c4fe68e957526ffd6bcbb403a21baa6b3344aaae2d3252313df6199"}, {file = "rapidfuzz-3.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4514980a5d204c076dd5b756960f6b1b7598f030009456e6109d76c4c331d03c"}, {file = "rapidfuzz-3.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9a06a99f1335fe43464d7121bc6540de7cd9c9475ac2025babb373fe7f27846b"}, {file = "rapidfuzz-3.9.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6c1ed63345d1581c39d4446b1a8c8f550709656ce2a3c88c47850b258167f3c2"}, {file = "rapidfuzz-3.9.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:cd2e6e97daf17ebb3254285cf8dd86c60d56d6cf35c67f0f9a557ef26bd66290"}, {file = "rapidfuzz-3.9.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:9bc0f7e6256a9c668482c41c8a3de5d0aa12e8ca346dcc427b97c7edb82cba48"}, {file = "rapidfuzz-3.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7c09f4e87e82a164c9db769474bc61f8c8b677f2aeb0234b8abac73d2ecf9799"}, {file = "rapidfuzz-3.9.0-cp38-cp38-win32.whl", hash = "sha256:e65b8f7921bf60cbb207c132842a6b45eefef48c4c3b510eb16087d6c08c70af"}, {file = "rapidfuzz-3.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:9d6478957fb35c7844ad08f2442b62ba76c1857a56370781a707eefa4f4981e1"}, {file = "rapidfuzz-3.9.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:65d9250a4b0bf86320097306084bc3ca479c8f5491927c170d018787793ebe95"}, {file = "rapidfuzz-3.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:47b7c0840afa724db3b1a070bc6ed5beab73b4e659b1d395023617fc51bf68a2"}, {file = "rapidfuzz-3.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3a16c48c6df8fb633efbbdea744361025d01d79bca988f884a620e63e782fe5b"}, {file = "rapidfuzz-3.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48105991ff6e4a51c7f754df500baa070270ed3d41784ee0d097549bc9fcb16d"}, {file = "rapidfuzz-3.9.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a7f273906b3c7cc6d63a76e088200805947aa0bc1ada42c6a0e582e19c390d7"}, {file = "rapidfuzz-3.9.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c396562d304e974b4b0d5cd3afc4f92c113ea46a36e6bc62e45333d6aa8837e"}, {file = "rapidfuzz-3.9.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:68da1b70458fea5290ec9a169fcffe0c17ff7e5bb3c3257e63d7021a50601a8e"}, {file = "rapidfuzz-3.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c5b8f9a7b177af6ce7c6ad5b95588b4b73e37917711aafa33b2e79ee80fe709"}, {file = "rapidfuzz-3.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3c42a238bf9dd48f4ccec4c6934ac718225b00bb3a438a008c219e7ccb3894c7"}, {file = "rapidfuzz-3.9.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a365886c42177b2beab475a50ba311b59b04f233ceaebc4c341f6f91a86a78e2"}, {file = "rapidfuzz-3.9.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ce897b5dafb7fb7587a95fe4d449c1ea0b6d9ac4462fbafefdbbeef6eee4cf6a"}, {file = "rapidfuzz-3.9.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:413ac49bae291d7e226a5c9be65c71b2630b3346bce39268d02cb3290232e4b7"}, {file = "rapidfuzz-3.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8982fc3bd49d55a91569fc8a3feba0de4cef0b391ff9091be546e9df075b81"}, {file = "rapidfuzz-3.9.0-cp39-cp39-win32.whl", hash = "sha256:3904d0084ab51f82e9f353031554965524f535522a48ec75c30b223eb5a0a488"}, {file = "rapidfuzz-3.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:3733aede16ea112728ffeafeb29ccc62e095ed8ec816822fa2a82e92e2c08696"}, {file = "rapidfuzz-3.9.0-cp39-cp39-win_arm64.whl", hash = "sha256:fc4e26f592b51f97acf0a3f8dfed95e4d830c6a8fbf359361035df836381ab81"}, {file = "rapidfuzz-3.9.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e33362e98c7899b5f60dcb06ada00acd8673ce0d59aefe9a542701251fd00423"}, {file = "rapidfuzz-3.9.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb67cf43ad83cb886cbbbff4df7dcaad7aedf94d64fca31aea0da7d26684283c"}, {file = "rapidfuzz-3.9.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e2e106cc66453bb80d2ad9c0044f8287415676df5c8036d737d05d4b9cdbf8e"}, {file = "rapidfuzz-3.9.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1256915f7e7a5cf2c151c9ac44834b37f9bd1c97e8dec6f936884f01b9dfc7d"}, {file = "rapidfuzz-3.9.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:ae643220584518cbff8bf2974a0494d3e250763af816b73326a512c86ae782ce"}, {file = "rapidfuzz-3.9.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:491274080742110427f38a6085bb12dffcaff1eef12dccf9e8758398c7e3957e"}, {file = "rapidfuzz-3.9.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bc5559b9b94326922c096b30ae2d8e5b40b2e9c2c100c2cc396ad91bcb84d30"}, {file = "rapidfuzz-3.9.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:849160dc0f128acb343af514ca827278005c1d00148d025e4035e034fc2d8c7f"}, {file = "rapidfuzz-3.9.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:623883fb78e692d54ed7c43b09beec52c6685f10a45a7518128e25746667403b"}, {file = "rapidfuzz-3.9.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d20ab9abc7e19767f1951772a6ab14cb4eddd886493c2da5ee12014596ad253f"}, {file = "rapidfuzz-3.9.0.tar.gz", hash = "sha256:b182f0fb61f6ac435e416eb7ab330d62efdbf9b63cf0c7fa12d1f57c2eaaf6f3"}, ] [package.extras] full = ["numpy"] [[package]] name = "requests" version = "2.31.0" description = "Python HTTP for Humans." optional = false python-versions = ">=3.7" files = [ {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, ] [package.dependencies] certifi = ">=2017.4.17" charset-normalizer = ">=2,<4" idna = ">=2.5,<4" urllib3 = ">=1.21.1,<3" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "soupsieve" version = "2.5" description = "A modern CSS selector implementation for Beautiful Soup." optional = false python-versions = ">=3.8" files = [ {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, ] [[package]] name = "tomli" version = "2.0.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.7" files = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] [[package]] name = "typing-extensions" version = "4.11.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, ] [[package]] name = "urllib3" version = "2.2.1" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, ] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] [metadata] lock-version = "2.0" python-versions = ">=3.8" content-hash = "2416d5560d4b031c699fdd04ac77f3e4e4dfec4d41b0cac7ffb74c7cbff031d5" syncedlyrics-1.0.1/pyproject.toml000066400000000000000000000014771465141114000171460ustar00rootroot00000000000000[tool.poetry] name = "syncedlyrics" version = "1.0.1" description = "Get an LRC format (synchronized) lyrics for your music" repository = "https://github.com/rtcq/syncedlyrics" urls = { "Bug Tracker" = "https://github.com/rtcq/syncedlyrics/issues" } authors = ["Momo "] license = "MIT" readme = "README.md" classifiers = [ "Topic :: Multimedia :: Sound/Audio", "Topic :: Multimedia :: Sound/Audio :: Players", "Topic :: Multimedia :: Sound/Audio :: Speech", ] [tool.poetry.dependencies] python = ">=3.8" requests = "^2.31.0" beautifulsoup4 = "^4.12.3" rapidfuzz = "^3.6.2" [tool.poetry.scripts] syncedlyrics = "syncedlyrics.cli:cli_handler" [tool.poetry.group.dev.dependencies] pytest = "^8.0.2" black = "^24.4.2" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" syncedlyrics-1.0.1/syncedlyrics/000077500000000000000000000000001465141114000167345ustar00rootroot00000000000000syncedlyrics-1.0.1/syncedlyrics/__init__.py000066400000000000000000000075151465141114000210550ustar00rootroot00000000000000""" Search for an LRC format (synchronized lyrics) of a music. ```py import syncedlyrics lrc_text = syncedlyrics.search("[TRACK_NAME] [ARTIST_NAME]") ``` """ import logging from typing import List, Optional from .providers import Deezer, Lrclib, Musixmatch, NetEase, Megalobiz, Genius from .utils import Lyrics, TargetType from .providers.base import LRCProvider logger = logging.getLogger(__name__) def search( search_term: str, plain_only: bool = False, synced_only: bool = False, save_path: Optional[str] = None, providers: List[str] = [], lang: Optional[str] = None, enhanced: bool = False, ) -> Optional[str]: """ Returns the synced lyrics of the song in [LRC](https://en.wikipedia.org/wiki/LRC_(file_format)) format if found. ### Arguments - `search_term`: The search term to find the track - `plain_only`: Only look for plain text (not synced) lyrics - `synced_only`: Only look for synced lyrics - `save_path`: Path to save `.lrc` lyrics. No saving if `None` - `providers`: A list of provider names to include in searching; loops over all the providers as soon as an LRC is found - `lang`: Language of the translation along with the lyrics. **Only supported by Musixmatch** - `enhanced`: Returns word by word synced lyrics if available. **Only supported by Musixmatch** """ if plain_only and synced_only: logger.error( "--plaintext-only and --synced-only flags cannot be used together." ) return None target_type = TargetType.PREFER_SYNCED if plain_only: target_type = TargetType.PLAINTEXT elif synced_only: target_type = TargetType.SYNCED_ONLY lrc = Lyrics() _providers = [ Musixmatch(lang=lang, enhanced=enhanced), Lrclib(), # Deezer(), NetEase(), Megalobiz(), Genius(), ] for provider in _select_providers(_providers, providers): logger.debug(f"Looking for an LRC on {provider}") try: lrc.update(provider.get_lrc(search_term)) except Exception as e: logger.error(f"An error occurred while searching for an LRC on {provider}") logger.error(e) if lang: logger.error("Aborting, since `lang` is only supported by Musixmatch") return None continue if lrc.is_preferred(target_type): logger.info(f'Lyrics found for "{search_term}" on {provider}') break elif lrc.is_acceptable(target_type): logger.info( f"Found plaintext lyrics on {provider}, but continuing search for synced lyrics" ) else: logger.debug( f"No suitable lyrics found on {provider}, continuing search..." ) if not lrc.is_acceptable(target_type): logger.info(f'No suitable lyrics found for "{search_term}" :(') return None if save_path: save_path = save_path.format(search_term=search_term) lrc.save_lrc_file(save_path, target_type) return lrc.to_str(target_type) def _select_providers( providers: List[LRCProvider], string_list: List[str] ) -> List[LRCProvider]: """ Returns a list of provider classes based on the given string list. """ strings_lowercase = [p.lower() for p in string_list] selection = [p for p in providers if str(p).lower() in strings_lowercase] if not selection: if string_list: # List of providers specified but not found. # Deliberately returning nothing instead of all to avoid unexpected behaviour. logger.error( f"Providers {string_list} not found in the list of available providers." ) return [] else: # No providers specified, using all return providers return selection syncedlyrics-1.0.1/syncedlyrics/__main__.py000066400000000000000000000001271465141114000210260ustar00rootroot00000000000000from syncedlyrics.cli import cli_handler if __name__ == "__main__": cli_handler() syncedlyrics-1.0.1/syncedlyrics/cli.py000066400000000000000000000034151465141114000200600ustar00rootroot00000000000000import argparse import logging from syncedlyrics import search def cli_handler(): """ Console entry point handler function. This parses the CLI arguments passed to `syncedlyrics -OPTIONS` command """ parser = argparse.ArgumentParser( description="Search for an LRC format (synchronized lyrics) of a music" ) parser.add_argument("search_term", help="The search term to find the track.") parser.add_argument( "-p", help="Providers to include in the searching (separated by space). Default: all providers", default="", choices=["musixmatch", "lrclib", "netease", "megalobiz", "genius"], nargs="+", type=str.lower, ) parser.add_argument( "-l", "--lang", help="Language of the translation along with the lyrics" ) parser.add_argument( "-o", "--output", help="Path to save '.lrc' lyrics", default="{search_term}.lrc" ) parser.add_argument( "-v", "--verbose", help="Use this flag to show the logs", action="store_true" ) parser.add_argument( "--plain-only", help="Only look for plain text (not synced) lyrics", action="store_true", ) parser.add_argument( "--synced-only", help="Only look for synced lyrics", action="store_true", ) parser.add_argument( "--enhanced", help="Returns word by word synced lyrics (if available)", action="store_true", ) args = parser.parse_args() if args.verbose: logging.basicConfig(level=logging.DEBUG) lrc = search( args.search_term, args.plain_only, args.synced_only, args.output, args.p, lang=args.lang, enhanced=args.enhanced, ) if lrc: print(lrc) syncedlyrics-1.0.1/syncedlyrics/providers/000077500000000000000000000000001465141114000207515ustar00rootroot00000000000000syncedlyrics-1.0.1/syncedlyrics/providers/__init__.py000066400000000000000000000003701465141114000230620ustar00rootroot00000000000000"""Synchronized Lyrics providers""" from .netease import NetEase from .deezer import Deezer from .lyricsify import Lyricsify from .megalobiz import Megalobiz from .musixmatch import Musixmatch from .lrclib import Lrclib from .genius import Genius syncedlyrics-1.0.1/syncedlyrics/providers/base.py000066400000000000000000000026021465141114000222350ustar00rootroot00000000000000import requests from typing import Optional import logging from ..utils import Lyrics class TimeoutSession(requests.Session): def request(self, method, url, **kwargs): kwargs.setdefault("timeout", (2, 10)) return super().request(method, url, **kwargs) class LRCProvider: """ Base class for all of the synced (LRC format) lyrics providers. """ def __init__(self) -> None: self.session = TimeoutSession() # Logging setup formatter = logging.Formatter("[%(name)s] %(message)s") handler = logging.StreamHandler() handler.setFormatter(formatter) self.logger = logging.getLogger(self.__class__.__name__) self.logger.addHandler(handler) def __str__(self) -> str: return self.__class__.__name__ def get_lrc_by_id(self, track_id: str) -> Optional[Lyrics]: """ Returns the synced lyrics of the song in [LRC](https://en.wikipedia.org/wiki/LRC_(file_format)) format if found. ### Arguments - track_id: The ID of the track defined in the provider database. e.g. Spotify/Deezer track ID """ raise NotImplementedError def get_lrc(self, search_term: str) -> Optional[Lyrics]: """ Returns the synced lyrics of the song in [LRC](https://en.wikipedia.org/wiki/LRC_(file_format)) format if found. """ raise NotImplementedError syncedlyrics-1.0.1/syncedlyrics/providers/deezer.py000066400000000000000000000037351465141114000226110ustar00rootroot00000000000000"""Deezer LRC provider (API powered by LyricFind)""" from typing import Optional from .base import LRCProvider from ..utils import Lyrics, get_best_match # Currently broken # TODO: Fix invalid CSRF token # Mostly based on https://gist.github.com/akashrchandran/95915c2081815397c454bd8aa4a118b5 class Deezer(LRCProvider): """Deezer provider class""" SEARCH_ENDPOINT = "https://api.deezer.com/search?q=" API_ENDPOINT = "http://www.deezer.com/ajax/gw-light.php" token = "null" def __init__(self) -> None: super().__init__() self.token = self._api_call("deezer.getUserData")["results"]["checkForm"] def _api_call(self, method: str, json=None) -> dict: params = { "api_version": "1.0", "api_token": self.token, "input": "3", "method": method, } response = self.session.post(self.API_ENDPOINT, params=params, json=json) return response.json() def get_lrc_by_id(self, track_id: str) -> Optional[Lyrics]: lrc = Lyrics() lrc_response = self._api_call("song.getLyrics", json={"sng_id": track_id}) lrc_json_objs = lrc_response["results"].get("LYRICS_SYNC_JSON") if not lrc_json_objs: lrc.unsynced = lrc_response["results"].get("LYRICS_TEXT") return lrc lrc_str = "" for chunk in lrc_json_objs: if chunk.get("lrc_timestamp") and chunk.get("line"): lrc_str += f"{chunk['lrc_timestamp']} {chunk['line']}\n" lrc.synced = lrc_str return lrc def get_lrc(self, search_term: str) -> Optional[Lyrics]: url = self.SEARCH_ENDPOINT + search_term.replace(" ", "+") search_results = self.session.get(url).json() cmp_key = lambda t: f"{t.get('title')} {t.get('artist').get('name')}" track = get_best_match(search_results.get("data", []), search_term, cmp_key) if not track: return None return self.get_lrc_by_id(track["id"]) syncedlyrics-1.0.1/syncedlyrics/providers/genius.py000066400000000000000000000021751465141114000226220ustar00rootroot00000000000000"""Genius (genius.com) provider API""" from typing import Optional from .base import LRCProvider from ..utils import Lyrics, generate_bs4_soup class Genius(LRCProvider): """Genius provider class""" SEARCH_ENDPOINT = "https://genius.com/api/search/multi?per_page=5&q=" def get_lrc(self, search_term: str) -> Optional[Lyrics]: params = {"q": search_term, "per_page": 5} cookies = { "obuid": "e3ee67e0-7df9-4181-8324-d977c6dc9250", } r = self.session.get(self.SEARCH_ENDPOINT, params=params, cookies=cookies) if not r.ok: return None data = r.json() data = data["response"]["sections"][1]["hits"] if not data: return None url = data[0]["result"]["url"] soup = generate_bs4_soup(self.session, url) els = soup.find_all("div", attrs={"data-lyrics-container": True}) if not els: return None lrc_str = "" for el in els: lrc_str += el.get_text(separator="\n", strip=True).replace("\n[", "\n\n[") lrc = Lyrics() lrc.unsynced = lrc_str return lrc syncedlyrics-1.0.1/syncedlyrics/providers/lrclib.py000066400000000000000000000030161465141114000225720ustar00rootroot00000000000000"""Lrclib (lrclib.net) LRC provider""" from typing import Optional from .base import LRCProvider from ..utils import Lyrics, sort_results class Lrclib(LRCProvider): """Lrclib LRC provider class""" ROOT_URL = "https://lrclib.net" API_ENDPOINT = ROOT_URL + "/api" SEARCH_ENDPOINT = API_ENDPOINT + "/search" LRC_ENDPOINT = API_ENDPOINT + "/get/" def __init__(self) -> None: super().__init__() def get_lrc_by_id(self, track_id: str) -> Optional[Lyrics]: url = self.LRC_ENDPOINT + track_id r = self.session.get(url) if not r.ok: return None track = r.json() lrc = Lyrics() lrc.synced = track.get("syncedLyrics") lrc.unsynced = track.get("plainLyrics") return lrc def get_lrc(self, search_term: str) -> Optional[Lyrics]: url = self.SEARCH_ENDPOINT r = self.session.get(url, params={"q": search_term}) if not r.ok: return None tracks = r.json() if not tracks: return None tracks = sort_results( tracks, search_term, lambda t: f'{t["artistName"]} - {t["trackName"]}' ) _id = str(tracks[0]["id"]) # Getting the first track that its `syncedLyrics` is not empty # _id = None # for track in tracks: # if (track.get("syncedLyrics", "") or "").strip(): # _id = str(track["id"]) # break # if not _id: # return None return self.get_lrc_by_id(_id) syncedlyrics-1.0.1/syncedlyrics/providers/lyricsify.py000066400000000000000000000024711465141114000233440ustar00rootroot00000000000000"""Lyricsify (lyricsify.com) LRC provider""" from typing import Optional from bs4 import SoupStrainer from .base import LRCProvider from ..utils import Lyrics, generate_bs4_soup, get_best_match # Currently broken # TODO: Bypassing Cloudflare anti-bot system class Lyricsify(LRCProvider): """Lyricsify LRC provider class""" ROOT_URL = "https://www.lyricsify.com" SEARCH_ENDPOINT = ROOT_URL + "/search?q=" def __init__(self) -> None: super().__init__() self.parser = "html.parser" def get_lrc(self, search_term: str) -> Optional[Lyrics]: url = self.SEARCH_ENDPOINT + search_term.replace(" ", "+") href_match = lambda h: h.startswith("/lyric/") a_tags_boud = SoupStrainer("a", href=href_match) soup = generate_bs4_soup(self.session, url, parse_only=a_tags_boud) cmp_key = lambda t: t.get_text().lower().replace("-", "") a_tag = get_best_match(soup.find_all("a"), search_term, cmp_key) if not a_tag: return None # Scraping from the LRC page lrc_id = a_tag["href"].split(".")[-1] soup = generate_bs4_soup(self.session, self.ROOT_URL + a_tag["href"]) lrc_str = soup.find("div", {"id": f"lyrics_{lrc_id}_details"}).get_text() lrc = Lyrics() lrc.add_unknown(lrc_str) return lrc syncedlyrics-1.0.1/syncedlyrics/providers/megalobiz.py000066400000000000000000000030201465141114000232670ustar00rootroot00000000000000"""Megalobiz (megalobiz.com) LRC provider""" from typing import Optional from bs4 import SoupStrainer from .base import LRCProvider from ..utils import Lyrics, generate_bs4_soup, get_best_match class Megalobiz(LRCProvider): """Megabolz provider class""" ROOT_URL = "https://www.megalobiz.com" SEARCH_ENDPOINT = ROOT_URL + "/search/all?qry={q}&searchButton.x=0&searchButton.y=0" def get_lrc(self, search_term: str) -> Optional[Lyrics]: url = self.SEARCH_ENDPOINT.format(q=search_term.replace(" ", "+")) def href_match(h: Optional[str]): if h and h.startswith("/lrc/maker/"): return True return False a_tags_boud = SoupStrainer("a", href=href_match) soup = generate_bs4_soup(self.session, url, parse_only=a_tags_boud) def a_text(a): # In MegaLobiz, we have some `a` tags that have the following text: # artist track ( lyrics ) [05:10.47] (we don't want that extra text) part = a.get_text().replace("by", "").split()[: search_term.count(" ") + 1] return " ".join(part) a_tag = get_best_match(soup.find_all("a"), search_term, a_text) if not a_tag: return None # Scraping from the LRC page lrc_id = a_tag["href"].split(".")[-1] soup = generate_bs4_soup(self.session, self.ROOT_URL + a_tag["href"]) lrc_str = soup.find("div", {"id": f"lrc_{lrc_id}_details"}).get_text() lrc = Lyrics() lrc.add_unknown(lrc_str) return lrc syncedlyrics-1.0.1/syncedlyrics/providers/musixmatch.py000066400000000000000000000126511465141114000235120ustar00rootroot00000000000000"""Musixmatch LRC provider""" from typing import Optional, List import time import json import os from .base import LRCProvider from ..utils import Lyrics, get_best_match, format_time, get_cache_path # Inspired from https://github.com/Marekkon5/onetagger/blob/0654131188c4df2b4b171ded7cdb927a4369746e/crates/onetagger-platforms/src/musixmatch.rs # Huge part converted from Rust to Py by ChatGPT :) # Whyyyy did you convert it from a good language to a bad one? :P class Musixmatch(LRCProvider): """Musixmatch provider class""" ROOT_URL = "https://apic-desktop.musixmatch.com/ws/1.1/" def __init__(self, lang: Optional[str] = None, enhanced: bool = False) -> None: super().__init__() self.lang = lang self.enhanced = enhanced self.token = None def _get(self, action: str, query: List[tuple]): if action != "token.get" and self.token is None: self._get_token() query.append(("app_id", "web-desktop-app-v1.0")) if self.token is not None: query.append(("usertoken", self.token)) t = str(int(time.time() * 1000)) query.append(("t", t)) url = self.ROOT_URL + action response = self.session.get(url, params=query) return response def _get_token(self): token_path = get_cache_path("syncedlyrics", False) / "musixmatch_token.json" current_time = int(time.time()) if token_path.exists(): with open(token_path, "r") as token_file: cached_token_data = json.load(token_file) cached_token = cached_token_data.get("token") expiration_time = cached_token_data.get("expiration_time") if cached_token and expiration_time and current_time < expiration_time: self.token = cached_token return # Token not cached or expired, fetch a new token d = self._get("token.get", [("user_language", "en")]).json() if d["message"]["header"]["status_code"] == 401: time.sleep(10) return self._get_token() new_token = d["message"]["body"]["user_token"] expiration_time = current_time + 600 # 10 minutes expiration # Cache the new token self.token = new_token token_data = {"token": new_token, "expiration_time": expiration_time} token_path.parent.mkdir(parents=True, exist_ok=True) with open(token_path, "w") as token_file: json.dump(token_data, token_file) def get_lrc_by_id(self, track_id: str) -> Optional[Lyrics]: r = self._get( "track.subtitle.get", [("track_id", track_id), ("subtitle_format", "lrc")], ) if self.lang is not None: r_tr = self._get( "crowd.track.translations.get", [ ("track_id", track_id), ("subtitle_format", "lrc"), ("translation_fields_set", "minimal"), ("selected_language", self.lang), ], ) body_tr = r_tr.json()["message"]["body"] if not body_tr["translations_list"]: raise Exception("Couldn't find translations") if not r.ok: return None body = r.json()["message"]["body"] if not body: return None lrc_str = body["subtitle"]["subtitle_body"] if self.lang is not None: for i in body_tr["translations_list"]: org, tr = ( i["translation"]["subtitle_matched_line"], i["translation"]["description"], ) lrc_str = lrc_str.replace(org, org + "\n" + f"({tr})") lrc = Lyrics() lrc.synced = lrc_str return lrc def get_lrc_word_by_word(self, track_id: str) -> Optional[Lyrics]: lrc = Lyrics() r = self._get("track.richsync.get", [("track_id", track_id)]) if r.ok and r.json()["message"]["header"]["status_code"] == 200: lrc_raw = r.json()["message"]["body"]["richsync"]["richsync_body"] lrc_raw = json.loads(lrc_raw) lrc_str = "" for i in lrc_raw: lrc_str += f"[{format_time(i['ts'])}] " for l in i["l"]: t = format_time(float(i["ts"]) + float(l["o"])) lrc_str += f"<{t}> {l['c']} " lrc_str += "\n" lrc.synced = lrc_str return lrc def get_lrc(self, search_term: str) -> Optional[Lyrics]: r = self._get( "track.search", [ ("q", search_term), ("page_size", "5"), ("page", "1"), ], ) status_code = r.json()["message"]["header"]["status_code"] if status_code != 200: self.logger.warning(f"Got status code {status_code} for {search_term}") return None body = r.json()["message"]["body"] if not isinstance(body, dict): return None tracks = body["track_list"] cmp_key = lambda t: f"{t['track']['track_name']} {t['track']['artist_name']}" track = get_best_match(tracks, search_term, cmp_key) if not track: return None track_id = track["track"]["track_id"] if self.enhanced: lrc = self.get_lrc_word_by_word(track_id) if lrc and lrc.synced: return lrc return self.get_lrc_by_id(track_id) syncedlyrics-1.0.1/syncedlyrics/providers/netease.py000066400000000000000000000053301465141114000227500ustar00rootroot00000000000000"""NetEase (music.163.com) china-based provider""" from typing import Optional from .base import LRCProvider from ..utils import Lyrics, get_best_match class NetEase(LRCProvider): """NetEase provider class""" API_ENDPOINT_METADATA = "https://music.163.com/api/search/pc" API_ENDPOINT_LYRICS = "https://music.163.com/api/song/lyric" def __init__(self) -> None: super().__init__() self.session.headers["cookie"] = ( "NMTID=00OAVK3xqDG726ITU6jopU6jF2yMk0AAAGCO8l1BA; JSESSIONID-WYYY=8KQo11YK2GZP45RMlz8Kn80vHZ9%2FGvwzRKQXXy0iQoFKycWdBlQjbfT0MJrFa6hwRfmpfBYKeHliUPH287JC3hNW99WQjrh9b9RmKT%2Fg1Exc2VwHZcsqi7ITxQgfEiee50po28x5xTTZXKoP%2FRMctN2jpDeg57kdZrXz%2FD%2FWghb%5C4DuZ%3A1659124633932; _iuqxldmzr_=32; _ntes_nnid=0db6667097883aa9596ecfe7f188c3ec,1659122833973; _ntes_nuid=0db6667097883aa9596ecfe7f188c3ec; WNMCID=xygast.1659122837568.01.0; WEVNSM=1.0.0; WM_NI=CwbjWAFbcIzPX3dsLP%2F52VB%2Bxr572gmqAYwvN9KU5X5f1nRzBYl0SNf%2BV9FTmmYZy%2FoJLADaZS0Q8TrKfNSBNOt0HLB8rRJh9DsvMOT7%2BCGCQLbvlWAcJBJeXb1P8yZ3RHA%3D; WM_NIKE=9ca17ae2e6ffcda170e2e6ee90c65b85ae87b9aa5483ef8ab3d14a939e9a83c459959caeadce47e991fbaee82af0fea7c3b92a81a9ae8bd64b86beadaaf95c9cedac94cf5cedebfeb7c121bcaefbd8b16dafaf8fbaf67e8ee785b6b854f7baff8fd1728287a4d1d246a6f59adac560afb397bbfc25ad9684a2c76b9a8d00b2bb60b295aaafd24a8e91bcd1cb4882e8beb3c964fb9cbd97d04598e9e5a4c6499394ae97ef5d83bd86a3c96f9cbeffb1bb739aed9ea9c437e2a3; WM_TID=AAkRFnl03RdABEBEQFOBWHCPOeMra4IL; playerid=94262567" ) def search_track(self, search_term: str) -> Optional[dict]: """Returns a `dict` containing some metadata for the found track.""" params = {"limit": 10, "type": 1, "offset": 0, "s": search_term} response = self.session.get(self.API_ENDPOINT_METADATA, params=params) results = response.json().get("result", {}).get("songs") if not results: return None cmp_key = lambda t: f"{t.get('name')} {t.get('artists')[0].get('name')}" track = get_best_match(results, search_term, cmp_key) # Update the session cookies from the new sent cookies for the next request. self.session.cookies.update(response.cookies) self.session.headers.update({"referer": response.url}) return track def get_lrc_by_id(self, track_id: str) -> Optional[Lyrics]: params = {"id": track_id, "lv": 1} response = self.session.get(self.API_ENDPOINT_LYRICS, params=params) lrc = Lyrics() lrc.add_unknown(response.json().get("lrc", {}).get("lyric")) return lrc def get_lrc(self, search_term: str) -> Optional[Lyrics]: track = self.search_track(search_term) if not track: return None return self.get_lrc_by_id(track["id"]) syncedlyrics-1.0.1/syncedlyrics/providers/spotify.py000066400000000000000000000016501465141114000230220ustar00rootroot00000000000000"""Spotify LRC provider (the API is powered by Musixmatch)""" from typing import Optional from .base import LRCProvider from ..utils import Lyrics class Spotify(LRCProvider): """Spotify provider class""" def __init__(self) -> None: super().__init__() @classmethod def get_track_id(cls, search_term: str) -> Optional[str]: """Returns a Spotify track ID for given `search_term`""" # TODO: self.client.search(search_term) and processing the results raise NotImplementedError def get_lrc_by_id(self, track_id: str) -> Optional[Lyrics]: # TODO: raise NotImplementedError def get_lrc(self, search_term: str) -> Optional[Lyrics]: # TODO: Use https://github.com/akashrchandran/spotify-lyrics-api # Note: as of recently, only premium users can get lyrics from spotify, so this would require an account access token. raise NotImplementedError syncedlyrics-1.0.1/syncedlyrics/utils.py000066400000000000000000000134251465141114000204530ustar00rootroot00000000000000"""Utility functions for `syncedlyrics` package""" from dataclasses import dataclass from bs4 import BeautifulSoup, FeatureNotFound import rapidfuzz from typing import Union, Callable, Optional import datetime from enum import Enum, auto import re import os from pathlib import Path R_FEAT = re.compile(r"\((feat.+)\)", re.IGNORECASE) class TargetType(Enum): PLAINTEXT = auto() PREFER_SYNCED = auto() SYNCED_ONLY = auto() @dataclass class Lyrics: synced: Optional[str] = None unsynced: Optional[str] = None def add_unknown(self, unknown: str): type = identify_lyrics_type(unknown) if type == "synced": self.synced = unknown elif type == "plaintext": self.unsynced = unknown def update(self, other: Optional["Lyrics"]): if not other: return if other.synced: self.synced = other.synced if other.unsynced: self.unsynced = other.unsynced def is_preferred(self, target_type: TargetType) -> bool: return bool( self.synced or (target_type == TargetType.PLAINTEXT and self.unsynced) ) def is_acceptable(self, target_type: TargetType) -> bool: return bool( self.synced or (target_type != TargetType.SYNCED_ONLY and self.unsynced) ) def to_str(self, target_type: TargetType) -> str: if target_type == TargetType.PLAINTEXT: return self.unsynced or synced_to_plaintext(self.synced) elif target_type == TargetType.PREFER_SYNCED: return self.synced or self.unsynced return self.synced def save_lrc_file(self, path: str, target_type: TargetType): """Saves the `.lrc` file""" with open(path, "w", encoding="utf-8") as f: f.write(self.to_str(target_type)) def get_cache_path(lib_name: str = "syncedlyrics", auto_create: bool = True) -> Path: """Get or create a cache directory for the given library name.""" if os.name == "nt": # Windows base_dir = os.getenv("LOCALAPPDATA", os.path.expanduser("~")) elif os.name == "posix": if "Darwin" in os.uname().sysname: # macOS base_dir = os.path.expanduser("~/Library/Caches") else: # Linux base_dir = os.path.expanduser("~/.cache") else: base_dir = os.path.expanduser("~") target_dir = Path(base_dir) / lib_name if auto_create: target_dir.mkdir(parents=True, exist_ok=True) return target_dir def synced_to_plaintext(synced_lyrics: str) -> str: return re.sub(r"\[\d+:\d+\.\d+\] ", "", synced_lyrics) def identify_lyrics_type(lrc: str) -> str: """Identifies the type of the LRC string""" if not lrc: return "invalid" lines = lrc.split("\n")[5:10] if all("[" in l for l in lines): return "synced" return "plaintext" def has_translation(lrc: str) -> bool: """Checks whether the LRC string has a translation or not""" lines = lrc.split("\n")[5:10] for i, line in enumerate(lines): if "[" in line: if i + 1 < len(lines): next_line = lines[i + 1] if "(" not in next_line: return False return True def generate_bs4_soup(session, url: str, **kwargs): """Returns a `BeautifulSoup` from the given `url`. Tries to use `lxml` as the parser if available, otherwise `html.parser` """ r = session.get(url) try: soup = BeautifulSoup(r.text, features="lxml", **kwargs) except FeatureNotFound: soup = BeautifulSoup(r.text, features="html.parser", **kwargs) return soup def format_time(time_in_seconds: float): """Returns a [mm:ss.xx] formatted string from the given time in seconds.""" time = datetime.timedelta(seconds=time_in_seconds) minutes, seconds = divmod(time.seconds, 60) return f"{minutes:02}:{seconds:02}.{time.microseconds//10000:02}" def str_score(a: str, b: str) -> float: """Returns the similarity score of the two strings""" # if user does not specify any "feat" in the search term, # remove the "feat" from the search results' names a, b = a.lower(), b.lower() if "feat" not in b: a, b = R_FEAT.sub("", a), R_FEAT.sub("", b) return rapidfuzz.fuzz.token_set_ratio(a, b) def str_same(a: str, b: str, n: int) -> bool: """Returns `True` if the similarity score of the two strings is greater than `n`""" return round(str_score(a, b)) >= n def sort_results( results: list, search_term: str, compare_key: Union[str, Callable[[dict], str]] = "name", ) -> list: """ Sorts the API results based on the similarity score of the `compare_key` with the `search_term`. ## Parameters - `results`: The API results - `search_term`: The search term - `compare_key`: The key to compare the `search_term` with. Can be a string or a function that takes a track and returns a string. """ if isinstance(compare_key, str): def compare_key(t): return t[compare_key] def sort_key(t): return str_score(compare_key(t), search_term) return sorted(results, key=sort_key, reverse=True) def get_best_match( results: list, search_term: str, compare_key: Union[str, Callable[[dict], str]] = "name", min_score: int = 65, ) -> Optional[dict]: """ Returns the best match from the API results based on the similarity score of the `compare_key` with the `search_term`. """ if not results: return None results = sort_results(results, search_term, compare_key=compare_key) best_match = results[0] value_to_compare = ( best_match[compare_key] if isinstance(compare_key, str) else compare_key(best_match) ) if not str_same(value_to_compare, search_term, n=min_score): return None return best_match syncedlyrics-1.0.1/tests.py000066400000000000000000000025561465141114000157450ustar00rootroot00000000000000"""Some simple tests for geting notifed for API changes of the providers""" import os import syncedlyrics import logging logging.basicConfig(level=logging.DEBUG) q = os.getenv("TEST_Q", "bad guy billie eilish") def _test_provider(provider: str, **kwargs): lrc = syncedlyrics.search(search_term=q, providers=[provider], **kwargs) logging.debug(lrc) assert isinstance(lrc, str) return lrc def test_netease(): _test_provider("NetEase") def test_musixmatch(): _test_provider("Musixmatch") def test_musixmatch_translation(): lrc = _test_provider("Musixmatch", lang="es") # not only testing there is a result, but the translation is also included assert syncedlyrics.utils.has_translation(lrc) def test_musixmatch_enhanced(): _test_provider("Musixmatch", enhanced=True) def test_lrclib(): _test_provider("Lrclib") def test_genius(): _test_provider("Genius") def test_plaintext_only(): lrc = _test_provider("Lrclib", plain_only=True) assert syncedlyrics.utils.identify_lyrics_type(lrc) == "plaintext" def test_synced_only(): lrc = _test_provider("Lrclib", synced_only=True) assert syncedlyrics.utils.identify_lyrics_type(lrc) == "synced" # Not working (at least temporarily) # def test_deezer(): # _test_provider("Deezer") # Fails randomly on CI # def test_megalobiz(): # _test_provider("Megalobiz")