├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── build.yml │ ├── docs.yml │ ├── lint.yml │ ├── publish-to-test-pypi.yml │ └── publish.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .python-version ├── .ruff.toml ├── LICENSE.txt ├── README.md ├── docs ├── Makefile ├── make.bat ├── requirements.txt └── src │ ├── 404.png │ ├── 404.rst │ ├── conf.py │ ├── contributing.rst │ ├── examples │ ├── example projects.rst │ └── snippets.rst │ ├── header.png │ ├── how_it_works.rst │ ├── index.rst │ ├── other_guides.rst │ ├── reference.rst │ ├── reference │ ├── api.rst │ ├── auth.rst │ ├── genius.rst │ ├── sender.rst │ ├── types.rst │ └── utils.rst │ ├── release_notes.rst │ ├── setup.rst │ ├── text_formatting.rst │ └── usage.rst ├── lyricsgenius ├── __init__.py ├── __main__.py ├── api │ ├── __init__.py │ ├── api.py │ ├── base.py │ └── public_methods │ │ ├── __init__.py │ │ ├── album.py │ │ ├── annotation.py │ │ ├── article.py │ │ ├── artist.py │ │ ├── cover_art.py │ │ ├── discussion.py │ │ ├── leaderboard.py │ │ ├── misc.py │ │ ├── question.py │ │ ├── referent.py │ │ ├── search.py │ │ ├── song.py │ │ ├── user.py │ │ └── video.py ├── auth.py ├── errors.py ├── genius.py ├── types │ ├── __init__.py │ ├── album.py │ ├── artist.py │ ├── base.py │ └── song.py └── utils.py ├── pyproject.toml ├── tests ├── __init__.py ├── test_album.py ├── test_api.py ├── test_artist.py ├── test_auth.py ├── test_base.py ├── test_genius.py ├── test_public_methods.py ├── test_song.py └── test_utils.py ├── tox.ini └── uv.lock /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: johnwmillr 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Describe a bug associated with LyricsGenius 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | Write a clear and concise description of what the bug is. 9 | 10 | **Expected behavior** 11 | Write a clear and concise description of what you expected to happen. 12 | 13 | **To Reproduce** 14 | Describe the steps required to reproduce the behavior. 15 | 1. Step one... 16 | 2. Step two... 17 | 18 | Include the error message associated with the bug. 19 | 20 | **Version info** 21 | - Package version [`import lyricsgenius; print(lyricsgenius.__version__)`] 22 | - OS: [e.g. macOS, Windows, etc.] 23 | 24 | **Additional context** 25 | Add any other context about the problem here. 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for improving LyricsGenius 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | Write a clear and concise description of what the problem is -- e.g. "I'm always frustrated when [...]" 9 | 10 | **Describe the solution you'd like** 11 | Write a clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | Write a clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | name: build 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout the repository 17 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 18 | with: 19 | persist-credentials: false 20 | 21 | - name: Install uv and set the Python version 22 | uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1 23 | with: 24 | version: 0.6.11 25 | enable-cache: true 26 | cache-dependency-glob: uv.lock 27 | 28 | - name: Set up Python 29 | uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 30 | with: 31 | python-version-file: .python-version 32 | 33 | - name: Build the package 34 | run: uv build 35 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | docs: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout the repository 17 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 18 | with: 19 | persist-credentials: false 20 | 21 | - name: Install uv and set the Python version 22 | uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1 23 | with: 24 | version: 0.6.11 25 | enable-cache: true 26 | cache-dependency-glob: uv.lock 27 | 28 | - name: Set up Python 29 | uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 30 | with: 31 | python-version-file: .python-version 32 | 33 | - name: Install dependencies 34 | run: uv sync 35 | 36 | - name: Build Docs 37 | working-directory: ./docs 38 | run: uv run sphinx-build -W -b html src build 39 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | ruff: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 13 | - uses: astral-sh/ruff-action@9828f49eb4cadf267b40eaa330295c412c68c1f9 # v3.2.2 14 | 15 | lint: 16 | if: false # Temporarily disabled 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout the repository 20 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 21 | with: 22 | persist-credentials: false 23 | 24 | - name: Install uv and set the Python version 25 | uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1 26 | with: 27 | version: 0.6.11 28 | enable-cache: true 29 | cache-dependency-glob: uv.lock 30 | 31 | - name: Set up Python 32 | uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 33 | with: 34 | python-version-file: ".python-version" 35 | 36 | - name: Install dependencies 37 | run: uv sync 38 | 39 | - name: Lint 40 | run: tox -e lint 41 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-test-pypi.yml: -------------------------------------------------------------------------------- 1 | name: publish-to-testpypi 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build: 8 | name: build 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout the repository 13 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 14 | with: 15 | persist-credentials: false 16 | 17 | - name: Install uv and set the Python version 18 | uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1 19 | with: 20 | version: "0.6.11" 21 | enable-cache: true 22 | cache-dependency-glob: "uv.lock" 23 | 24 | - name: Set up Python 25 | uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 26 | with: 27 | python-version-file: ".python-version" 28 | 29 | - name: Build the package 30 | run: uv build 31 | 32 | - name: Upload artifacts 33 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 34 | with: 35 | name: built-package 36 | path: dist/ 37 | 38 | publish-to-testpypi: 39 | name: Upload release to TestPyPI 40 | needs: build 41 | environment: 42 | name: testpypi 43 | url: https://test.pypi.org/p/lyricsgenius 44 | runs-on: ubuntu-latest 45 | permissions: 46 | id-token: write 47 | steps: 48 | - name: Download all the dists 49 | uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v.4.2.1 50 | with: 51 | name: built-package 52 | path: dist/ 53 | 54 | - name: Publish package distributions to TestPyPI 55 | uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4 56 | with: 57 | repository-url: https://test.pypi.org/legacy/ 58 | verbose: false 59 | skip-existing: true 60 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | workflow_dispatch: 8 | 9 | jobs: 10 | project-version-is-changed: 11 | name: Check if project version is changed 12 | runs-on: ubuntu-latest 13 | outputs: 14 | CHANGED: ${{ steps.set-version-changed-var.outputs.CHANGED }} 15 | steps: 16 | - name: Checkout the repository 17 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 18 | with: 19 | persist-credentials: false 20 | 21 | - name: Get the latest version from PyPI 22 | id: get-version-from-pypi 23 | run: | 24 | pypi_version=$(curl -s https://pypi.org/pypi/lyricsgenius/json | jq -r .info.version) 25 | echo "Latest version: $pypi_version" 26 | echo "::set-output name=pypi_version::$pypi_version" 27 | 28 | - name: Parse the version from pyproject.toml 29 | id: get-version-from-pyproject 30 | run: | 31 | pyproject_version=$(grep -oP '(?<=^version = ")[^"]*' pyproject.toml) 32 | echo "pyproject.toml version: $pyproject_version" 33 | echo "::set-output name=pyproject_version::$pyproject_version" 34 | 35 | - name: Check if the project version is changed 36 | id: set-version-changed-var 37 | run: | 38 | if [ "${{ steps.get-version-from-pypi.outputs.pypi_version }}" != "${{ steps.get-version-from-pyproject.outputs.pyproject_version }}" ]; then 39 | echo "The local project differs from the PyPI version. Need to build and publish." 40 | echo "CHANGED=true" >> $GITHUB_OUTPUT 41 | else 42 | echo "The local project is the same as the PyPI version. No need to build or publish." 43 | echo "CHANGED=false" >> $GITHUB_OUTPUT 44 | fi 45 | 46 | build: 47 | name: build 48 | runs-on: ubuntu-latest 49 | needs: project-version-is-changed 50 | if: needs.project-version-is-changed.outputs.CHANGED == 'true' 51 | steps: 52 | - name: Checkout the repository 53 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 54 | with: 55 | persist-credentials: false 56 | 57 | - name: Install uv and set the Python version 58 | uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1 59 | with: 60 | version: 0.6.11 61 | enable-cache: true 62 | cache-dependency-glob: uv.lock 63 | 64 | - name: Set up Python 65 | uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 66 | with: 67 | python-version-file: .python-version 68 | 69 | - name: Build the package 70 | run: uv build 71 | 72 | - name: Upload artifacts 73 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 74 | with: 75 | name: built-package 76 | path: dist/ 77 | 78 | publish-to-pypi: 79 | name: Upload release to PyPI 80 | runs-on: ubuntu-latest 81 | needs: build 82 | environment: 83 | name: pypi 84 | url: https://pypi.org/p/lyricsgenius 85 | permissions: 86 | id-token: write 87 | steps: 88 | - name: Download all the dists 89 | uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v.4.2.1 90 | with: 91 | name: built-package 92 | path: dist/ 93 | 94 | - name: Publish package distributions to PyPI 95 | uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4 96 | with: 97 | repository-url: https://upload.pypi.org/legacy/ 98 | verbose: false 99 | skip-existing: false 100 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | *.DS_Store 3 | 4 | # API credentials 5 | *.ini 6 | 7 | # May not want to commit lyrics text files 8 | *.txt 9 | *.json 10 | 11 | # Jupyter 12 | /Notebooks 13 | .ipynb_checkpoints/ 14 | *.ipynb 15 | 16 | # Test files 17 | .cache/ 18 | .tox 19 | lyricsgenius.egg-info 20 | fixtures 21 | 22 | # Python module things 23 | *.pyc 24 | 25 | # Python cache stuff 26 | *.egg-info 27 | __pycache__ 28 | dist 29 | build 30 | .idea 31 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | fail_fast: true 2 | 3 | repos: 4 | - repo: https://github.com/astral-sh/uv-pre-commit 5 | rev: 0.6.14 # uv version. 6 | hooks: 7 | - id: uv-lock 8 | - repo: https://github.com/astral-sh/ruff-pre-commit 9 | rev: v0.11.4 # Ruff version. 10 | hooks: 11 | - id: ruff 12 | name: Lint with ruff 13 | types_or: [python, pyi] 14 | args: [--fix] 15 | - id: ruff-format 16 | name: Format with ruff 17 | types_or: [python, pyi] 18 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.13 2 | -------------------------------------------------------------------------------- /.ruff.toml: -------------------------------------------------------------------------------- 1 | [lint] 2 | # Enable flake8-bugbear (`B`) rules, in addition to the defaults. 3 | select = ["E4", "E7", "E9", "F", "B", "Q", "I"] 4 | 5 | # Avoid enforcing line-length violations (`E501`) 6 | ignore = ["E501"] 7 | 8 | # Avoid trying to fix flake8-bugbear (`B`) violations. 9 | unfixable = ["B"] 10 | 11 | # Ignore `E402` (import violations) in all `__init__.py` files, and in selected subdirectories. 12 | [lint.per-file-ignores] 13 | "__init__.py" = ["E402", "F401"] 14 | "**/{tests,docs,tools}/*" = ["E402", "F401"] 15 | 16 | [lint.pydocstyle] 17 | convention = "google" 18 | 19 | [format] 20 | quote-style = "double" 21 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 John W. R. Miller 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LyricsGenius: a Python client for the Genius.com API 2 | [![Build Status](https://github.com/johnwmillr/LyricsGenius/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/johnwmillr/LyricsGenius/actions/workflows/build.yml) 3 | [![Documentation Status](https://readthedocs.org/projects/lyricsgenius/badge/?version=master)](https://lyricsgenius.readthedocs.io/en/latest/?badge=master) 4 | [![PyPI version](https://badge.fury.io/py/lyricsgenius.svg)](https://pypi.org/project/lyricsgenius/) 5 | [![Support this project](https://img.shields.io/badge/Support%20this%20project-grey.svg?logo=github%20sponsors)](https://github.com/sponsors/johnwmillr) 6 | 7 | `lyricsgenius` provides a simple interface to the song, artist, and lyrics data stored on [Genius.com](https://www.genius.com). 8 | 9 | The full documentation for `lyricsgenius` is available online at [Read the Docs](https://lyricsgenius.readthedocs.io/en/master/). 10 | 11 | ## Setup 12 | Before using this package you'll need to sign up for a (free) account that authorizes access to [the Genius API](http://genius.com/api-clients). The Genius account provides a `access_token` that is required by the package. See the [Usage section](https://github.com/johnwmillr/LyricsGenius#usage) below for examples. 13 | 14 | ## Installation 15 | `lyricsgenius` requires Python 3. 16 | 17 | Use `pip` to install the package from PyPI: 18 | 19 | ```bash 20 | pip install lyricsgenius 21 | ``` 22 | 23 | Or, install the latest version of the package from GitHub: 24 | 25 | ```bash 26 | pip install git+https://github.com/johnwmillr/LyricsGenius.git 27 | ``` 28 | 29 | ## Usage 30 | Import the package and initiate Genius: 31 | 32 | ```python 33 | import lyricsgenius 34 | genius = lyricsgenius.Genius(token) 35 | ``` 36 | 37 | If you don't pass a token to the `Genius` class, `lyricsgenus` will look for an environment variable called `GENIUS_ACCESS_TOKEN` and attempt to use that for authentication. 38 | 39 | ```python 40 | genius = Genius() 41 | ``` 42 | 43 | Search for songs by a given artist: 44 | 45 | ```python 46 | artist = genius.search_artist("Andy Shauf", max_songs=3, sort="title") 47 | print(artist.songs) 48 | ``` 49 | By default, the `search_artist()` only returns songs where the given artist is the primary artist. 50 | However, there may be instances where it is desirable to get all of the songs that the artist appears on. 51 | You can do this by setting the `include_features` argument to `True`. 52 | 53 | ```python 54 | artist = genius.search_artist("Andy Shauf", max_songs=3, sort="title", include_features=True) 55 | print(artist.songs) 56 | ``` 57 | 58 | Search for a single song by the same artist: 59 | 60 | ```python 61 | song = artist.song("To You") 62 | # or: 63 | # song = genius.search_song("To You", artist.name) 64 | print(song.lyrics) 65 | ``` 66 | 67 | Add the song to the artist object: 68 | 69 | ```python 70 | artist.add_song(song) 71 | # the Artist object also accepts song names: 72 | # artist.add_song("To You") 73 | ``` 74 | 75 | Save the artist's songs to a JSON file: 76 | 77 | ```python 78 | artist.save_lyrics() 79 | ``` 80 | 81 | Searching for an album and saving it: 82 | 83 | ```python 84 | album = genius.search_album("The Party", "Andy Shauf") 85 | album.save_lyrics() 86 | ``` 87 | 88 | There are various options configurable as parameters within the `Genius` class: 89 | 90 | ```python 91 | genius.verbose = False # Turn off status messages 92 | genius.remove_section_headers = True # Remove section headers (e.g. [Chorus]) from lyrics when searching 93 | genius.skip_non_songs = False # Include hits thought to be non-songs (e.g. track lists) 94 | genius.excluded_terms = ["(Remix)", "(Live)"] # Exclude songs with these words in their title 95 | ``` 96 | 97 | You can also call the package from the command line: 98 | 99 | ```bash 100 | export GENIUS_ACCESS_TOKEN="my_access_token_here" 101 | python -m lyricsgenius --help 102 | 103 | # Print a song's lyrics to stdout in text format 104 | python -m lyricsgenius song "Check the Rhyme" "A Tribe Called Quest" --format txt 105 | 106 | # Save a song's lyrics in JSON format 107 | python -m lyricsgenius song "Begin Again" "Andy Shauf" --format json --save 108 | 109 | # Save a song's lyrics in both JSON and text formats 110 | python -m lyricsgenius song "Begin Again" "Andy Shauf" --format json txt --save 111 | 112 | # Save an artist's lyrics to text files (stopping after 2 songs) 113 | python -m lyricsgenius artist "The Beatles" --max-songs 2 --format txt --save 114 | ``` 115 | 116 | ## Example projects 117 | 118 | - [Trucks and Beer: A textual analysis of popular country music](http://www.johnwmillr.com/trucks-and-beer/) 119 | - [Neural machine translation: Explaining the Meaning Behind Lyrics](https://github.com/tsandefer/dsi_capstone_3) 120 | - [What makes some blink-182 songs more popular than others?](http://jdaytn.com/posts/download-blink-182-data/) 121 | - [Sentiment analysis on hip-hop lyrics](https://github.com/Hugo-Nattagh/2017-Hip-Hop) 122 | - [Does Country Music Drink More Than Other Genres?](https://towardsdatascience.com/does-country-music-drink-more-than-other-genres-a21db901940b) 123 | - [49 Years of Lyrics: Why So Angry?](https://towardsdatascience.com/49-years-of-lyrics-why-so-angry-1adf0a3fa2b4) 124 | 125 | ## Contributing 126 | Please contribute! If you want to fix a bug, suggest improvements, or add new features to the project, just [open an issue](https://github.com/johnwmillr/LyricsGenius/issues) or send me a pull request. 127 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = src 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=src 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinx-rtd-theme 3 | -------------------------------------------------------------------------------- /docs/src/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnwmillr/LyricsGenius/30a38b66b249fc3cb062d7e72443c3a1c57a9334/docs/src/404.png -------------------------------------------------------------------------------- /docs/src/404.rst: -------------------------------------------------------------------------------- 1 | .. image:: 404.png 2 | 3 | 4 | Whoops! 5 | ======== 6 | The page you're looking for doesn't exist. Let's go 7 | :ref:`home `. 8 | 9 | .. toctree:: 10 | :maxdepth: 1 11 | :hidden: 12 | :caption: Library 13 | 14 | reference 15 | release_notes 16 | 17 | .. toctree:: 18 | :maxdepth: 1 19 | :hidden: 20 | :caption: Guide 21 | 22 | setup 23 | usage 24 | contributing 25 | 26 | 27 | .. _Genius.com: https://www.genius.com 28 | -------------------------------------------------------------------------------- /docs/src/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | sys.path.insert(0, os.path.abspath("../..")) 5 | 6 | # -- Project information ----------------------------------------------------- 7 | 8 | project = "lyricsgenius" 9 | copyright = "2025, John W. R. Miller, Allerter" 10 | author = "John W. R. Miller, Allerter" 11 | 12 | # -- General configuration --------------------------------------------------- 13 | 14 | extensions = [ 15 | "sphinx_rtd_theme", 16 | "sphinx.ext.napoleon", 17 | "sphinx.ext.autodoc", 18 | "sphinx.ext.autosummary", 19 | "sphinx.ext.extlinks", 20 | ] 21 | 22 | exclude_patterns = ["build"] 23 | master_doc = "index" 24 | autosummary_generate = True 25 | # -- Options for HTML output ------------------------------------------------- 26 | 27 | highlight_language = "python3" 28 | html_theme = "sphinx_rtd_theme" 29 | 30 | # -- Other ------------------------------------------------------------------- 31 | 32 | extlinks = { 33 | "issue": ("https://github.com/johnwmillr/LyricsGenius/issues/%s", "%s"), 34 | "commit": ("https://github.com/johnwmillr/LyricsGenius/commit/%s", "%s"), 35 | } 36 | -------------------------------------------------------------------------------- /docs/src/contributing.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ------------ 3 | Please contribute! Genius has a lot of undocumented API endpoints. 4 | You could try to look through Genius yourself to uncover new ones, and 5 | implement them. Or you could go through the only ones that have already 6 | been implemented and try to make more sense of the parameters they take. 7 | 8 | If you want to fix a bug, suggest improvements, or 9 | add new features to the project, just `open an issue`_ on GitHub. 10 | 11 | If you want to run the tests on your machine before opening a 12 | PR, do the following: 13 | 14 | .. code:: bash 15 | 16 | cd LyricsGenius 17 | pip install -e .[dev] 18 | 19 | This will install the package in developer mode with all the packages 20 | necessary for running the tests. Now you can run three types of commands 21 | to test your changes: 22 | 23 | - ``tox -e test``: runs the unit tests. 24 | - ``tox -e lint``: runs flake8 (PEP8 for code), doc8 (PEP8 for docs) 25 | and tests creating docs. 26 | - ``tox``: runs all tests (both of the ones above). 27 | 28 | 29 | 30 | .. _open an issue: https://github.com/johnwmillr/LyricsGenius/issues 31 | -------------------------------------------------------------------------------- /docs/src/examples/example projects.rst: -------------------------------------------------------------------------------- 1 | Example projects 2 | ================== 3 | 4 | - `Trucks and Beer: A textual analysis of popular country music`_ 5 | - `Neural machine translation: Explaining the Meaning Behind Lyrics`_ 6 | - `What makes some blink-182 songs more popular than others?`_ 7 | - `Sentiment analysis on hip-hop lyrics`_ 8 | - `Does Country Music Drink More Than Other Genres?`_ 9 | - `49 Years of Lyrics: Why So Angry?`_ 10 | 11 | .. _`Trucks and Beer: A textual analysis of popular country music`: http://www.johnwmillr.com/trucks-and-beer/ 12 | .. _`Neural machine translation: Explaining the Meaning Behind Lyrics`: https://github.com/tsandefer/dsi_capstone_3 13 | .. _What makes some blink-182 songs more popular than others?: http://jdaytn.com/posts/download-blink-182-data/ 14 | .. _Sentiment analysis on hip-hop lyrics: https://github.com/Hugo-Nattagh/2017-Hip-Hop 15 | .. _Does Country Music Drink More Than Other Genres?: https://towardsdatascience.com/does-country-music-drink-more-than-other-genres-a21db901940b 16 | .. _`49 Years of Lyrics: Why So Angry?`: https://towardsdatascience.com/49-years-of-lyrics-why-so-angry-1adf0a3fa2b4 17 | -------------------------------------------------------------------------------- /docs/src/examples/snippets.rst: -------------------------------------------------------------------------------- 1 | .. _snippets: 2 | .. currentmodule:: lyricsgenius 3 | 4 | Snippets 5 | ================== 6 | Here are some snippets showcasing how the library can be used. 7 | 8 | - `All the songs of an artist`_ 9 | - `Artist's least popular song`_ 10 | - `Authenticating using OAuth2`_ 11 | - `Getting song lyrics by URL or ID`_ 12 | - `Getting songs by tag (genre)`_ 13 | - `Getting the lyrics for all songs of a search`_ 14 | - `Searching for a song by lyrics`_ 15 | - `YouTube URL of artist's songs`_ 16 | 17 | 18 | Getting song lyrics by URL or ID 19 | -------------------------------- 20 | .. code:: python 21 | 22 | genius = Genius(token) 23 | 24 | # Using Song URL 25 | url = "https://genius.com/Andy-shauf-begin-again-lyrics" 26 | genius.lyrics(song_url=url) 27 | 28 | # Using Song ID 29 | # Requires an extra request to get song URL 30 | id = 2885745 31 | genius.lyrics(id) 32 | 33 | All the songs of an artist 34 | -------------------------- 35 | 36 | .. code:: python 37 | 38 | genius = Genius(token) 39 | artist = genius.search_artist('Andy Shauf') 40 | artist.save_lyrics() 41 | 42 | 43 | Artist's least popular song 44 | --------------------------- 45 | .. code:: python 46 | 47 | genius = Genius(token) 48 | 49 | artist = genius.search_artist('Andy Shauf', max_songs=1) 50 | page = 1 51 | songs = [] 52 | while page: 53 | request = genius.artist_songs(artist._id, 54 | sort='popularity', 55 | per_page=50, 56 | page=page, 57 | ) 58 | songs.extend(request['songs']) 59 | page = request['next_page'] 60 | least_popular_song = genius.search_song(songs[-1]['title'], artist.name) 61 | print(least_popular_song.lyrics) 62 | 63 | 64 | YouTube URL of artist's songs 65 | ----------------------------- 66 | .. code:: python 67 | 68 | import json 69 | # we have saved the songs with artist.save_lyrics() before this 70 | with open('saved_file.json') as f: 71 | data = json.load(f) 72 | for song in data['songs']: 73 | links = song['media'] 74 | if links: 75 | for media in links: 76 | if media['provider'] == 'youtube': 77 | print(print(['song'] + ': ' + media['url']) 78 | break 79 | 80 | 81 | Searching for a song by lyrics 82 | ------------------------------ 83 | Using :meth:`search_lyrics 84 | `: 85 | 86 | .. code:: python 87 | 88 | genius = Genius(token) 89 | 90 | request = genius.search_lyrics('Jeremy can we talk a minute?') 91 | for hit in request['sections'][0]['hits']: 92 | print(hit['result']['title']) 93 | 94 | Using :meth:`search_all `: 95 | 96 | .. code:: python 97 | 98 | genius = Genius(token) 99 | 100 | request = genius.search_all('Jeremy can we talk a minute?') 101 | for hit in request['sections'][2]['hits']: 102 | print(hit['result']['title']) 103 | 104 | Getting songs by tag (genre) 105 | ---------------------------- 106 | Genius has the following main tags: 107 | ``rap``, ``pop``, ``r-b``, ``rock``, ``country``, ``non-music`` 108 | To discover more tags, visit the `Genius Tags`_ page. 109 | 110 | Genius returns 20 results per page, but it seems that it won't return 111 | results past the 50th page if a tag has that many results. So, a popular 112 | tag like ``pop`` won't return results for the 51st page, even though 113 | Genius probably has more than 1000 songs with the pop tag. 114 | 115 | .. code:: python 116 | 117 | # this gets the lyrics of all the songs that have the pop tag. 118 | genius = Genius(token) 119 | page = 1 120 | lyrics = [] 121 | while page: 122 | res = genius.tag('pop', page=page) 123 | for hit in res['hits']: 124 | song_lyrics = genius.lyrics(song_url=hit['url']) 125 | lyrics.append(song_lyrics) 126 | page = res['next_page'] 127 | 128 | Getting the lyrics for all songs of a search 129 | -------------------------------------------- 130 | .. code:: python 131 | 132 | genius = Genius(token) 133 | lyrics = [] 134 | 135 | songs = genius.search_songs('Begin Again Andy Shauf') 136 | for song in songs['hits']: 137 | url = song['result']['url'] 138 | song_lyrics = genius.lyrics(song_url=url) 139 | # id = song['result']['id'] 140 | # song_lyrics = genius.lyrics(id) 141 | lyrics.append(song_lyrics) 142 | 143 | 144 | Authenticating using OAuth2 145 | --------------------------- 146 | Genius provides two flows for getting a user token: the code flow 147 | (called full code exchange) and the token flow (called client-only app). 148 | LyricsGenius provides two class methods 149 | :meth:`OAuth2.full_code_exchange` and :meth:`OAuth2.client_only_app` for 150 | the aforementioned flow. Visit the `Authentication section`_ in the 151 | Genius API documentation read more about the code and the token flow. 152 | 153 | You'll need the client ID and the redirect URI for a client-only app. 154 | For the full-code exchange you'll also need the client secret. The package 155 | can get them for using these environment variables: 156 | ``GENIU_CLIENT_ID``, ``GENIUS_REDIRECT_URI``, ``GENIUS_CLIENT_SECRET`` 157 | 158 | .. code:: python 159 | 160 | import lyricsgenius as lg 161 | 162 | client_id, redirect_uri, client_secret = lg.auth_from_environment() 163 | 164 | Authenticating yourself 165 | ^^^^^^^^^^^^^^^^^^^^^^^ 166 | Whitelist a redirect URI in your app's page on Genius. Any redirect 167 | URI will work (for example ``http://example.com/callback``) 168 | 169 | .. code:: python 170 | 171 | from lyricsgenius import OAuth2, Genius 172 | 173 | auth = OAuth2.client_only_app( 174 | 'my_client_id', 175 | 'my_redirect_uri', 176 | scope='all' 177 | ) 178 | 179 | token = auth.prompt_user() 180 | 181 | genius = Genius(token) 182 | 183 | Authenticating another user 184 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 185 | .. code:: python 186 | 187 | from lyricsgenius import OAuth2, Genius 188 | 189 | # client-only app 190 | auth = OAuth2.client_only_app( 191 | 'my_client_id', 192 | 'my_redirect_uri', 193 | scope='all' 194 | ) 195 | 196 | # full code exhange app 197 | auth = OAuth2.full_code_exchange( 198 | 'my_client_id', 199 | 'my_redirect_uri', 200 | 'my_client_secret', 201 | scope='all', 202 | state='some_unique_value' 203 | ) 204 | 205 | # this part is the same 206 | url_for_user = auth.url 207 | print('Redirecting you to ' + url_for_user) 208 | 209 | # If we were using Flask: 210 | code = request.args.get('code') 211 | state = request.args.get('state') 212 | token = auth.get_user_token(code, state) 213 | 214 | genius = Genius(token) 215 | 216 | .. _`Authentication section`: https://docs.genius.com/#/authentication-h1 217 | .. _`Genius Tags`: https://genius.com/tags/tags` 218 | -------------------------------------------------------------------------------- /docs/src/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnwmillr/LyricsGenius/30a38b66b249fc3cb062d7e72443c3a1c57a9334/docs/src/header.png -------------------------------------------------------------------------------- /docs/src/how_it_works.rst: -------------------------------------------------------------------------------- 1 | .. _how-it-works: 2 | .. currentmodule:: lyricsgenius 3 | 4 | 5 | How It Works 6 | ############# 7 | LyricsGenius interfaces with Genius.com in two different ways: 8 | the authenticated developer API and the unauthenticated public API. 9 | 10 | 11 | Developer API 12 | ************* 13 | The developer API is available through 14 | `api.genius.com `_, and provides access to song, artist, 15 | and annotation search amongst other data sources. The endpoints within the 16 | developer API require a (free) access token. The developer API is accessed 17 | through the :class:`API` class. 18 | 19 | 20 | Public API 21 | ********** 22 | The public API can be accessed without authentication and is essentially the 23 | same service end-users access through the browser. The public API 24 | can be called at `genius.com/api `_. These public API 25 | methods are available through the :class:`PublicAPI` class. 26 | 27 | 28 | Genius Class 29 | ************ 30 | The :ref:`genius` class is a high-level interface for the content on 31 | Genius.com, inheriting from both the :class:`API` and :class:`PublicAPI` 32 | classes. The :ref:`genius` class provides methods from the two API classes 33 | mentioned above as well as useful methods like :meth:`Genius.search_song` 34 | for accessing song lyrics and more. 35 | 36 | 37 | Lyrics 38 | ****** 39 | Genius has legal agreements with music publishers and considers the lyrics 40 | on their website to be a legal property of Genius, and won't allow you 41 | to re-use their lyrics without explicit licensing. They even 42 | `sued Google on grounds of stolen lyrics`_, asking for $50 million in damages, 43 | but `to no avail`_. So it shouldn't come as a surprise if they don't 44 | provide lyrics in calls to the API. So how does LyricsGenius get the lyrics? 45 | 46 | LyricsGenius uses a web-scraping library called `Beautiful Soup`_ 47 | to scrape lyrics from the song's page on Genius. Scraping the lyrics in 48 | this way violates Genius' terms of service. If you intend to use the lyrics for 49 | personal purposes, that shouldn't be cause for trouble, but other than that, 50 | you should inquire what happens when you violate the terms this way. 51 | As a reminder, LyricsGenius is not responsible for your usage of the library. 52 | 53 | 54 | .. _the Genius API: http://genius.com/api-clients\ 55 | .. _create a new API client: https://genius.com/api-clients/new 56 | .. _sued Google on grounds of stolen lyrics: https://www.theverge.com/ 57 | 2020/8/11/21363692/google-genius-lyrics-lawsuit-scraping-copyright 58 | -yelp-antitrust-competition 59 | .. _to no avail: https://www.theverge.com/2020/8/11/21363692/ 60 | google-genius-lyrics-lawsuit-scraping-copyright-yelp-antitrust-competition 61 | .. _Beautiful Soup: https://pypi.org/project/beautifulsoup4/ 62 | -------------------------------------------------------------------------------- /docs/src/index.rst: -------------------------------------------------------------------------------- 1 | .. _index: 2 | .. image:: header.png 3 | 4 | 5 | LyricsGenius: a Python client for the Genius.com API 6 | ==================================================== 7 | .. image:: https://travis-ci.org/johnwmillr/LyricsGenius.svg?branch=master 8 | :target: https://travis-ci.org/johnwmillr/LyricsGenius 9 | .. image:: https://badge.fury.io/py/lyricsgenius.svg 10 | :target: https://pypi.org/project/lyricsgenius/ 11 | .. image:: https://img.shields.io/badge/python-3.x-brightgreen.svg 12 | :target: https://pypi.org/project/lyricsgenius/ 13 | .. image:: https://img.shields.io/badge/Support%20this%20project-grey.svg?logo=github%20sponsors 14 | :target: https://github.com/sponsors/johnwmillr 15 | 16 | 17 | `Genius.com`_ is a fun website. If you aren't familiar with it, Genius hosts a 18 | bunch of song lyrics and lets users highlight and annotate passages with 19 | interpretations, explanations, and references. Originally called RapGenius.com 20 | and devoted to lyrics from rap and hip-hop songs, the website now includes 21 | lyrics and annotations from all genres of music. You can figure out what 22 | `“Words are flowing out like endless rain into a paper cup”`_ from Across the 23 | Universe really means, or what Noname was referring to when she said `“Moses 24 | wrote my name in gold and Kanye did the eulogy”`_. 25 | 26 | It's actually not too difficult to start pulling data from the Genius website. 27 | Genius is hip enough to provide a free application programming interface (API) 28 | that makes it easy for nerds to programmatically access song and artist data 29 | from their website. What the Genius API doesn't provide, however, 30 | is a way to download the lyrics themselves. With a little help from 31 | `Beautiful Soup`_ though, it's possible to grab the song lyrics without too 32 | much extra work. And LyricsGenius has done all of that for you already. 33 | 34 | .. 35 | source::https://www.johnwmillr.com/scraping-genius-lyrics/ 36 | 37 | 38 | ``lyricsgenius`` provides a simple interface to the song, artist, and 39 | lyrics data stored on `Genius.com`_. 40 | 41 | Using this library you can conveniently access the content on Genius.com 42 | And much more using the public API. 43 | 44 | You can use ``pip`` to install lyricsgenius: 45 | 46 | .. code:: bash 47 | 48 | pip install lyricsgenius 49 | 50 | LyricsGenius provides lots of features to work with. For example, let's 51 | download all the lyrics of an artist's songs, and save them to a JSON 52 | file: 53 | 54 | .. code:: python 55 | 56 | from lyricsgenius import Genius 57 | 58 | genius = Genius(token) 59 | artist = genius.search_artist('Andy Shauf') 60 | artist.save_lyrics() 61 | 62 | But before using the library you will need to get an access token. Head over 63 | to :ref:`setup` to get started. 64 | 65 | .. toctree:: 66 | :maxdepth: 1 67 | :caption: Library 68 | 69 | reference 70 | release_notes 71 | 72 | 73 | .. toctree:: 74 | :maxdepth: 1 75 | :caption: Guide 76 | 77 | setup 78 | usage 79 | how_it_works 80 | text_formatting 81 | other_guides 82 | 83 | 84 | .. toctree:: 85 | :maxdepth: 2 86 | :caption: Misc 87 | 88 | contributing 89 | 90 | .. toctree:: 91 | :hidden: 92 | 93 | 404.rst 94 | 95 | .. _Genius.com: https://www.genius.com 96 | .. _“Words are flowing out like endless rain into a paper cup”: 97 | https://genius.com/3287551 98 | .. _“Moses wrote my name in gold and Kanye did the eulogy”: 99 | https://genius.com/10185147 100 | .. _`Beautiful Soup`: https://www.crummy.com/software/BeautifulSoup/ 101 | -------------------------------------------------------------------------------- /docs/src/other_guides.rst: -------------------------------------------------------------------------------- 1 | .. _other_guides: 2 | 3 | Other Guides 4 | ============ 5 | 6 | 7 | Request Errors 8 | -------------- 9 | The package will raise all HTTP and timeout erros using 10 | the ``HTTPError`` and ``Timeout`` classes of the 11 | ``requests`` package. Whenever an HTTP error is raised, 12 | proper description of the error will be printed to 13 | output. You can also access the response's status code. 14 | 15 | .. code:: python 16 | 17 | from requests.exceptions import HTTPError, Timeout 18 | from lyricsgenius import Genius 19 | 20 | try: 21 | Genius('') 22 | except HTTPError as e: 23 | print(e.errno) # status code 24 | print(e.args[0]) # status code 25 | print(e.args[1]) # error message 26 | except Timeout: 27 | pass 28 | -------------------------------------------------------------------------------- /docs/src/reference.rst: -------------------------------------------------------------------------------- 1 | .. _reference: 2 | 3 | ========= 4 | Reference 5 | ========= 6 | All public objects are documented in these sections. 7 | 8 | ================== ============================= 9 | :ref:`api` API and PublicAPI classes 10 | :ref:`auth` OAuth2 class 11 | :ref:`Genius` Genius class 12 | :ref:`sender` Request sender 13 | :ref:`types` Types 14 | :ref:`utils` Utility functions 15 | ================== ============================= 16 | 17 | .. toctree:: 18 | :maxdepth: 2 19 | :caption: Index 20 | :hidden: 21 | :glob: 22 | 23 | reference/* 24 | -------------------------------------------------------------------------------- /docs/src/reference/api.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | .. currentmodule:: lyricsgenius 3 | .. toctree:: 4 | :maxdepth: 2 5 | :hidden: 6 | :caption: API 7 | 8 | API 9 | === 10 | The :ref:`Genius` class inherits this class, and it's recommended to 11 | call the methods using the Genius class rather than accessing this 12 | class directly. 13 | 14 | .. autoclass:: API 15 | :show-inheritance: 16 | 17 | 18 | Account Methods 19 | --------------- 20 | .. autosummary:: 21 | :nosignatures: 22 | 23 | API.account 24 | 25 | 26 | .. automethod:: API.account 27 | 28 | 29 | Annotation Methods 30 | ------------------ 31 | .. autosummary:: 32 | :nosignatures: 33 | 34 | API.annotation 35 | API.create_annotation 36 | API.delete_annotation 37 | API.downvote_annotation 38 | API.unvote_annotation 39 | API.upvote_annotation 40 | 41 | .. automethod:: API.annotation 42 | .. automethod:: API.create_annotation 43 | .. automethod:: API.delete_annotation 44 | .. automethod:: API.downvote_annotation 45 | .. automethod:: API.unvote_annotation 46 | .. automethod:: API.upvote_annotation 47 | 48 | 49 | Artist Methods 50 | -------------- 51 | .. autosummary:: 52 | :nosignatures: 53 | 54 | API.artist 55 | API.artist_songs 56 | 57 | .. automethod:: API.artist 58 | .. automethod:: API.artist_songs 59 | 60 | 61 | Referents Methods 62 | ----------------- 63 | .. autosummary:: 64 | :nosignatures: 65 | 66 | API.referents 67 | 68 | .. automethod:: API.referents 69 | 70 | 71 | Search Methods 72 | ----------------- 73 | .. autosummary:: 74 | :nosignatures: 75 | 76 | API.search_songs 77 | 78 | .. automethod:: API.search_songs 79 | 80 | 81 | Song Methods 82 | ----------------- 83 | .. autosummary:: 84 | :nosignatures: 85 | 86 | API.song 87 | 88 | .. automethod:: API.song 89 | 90 | 91 | Web Page Methods 92 | ----------------- 93 | .. autosummary:: 94 | :nosignatures: 95 | 96 | API.web_page 97 | 98 | .. automethod:: API.web_page 99 | 100 | 101 | Public API 102 | ========== 103 | The :ref:`Genius` class inherits this class, and it's recommended to 104 | call the methods using the Genius class rather than accessing this 105 | class directly. 106 | 107 | .. autoclass:: PublicAPI 108 | :members: 109 | :member-order: bysource 110 | :no-show-inheritance: 111 | 112 | 113 | Album Methods 114 | ------------- 115 | .. autosummary:: 116 | :nosignatures: 117 | 118 | PublicAPI.album 119 | PublicAPI.albums_charts 120 | PublicAPI.album_comments 121 | PublicAPI.album_cover_arts 122 | PublicAPI.album_leaderboard 123 | PublicAPI.album_tracks 124 | 125 | 126 | .. automethod:: PublicAPI.album 127 | .. automethod:: PublicAPI.albums_charts 128 | .. automethod:: PublicAPI.album_comments 129 | .. automethod:: PublicAPI.album_cover_arts 130 | .. automethod:: PublicAPI.album_leaderboard 131 | .. automethod:: PublicAPI.album_tracks 132 | 133 | 134 | Annotation Methods 135 | ------------------ 136 | .. autosummary:: 137 | :nosignatures: 138 | 139 | PublicAPI.annotation 140 | PublicAPI.annotation_edits 141 | PublicAPI.annotation_comments 142 | 143 | .. automethod:: PublicAPI.annotation 144 | .. automethod:: PublicAPI.annotation_edits 145 | .. automethod:: PublicAPI.annotation_comments 146 | 147 | 148 | Article Methods 149 | --------------- 150 | .. autosummary:: 151 | :nosignatures: 152 | 153 | PublicAPI.article 154 | PublicAPI.article_comments 155 | PublicAPI.latest_articles 156 | 157 | .. automethod:: PublicAPI.article 158 | .. automethod:: PublicAPI.article_comments 159 | .. automethod:: PublicAPI.latest_articles 160 | 161 | 162 | Artist Methods 163 | -------------- 164 | .. autosummary:: 165 | :nosignatures: 166 | 167 | PublicAPI.artist 168 | PublicAPI.artist_activity 169 | PublicAPI.artist_albums 170 | PublicAPI.artist_contribution_opportunities 171 | PublicAPI.artist_followers 172 | PublicAPI.artist_leaderboard 173 | PublicAPI.artist_songs 174 | PublicAPI.search_artist_songs 175 | 176 | .. automethod:: PublicAPI.artist 177 | .. automethod:: PublicAPI.artist_activity 178 | .. automethod:: PublicAPI.artist_albums 179 | .. automethod:: PublicAPI.artist_contribution_opportunities 180 | .. automethod:: PublicAPI.artist_followers 181 | .. automethod:: PublicAPI.artist_leaderboard 182 | .. automethod:: PublicAPI.artist_songs 183 | .. automethod:: PublicAPI.search_artist_songs 184 | 185 | Cover Art Methods 186 | ----------------- 187 | .. autosummary:: 188 | :nosignatures: 189 | 190 | PublicAPI.cover_arts 191 | 192 | .. automethod:: PublicAPI.cover_arts 193 | 194 | 195 | Discussion Methods 196 | ------------------ 197 | .. autosummary:: 198 | :nosignatures: 199 | 200 | PublicAPI.discussion 201 | PublicAPI.discussions 202 | PublicAPI.discussion_replies 203 | 204 | .. automethod:: PublicAPI.discussion 205 | .. automethod:: PublicAPI.discussions 206 | .. automethod:: PublicAPI.discussion_replies 207 | 208 | 209 | Leaderboard Methods 210 | ------------------- 211 | .. autosummary:: 212 | :nosignatures: 213 | 214 | PublicAPI.leaderboard 215 | PublicAPI.charts 216 | 217 | .. automethod:: PublicAPI.leaderboard 218 | .. automethod:: PublicAPI.charts 219 | 220 | 221 | Question & Answer Methods 222 | ------------------------- 223 | .. autosummary:: 224 | :nosignatures: 225 | 226 | PublicAPI.questions 227 | 228 | .. automethod:: PublicAPI.questions 229 | 230 | 231 | Referent Methods 232 | ---------------- 233 | .. autosummary:: 234 | :nosignatures: 235 | 236 | PublicAPI.referent 237 | PublicAPI.referents 238 | PublicAPI.referents_charts 239 | 240 | .. automethod:: PublicAPI.referent 241 | .. automethod:: PublicAPI.referents 242 | .. automethod:: PublicAPI.referents_charts 243 | 244 | 245 | Search Methods 246 | -------------- 247 | .. autosummary:: 248 | :nosignatures: 249 | 250 | PublicAPI.search 251 | PublicAPI.search_all 252 | PublicAPI.search_albums 253 | PublicAPI.search_artists 254 | PublicAPI.search_lyrics 255 | PublicAPI.search_songs 256 | PublicAPI.search_users 257 | PublicAPI.search_videos 258 | 259 | .. automethod:: PublicAPI.search 260 | .. automethod:: PublicAPI.search_all 261 | .. automethod:: PublicAPI.search_albums 262 | .. automethod:: PublicAPI.search_artists 263 | .. automethod:: PublicAPI.search_lyrics 264 | .. automethod:: PublicAPI.search_songs 265 | .. automethod:: PublicAPI.search_users 266 | .. automethod:: PublicAPI.search_videos 267 | 268 | 269 | Song Methods 270 | ------------ 271 | .. autosummary:: 272 | :nosignatures: 273 | 274 | PublicAPI.song_activity 275 | PublicAPI.song_comments 276 | PublicAPI.song_contributors 277 | 278 | .. automethod:: PublicAPI.song 279 | .. automethod:: PublicAPI.song_activity 280 | .. automethod:: PublicAPI.song_comments 281 | .. automethod:: PublicAPI.song_contributors 282 | 283 | 284 | User Methods 285 | ------------ 286 | .. autosummary:: 287 | :nosignatures: 288 | 289 | PublicAPI.user 290 | PublicAPI.user_accomplishments 291 | PublicAPI.user_following 292 | PublicAPI.user_followers 293 | PublicAPI.user_contributions 294 | PublicAPI.user_annotations 295 | PublicAPI.user_articles 296 | PublicAPI.user_pyongs 297 | PublicAPI.user_questions_and_answers 298 | PublicAPI.user_suggestions 299 | PublicAPI.user_transcriptions 300 | PublicAPI.user_unreviewed 301 | 302 | .. automethod:: PublicAPI.user 303 | .. automethod:: PublicAPI.user_accomplishments 304 | .. automethod:: PublicAPI.user_following 305 | .. automethod:: PublicAPI.user_followers 306 | .. automethod:: PublicAPI.user_contributions 307 | .. automethod:: PublicAPI.user_annotations 308 | .. automethod:: PublicAPI.user_articles 309 | .. automethod:: PublicAPI.user_pyongs 310 | .. automethod:: PublicAPI.user_questions_and_answers 311 | .. automethod:: PublicAPI.user_suggestions 312 | .. automethod:: PublicAPI.user_transcriptions 313 | .. automethod:: PublicAPI.user_unreviewed 314 | 315 | 316 | Video Methods 317 | ------------- 318 | .. autosummary:: 319 | :nosignatures: 320 | 321 | PublicAPI.video 322 | PublicAPI.videos 323 | 324 | .. automethod:: PublicAPI.video 325 | .. automethod:: PublicAPI.videos 326 | 327 | 328 | Misc. Methods 329 | ------------- 330 | Miscellaneous methods that are mostly standalones. 331 | 332 | .. autosummary:: 333 | :nosignatures: 334 | 335 | PublicAPI.line_item 336 | PublicAPI.voters 337 | 338 | .. automethod:: PublicAPI.line_item 339 | .. automethod:: PublicAPI.voters 340 | -------------------------------------------------------------------------------- /docs/src/reference/auth.rst: -------------------------------------------------------------------------------- 1 | .. _auth: 2 | .. currentmodule:: lyricsgenius.auth 3 | .. toctree:: 4 | :maxdepth: 2 5 | :hidden: 6 | :caption: Auth 7 | 8 | Auth 9 | ==== 10 | 11 | OAuth2 12 | ------ 13 | You can use this class to authenticate yourself or 14 | get URLs to redirect your users to and get them to 15 | give your Genius app the premissions you need. 16 | To find out more about how to use this class 17 | visit the :ref:`snippets`. 18 | 19 | .. autoclass:: OAuth2 20 | :members: 21 | :member-order: bysource 22 | :no-show-inheritance: 23 | -------------------------------------------------------------------------------- /docs/src/reference/genius.rst: -------------------------------------------------------------------------------- 1 | .. _genius: 2 | .. currentmodule:: lyricsgenius 3 | .. toctree:: 4 | :maxdepth: 2 5 | :hidden: 6 | :caption: Genius 7 | 8 | 9 | Genius 10 | ======================= 11 | The Genius class provides a high-level interface to the Genius API. This 12 | class provides convenient access to the standard API (:class:`API`), the 13 | public API (:class:`PublicAPI`), and additional features such as 14 | downloading lyrics. 15 | 16 | .. autoclass:: Genius 17 | :show-inheritance: 18 | 19 | 20 | Account Methods 21 | --------------- 22 | .. autosummary:: 23 | :nosignatures: 24 | 25 | Genius.account 26 | 27 | 28 | .. automethod:: Genius.account 29 | 30 | 31 | Album Methods 32 | ------------- 33 | .. autosummary:: 34 | :nosignatures: 35 | 36 | Genius.album 37 | Genius.albums_charts 38 | Genius.album_comments 39 | Genius.album_cover_arts 40 | Genius.album_leaderboard 41 | Genius.album_tracks 42 | 43 | 44 | .. automethod:: Genius.album 45 | .. automethod:: Genius.albums_charts 46 | .. automethod:: Genius.album_comments 47 | .. automethod:: Genius.album_cover_arts 48 | .. automethod:: Genius.album_leaderboard 49 | .. automethod:: Genius.album_tracks 50 | 51 | 52 | Annotation Methods 53 | ------------------ 54 | .. autosummary:: 55 | :nosignatures: 56 | 57 | Genius.annotation 58 | Genius.annotation_edits 59 | Genius.annotation_comments 60 | Genius.create_annotation 61 | Genius.delete_annotation 62 | Genius.downvote_annotation 63 | Genius.unvote_annotation 64 | Genius.upvote_annotation 65 | 66 | .. automethod:: Genius.annotation 67 | .. automethod:: Genius.annotation_edits 68 | .. automethod:: Genius.annotation_comments 69 | .. automethod:: Genius.create_annotation 70 | .. automethod:: Genius.delete_annotation 71 | .. automethod:: Genius.downvote_annotation 72 | .. automethod:: Genius.unvote_annotation 73 | .. automethod:: Genius.upvote_annotation 74 | 75 | 76 | Article Methods 77 | --------------- 78 | .. autosummary:: 79 | :nosignatures: 80 | 81 | Genius.article 82 | Genius.article_comments 83 | Genius.latest_articles 84 | 85 | .. automethod:: Genius.article 86 | .. automethod:: Genius.article_comments 87 | .. automethod:: Genius.latest_articles 88 | 89 | 90 | Artist Methods 91 | -------------- 92 | .. autosummary:: 93 | :nosignatures: 94 | 95 | Genius.artist 96 | Genius.artist_activity 97 | Genius.artist_albums 98 | Genius.artist_contribution_opportunities 99 | Genius.artist_followers 100 | Genius.artist_leaderboard 101 | Genius.artist_songs 102 | Genius.save_artists 103 | Genius.search_artist_songs 104 | 105 | 106 | .. automethod:: Genius.artist 107 | .. automethod:: Genius.artist_activity 108 | .. automethod:: Genius.artist_albums 109 | .. automethod:: Genius.artist_contribution_opportunities 110 | .. automethod:: Genius.artist_followers 111 | .. automethod:: Genius.artist_leaderboard 112 | .. automethod:: Genius.artist_songs 113 | .. automethod:: Genius.save_artists 114 | .. automethod:: Genius.search_artist_songs 115 | 116 | 117 | Cover Art Methods 118 | ----------------- 119 | .. autosummary:: 120 | :nosignatures: 121 | 122 | Genius.cover_arts 123 | 124 | .. automethod:: Genius.cover_arts 125 | 126 | 127 | Discussion Methods 128 | ------------------ 129 | .. autosummary:: 130 | :nosignatures: 131 | 132 | Genius.discussion 133 | Genius.discussions 134 | Genius.discussion_replies 135 | 136 | .. automethod:: Genius.discussion 137 | .. automethod:: Genius.discussions 138 | .. automethod:: Genius.discussion_replies 139 | 140 | 141 | Leaderboard Methods 142 | ------------------- 143 | .. autosummary:: 144 | :nosignatures: 145 | 146 | Genius.leaderboard 147 | Genius.charts 148 | 149 | .. automethod:: Genius.leaderboard 150 | .. automethod:: Genius.charts 151 | 152 | 153 | Question & Answer Methods 154 | ------------------------- 155 | .. autosummary:: 156 | :nosignatures: 157 | 158 | Genius.questions 159 | 160 | .. automethod:: Genius.questions 161 | 162 | 163 | Referent Methods 164 | ---------------- 165 | .. autosummary:: 166 | :nosignatures: 167 | 168 | Genius.referent 169 | Genius.referents 170 | Genius.referents_charts 171 | 172 | .. automethod:: Genius.referent 173 | .. automethod:: Genius.referents 174 | .. automethod:: Genius.referents_charts 175 | 176 | 177 | Search Methods 178 | -------------- 179 | .. autosummary:: 180 | :nosignatures: 181 | 182 | Genius.search 183 | Genius.search_all 184 | Genius.search_albums 185 | Genius.search_artist 186 | Genius.search_artists 187 | Genius.search_lyrics 188 | Genius.search_song 189 | Genius.search_songs 190 | Genius.search_users 191 | Genius.search_videos 192 | 193 | .. automethod:: Genius.search 194 | .. automethod:: Genius.search_all 195 | .. automethod:: Genius.search_albums 196 | .. automethod:: Genius.search_artist 197 | .. automethod:: Genius.search_artists 198 | .. automethod:: Genius.search_lyrics 199 | .. automethod:: Genius.search_song 200 | .. automethod:: Genius.search_songs 201 | .. automethod:: Genius.search_users 202 | .. automethod:: Genius.search_videos 203 | 204 | 205 | Song Methods 206 | ------------ 207 | .. autosummary:: 208 | :nosignatures: 209 | 210 | Genius.song 211 | Genius.song_activity 212 | Genius.song_annotations 213 | Genius.song_comments 214 | Genius.song_contributors 215 | Genius.lyrics 216 | 217 | .. automethod:: Genius.song 218 | .. automethod:: Genius.song_activity 219 | .. automethod:: Genius.song_annotations 220 | .. automethod:: Genius.song_comments 221 | .. automethod:: Genius.song_contributors 222 | .. automethod:: Genius.lyrics 223 | 224 | 225 | User Methods 226 | ------------ 227 | .. autosummary:: 228 | :nosignatures: 229 | 230 | Genius.user 231 | Genius.user_accomplishments 232 | Genius.user_following 233 | Genius.user_followers 234 | Genius.user_contributions 235 | Genius.user_annotations 236 | Genius.user_articles 237 | Genius.user_pyongs 238 | Genius.user_questions_and_answers 239 | Genius.user_suggestions 240 | Genius.user_transcriptions 241 | Genius.user_unreviewed 242 | 243 | .. automethod:: Genius.user 244 | .. automethod:: Genius.user_accomplishments 245 | .. automethod:: Genius.user_following 246 | .. automethod:: Genius.user_followers 247 | .. automethod:: Genius.user_contributions 248 | .. automethod:: Genius.user_annotations 249 | .. automethod:: Genius.user_articles 250 | .. automethod:: Genius.user_pyongs 251 | .. automethod:: Genius.user_questions_and_answers 252 | .. automethod:: Genius.user_suggestions 253 | .. automethod:: Genius.user_transcriptions 254 | .. automethod:: Genius.user_unreviewed 255 | 256 | 257 | Video Methods 258 | ------------- 259 | .. autosummary:: 260 | :nosignatures: 261 | 262 | Genius.video 263 | Genius.videos 264 | 265 | .. automethod:: Genius.video 266 | .. automethod:: Genius.videos 267 | 268 | 269 | Web Page Methods 270 | ----------------- 271 | .. autosummary:: 272 | :nosignatures: 273 | 274 | Genius.web_page 275 | 276 | .. automethod:: Genius.web_page 277 | 278 | 279 | Misc. Methods 280 | ------------- 281 | Miscellaneous methods that are mostly standalones. 282 | 283 | .. autosummary:: 284 | :nosignatures: 285 | 286 | Genius.tag 287 | Genius.line_item 288 | Genius.voters 289 | 290 | .. automethod:: Genius.tag 291 | .. automethod:: Genius.line_item 292 | .. automethod:: Genius.voters 293 | -------------------------------------------------------------------------------- /docs/src/reference/sender.rst: -------------------------------------------------------------------------------- 1 | .. _sender: 2 | .. currentmodule:: lyricsgenius.api.base 3 | .. toctree:: 4 | :maxdepth: 2 5 | :hidden: 6 | :caption: Sender 7 | 8 | 9 | Sender 10 | ====== 11 | .. autoclass:: Sender 12 | :members: _make_request 13 | -------------------------------------------------------------------------------- /docs/src/reference/types.rst: -------------------------------------------------------------------------------- 1 | .. _types: 2 | .. currentmodule:: lyricsgenius.types 3 | 4 | Types 5 | ===== 6 | Package-defined types. 7 | 8 | Currently, the types defined here are only returned by 9 | :meth:`Genius.search_album`, :meth:`Genius.search_artist` 10 | and :meth:`Genius.search_song`. 11 | 12 | 13 | All of the attributes listed in the types are guaranteed to be present 14 | in the returned object. To access other values that are 15 | in the response body, use :meth:`to_dict`. 16 | 17 | Base 18 | ---- 19 | Base classes. 20 | 21 | 22 | Classes 23 | ^^^^^^^ 24 | .. autosummary:: 25 | :nosignatures: 26 | 27 | Stats 28 | Track 29 | 30 | .. autoclass:: Stats 31 | :members: 32 | :member-order: bysource 33 | :no-show-inheritance: 34 | 35 | .. autoclass:: Track 36 | :members: 37 | :member-order: bysource 38 | :no-show-inheritance: 39 | 40 | 41 | Album 42 | ------ 43 | An album from Genius that has the album's songs and their lyrics. 44 | 45 | Attributes 46 | ^^^^^^^^^^ 47 | .. list-table:: 48 | :header-rows: 1 49 | 50 | * - Attribute 51 | - Type 52 | 53 | * - _type 54 | - :obj:`str` 55 | 56 | * - api_path 57 | - :obj:`str` 58 | 59 | * - artist 60 | - :class:`Artist` 61 | 62 | * - cover_art_thumbnail_url 63 | - :obj:`str` 64 | 65 | * - cover_art_url 66 | - :obj:`str` 67 | 68 | * - full_title 69 | - :obj:`str` 70 | 71 | * - id 72 | - :obj:`int` 73 | 74 | * - name 75 | - :obj:`str` 76 | 77 | * - name_with_artist 78 | - :obj:`str` 79 | 80 | * - release_date_components 81 | - :class:`datetime` 82 | 83 | * - tracks 84 | - :obj:`list` of :class:`Track` 85 | 86 | * - url 87 | - :obj:`str` 88 | 89 | 90 | Methods 91 | ^^^^^^^^ 92 | .. autosummary:: 93 | :nosignatures: 94 | 95 | Album.to_dict 96 | Album.to_json 97 | Album.to_text 98 | Album.save_lyrics 99 | 100 | 101 | .. autoclass:: Album 102 | :members: 103 | :member-order: bysource 104 | :no-show-inheritance: 105 | 106 | 107 | Artist 108 | ------ 109 | The Artist object which holds the details of the artist 110 | and the `Song`_ objects of that artist. 111 | 112 | Attributes 113 | ^^^^^^^^^^ 114 | .. list-table:: 115 | :header-rows: 1 116 | 117 | * - Attribute 118 | - Type 119 | 120 | 121 | * - api_path 122 | - :obj:`str` 123 | 124 | * - header_image_url 125 | - :obj:`str` 126 | 127 | * - id 128 | - :obj:`int` 129 | 130 | * - image_url 131 | - :obj:`str` 132 | 133 | * - is_meme_verified 134 | - :obj:`bool` 135 | 136 | * - is_verified 137 | - :obj:`bool` 138 | 139 | * - name 140 | - :obj:`str` 141 | 142 | * - songs 143 | - :obj:`list` 144 | 145 | * - url 146 | - :obj:`str` 147 | 148 | Methods 149 | ^^^^^^^^ 150 | .. autosummary:: 151 | :nosignatures: 152 | 153 | Artist.song 154 | Artist.add_song 155 | Artist.to_dict 156 | Artist.to_json 157 | Artist.to_text 158 | Artist.save_lyrics 159 | 160 | 161 | .. autoclass:: Artist 162 | :members: 163 | :member-order: bysource 164 | :no-show-inheritance: 165 | 166 | 167 | Song 168 | ---- 169 | This is the Song object which holds the details of the song. 170 | 171 | Attributes 172 | ^^^^^^^^^^ 173 | .. list-table:: 174 | :header-rows: 1 175 | 176 | * - Attribute 177 | - Type 178 | 179 | 180 | * - annotation_count 181 | - :obj:`int` 182 | 183 | * - api_path 184 | - :obj:`str` 185 | 186 | * - artist 187 | - :obj:`str` 188 | 189 | * - full_title 190 | - :obj:`str` 191 | 192 | * - header_image_thumbnail_url 193 | - :obj:`str` 194 | 195 | * - header_image_url 196 | - :obj:`str` 197 | 198 | * - id 199 | - :obj:`int` 200 | 201 | * - lyrics 202 | - :obj:`str` 203 | 204 | * - lyrics_owner_id 205 | - :obj:`int` 206 | 207 | * - lyrics_state 208 | - :obj:`str` 209 | 210 | * - path 211 | - :obj:`str` 212 | 213 | * - primary_artist 214 | - :class:`Artist` 215 | 216 | * - pyongs_count 217 | - :obj:`int` 218 | 219 | * - song_art_image_thumbnail_url 220 | - :obj:`str` 221 | 222 | * - song_art_image_url 223 | - :obj:`str` 224 | 225 | * - stats 226 | - :class:`Stats` 227 | 228 | * - title 229 | - :obj:`str` 230 | 231 | * - title_with_featured 232 | - :obj:`str` 233 | 234 | * - url 235 | - :obj:`str` 236 | 237 | Methods 238 | ^^^^^^^^ 239 | .. autosummary:: 240 | :nosignatures: 241 | 242 | Song.to_dict 243 | Song.to_json 244 | Song.to_text 245 | Song.save_lyrics 246 | 247 | 248 | .. autoclass:: Song 249 | :members: 250 | :member-order: bysource 251 | :no-show-inheritance: 252 | 253 | -------------------------------------------------------------------------------- /docs/src/reference/utils.rst: -------------------------------------------------------------------------------- 1 | .. _utils: 2 | .. currentmodule:: lyricsgenius 3 | .. toctree:: 4 | :maxdepth: 2 5 | :hidden: 6 | :caption: Utils 7 | 8 | Utils 9 | ======================= 10 | 11 | .. automodule:: lyricsgenius.utils 12 | :members: 13 | :no-show-inheritance: 14 | -------------------------------------------------------------------------------- /docs/src/release_notes.rst: -------------------------------------------------------------------------------- 1 | .. _release-notes: 2 | .. currentmodule:: lyricsgenius 3 | 4 | Release notes 5 | ============= 6 | 7 | 3.6.4 (2025-05-31) 8 | ------------------ 9 | Changed 10 | ******* 11 | 12 | - Added a `DeprecationWarning` to the ``Song``, ``Artist``, and ``Album`` 13 | classes. The ``Genius`` client will be removed from these classes in 14 | a future release. 15 | - Added a `DeprecationWarning` to the ``Track`` class. This class will 16 | be removed in a future release. Its functionality will be 17 | incorporated into the ``Song`` class. 18 | - Added a `DeprecationWarning` to the ``Stats`` class. This class will 19 | be removed in a future release. 20 | - Added a `FutureWarning` to the ``Song`` constructor. Its signature 21 | will change to ``Song(lyrics, body)`` instead of 22 | ``Song(client, json_dict, lyrics)``. 23 | 24 | 3.6.3 (2025-05-31) 25 | ------------------ 26 | Changed 27 | ******* 28 | 29 | - Fixed a bug where ``Genius.search_artist()`` wouldn't obey the 30 | ``max_songs`` parameter. Now it will return the correct number of 31 | songs as specified. 32 | - Fixed typos and removed random unicode characters. 33 | 34 | 3.0.0 (2021-02-08) 35 | ------------------ 36 | New 37 | ***** 38 | 39 | - All requests now go through the ``Sender`` object. This provides 40 | features such as retries ``genius.retries`` and handling HTTP and 41 | timeout errors. For more info have a look at the guide about `request 42 | error handling`_. 43 | - Added ``OAuth2`` class to help with OAuth2 authentication. 44 | - Added ``PublicAPI`` class to allow accessing methods of the public 45 | API (genius.com/api). Check `this page`_ for a list of available 46 | methods. 47 | - Added the ``Album`` type and the ``genius.search_album()`` method. 48 | - Added the ``genius.tag()`` method to get songs by tag. 49 | - All API endpoints are now supported (e.g. ``upvote_annotation``). 50 | - New additions to the docs. 51 | 52 | Changed 53 | ******* 54 | 55 | - ``GENIUS_CLIENT_ACCESS_TOKEN`` env var has been renamed to 56 | ``GENIUS_ACCESS_TOKEN``. 57 | - ``genius.client_access_token`` has been renamed to 58 | ``genius.access_token``. 59 | - ``genius.search_song()`` will also accept ``song_id``. 60 | - Lyrics won't be fetched for instrumental songs and their lyrics will 61 | be set to ``""``. You can check to see if a song is instrumental 62 | using ``Song.instrumental``. 63 | - Renamed all interface methods to remove redundant ``get_`` 64 | (``genius.get_song`` is now ``genius.song``). 65 | - Renamed the lyrics method to ``genius.lyrics()`` to allow use by 66 | users. It accepts song URLs and song IDs. 67 | - Reformatted the types. Some attributes won't be available anymore. 68 | More info on the `types page`_. 69 | - ``save_lyrics()`` will save songs with ``utf8`` encoding when 70 | ``extension='txt'``. 71 | - Using ``Genius()`` will check for the env var 72 | ``GENIUS_ACCESS_TOKEN``. 73 | 74 | Other (CI, etc) 75 | *************** 76 | 77 | - Bumped ``Sphinx`` to 3.3.0 78 | 79 | .. _request error handling: https://lyricsgenius.readthedocs.io/en/master/other_guides.html#request-errors 80 | .. _this page: https://lyricsgenius.readthedocs.io/en/latest/reference/genius.html 81 | .. _types page: https://lyricsgenius.readthedocs.io/en/latest/reference/types.html#types 82 | 83 | 84 | 2.0.2 (2020-09-26) 85 | ------------------ 86 | Added 87 | ***** 88 | 89 | - Added optional ``ensure_ascii`` parameter to the following methods: 90 | :meth:`Genius.save_artists `, 91 | :meth:`Song.save_lyrics `, 92 | :meth:`Song.to_json `, 93 | :meth:`Artist.save_lyrics ` 94 | and :meth:`Artist.to_json ` 95 | 96 | 2.0.1 (2020-09-20) 97 | ------------------ 98 | Changed 99 | ******* 100 | 101 | - :func:`Genius.lyrics` - Switched to using regular expressions to find 102 | the ``new_div`` (:issue:`154`). 103 | -------------------------------------------------------------------------------- /docs/src/setup.rst: -------------------------------------------------------------------------------- 1 | .. _setup: 2 | 3 | 4 | Setup 5 | ===== 6 | Before we start installing the package, we'll need to get an access token. 7 | 8 | Authorization 9 | ------------- 10 | First you'll need to sign up for a (free) account 11 | that authorizes access to `the Genius API`_. After signing up/ 12 | logging in to your account, head out to the API section on Genius 13 | and `create a new API client`_. After creating your client, you can 14 | generate an access token to use with the library. Genius provides 15 | two kinds of tokens: 16 | 17 | - **client access token**: 18 | Mostly LyricsGenius is used to get song lyrics and song 19 | info. And this is also what the client access tokens are used for. They 20 | don't need a user to authenticate their use (through OAuth2 or etc) and 21 | you can easily get yours by visiting the `API Clients`_ page and click 22 | on *Generate Access Token*. This will give you an access token, and 23 | now you're good to go. 24 | 25 | - **user token**: 26 | These tokens can do what client access tokens do and 27 | more. Using these you can get information of the account you have 28 | authenticated, create web-pages and create, manage and up-vote 29 | annotations that are hosted on your website. These tokens are 30 | really useful if you use Genius's `Web Annotator`_ on your website. 31 | Otherwise you won't have much need for this. Read more about 32 | user tokens on Genius's `documentation`_. LyricsGenius has a 33 | :ref:`auth` class that provides some helpful methods to get 34 | authenticate the user and get an access token. 35 | 36 | Installation 37 | ------------ 38 | 39 | ``lyricsgenius`` requires Python 3. 40 | 41 | Use ``pip`` to install the package from PyPI: 42 | 43 | .. code:: bash 44 | 45 | pip install lyricsgenius 46 | 47 | Or, install the latest version of the package from GitHub: 48 | 49 | .. code:: bash 50 | 51 | pip install git+https://github.com/johnwmillr/LyricsGenius.git 52 | 53 | 54 | Now that you have the library intalled, you can get started with using 55 | the library. See the :ref:`usage` for examples. 56 | 57 | .. _Web Annotator: https://genius.com/web-annotator 58 | .. _the Genius API: http://genius.com/api-clients 59 | .. _API Clients: https://genius.com/api-clients 60 | .. _Web Annotator: https://genius.com/web-annotator 61 | .. _documentation: https://docs.genius.com/#/authentication-h1 62 | .. _create a new API client: https://genius.com/api-clients/new 63 | .. _create an app: http://genius.com/api-clients 64 | .. _OAuth2: https://lyricsgenius.readthedocs.io/en/latest/reference/auth.html#auth 65 | -------------------------------------------------------------------------------- /docs/src/text_formatting.rst: -------------------------------------------------------------------------------- 1 | 2 | Text Formatting 3 | =============== 4 | Usually, when making calls to the API, there is a ``text_format`` parameter to 5 | determine the text format of the text inside the response. LyricsGenius 6 | provides a ``Genius.response_format`` that you can set and will be used for 7 | all the calls that support a ``text_format`` parameter (not all endpoints have 8 | a text body to return so the parameter would be pointless). Let's read what 9 | Genius docs say about text formatting: 10 | 11 | Many API requests accept a text_format query parameter that can be used to 12 | specify how text content is formatted. The value for the parameter must be 13 | one or more of plain, html, and dom. The value returned will be an object 14 | with key-value pairs of formats and results: 15 | 16 | * **plain** is just plain text, no markup 17 | * **html** is a string of unescaped HTML suitable for rendering by a 18 | browser 19 | * **dom** is a nested object representing and HTML DOM hierarchy that 20 | can be used to programmatically present structured content 21 | 22 | 23 | Now what Genius hasn't documented is that there is one more format except the 24 | three ones above and that you can use more that one at a time. The other format 25 | is the ``markdown`` format that returns results in the Markdown format. 26 | Besides the four available formats, you can get more than one format in a call. 27 | To do this you simply set the ``Genius.response_format`` like this: 28 | 29 | .. code:: python 30 | 31 | genius.response_format = 'plain,html' 32 | 33 | Doing this will return the ``plain`` and ``html`` formats in the body of the 34 | results. Let's give it a try: 35 | 36 | .. code:: python 37 | 38 | import lyricsgenius 39 | genius = lyricsgenius.Genius('token') # you can also set the attribute here 40 | genius.response_format = 'plain,html' 41 | 42 | res = genius.annotation(10225840) 43 | 44 | # Annotation in plain formatting 45 | print(res['annotation']['body']['plain']) 46 | 47 | # Annotation in html formatting 48 | print(res['annotation']['body']['html']) 49 | 50 | You can also specify the text formatting in the call: 51 | 52 | .. code:: python 53 | 54 | genius.annotation(10225840, text_format='html') 55 | 56 | Using this will override ``response_format`` for this specific call. 57 | If you pass no ``text_format``, the formatting will default to 58 | the ``response_format`` attribute if the method supports text formatting. 59 | 60 | 61 | Available Formats 62 | ----------------- 63 | * **plain** is just plain text, no markup 64 | * **html** is a string of unescaped HTML suitable for rendering by a browser 65 | * **dom** is a nested object representing an HTML DOM hierarchy that can be 66 | used to programmatically present structured content 67 | * **markdown** is text with Markdown formatting 68 | -------------------------------------------------------------------------------- /docs/src/usage.rst: -------------------------------------------------------------------------------- 1 | .. _usage: 2 | .. currentmodule:: lyricsgenius 3 | 4 | Usage 5 | ================== 6 | 7 | Import the package and search for songs by a given artist: 8 | 9 | .. code:: python 10 | 11 | from lyricsgenius import Genius 12 | 13 | genius = Genius(token) 14 | artist = genius.search_artist("Andy Shauf", max_songs=3, sort="title") 15 | print(artist.songs) 16 | 17 | Search for a single song by the same artist: 18 | 19 | .. code:: python 20 | 21 | # Way 1 22 | song = genius.search_song("To You", artist.name) 23 | 24 | # Way 2 25 | # this will search artist.songs first 26 | # and if not found, uses search_song 27 | song = artist.song("To You") 28 | 29 | print(song.lyrics) 30 | 31 | Add the song to the :class:`Artist ` object: 32 | 33 | .. code:: python 34 | 35 | artist.add_song(song) 36 | # add_song accepts song names as well: 37 | # artist.add_song("To You") 38 | 39 | Save the artist's songs to a JSON file: 40 | 41 | .. code:: python 42 | 43 | artist.save_lyrics() 44 | 45 | Searching for an album and saving it: 46 | 47 | .. code:: python 48 | 49 | album = genius.search_album("The Party", "Andy Shauf") 50 | album.save_lyrics() 51 | 52 | There are various options configurable as parameters within the 53 | :ref:`genius` class: 54 | 55 | .. code:: python 56 | 57 | # Turn off status messages 58 | genius.verbose = False 59 | 60 | # Remove section headers (e.g. [Chorus]) from lyrics when searching 61 | genius.remove_section_headers = True 62 | 63 | # Include hits thought to be non-songs (e.g. track lists) 64 | genius.skip_non_songs = False 65 | 66 | # Exclude songs with these words in their title 67 | genius.excluded_terms = ["(Remix)", "(Live)"] 68 | 69 | You can also call the package from the command line: 70 | 71 | .. code:: bash 72 | 73 | export GENIUS_ACCESS_TOKEN="my_access_token_here" 74 | python3 -m lyricsgenius --help 75 | 76 | Search for and save lyrics to a given song and album: 77 | 78 | .. code:: bash 79 | 80 | python3 -m lyricsgenius song "Begin Again" "Andy Shauf" --save 81 | python3 -m lyricsgenius album "The Party" "Andy Shauf" --save 82 | 83 | Search for five songs by 'The Beatles' and save the lyrics: 84 | 85 | .. code:: bash 86 | 87 | python3 -m lyricsgenius artist "The Beatles" --max-songs 5 --save 88 | 89 | 90 | You might also like checking out the :ref:`snippets` page. 91 | 92 | 93 | .. toctree:: 94 | :maxdepth: 1 95 | :hidden: 96 | :caption: Index 97 | 98 | examples/snippets 99 | examples/example projects 100 | -------------------------------------------------------------------------------- /lyricsgenius/__init__.py: -------------------------------------------------------------------------------- 1 | # GeniusAPI 2 | # John W. R. Miller 3 | # See LICENSE for details 4 | """A library that provides a Python interface to the Genius API""" 5 | 6 | import sys 7 | 8 | assert sys.version_info[0] == 3, "LyricsGenius requires Python 3." 9 | from lyricsgenius.api import API, PublicAPI 10 | from lyricsgenius.auth import OAuth2 11 | from lyricsgenius.genius import Genius 12 | from lyricsgenius.utils import auth_from_environment 13 | -------------------------------------------------------------------------------- /lyricsgenius/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | 4 | from . import Genius 5 | 6 | 7 | class Searcher: 8 | """Executes the search specified by the CLI args""" 9 | 10 | def __init__(self, api, search_type): 11 | self.api = api 12 | self.search_type = search_type 13 | if search_type == "song": 14 | self.search_func = api.search_song 15 | elif search_type == "artist": 16 | self.search_func = api.search_artist 17 | elif search_type == "album": 18 | self.search_func = api.search_album 19 | else: 20 | raise ValueError(f"Unknown search type: {search_type}") 21 | 22 | def __call__(self, args): 23 | if self.search_type == "artist": 24 | result = self.search_func(*args.terms, max_songs=args.max_songs) 25 | else: 26 | result = self.search_func(*args.terms) 27 | if not result: 28 | return 29 | for format in args.format: 30 | if not args.save: 31 | print(result.to_text() if format == "txt" else result.to_json()) 32 | else: 33 | if args.verbose: 34 | print(f"Saving lyrics in {format.upper()} format.") 35 | result.save_lyrics(extension=format, overwrite=args.overwrite) 36 | 37 | 38 | def main(args=None): 39 | msg = "Download song lyrics from Genius.com" 40 | parser = argparse.ArgumentParser(prog="lyricsgenius", description=msg) 41 | positional = parser.add_argument_group("Required Arguments") 42 | positional.add_argument( 43 | "search_type", 44 | type=str.lower, 45 | choices=["song", "artist", "album"], 46 | help="Specify whether search is for 'song', 'artist' or 'album'.", 47 | ) 48 | positional.add_argument( 49 | "terms", 50 | type=str, 51 | nargs="+", 52 | help="Provide terms for the search (e.g. 'All You Need Is Love' 'The Beatles').", 53 | ) 54 | 55 | optional = parser.add_argument_group("Optional Arguments") 56 | optional.add_argument( 57 | "-f", 58 | "--format", 59 | type=str.lower, 60 | nargs="+", 61 | default=["txt"], 62 | choices=["txt", "json"], 63 | help="Specify output format(s): 'txt' (default) or 'json'. You can specify multiple formats.", 64 | ) 65 | optional.add_argument( 66 | "-s", 67 | "--save", 68 | action="store_true", 69 | help="Save the lyrics to a file in the specified format instead of printing to stdout", 70 | ) 71 | optional.add_argument( 72 | "-o", 73 | "--overwrite", 74 | action="store_true", 75 | default=False, 76 | help="Overwrite the file if it already exists", 77 | ) 78 | optional.add_argument( 79 | "-n", 80 | "--max-songs", 81 | type=int, 82 | help="Specify number of songs when searching for artist", 83 | ) 84 | optional.add_argument( 85 | "-t", 86 | "--token", 87 | type=str, 88 | default=None, 89 | help="Specify your Genius API access token (optional). If not provided, it will be read from the GENIUS_ACCESS_TOKEN environment variable.", 90 | ) 91 | optional.add_argument( 92 | "-v", "--verbose", action="store_true", help="Turn on the API verbosity" 93 | ) 94 | args = parser.parse_args() 95 | 96 | # Create an instance of the Genius class 97 | token = args.token if args.token else os.environ.get("GENIUS_ACCESS_TOKEN", None) 98 | if token is None: 99 | raise ValueError( 100 | "Must provide access token either as an argument or as an environment variable." 101 | ) 102 | 103 | api = Genius(token, verbose=args.verbose, timeout=10) 104 | 105 | # Handle the command-line inputs 106 | Searcher(api, args.search_type)(args) 107 | 108 | 109 | if __name__ == "__main__": 110 | main() 111 | -------------------------------------------------------------------------------- /lyricsgenius/api/__init__.py: -------------------------------------------------------------------------------- 1 | from .api import API, PublicAPI 2 | from .base import Sender 3 | -------------------------------------------------------------------------------- /lyricsgenius/api/base.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | import time 4 | from json.decoder import JSONDecodeError 5 | 6 | import requests 7 | from requests.exceptions import HTTPError, Timeout 8 | 9 | 10 | class Sender(object): 11 | """Sends requests to Genius.""" 12 | 13 | # Create a persistent requests connection 14 | API_ROOT = "https://api.genius.com/" 15 | PUBLIC_API_ROOT = "https://genius.com/api/" 16 | WEB_ROOT = "https://genius.com/" 17 | 18 | def __init__( 19 | self, 20 | access_token=None, 21 | response_format="plain", 22 | timeout=5, 23 | sleep_time=0.2, 24 | retries=0, 25 | public_api_constructor=False, 26 | user_agent="", 27 | proxy=None, 28 | ): 29 | self._session = requests.Session() 30 | user_agent_root = f"{platform.system()} {platform.release()}; Python {platform.python_version()}" 31 | self._session.headers = { 32 | "application": "LyricsGenius", 33 | "User-Agent": f"({user_agent}) ({user_agent_root})" 34 | if user_agent 35 | else user_agent_root, 36 | } 37 | if proxy: 38 | self._session.proxies = proxy 39 | if access_token is None: 40 | access_token = os.environ["GENIUS_ACCESS_TOKEN"] 41 | 42 | if public_api_constructor: 43 | self.authorization_header = {} 44 | else: 45 | if not access_token or not isinstance(access_token, str): 46 | raise TypeError("Invalid token") 47 | self.access_token = "Bearer " + access_token 48 | self.authorization_header = {"authorization": self.access_token} 49 | 50 | self.response_format = response_format.lower() 51 | self.timeout = timeout 52 | self.sleep_time = sleep_time 53 | self.retries = retries 54 | 55 | def _make_request( 56 | self, path, method="GET", params_=None, public_api=False, web=False, **kwargs 57 | ): 58 | """Makes a request to Genius.""" 59 | if public_api: 60 | uri = self.PUBLIC_API_ROOT 61 | header = None 62 | elif web: 63 | uri = self.WEB_ROOT 64 | header = None 65 | else: 66 | uri = self.API_ROOT 67 | header = self.authorization_header 68 | uri += path 69 | 70 | params_ = params_ if params_ else {} 71 | 72 | # Make the request 73 | response = None 74 | tries = 0 75 | while response is None and tries <= self.retries: 76 | tries += 1 77 | try: 78 | response = self._session.request( 79 | method, 80 | uri, 81 | timeout=self.timeout, 82 | params=params_, 83 | headers=header, 84 | **kwargs, 85 | ) 86 | response.raise_for_status() 87 | except Timeout as e: 88 | error = "Request timed out:\n{e}".format(e=e) 89 | if tries > self.retries: 90 | raise Timeout(error) from e 91 | except HTTPError as e: 92 | error = get_description(e) 93 | if response.status_code < 500 or tries > self.retries: 94 | raise HTTPError(response.status_code, error) from e 95 | 96 | # Enforce rate limiting 97 | time.sleep(self.sleep_time) 98 | 99 | if web: 100 | return response.text 101 | elif response.status_code == 200: 102 | res = response.json() 103 | return res.get("response", res) 104 | elif response.status_code == 204: 105 | return 204 106 | else: 107 | raise AssertionError( 108 | "Response status code was neither 200, nor 204! It was {}".format( 109 | response.status_code 110 | ) 111 | ) 112 | 113 | 114 | def get_description(e): 115 | error = str(e) 116 | try: 117 | res = e.response.json() 118 | except JSONDecodeError: 119 | res = {} 120 | description = ( 121 | res["meta"]["message"] if res.get("meta") else res.get("error_description") 122 | ) 123 | error += "\n{}".format(description) if description else "" 124 | return error 125 | -------------------------------------------------------------------------------- /lyricsgenius/api/public_methods/__init__.py: -------------------------------------------------------------------------------- 1 | from .album import AlbumMethods 2 | from .annotation import AnnotationMethods 3 | from .article import ArticleMethods 4 | from .artist import ArtistMethods 5 | from .cover_art import CoverArtMethods 6 | from .discussion import DiscussionMethods 7 | from .leaderboard import LeaderboardMethods 8 | from .misc import MiscMethods 9 | from .question import QuestionMethods 10 | from .referent import ReferentMethods 11 | from .search import SearchMethods 12 | from .song import SongMethods 13 | from .user import UserMethods 14 | from .video import VideoMethods 15 | -------------------------------------------------------------------------------- /lyricsgenius/api/public_methods/album.py: -------------------------------------------------------------------------------- 1 | class AlbumMethods(object): 2 | """Album methods of the public API.""" 3 | 4 | def album(self, album_id, text_format=None): 5 | """Gets data for a specific album. 6 | 7 | Args: 8 | album_id (:obj:`int`): Genius album ID 9 | text_format (:obj:`str`, optional): Text format of the results 10 | ('dom', 'html', 'markdown' or 'plain'). 11 | 12 | Returns: 13 | :obj:`dict` 14 | 15 | Examples: 16 | .. code:: python 17 | 18 | genius = Genius(token) 19 | song = genius.search_song(378195) 20 | album_id = song['album']['id'] 21 | album = genius.album(album_id) 22 | print(album['name']) 23 | 24 | """ 25 | endpoint = "albums/{}".format(album_id) 26 | params = {"text_format": text_format or self.response_format} 27 | return self._make_request(path=endpoint, params_=params, public_api=True) 28 | 29 | def albums_charts( 30 | self, 31 | time_period="day", 32 | chart_genre="all", 33 | per_page=None, 34 | page=None, 35 | text_format=None, 36 | ): 37 | """Gets the album charts. 38 | 39 | Alias for :meth:`charts() `. 40 | 41 | Args: 42 | time_period (:obj:`str`, optional): Time period of the results 43 | ('day', 'week', 'month' or 'all_time'). 44 | chart_genre (:obj:`str`, optional): The genre of the results. 45 | per_page (:obj:`int`, optional): Number of results to 46 | return per request. It can't be more than 50. 47 | page (:obj:`int`, optional): Paginated offset (number of the page). 48 | text_format (:obj:`str`, optional): Text format of the results 49 | ('dom', 'html', 'markdown' or 'plain'). 50 | 51 | Returns: 52 | :obj:`dict` 53 | 54 | """ 55 | return self.charts( 56 | time_period=time_period, 57 | chart_genre=chart_genre, 58 | per_page=per_page, 59 | page=page, 60 | text_format=text_format, 61 | type_="albums", 62 | ) 63 | 64 | def album_comments(self, album_id, per_page=None, page=None, text_format=None): 65 | """Gets the comments on an album page. 66 | 67 | Args: 68 | album_id (:obj:`int`): Genius album ID 69 | per_page (:obj:`int`, optional): Number of results to 70 | return per request. It can't be more than 50. 71 | page (:obj:`int`, optional): Paginated offset (number of the page). 72 | text_format (:obj:`str`, optional): Text format of the results 73 | ('dom', 'html', 'markdown' or 'plain'). 74 | 75 | Returns: 76 | :obj:`dict` 77 | 78 | """ 79 | endpoint = "albums/{}/comments".format(album_id) 80 | params = { 81 | "per_page": per_page, 82 | "page": page, 83 | "text_format": text_format or self.response_format, 84 | } 85 | return self._make_request(path=endpoint, params_=params, public_api=True) 86 | 87 | def album_cover_arts(self, album_id, text_format=None): 88 | """Gets cover arts of a specific album. 89 | 90 | Alias for :meth:`cover_arts `. 91 | 92 | Args: 93 | album_id (:obj:`int`): Genius album ID 94 | text_format (:obj:`str`, optional): Text format of the results 95 | ('dom', 'html', 'markdown' or 'plain'). Defines the text 96 | formatting for the annotation of the cover arts, 97 | if there are any. 98 | 99 | Returns: 100 | :obj:`dict` 101 | 102 | Examples: 103 | Downloading album's cover art: 104 | 105 | .. code:: python 106 | 107 | import requests 108 | 109 | genius = Genius(token) 110 | res = genius.album_cover_arts(104614) 111 | cover_art = requests.get(res['cover_arts'][0]['image_url']) 112 | 113 | """ 114 | return self.cover_arts(album_id=album_id, text_format=text_format) 115 | 116 | def album_leaderboard(self, album_id, per_page=None, page=None): 117 | """Gets the leaderboard of an album. 118 | 119 | This method returns the album's top contributors. 120 | 121 | Args: 122 | album_id (:obj:`int`): Genius album ID 123 | per_page (:obj:`int`, optional): Number of results to 124 | return per request. It can't be more than 50. 125 | page (:obj:`int`, optional): Paginated offset (number of the page). 126 | 127 | Returns: 128 | :obj:`dict` 129 | 130 | """ 131 | endpoint = "albums/{}/leaderboard".format(album_id) 132 | params = {"per_page": per_page, "page": page} 133 | return self._make_request(path=endpoint, params_=params, public_api=True) 134 | 135 | def album_tracks(self, album_id, per_page=None, page=None, text_format=None): 136 | """Gets the tracks of a specific album. 137 | 138 | Args: 139 | album_id (:obj:`int`): Genius album ID 140 | per_page (:obj:`int`, optional): Number of results to 141 | return per request. It can't be more than 50. 142 | page (:obj:`int`, optional): Paginated offset (number of the page). 143 | text_format (:obj:`str`, optional): Text format of the results 144 | ('dom', 'html', 'markdown' or 'plain'). 145 | 146 | Returns: 147 | :obj:`dict` 148 | 149 | """ 150 | endpoint = "albums/{}/tracks".format(album_id) 151 | params = { 152 | "per_page": per_page, 153 | "page": page, 154 | "text_format": text_format or self.response_format, 155 | } 156 | return self._make_request(path=endpoint, params_=params, public_api=True) 157 | -------------------------------------------------------------------------------- /lyricsgenius/api/public_methods/annotation.py: -------------------------------------------------------------------------------- 1 | class AnnotationMethods(object): 2 | """Annotation methods of the public API.""" 3 | 4 | def annotation(self, annotation_id, text_format=None): 5 | """Gets data for a specific annotation. 6 | 7 | Args: 8 | annotation_id (:obj:`int`): Genius annotation ID 9 | text_format (:obj:`str`, optional): Text format of the results 10 | ('dom', 'html', 'markdown' or 'plain'). 11 | 12 | Returns: 13 | :obj:`dict` 14 | 15 | """ 16 | endpoint = "annotations/{}".format(annotation_id) 17 | params = {"text_format": text_format or self.response_format} 18 | return self._make_request(path=endpoint, params_=params, public_api=True) 19 | 20 | def annotation_edits(self, annotation_id, text_format=None): 21 | """Gets the edits on annotation (its versions). 22 | 23 | Args: 24 | annotation_id (:obj:`int`): Genius annotation ID 25 | text_format (:obj:`str`, optional): Text format of the results 26 | ('dom', 'html', 'markdown' or 'plain'). 27 | 28 | Returns: 29 | :obj:`dict` 30 | 31 | """ 32 | endpoint = "annotations/{}/versions".format(annotation_id) 33 | params = {"text_format": text_format or self.response_format} 34 | return self._make_request(path=endpoint, params_=params, public_api=True) 35 | 36 | def annotation_comments( 37 | self, annotation_id, per_page=None, page=None, text_format=None 38 | ): 39 | """Gets the comments on an annotation. 40 | 41 | Args: 42 | annotation_id (:obj:`int`): Genius annotation ID 43 | per_page (:obj:`int`, optional): Number of results to 44 | return per request. It can't be more than 50. 45 | page (:obj:`int`, optional): Paginated offset (number of the page). 46 | text_format (:obj:`str`, optional): Text format of the results 47 | ('dom', 'html', 'markdown' or 'plain'). 48 | 49 | Returns: 50 | :obj:`dict` 51 | 52 | """ 53 | endpoint = "annotations/{}/comments".format(annotation_id) 54 | params = { 55 | "per_page": per_page, 56 | "page": page, 57 | "text_format": text_format or self.response_format, 58 | } 59 | return self._make_request(path=endpoint, params_=params, public_api=True) 60 | -------------------------------------------------------------------------------- /lyricsgenius/api/public_methods/article.py: -------------------------------------------------------------------------------- 1 | class ArticleMethods(object): 2 | """Article methods of the public API.""" 3 | 4 | def article(self, article_id, text_format=None): 5 | """Gets data for a specific article. 6 | 7 | Args: 8 | article_id (:obj:`int`): Genius article ID 9 | text_format (:obj:`str`, optional): Text format of the results 10 | ('dom', 'html', 'markdown' or 'plain'). 11 | 12 | Returns: 13 | :obj:`dict` 14 | 15 | """ 16 | endpoint = "articles/{}".format(article_id) 17 | params = {"text_format": text_format or self.response_format} 18 | return self._make_request(path=endpoint, params_=params, public_api=True) 19 | 20 | def article_comments(self, article_id, per_page=None, page=None, text_format=None): 21 | """Gets the comments on an article. 22 | 23 | Args: 24 | article_id (:obj:`int`): Genius article ID 25 | per_page (:obj:`int`, optional): Number of results to 26 | return per request. It can't be more than 50. 27 | page (:obj:`int`, optional): Paginated offset (number of the page). 28 | text_format (:obj:`str`, optional): Text format of the results 29 | ('dom', 'html', 'markdown' or 'plain'). 30 | 31 | Returns: 32 | :obj:`dict` 33 | 34 | """ 35 | endpoint = "articles/{}/comments".format(article_id) 36 | params = { 37 | "per_page": per_page, 38 | "page": page, 39 | "text_format": text_format or self.response_format, 40 | } 41 | return self._make_request(path=endpoint, params_=params, public_api=True) 42 | 43 | def latest_articles(self, per_page=None, page=None, text_format=None): 44 | """Gets the latest articles on the homepage. 45 | 46 | This method will return the featured articles that are placed 47 | on top of the Genius.com page. 48 | 49 | Args: 50 | article_id (:obj:`int`): Genius article ID 51 | per_page (:obj:`int`, optional): Number of results to 52 | return per request. It can't be more than 50. 53 | page (:obj:`int`, optional): Paginated offset (number of the page). 54 | text_format (:obj:`str`, optional): Text format of the results 55 | ('dom', 'html', 'markdown' or 'plain'). 56 | 57 | Returns: 58 | :obj:`dict` 59 | 60 | """ 61 | endpoint = "editorial_placements/latest" 62 | params = { 63 | "per_page": per_page, 64 | "page": page, 65 | "text_format": text_format or self.response_format, 66 | } 67 | return self._make_request(path=endpoint, params_=params, public_api=True) 68 | -------------------------------------------------------------------------------- /lyricsgenius/api/public_methods/artist.py: -------------------------------------------------------------------------------- 1 | class ArtistMethods(object): 2 | """Artist methods of the public API.""" 3 | 4 | def artist(self, artist_id, text_format=None): 5 | """Gets data for a specific artist. 6 | 7 | Args: 8 | artist_id (:obj:`int`): Genius artist ID 9 | text_format (:obj:`str`, optional): Text format of the results 10 | ('dom', 'html', 'markdown' or 'plain'). 11 | 12 | Returns: 13 | :obj:`dict` 14 | 15 | """ 16 | endpoint = "artists/{}".format(artist_id) 17 | params = {"text_format": text_format or self.response_format} 18 | 19 | return self._make_request(path=endpoint, params_=params, public_api=True) 20 | 21 | def artist_activity(self, artist_id, per_page=None, page=None, text_format=None): 22 | """Gets activities on artist's songs. 23 | 24 | Args: 25 | artist_id (:obj:`int`): Genius artist ID 26 | per_page (:obj:`int`, optional): Number of results to 27 | return per request. It can't be more than 50. 28 | page (:obj:`int`, optional): Paginated offset (number of the page). 29 | text_format (:obj:`str`, optional): Text format of the results 30 | ('dom', 'html', 'markdown' or 'plain'). 31 | 32 | Returns: 33 | :obj:`dict` 34 | 35 | """ 36 | endpoint = "artists/{}/activity_stream/line_items".format(artist_id) 37 | params = { 38 | "per_page": per_page, 39 | "page": page, 40 | "text_format": text_format or self.response_format, 41 | } 42 | return self._make_request(path=endpoint, params_=params, public_api=True) 43 | 44 | def artist_albums(self, artist_id, per_page=None, page=None): 45 | """Gets artist's albums. 46 | 47 | Args: 48 | artist_id (:obj:`int`): Genius artist ID 49 | per_page (:obj:`int`, optional): Number of results to 50 | return per request. It can't be more than 50. 51 | page (:obj:`int`, optional): Paginated offset (number of the page). 52 | 53 | Returns: 54 | :obj:`dict` 55 | 56 | """ 57 | endpoint = "artists/{}/albums".format(artist_id) 58 | params = {"per_page": per_page, "page": page} 59 | return self._make_request(path=endpoint, params_=params, public_api=True) 60 | 61 | def artist_contribution_opportunities( 62 | self, artist_id, per_page=None, next_curosr=None, text_format=None 63 | ): 64 | """Gets contribution opportunities related to the artist. 65 | 66 | Args: 67 | artist_id (:obj:`int`): Genius artist ID 68 | per_page (:obj:`int`, optional): Number of results to 69 | return per request. It can't be more than 50. 70 | next_cursor (:obj:`int`, optional): Paginated offset 71 | (address of the next cursor). 72 | text_format (:obj:`str`, optional): Text format of the results 73 | ('dom', 'html', 'markdown' or 'plain'). 74 | 75 | Returns: 76 | :obj:`dict` 77 | 78 | Warning: 79 | This method requires a logged in user and will raise 80 | ``NotImplementedError``. 81 | 82 | """ 83 | raise NotImplementedError("This action requires a logged in user.") 84 | endpoint = "artists/{}/contribution_opportunities".format(artist_id) 85 | params = { 86 | "per_page": per_page, 87 | "next_curosr": next_curosr, 88 | "text_format": text_format or self.response_format, 89 | } 90 | return self._make_request(path=endpoint, params_=params, public_api=True) 91 | 92 | def artist_followers(self, artist_id, per_page=None, page=None): 93 | """Gets artist's followers. 94 | 95 | Args: 96 | artist_id (:obj:`int`): Genius artist ID 97 | per_page (:obj:`int`, optional): Number of results to 98 | return per request. It can't be more than 50. 99 | page (:obj:`int`, optional): Paginated offset (number of the page). 100 | 101 | Returns: 102 | :obj:`dict` 103 | 104 | """ 105 | endpoint = "artists/{}/followers".format(artist_id) 106 | params = {"per_page": per_page, "page": page} 107 | return self._make_request(path=endpoint, params_=params, public_api=True) 108 | 109 | def artist_leaderboard(self, artist_id, per_page=None, page=None): 110 | """Gets artist's top scholars. 111 | 112 | Args: 113 | artist_id (:obj:`int`): Genius artist ID 114 | per_page (:obj:`int`, optional): Number of results to 115 | return per request. It can't be more than 50. 116 | page (:obj:`int`, optional): Paginated offset (number of the page). 117 | 118 | Returns: 119 | :obj:`dict` 120 | 121 | """ 122 | endpoint = "artists/{}/leaderboard".format(artist_id) 123 | params = {"per_page": per_page, "page": page} 124 | return self._make_request(path=endpoint, params_=params, public_api=True) 125 | 126 | def artist_songs(self, artist_id, per_page=None, page=None, sort="popularity"): 127 | """Gets artist's songs. 128 | 129 | Args: 130 | artist_id (:obj:`int`): Genius artist ID 131 | per_page (:obj:`int`, optional): Number of results to 132 | return per request. It can't be more than 50. 133 | page (:obj:`int`, optional): Paginated offset (number of the page). 134 | sort (:obj:`str`, optional): Sorting preference. 135 | ('title' or 'popularity') 136 | 137 | Returns: 138 | :obj:`dict` 139 | 140 | """ 141 | endpoint = "artists/{}/songs".format(artist_id) 142 | params = {"per_page": per_page, "page": page, "sort": sort} 143 | return self._make_request(path=endpoint, params_=params, public_api=True) 144 | 145 | def search_artist_songs( 146 | self, artist_id, search_term, per_page=None, page=None, sort="popularity" 147 | ): 148 | """Searches artist's songs. 149 | 150 | Args: 151 | artist_id (:obj:`int`): Genius artist ID 152 | search_term (:obj:`str`): A term to search on Genius. 153 | per_page (:obj:`int`, optional): Number of results to 154 | return per request. It can't be more than 50. 155 | page (:obj:`int`, optional): Paginated offset (number of the page). 156 | sort (:obj:`str`, optional): Sorting preference. 157 | ('title' or 'popularity') 158 | 159 | Returns: 160 | :obj:`dict` 161 | 162 | """ 163 | endpoint = "artists/{}/songs/search".format(artist_id) 164 | params = {"q": search_term, "per_page": per_page, "page": page, "sort": sort} 165 | return self._make_request(path=endpoint, params_=params, public_api=True) 166 | -------------------------------------------------------------------------------- /lyricsgenius/api/public_methods/cover_art.py: -------------------------------------------------------------------------------- 1 | class CoverArtMethods(object): 2 | """Cover art methods of the public API.""" 3 | 4 | def cover_arts(self, album_id=None, song_id=None, text_format=None): 5 | """Gets the cover arts of an album or a song. 6 | 7 | You must supply one of :obj:`album_id` or :obj:`song_id`. 8 | 9 | Args: 10 | album_id (:obj:`int`, optional): Genius album ID 11 | song_id (:obj:`int`, optional): Genius song ID 12 | text_format (:obj:`str`, optional): Text format of the results 13 | ('dom', 'html', 'markdown' or 'plain'). Defines the text 14 | formatting for the annotation of the cover arts, 15 | if there are any. 16 | 17 | Returns: 18 | :obj:`dict` 19 | 20 | """ 21 | msg = "Must supply `album_id` or `song_id`." 22 | assert any([album_id, song_id]), msg 23 | msg = "Pass only one of `album_id` or `song_id`, not both." 24 | condition = sum([bool(album_id), bool(song_id)]) == 1 25 | assert condition, msg 26 | endpoint = "cover_arts" 27 | params = {"text_format": text_format or self.response_format} 28 | if album_id is not None: 29 | params["album_id"] = album_id 30 | else: 31 | params["song_id"] = song_id 32 | return self._make_request(path=endpoint, params_=params, public_api=True) 33 | -------------------------------------------------------------------------------- /lyricsgenius/api/public_methods/discussion.py: -------------------------------------------------------------------------------- 1 | class DiscussionMethods(object): 2 | """Discussion methods of the public API.""" 3 | 4 | def discussion(self, discussion_id, text_format=None): 5 | """Gets data for a specific discussion. 6 | 7 | Args: 8 | discussion_id (:obj:`int`): Genius discussion ID 9 | text_format (:obj:`str`, optional): Text format of the results 10 | ('dom', 'html', 'markdown' or 'plain'). 11 | 12 | Returns: 13 | :obj:`dict` 14 | 15 | Note: 16 | This request returns a 403 error and will raise ``NotImplementedError``. 17 | 18 | """ 19 | raise NotImplementedError("This request returns a 403 error.") 20 | 21 | endpoint = "discussions/{}".format(discussion_id) 22 | params = {"text_format": text_format or self.response_format} 23 | return self._make_request(path=endpoint, params_=params, public_api=True) 24 | 25 | def discussions(self, page=None): 26 | """Gets discussions. 27 | 28 | Args: 29 | page (:obj:`int`, optional): Paginated offset (number of the page). 30 | 31 | Returns: 32 | :obj:`dict` 33 | 34 | """ 35 | 36 | endpoint = "discussions" 37 | params = {"page": page} 38 | return self._make_request(path=endpoint, params_=params, public_api=True) 39 | 40 | def discussion_replies( 41 | self, discussion_id, per_page=None, page=None, text_format=None 42 | ): 43 | """Gets the replies on a discussion. 44 | 45 | Args: 46 | discussion_id (:obj:`int`): Genius discussion ID 47 | per_page (:obj:`int`, optional): Number of results to 48 | return per request. It can't be more than 50. 49 | page (:obj:`int`, optional): Paginated offset (number of the page). 50 | text_format (:obj:`str`, optional): Text format of the results 51 | ('dom', 'html', 'markdown' or 'plain'). 52 | 53 | Returns: 54 | :obj:`dict` 55 | 56 | Note: 57 | This request returns a 403 error and will raise ``NotImplementedError``. 58 | 59 | """ 60 | raise NotImplementedError("This request returns a 403 error.") 61 | 62 | endpoint = "discussions/{}/forum_posts".format(discussion_id) 63 | params = { 64 | "per_page": per_page, 65 | "page": page, 66 | "text_format": text_format or self.response_format, 67 | } 68 | return self._make_request(path=endpoint, params_=params, public_api=True) 69 | -------------------------------------------------------------------------------- /lyricsgenius/api/public_methods/leaderboard.py: -------------------------------------------------------------------------------- 1 | class LeaderboardMethods(object): 2 | """Leaderboard methods of the public API.""" 3 | 4 | def leaderboard( 5 | self, time_period="day", per_page=None, page=None, text_format=None 6 | ): 7 | """Gets the Genius community leaderboard. 8 | 9 | This method gets data of the community charts on the Genius.com page. 10 | 11 | Args: 12 | time_period (:obj:`str`, optional): Time period of the results. 13 | ('day', 'week', 'month' or 'all_time'). 14 | per_page (:obj:`int`, optional): Number of results to 15 | return per request. It can't be more than 50. 16 | page (:obj:`int`, optional): Paginated offset (number of the page). 17 | text_format (:obj:`str`, optional): Text format of the results 18 | ('dom', 'html', 'markdown' or 'plain'). 19 | 20 | Returns: 21 | :obj:`dict` 22 | 23 | """ 24 | path = "leaderboard" 25 | params = { 26 | "time_period": time_period, 27 | "per_page": per_page, 28 | "page": page, 29 | "text_format": text_format or self.response_format, 30 | } 31 | return self._make_request(path=path, params_=params, public_api=True) 32 | 33 | def charts( 34 | self, 35 | time_period="day", 36 | chart_genre="all", 37 | per_page=None, 38 | page=None, 39 | text_format=None, 40 | type_="songs", 41 | ): 42 | """Gets the Genius charts. 43 | 44 | This method gets data of the chart on the Genius.com page. 45 | 46 | Args: 47 | time_period (:obj:`str`, optional): Time period of the results. 48 | The default is `all`. 49 | ('day', 'week', 'month' or 'all_time'). 50 | chart_genre (:obj:`str`, optional): The genre of the results. 51 | The default value is ``all``. 52 | ('all', 'rap', 'pop', 'rb', 'rock' or 'country') 53 | per_page (:obj:`int`, optional): Number of results to 54 | return per request. It can't be more than 50. 55 | page (:obj:`int`, optional): Paginated offset (number of the page). 56 | text_format (:obj:`str`, optional): Text format of the results 57 | ('dom', 'html', 'markdown' or 'plain'). 58 | type_ (:obj:`int`, optional): The type to get the charts for. 59 | The default is ``songs``. 60 | ('songs', 'albums', 'artists' or 'referents'). 61 | 62 | Returns: 63 | :obj:`dict` 64 | 65 | .. Note:: 66 | The *referents* mentioned in the description of the :obj:`type_` 67 | argument is shown as *Lyrics* in the drop-down menu on Genius.com 68 | where you choose the *Type*. 69 | 70 | """ 71 | endpoint = type_ + "/chart" 72 | params = { 73 | "time_period": time_period, 74 | "chart_genre": chart_genre, 75 | "per_page": per_page, 76 | "page": page, 77 | "text_format": text_format or self.response_format, 78 | } 79 | return self._make_request(path=endpoint, params_=params, public_api=True) 80 | -------------------------------------------------------------------------------- /lyricsgenius/api/public_methods/misc.py: -------------------------------------------------------------------------------- 1 | class MiscMethods(object): 2 | """Miscellaneous Methods""" 3 | 4 | def line_item(self, line_item_id, text_format=None): 5 | """Gets data for a specific line item. 6 | 7 | Args: 8 | line_item_id (:obj:`int`): Genius line item ID 9 | text_format (:obj:`str`, optional): Text format of the results 10 | ('dom', 'html', 'markdown' or 'plain'). 11 | 12 | Returns: 13 | :obj:`dict` 14 | 15 | Warning: 16 | This method requires a logged in user and will raise 17 | ``NotImplementedError``. 18 | 19 | """ 20 | raise NotImplementedError("This action requires a logged in user.") 21 | 22 | endpoint = "line_items/{}".format(line_item_id) 23 | params = {"text_format": text_format or self.response_format} 24 | return self._make_request(path=endpoint, params_=params, public_api=True) 25 | 26 | def page_data(self, album=None, song=None, artist=None): 27 | """Gets page data of an item. 28 | 29 | If you want the page data of a song, you must supply 30 | song and artist. But if you want the page data of an album, 31 | you only have to supply the album. 32 | 33 | Page data will return all possible values for the album/song and 34 | the lyrics in HTML format if the item is a song! 35 | Album page data will contain album info and tracks info as well. 36 | 37 | Args: 38 | album (:obj:`str`, optional): Album path 39 | (e.g. '/albums/Eminem/Music-to-be-murdered-by') 40 | song (:obj:`str`, optional): Song path 41 | (e.g. '/Sia-chandelier-lyrics') 42 | artist (:obj:`str`, optional): Artist slug. (e.g. 'Andy-shauf') 43 | 44 | Returns: 45 | :obj:`dict` 46 | 47 | Warning: 48 | Some albums/songs either don't have page data or 49 | their page data path can't be inferred easily from 50 | the artist slug and their API path. So make sure to 51 | use this method with a try/except clause that catches 52 | 404 errors. Check out the example below. 53 | 54 | 55 | Examples: 56 | Getting the lyrics of a song from its page data 57 | 58 | .. code:: python 59 | 60 | from lyricsgenius import Genius, PublicAPI 61 | from bs4 import BeautifulSoup 62 | from requests import HTTPError 63 | 64 | genius = Genius(token) 65 | public = PublicAPI() 66 | 67 | # We need the PublicAPI to get artist's slug 68 | artist = public.artist(1665) 69 | artist_slug = artist['artist']['slug'] 70 | 71 | # The rest can be done using Genius 72 | song = genius.song(4558484) 73 | song_path = song['song']['path'] 74 | 75 | try: 76 | page_data = genius.page_data(artist=artist_slug, song=song_path) 77 | except HTTPError as e: 78 | print("Couldn't find page data {}".format(e.status_code)) 79 | page_data = None 80 | 81 | if page_data is not None: 82 | lyrics_html = page_data['page_data']['lyrics_data']['body']['html'] 83 | lyrics_text = BeautifulSoup(lyrics_html, 'html.parser').get_text() 84 | 85 | """ 86 | assert any([album, song]), "You must pass either song or album." 87 | if song: 88 | assert all([song, artist]), "You must pass artist." 89 | 90 | if album: 91 | endpoint = "page_data/album" 92 | page_type = "albums" 93 | item_path = album.replace("/albums/", "") 94 | else: 95 | endpoint = "page_data/song" 96 | page_type = "songs" 97 | 98 | # item path becomes something like: Artist/Song 99 | item_path = ( 100 | song[1:].replace(artist + "-", artist + "/").replace("-lyrics", "") 101 | ) 102 | 103 | page_path = "/{page_type}/{item_path}".format( 104 | page_type=page_type, item_path=item_path 105 | ) 106 | params = {"page_path": page_path} 107 | 108 | return self._make_request(endpoint, params_=params, public_api=True) 109 | 110 | def voters( 111 | self, annotation_id=None, answer_id=None, article_id=None, comment_id=None 112 | ): 113 | """Gets the voters of an item. 114 | 115 | You must supply one of :obj:`annotation_id`, :obj:`answer_id`, :obj:`article_id` 116 | or :obj:`comment_id`. 117 | 118 | Args: 119 | annotation_id (:obj:`int`, optional): Genius annotation ID 120 | answer_id (:obj:`int`, optional): Genius answer ID 121 | article_id (:obj:`int`, optional): Genius article ID 122 | comment_id (:obj:`int`, optional): Genius comment ID 123 | 124 | Returns: 125 | :obj:`dict` 126 | 127 | """ 128 | msg = "Must supply `annotation_id`, `answer_id`, `comment_id` or `article_id`" 129 | assert any([annotation_id, answer_id, article_id, comment_id]), msg 130 | msg = ( 131 | "Pass only one of " 132 | "`annotation_id`, `answer_id`, `article_id` or `comment_id`" 133 | ", not more than one." 134 | ) 135 | condition = ( 136 | sum( 137 | [ 138 | bool(annotation_id), 139 | bool(answer_id), 140 | bool(article_id), 141 | bool(comment_id), 142 | ] 143 | ) 144 | == 1 145 | ) 146 | assert condition, msg 147 | 148 | endpoint = "voters" 149 | 150 | params = {} 151 | if annotation_id: 152 | params["annotation_id"] = annotation_id 153 | elif answer_id: 154 | params["answer_id"] = answer_id 155 | elif article_id: 156 | params["article_id"] = article_id 157 | elif comment_id: 158 | params["comment_id"] = comment_id 159 | return self._make_request(path=endpoint, params_=params, public_api=True) 160 | -------------------------------------------------------------------------------- /lyricsgenius/api/public_methods/question.py: -------------------------------------------------------------------------------- 1 | class QuestionMethods(object): 2 | """Question methods of the public API.""" 3 | 4 | def questions( 5 | self, 6 | album_id=None, 7 | song_id=None, 8 | per_page=None, 9 | page=None, 10 | state=None, 11 | text_format=None, 12 | ): 13 | """Gets the questions on an album or a song. 14 | 15 | You must supply one of :obj:`album_id` or :obj:`song_id`. 16 | 17 | Args: 18 | time_period (:obj:`str`, optional): Time period of the results 19 | ('day', 'week', 'month' or 'all_time'). 20 | per_page (:obj:`int`, optional): Number of results to 21 | return per request. It can't be more than 50. 22 | page (:obj:`int`, optional): Paginated offset (number of the page). 23 | state (:obj:`str`, optional): State of the question. 24 | text_format (:obj:`str`, optional): Text format of the results 25 | ('dom', 'html', 'markdown' or 'plain'). 26 | 27 | Returns: 28 | :obj:`dict` 29 | 30 | """ 31 | msg = "Must supply `album_id` or `song_id`." 32 | assert any([album_id, song_id]), msg 33 | msg = "Pass only one of `album_id` and `song_id`, not both." 34 | condition = sum([bool(album_id), bool(song_id)]) == 1 35 | assert condition, msg 36 | endpoint = "questions" 37 | params = { 38 | "per_page": per_page, 39 | "page": page, 40 | "state": state, 41 | "text_format": text_format or self.response_format, 42 | } 43 | if album_id: 44 | params["album_id"] = album_id 45 | elif song_id: 46 | params["song_id"] = song_id 47 | return self._make_request(path=endpoint, params_=params, public_api=True) 48 | -------------------------------------------------------------------------------- /lyricsgenius/api/public_methods/referent.py: -------------------------------------------------------------------------------- 1 | class ReferentMethods(object): 2 | """Referent methods of the public API.""" 3 | 4 | def referent(self, referent_ids, text_format=None): 5 | """Gets data of one or more referents. 6 | 7 | This method can get multiple referents in one call, 8 | thus increasing performance. 9 | 10 | Args: 11 | referent_ids (:obj:`list`): A list of referent IDs. 12 | text_format (:obj:`str`, optional): Text format of the results 13 | ('dom', 'html', 'markdown' or 'plain'). 14 | 15 | Returns: 16 | :obj:`dict` 17 | 18 | Note: 19 | Using this method you can get the referent itself instead of 20 | the referents of a song or webpage which is what 21 | :meth:`referents() ` gets. 22 | 23 | """ 24 | params = {"text_format": text_format or self.response_format} 25 | if len(referent_ids) == 1: 26 | endpoint = "referents/{}".format(referent_ids[0]) 27 | else: 28 | endpoint = "referents/multi" 29 | params = [("text_format", params["text_format"])] 30 | for id in referent_ids: 31 | params.append(("ids[]", id)) 32 | 33 | return self._make_request(path=endpoint, params_=params, public_api=True) 34 | 35 | def referents( 36 | self, 37 | song_id=None, 38 | web_page_id=None, 39 | created_by_id=None, 40 | per_page=None, 41 | page=None, 42 | text_format=None, 43 | ): 44 | """Gets item's referents 45 | 46 | You must supply :obj:`song_id`, :obj:`web_page_id`, or :obj:`created_by_id`. 47 | 48 | Args: 49 | song_id (:obj:`int`, optional): song ID 50 | web_page_id (:obj:`int`, optional): web page ID 51 | created_by_id (:obj:`int`, optional): User ID of the contributor 52 | who created the annotation(s). 53 | per_page (:obj:`int`, optional): Number of results to 54 | return per page. It can't be more than 50. 55 | text_format (:obj:`str`, optional): Text format of the results 56 | ('dom', 'html', 'markdown' or 'plain'). 57 | 58 | Returns: 59 | :obj:`dict` 60 | 61 | """ 62 | msg = "Must supply `song_id`, `web_page_id`, or `created_by_id`." 63 | assert any([song_id, web_page_id, created_by_id]), msg 64 | msg = "Pass only one of `song_id` and `web_page_id`, not both." 65 | assert bool(song_id) ^ bool(web_page_id), msg 66 | 67 | endpoint = "referents" 68 | params = { 69 | "song_id": song_id, 70 | "web_page_id": web_page_id, 71 | "created_by_id": created_by_id, 72 | "per_page": per_page, 73 | "page": page, 74 | "text_format": text_format or self.response_format, 75 | } 76 | return self._make_request(endpoint, params_=params, public_api=True) 77 | 78 | def referents_charts( 79 | self, 80 | time_period="day", 81 | chart_genre="all", 82 | per_page=None, 83 | page=None, 84 | text_format=None, 85 | ): 86 | """Gets the referents (lyrics) charts. 87 | 88 | Alias for :meth:`charts() `. 89 | 90 | Args: 91 | time_period (:obj:`str`, optional): Time period of the results 92 | ('day', 'week', 'month' or 'all_time'). 93 | chart_genre (:obj:`str`, optional): The genre of the results. 94 | per_page (:obj:`int`, optional): Number of results to 95 | return per request. It can't be more than 50. 96 | page (:obj:`int`, optional): Paginated offset (number of the page). 97 | text_format (:obj:`str`, optional): Text format of the results 98 | ('dom', 'html', 'markdown' or 'plain'). 99 | 100 | Returns: 101 | :obj:`dict` 102 | 103 | """ 104 | return self.charts( 105 | time_period=time_period, 106 | chart_genre=chart_genre, 107 | per_page=per_page, 108 | page=page, 109 | text_format=text_format, 110 | type_="referents", 111 | ) 112 | -------------------------------------------------------------------------------- /lyricsgenius/api/public_methods/search.py: -------------------------------------------------------------------------------- 1 | class SearchMethods(object): 2 | """Search methods of the public API.""" 3 | 4 | def search(self, search_term, per_page=None, page=None, type_=""): 5 | """Searches Genius. 6 | 7 | Args: 8 | search_term (:obj:`str`): A term to search on Genius. 9 | per_page (:obj:`int`, optional): Number of results to 10 | return per request. It can't be more than 50. 11 | page (:obj:`int`, optional): Paginated offset (number of the page). 12 | text_format (:obj:`str`, optional): Text format of the results 13 | ('dom', 'html', 'markdown' or 'plain'). 14 | type_ (:obj:`str`, optional): Type of item to search for 15 | ('song', 'lyric', 'artist', 'album', 'video', 16 | 'article', 'user' or 'multi'). 17 | 18 | Returns: 19 | :obj:`dict` 20 | 21 | .. Note:: 22 | Specifying no :obj:`type_` parameter (which defaults to ``''``) or 23 | setting it as ``song`` will return the same results. Both will return 24 | songs. The only different is they return the hits in different 25 | keys: 26 | 27 | * ``type_=''``: ``response['hits']`` 28 | * ``type_='song'``: ``response['sections'][0]['hits']`` 29 | 30 | By Setting the type as ``multi`` the method will perform a search 31 | for all the other types and return an extra section called ``top hits``. 32 | 33 | .. Note:: 34 | Instead of calling this method directly and specifying a type, you 35 | can use the alias methods. 36 | 37 | """ 38 | if type_ == "": 39 | path = "search" 40 | else: 41 | path = "search/" + type_ 42 | params = {"q": search_term, "per_page": per_page, "page": page} 43 | return self._make_request(path, params_=params, public_api=True) 44 | 45 | def search_albums(self, search_term, per_page=None, page=None): 46 | """Searches the albums on Genius. 47 | 48 | Alias for :meth:`search() ` 49 | 50 | Args: 51 | search_term (:obj:`str`): A term to search on Genius 52 | per_page (:obj:`int`, optional): Number of results to 53 | return per request. It can't be more than 50. 54 | page (:obj:`int`, optional): Paginated offset (number of the page). 55 | text_format (:obj:`str`, optional): Text format of the results 56 | ('dom', 'html', 'markdown' or 'plain'). 57 | 58 | Returns: 59 | :obj:`dict` 60 | 61 | """ 62 | endpoint = "album" 63 | return self.search(search_term, per_page, page, endpoint) 64 | 65 | def search_articles(self, search_term, per_page=None, page=None): 66 | """Searches the articles on Genius. 67 | 68 | Alias for :meth:`search() ` 69 | 70 | Args: 71 | search_term (:obj:`str`): A term to search on Genius 72 | per_page (:obj:`int`, optional): Number of results to 73 | return per request. It can't be more than 50. 74 | page (:obj:`int`, optional): Paginated offset (number of the page). 75 | text_format (:obj:`str`, optional): Text format of the results 76 | ('dom', 'html', 'markdown' or 'plain'). 77 | 78 | Returns: 79 | :obj:`dict` 80 | 81 | """ 82 | endpoint = "article" 83 | return self.search(search_term, per_page, page, endpoint) 84 | 85 | def search_artists(self, search_term, per_page=None, page=None): 86 | """Searches the artists on Genius. 87 | 88 | Alias for :meth:`search() ` 89 | 90 | Args: 91 | search_term (:obj:`str`): A term to search on Genius 92 | per_page (:obj:`int`, optional): Number of results to 93 | return per request. It can't be more than 50. 94 | page (:obj:`int`, optional): Paginated offset (number of the page). 95 | text_format (:obj:`str`, optional): Text format of the results 96 | ('dom', 'html', 'markdown' or 'plain'). 97 | 98 | Returns: 99 | :obj:`dict` 100 | 101 | """ 102 | endpoint = "artist" 103 | return self.search(search_term, per_page, page, endpoint) 104 | 105 | def search_lyrics(self, search_term, per_page=None, page=None): 106 | """Searches the lyrics on Genius. 107 | 108 | Alias for :meth:`search() ` 109 | 110 | Args: 111 | search_term (:obj:`str`): A term to search on Genius 112 | per_page (:obj:`int`, optional): Number of results to 113 | return per request. It can't be more than 50. 114 | page (:obj:`int`, optional): Paginated offset (number of the page). 115 | text_format (:obj:`str`, optional): Text format of the results 116 | ('dom', 'html', 'markdown' or 'plain'). 117 | 118 | Returns: 119 | :obj:`dict` 120 | 121 | """ 122 | endpoint = "lyric" 123 | return self.search(search_term, per_page, page, endpoint) 124 | 125 | def search_songs(self, search_term, per_page=None, page=None): 126 | """Searches the songs on Genius. 127 | 128 | Alias for :meth:`search() ` 129 | 130 | Args: 131 | search_term (:obj:`str`): A term to search on Genius 132 | per_page (:obj:`int`, optional): Number of results to 133 | return per request. It can't be more than 50. 134 | page (:obj:`int`, optional): Paginated offset (number of the page). 135 | text_format (:obj:`str`, optional): Text format of the results 136 | ('dom', 'html', 'markdown' or 'plain'). 137 | 138 | Returns: 139 | :obj:`dict` 140 | 141 | """ 142 | endpoint = "song" 143 | return self.search(search_term, per_page, page, endpoint) 144 | 145 | def search_users(self, search_term, per_page=None, page=None): 146 | """Searches the users on Genius. 147 | 148 | Alias for :meth:`search() ` 149 | 150 | Args: 151 | search_term (:obj:`str`): A term to search on Genius 152 | per_page (:obj:`int`, optional): Number of results to 153 | return per request. It can't be more than 50. 154 | page (:obj:`int`, optional): Paginated offset (number of the page). 155 | text_format (:obj:`str`, optional): Text format of the results 156 | ('dom', 'html', 'markdown' or 'plain'). 157 | 158 | Returns: 159 | :obj:`dict` 160 | 161 | """ 162 | endpoint = "user" 163 | return self.search(search_term, per_page, page, endpoint) 164 | 165 | def search_videos(self, search_term, per_page=None, page=None): 166 | """Searches the videos on Genius. 167 | 168 | Alias for :meth:`search() ` 169 | 170 | Args: 171 | search_term (:obj:`str`): A term to search on Genius 172 | per_page (:obj:`int`, optional): Number of results to 173 | return per request. It can't be more than 50. 174 | page (:obj:`int`, optional): Paginated offset (number of the page). 175 | text_format (:obj:`str`, optional): Text format of the results 176 | ('dom', 'html', 'markdown' or 'plain'). 177 | 178 | Returns: 179 | :obj:`dict` 180 | 181 | """ 182 | endpoint = "video" 183 | return self.search(search_term, per_page, page, endpoint) 184 | 185 | def search_all(self, search_term, per_page=None, page=None): 186 | """Searches all types. 187 | 188 | Including: albums, articles, lyrics, songs, users and 189 | videos. 190 | 191 | Alias for :meth:`search() ` 192 | 193 | Args: 194 | search_term (:obj:`str`): A term to search on Genius. 195 | per_page (:obj:`int`, optional): Number of results to 196 | return per page. It can't be more than 5 for this method. 197 | page (:obj:`int`, optional): Number of the page. 198 | 199 | Returns: 200 | :obj:`dict` 201 | 202 | Note: 203 | This method will also return a ``top hits`` section 204 | alongside other types. 205 | 206 | """ 207 | endpoint = "multi" 208 | return self.search(search_term, per_page, page, endpoint) 209 | -------------------------------------------------------------------------------- /lyricsgenius/api/public_methods/song.py: -------------------------------------------------------------------------------- 1 | class SongMethods(object): 2 | """Song methods of the public API.""" 3 | 4 | def song(self, song_id, text_format=None): 5 | """Gets data for a specific song. 6 | 7 | Args: 8 | song_id (:obj:`int`): Genius song ID 9 | text_format (:obj:`str`, optional): Text format of the results 10 | ('dom', 'html', 'markdown' or 'plain'). 11 | 12 | Returns: 13 | :obj:`dict` 14 | 15 | """ 16 | endpoint = "songs/{}".format(song_id) 17 | params = {"text_format": text_format or self.response_format} 18 | return self._make_request(path=endpoint, params_=params, public_api=True) 19 | 20 | def song_activity(self, song_id, per_page=None, page=None, text_format=None): 21 | """Gets activities on a song. 22 | 23 | Args: 24 | song_id (:obj:`int`): Genius song ID 25 | per_page (:obj:`int`, optional): Number of results to 26 | return per request. It can't be more than 50. 27 | page (:obj:`int`, optional): Paginated offset (number of the page). 28 | text_format (:obj:`str`, optional): Text format of the results 29 | ('dom', 'html', 'markdown' or 'plain'). 30 | 31 | Returns: 32 | :obj:`dict` 33 | 34 | """ 35 | endpoint = "songs/{}/activity_stream/line_items".format(song_id) 36 | params = { 37 | "text_format": text_format or self.response_format, 38 | "per_page": per_page, 39 | "page": page, 40 | } 41 | return self._make_request(path=endpoint, params_=params, public_api=True) 42 | 43 | def song_comments(self, song_id, per_page=None, page=None, text_format=None): 44 | """Gets the comments on a song. 45 | 46 | Args: 47 | song_id (:obj:`int`): Genius song ID 48 | per_page (:obj:`int`, optional): Number of results to 49 | return per request. It can't be more than 50. 50 | page (:obj:`int`, optional): Paginated offset (number of the page). 51 | text_format (:obj:`str`, optional): Text format of the results 52 | ('dom', 'html', 'markdown' or 'plain'). 53 | 54 | Returns: 55 | :obj:`dict` 56 | 57 | """ 58 | endpoint = "songs/{}/comments".format(song_id) 59 | params = { 60 | "per_page": per_page, 61 | "page": page, 62 | "text_format": text_format or self.response_format, 63 | } 64 | return self._make_request(path=endpoint, params_=params, public_api=True) 65 | 66 | def song_contributors(self, song_id): 67 | """Gets the contributors of a song. 68 | 69 | This method will return users who have contributed 70 | to this song by editing lyrics or song details. 71 | 72 | Args: 73 | song_id (:obj:`int`): Genius song ID 74 | 75 | Returns: 76 | :obj:`dict` 77 | 78 | """ 79 | endpoint = "songs/{}/contributors".format(song_id) 80 | return self._make_request(path=endpoint, public_api=True) 81 | -------------------------------------------------------------------------------- /lyricsgenius/api/public_methods/user.py: -------------------------------------------------------------------------------- 1 | class UserMethods(object): 2 | """User methods of the public API.""" 3 | 4 | def user(self, user_id, text_format=None): 5 | """Gets data for a specific user. 6 | 7 | Args: 8 | user_id (:obj:`int`): Genius user ID 9 | text_format (:obj:`str`, optional): Text format of the results 10 | ('dom', 'html', 'markdown' or 'plain'). 11 | 12 | Returns: 13 | :obj:`dict` 14 | 15 | """ 16 | path = "users/{}".format(user_id) 17 | params = {"text_format": text_format or self.response_format} 18 | return self._make_request(path, params_=params, public_api=True) 19 | 20 | def user_accomplishments( 21 | self, 22 | user_id, 23 | per_page=None, 24 | next_cursor=None, 25 | ): 26 | """Gets user's accomplishments. 27 | 28 | This methods gets the section titled "TOP ACCOMPLISHMENTS" in 29 | the user's profile. 30 | 31 | Args: 32 | user_id (:obj:`int`): Genius user ID 33 | per_page (:obj:`int`, optional): Number of results to 34 | return per request. It can't be more than 50. 35 | next_cursor (:obj:`str`, optional): Paginated offset 36 | (address of the next cursor). 37 | 38 | Returns: 39 | :obj:`dict` 40 | 41 | """ 42 | endpoint = "users/{}/accomplishments".format(user_id) 43 | params = {"next_cursor": next_cursor, "per_page": per_page} 44 | return self._make_request(path=endpoint, params_=params, public_api=True) 45 | 46 | def user_following(self, user_id, per_page=None, page=None): 47 | """Gets the accounts user follows. 48 | 49 | Args: 50 | user_id (:obj:`int`): Genius user ID 51 | per_page (:obj:`int`, optional): Number of results to 52 | return per request. It can't be more than 50. 53 | page (:obj:`int`, optional): Paginated offset (number of the page). 54 | 55 | Returns: 56 | :obj:`dict` 57 | 58 | """ 59 | endpoint = "users/{}/followed_users".format(user_id) 60 | params = {"page": page, "per_page": per_page} 61 | return self._make_request(path=endpoint, params_=params, public_api=True) 62 | 63 | def user_followers(self, user_id, per_page=None, page=None): 64 | """Gets user's followers. 65 | 66 | Args: 67 | user_id (:obj:`int`): Genius user ID 68 | per_page (:obj:`int`, optional): Number of results to 69 | return per request. It can't be more than 50. 70 | page (:obj:`int`, optional): Paginated offset (number of the page). 71 | 72 | Returns: 73 | :obj:`dict` 74 | 75 | """ 76 | endpoint = "users/{}/followers".format(user_id) 77 | params = {"page": page, "per_page": per_page} 78 | return self._make_request(path=endpoint, params_=params, public_api=True) 79 | 80 | def user_contributions( 81 | self, 82 | user_id, 83 | per_page=None, 84 | next_cursor=None, 85 | sort=None, 86 | text_format=None, 87 | type_=None, 88 | ): 89 | """Gets user's contributions. 90 | 91 | Args: 92 | user_id (:obj:`int`): Genius user ID 93 | per_page (:obj:`int`, optional): Number of results to 94 | return per request. It can't be more than 50. 95 | next_cursor (:obj:`str`, optional): Paginated offset 96 | (address of the next cursor). 97 | sort (:obj:`str`, optional): Sorting preference. 98 | ('title' or 'popularity') 99 | text_format (:obj:`str`, optional): Text format of the results 100 | ('dom', 'html', 'markdown' or 'plain'). 101 | type_ (:obj:`int`, optional): Type of the contribution 102 | ('annotations', 'articles', 'pyongs', 'questions_and_answers', 103 | 'comments', 'transcriptions' or 'unreviewed annotations'). 104 | 105 | Returns: 106 | :obj:`dict` 107 | 108 | 109 | Note: 110 | Not all types support a sorting preference. Setting the :obj:`sort` for 111 | these types won't result in erros, but won't make a difference in the 112 | results either. To find out which types support which features, look at 113 | the alias methods. 114 | 115 | Note: 116 | Setting no value for the :obj:`type_` will return the user's contributions 117 | (regardless of its type) in chronological order; just like visting a 118 | user's profile page and scrolling down, looking at their contributions over 119 | time. 120 | 121 | """ 122 | endpoint = "users/{}/contributions".format(user_id) 123 | if type_ is not None: 124 | endpoint += "/{}".format(type_) 125 | params = { 126 | "next_cursor": next_cursor, 127 | "per_page": per_page, 128 | "sort": sort, 129 | "text_format": text_format or self.response_format, 130 | } 131 | return self._make_request(path=endpoint, params_=params, public_api=True) 132 | 133 | def user_annotations( 134 | self, 135 | user_id, 136 | per_page=None, 137 | next_cursor=None, 138 | sort="popularity", 139 | text_format=None, 140 | ): 141 | """Gets user's annotations. 142 | 143 | Alias for :meth:`user_contributions() ` 144 | 145 | Args: 146 | user_id (:obj:`int`): Genius user ID 147 | per_page (:obj:`int`, optional): Number of results to 148 | return per request. It can't be more than 50. 149 | next_cursor (:obj:`str`, optional): Paginated offset 150 | (address of the next cursor). 151 | sort (:obj:`str`, optional): Sorting preference. 152 | ('title' or 'popularity') 153 | text_format (:obj:`str`, optional): Text format of the results 154 | ('dom', 'html', 'markdown' or 'plain'). 155 | 156 | Returns: 157 | :obj:`dict` 158 | 159 | """ 160 | return self.user_contributions( 161 | user_id=user_id, 162 | next_cursor=next_cursor, 163 | per_page=per_page, 164 | sort=sort, 165 | text_format=text_format, 166 | type_="annotations", 167 | ) 168 | 169 | def user_articles( 170 | self, 171 | user_id, 172 | per_page=None, 173 | next_cursor=None, 174 | sort="popularity", 175 | text_format=None, 176 | ): 177 | """Gets user's articles. 178 | 179 | Alias for :meth:`user_contributions() ` 180 | 181 | Args: 182 | user_id (:obj:`int`): Genius user ID 183 | per_page (:obj:`int`, optional): Number of results to 184 | return per request. It can't be more than 50. 185 | next_cursor (:obj:`str`, optional): Paginated offset 186 | (address of the next cursor). 187 | sort (:obj:`str`, optional): Sorting preference. 188 | ('title' or 'popularity') 189 | text_format (:obj:`str`, optional): Text format of the results 190 | ('dom', 'html', 'markdown' or 'plain'). 191 | 192 | Returns: 193 | :obj:`dict` 194 | 195 | """ 196 | return self.user_contributions( 197 | user_id=user_id, 198 | next_cursor=next_cursor, 199 | per_page=per_page, 200 | sort=sort, 201 | text_format=text_format, 202 | type_="articles", 203 | ) 204 | 205 | def user_pyongs(self, user_id, per_page=None, next_cursor=None, text_format=None): 206 | """Gets user's Pyongs. 207 | 208 | Alias for :meth:`user_contributions() ` 209 | 210 | Args: 211 | user_id (:obj:`int`): Genius user ID 212 | per_page (:obj:`int`, optional): Number of results to 213 | return per request. It can't be more than 50. 214 | next_cursor (:obj:`str`, optional): Paginated offset 215 | (address of the next cursor). 216 | text_format (:obj:`str`, optional): Text format of the results 217 | ('dom', 'html', 'markdown' or 'plain'). 218 | 219 | Returns: 220 | :obj:`dict` 221 | 222 | """ 223 | return self.user_contributions( 224 | user_id=user_id, 225 | next_cursor=next_cursor, 226 | per_page=per_page, 227 | text_format=text_format, 228 | type_="pyongs", 229 | ) 230 | 231 | def user_questions_and_answers( 232 | self, user_id, per_page=None, next_cursor=None, text_format=None 233 | ): 234 | """Gets user's Q&As. 235 | 236 | Alias for :meth:`user_contributions() ` 237 | 238 | Args: 239 | user_id (:obj:`int`): Genius user ID 240 | per_page (:obj:`int`, optional): Number of results to 241 | return per request. It can't be more than 50. 242 | next_cursor (:obj:`str`, optional): Paginated offset 243 | (address of the next cursor). 244 | text_format (:obj:`str`, optional): Text format of the results 245 | ('dom', 'html', 'markdown' or 'plain'). 246 | 247 | Returns: 248 | :obj:`dict` 249 | 250 | """ 251 | return self.user_contributions( 252 | user_id=user_id, 253 | next_cursor=next_cursor, 254 | per_page=per_page, 255 | text_format=text_format, 256 | type_="questions_and_answers", 257 | ) 258 | 259 | def user_suggestions( 260 | self, user_id, per_page=None, next_cursor=None, text_format=None 261 | ): 262 | """Gets user's suggestions (comments). 263 | 264 | Alias for :meth:`user_contributions() ` 265 | 266 | Args: 267 | user_id (:obj:`int`): Genius user ID 268 | per_page (:obj:`int`, optional): Number of results to 269 | return per request. It can't be more than 50. 270 | next_cursor (:obj:`str`, optional): Paginated offset 271 | (address of the next cursor). 272 | text_format (:obj:`str`, optional): Text format of the results 273 | ('dom', 'html', 'markdown' or 'plain'). 274 | 275 | Returns: 276 | :obj:`dict` 277 | 278 | """ 279 | return self.user_contributions( 280 | user_id=user_id, 281 | next_cursor=next_cursor, 282 | per_page=per_page, 283 | text_format=text_format, 284 | type_="comments", 285 | ) 286 | 287 | def user_transcriptions( 288 | self, 289 | user_id, 290 | per_page=None, 291 | next_cursor=None, 292 | sort="popularity", 293 | text_format=None, 294 | ): 295 | """Gets user's transcriptions. 296 | 297 | Alias for :meth:`user_contributions() ` 298 | 299 | Args: 300 | user_id (:obj:`int`): Genius user ID 301 | per_page (:obj:`int`, optional): Number of results to 302 | return per request. It can't be more than 50. 303 | next_cursor (:obj:`str`, optional): Paginated offset 304 | (address of the next cursor). 305 | sort (:obj:`str`, optional): Sorting preference. 306 | ('title' or 'popularity') 307 | text_format (:obj:`str`, optional): Text format of the results 308 | ('dom', 'html', 'markdown' or 'plain'). 309 | 310 | Returns: 311 | :obj:`dict` 312 | 313 | """ 314 | return self.user_contributions( 315 | user_id=user_id, 316 | next_cursor=next_cursor, 317 | per_page=per_page, 318 | sort=sort, 319 | text_format=text_format, 320 | type_="transcriptions", 321 | ) 322 | 323 | def user_unreviewed( 324 | self, 325 | user_id, 326 | per_page=None, 327 | next_cursor=None, 328 | sort="popularity", 329 | text_format=None, 330 | ): 331 | """Gets user's unreviewed annotations. 332 | 333 | Alias for :meth:`user_contributions() ` 334 | 335 | This method gets user annotations that have the 336 | "This annotations is unreviewed" sign above them. 337 | 338 | Args: 339 | user_id (:obj:`int`): Genius user ID 340 | per_page (:obj:`int`, optional): Number of results to 341 | return per request. It can't be more than 50. 342 | next_cursor (:obj:`str`, optional): Paginated offset 343 | (address of the next cursor). 344 | sort (:obj:`str`, optional): Sorting preference. 345 | ('title' or 'popularity') 346 | text_format (:obj:`str`, optional): Text format of the results 347 | ('dom', 'html', 'markdown' or 'plain'). 348 | 349 | Returns: 350 | :obj:`dict` 351 | 352 | """ 353 | return self.user_contributions( 354 | user_id=user_id, 355 | next_cursor=next_cursor, 356 | per_page=per_page, 357 | sort=sort, 358 | text_format=text_format, 359 | type_="unreviewed_annotations", 360 | ) 361 | -------------------------------------------------------------------------------- /lyricsgenius/api/public_methods/video.py: -------------------------------------------------------------------------------- 1 | class VideoMethods(object): 2 | """Video methods of the public API.""" 3 | 4 | def video(self, video_id, text_format=None): 5 | """Gets data for a specific video. 6 | 7 | Args: 8 | video_id (:obj:`int`): Genius video ID 9 | text_format (:obj:`str`, optional): Text format of the results 10 | ('dom', 'html', 'markdown' or 'plain'). 11 | 12 | Returns: 13 | :obj:`dict` 14 | 15 | """ 16 | endpoint = "videos/{}".format(video_id) 17 | params = {"text_format": text_format or self.response_format} 18 | 19 | return self._make_request(path=endpoint, params_=params, public_api=True) 20 | 21 | def videos( 22 | self, 23 | album_id=None, 24 | article_id=None, 25 | song_id=None, 26 | video_id=None, 27 | per_page=None, 28 | page=None, 29 | series=False, 30 | ): 31 | """Gets the videos of an album, article or song or the featured videos. 32 | 33 | Args: 34 | album_id (:obj:`int`, optional): Genius album ID 35 | article_id (:obj:`int`, optional): Genius article ID 36 | song_id (:obj:`int`, optional): Genius song ID 37 | video_id (:obj:`int`, optional): Genius video ID 38 | per_page (:obj:`int`, optional): Number of results to 39 | return per request. It can't be more than 50. 40 | page (:obj:`int`, optional): Paginated offset (number of the page). 41 | series (:obj:`bool`, optional): If set to `True`, returns episodes 42 | of Genius original video series that the item has been mentioned in. 43 | 44 | Returns: 45 | :obj:`dict` 46 | 47 | Note: 48 | If you specify no album, article or song, the method will return 49 | a series of videos. In this case, if `series=True`, the results 50 | will be the videos in the *VIDEOS* section on the homepage. But 51 | if `series=False`, the method returns another set of videos that 52 | we are not sure what they are at the moment. 53 | 54 | """ 55 | msg = ( 56 | "Pass only one of `album_id`, `article_id`, `song_id` and `video_id`." 57 | ", not more than one." 58 | ) 59 | condition = ( 60 | sum([bool(album_id), bool(article_id), bool(song_id), bool(video_id)]) == 1 61 | ) 62 | assert condition, msg 63 | 64 | if series: 65 | endpoint = "video_lists" 66 | else: 67 | endpoint = "videos" 68 | 69 | params = {"per_page": per_page, "page": page} 70 | 71 | if album_id: 72 | params["album_id"] = album_id 73 | elif article_id: 74 | params["article_id"] = article_id 75 | elif song_id: 76 | params["song_id"] = song_id 77 | elif video_id: 78 | params["video_id"] = video_id 79 | 80 | return self._make_request(path=endpoint, params_=params, public_api=True) 81 | -------------------------------------------------------------------------------- /lyricsgenius/auth.py: -------------------------------------------------------------------------------- 1 | import webbrowser 2 | from urllib.parse import urlencode 3 | 4 | from .api import Sender 5 | from .errors import InvalidStateError 6 | from .utils import parse_redirected_url 7 | 8 | 9 | class OAuth2(Sender): 10 | """Genius OAuth2 authorization flow. 11 | 12 | Using this class you can authenticate a user, 13 | and get their token. 14 | 15 | Args: 16 | client_id (:obj:`str`): Client ID 17 | redirect_uri (:obj:`str`): Whitelisted redirect URI. 18 | client_secret (:obj:`str`, optional): Client secret. 19 | scope (:obj:`tuple` | :obj:`"all"`, optional): Token privileges. 20 | state (:obj:`str`, optional): Request state. 21 | client_only_app (:obj:`bool`, optional): `True` to use the client-only 22 | authorization flow, otherwise `False`. 23 | 24 | Raises: 25 | AssertionError: If neither :obj:`client_secret`, nor 26 | :obj:`client_only_app` is supplied. 27 | 28 | """ 29 | 30 | auth_url = "https://api.genius.com/oauth/authorize" 31 | token_url = "https://api.genius.com/oauth/token" 32 | 33 | def __init__( 34 | self, 35 | client_id, 36 | redirect_uri, 37 | client_secret=None, 38 | scope=None, 39 | state=None, 40 | client_only_app=False, 41 | ): 42 | super().__init__() 43 | msg = ( 44 | "You must provide a client_secret " 45 | "if you intend to use the full code exchange." 46 | "\nIf you meant to use the client-only flow, " 47 | "set the client_only_app parameter to True." 48 | ) 49 | assert any([client_secret, client_only_app]), msg 50 | self.client_id = client_id 51 | self.client_secret = client_secret 52 | self.redirect_uri = redirect_uri 53 | if scope == "all": 54 | scope = ("me", "create_annotation", "manage_annotation", "vote") 55 | self.scope = scope if scope else () 56 | self.state = state 57 | self.flow = "token" if client_only_app else "code" 58 | self.client_only_app = client_only_app 59 | 60 | @property 61 | def url(self): 62 | """Returns the URL you redirect the user to. 63 | 64 | You can use this property to get a URL that when opened on the user's 65 | device, shows Genius's authorization page where user clicks *Agree* 66 | to give your app access, and then Genius redirects user back to your 67 | redirect URI. 68 | 69 | """ 70 | payload = { 71 | "client_id": self.client_id, 72 | "redirect_uri": self.redirect_uri, 73 | "response_type": self.flow, 74 | } 75 | if self.scope: 76 | payload["scope"] = " ".join(self.scope) 77 | if self.state: 78 | payload["state"] = self.state 79 | return OAuth2.auth_url + "?" + urlencode(payload) 80 | 81 | def get_user_token(self, code=None, url=None, state=None, **kwargs): 82 | """Gets a user token using the url or the code parameter.. 83 | 84 | If you supply value for :obj:`code`, this method will use the value of the 85 | :obj:`code` parameter to request a token from Genius. 86 | 87 | If you use the :method`client_only_app` and supplt the redirected URL, 88 | it will already have the token. 89 | You could pass the URL to this method or parse it yourself. 90 | 91 | If you provide a :obj:`state` the method will also compare 92 | it to the initial state and will raise an exception if 93 | they're not equal. 94 | 95 | Args: 96 | code (:obj:`str`): 'code' parameter of redirected URL. 97 | url (:obj:`str`): Redirected URL (used in client-only apps) 98 | state (:obj:`str`): state parameter of redirected URL (only 99 | provide if you want to compare with initial :obj:`self.state`) 100 | **kwargs: keywords for the POST request. 101 | returns: 102 | :obj:`str`: User token. 103 | 104 | """ 105 | assert any([code, url]), "You must pass either `code` or `url`." 106 | 107 | if state is not None and self.state != state: 108 | raise InvalidStateError("States do not match.") 109 | 110 | if code: 111 | payload = { 112 | "code": code, 113 | "client_id": self.client_id, 114 | "client_secret": self.client_secret, 115 | "redirect_uri": self.redirect_uri, 116 | "grant_type": "authorization_code", 117 | "response_type": "code", 118 | } 119 | url = OAuth2.token_url.replace("https://api.genius.com/", "") 120 | res = self._make_request(url, "POST", data=payload, **kwargs) 121 | token = res["access_token"] 122 | else: 123 | token = parse_redirected_url(url, self.flow) 124 | return token 125 | 126 | def prompt_user(self): 127 | """Prompts current user for authentication. 128 | 129 | Opens a web browser for you to log in with Genius. 130 | Prompts to paste the URL after logging in to parse the 131 | *token* URL parameter. 132 | 133 | returns: 134 | :obj:`str`: User token. 135 | 136 | """ 137 | 138 | url = self.url 139 | print("Opening browser for Genius login...") 140 | webbrowser.open(url) 141 | redirected = input("Please paste redirect URL: ").strip() 142 | 143 | if self.flow == "token": 144 | token = parse_redirected_url(redirected, self.flow) 145 | else: 146 | code = parse_redirected_url(redirected, self.flow) 147 | token = self.get_user_token(code) 148 | 149 | return token 150 | 151 | @classmethod 152 | def client_only_app(cls, client_id, redirect_uri, scope=None, state=None): 153 | """Returns an OAuth2 instance for a client-only app. 154 | 155 | Args: 156 | client_id (:obj:`str`): Client ID. 157 | redirect_uri (:obj:`str`): Whitelisted redirect URI. 158 | scope (:obj:`tuple` | :obj:`"all"`, optional): Token privilages. 159 | state (:obj:`str`, optional): Request state. 160 | 161 | returns: 162 | :class:`OAuth2` 163 | 164 | """ 165 | return cls( 166 | client_id=client_id, 167 | redirect_uri=redirect_uri, 168 | scope=scope, 169 | state=state, 170 | client_only_app=True, 171 | ) 172 | 173 | @classmethod 174 | def full_code_exchange( 175 | cls, client_id, redirect_uri, client_secret, scope=None, state=None 176 | ): 177 | """Returns an OAuth2 instance for a full-code exchange app. 178 | 179 | Args: 180 | client_id (:obj:`str`): Client ID. 181 | redirect_uri (:obj:`str`): Whitelisted redirect URI. 182 | client_secret (:obj:`str`): Client secret. 183 | scope (:obj:`tuple` | :obj:`"all"`, optional): Token privilages. 184 | state (:obj:`str`, optional): Request state. 185 | 186 | returns: 187 | :class:`OAuth2` 188 | 189 | """ 190 | return cls( 191 | client_id=client_id, 192 | client_secret=client_secret, 193 | redirect_uri=redirect_uri, 194 | scope=scope, 195 | state=state, 196 | ) 197 | 198 | def __repr__(self): 199 | return ( 200 | "{name}(" 201 | "flow={flow!r}, " 202 | "scope={scope!r}, " 203 | "state={state!r}, " 204 | "client_only_app={client_only_app!r})" 205 | ).format( 206 | name=self.__class__.__name__, 207 | flow=self.flow, 208 | scope=self.scope, 209 | state=self.state, 210 | client_only_app=self.client_only_app, 211 | ) 212 | -------------------------------------------------------------------------------- /lyricsgenius/errors.py: -------------------------------------------------------------------------------- 1 | class InvalidStateError(Exception): 2 | """Exception for non-matching states.""" 3 | -------------------------------------------------------------------------------- /lyricsgenius/types/__init__.py: -------------------------------------------------------------------------------- 1 | from .album import Album, Track 2 | from .artist import Artist 3 | from .base import Stats 4 | from .song import Song 5 | -------------------------------------------------------------------------------- /lyricsgenius/types/album.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | from ..utils import convert_to_datetime 4 | from .artist import Artist 5 | from .base import BaseEntity 6 | 7 | 8 | class Album(BaseEntity): 9 | """An album from the Genius.com database.""" 10 | 11 | def __init__(self, client, json_dict, tracks): 12 | warnings.warn( 13 | "The 'client' parameter and internal API client usage in the Album class " 14 | "are deprecated and will be removed in a future version.", 15 | DeprecationWarning, 16 | stacklevel=2, 17 | ) 18 | body = json_dict 19 | super().__init__(body["id"]) 20 | self._body = body 21 | self._client = client 22 | self.artist = Artist(client, body["artist"]) 23 | self.tracks = tracks 24 | self.release_date_components = convert_to_datetime( 25 | body.get("release_date_components") 26 | ) 27 | 28 | self.api_path = body.get("api_path") 29 | self.cover_art_thumbnail_url = body.get("cover_art_thumbnail_url") 30 | self.cover_art_url = body.get("cover_art_url") 31 | self.full_title = body.get("full_title") 32 | self.name = body.get("name") 33 | self.name_with_artist = body.get("name_with_artist") 34 | self.url = body.get("url") 35 | 36 | def to_dict(self): 37 | body = super().to_dict() 38 | body["tracks"] = [track.to_dict() for track in self.tracks] 39 | return body 40 | 41 | def to_json(self, filename=None, sanitize=True, ensure_ascii=True): 42 | data = self.to_dict() 43 | 44 | return super().to_json( 45 | data=data, filename=filename, sanitize=sanitize, ensure_ascii=ensure_ascii 46 | ) 47 | 48 | def to_text(self, filename=None, sanitize=True): 49 | data = "\n\n".join( 50 | f"[Song {n}: {track.song.title}]\n{track.song.lyrics}" 51 | for n, track in enumerate(self.tracks, start=1) 52 | ).strip() 53 | 54 | return super().to_text(data=data, filename=filename, sanitize=sanitize) 55 | 56 | def save_lyrics( 57 | self, 58 | filename=None, 59 | extension="json", 60 | overwrite=False, 61 | ensure_ascii=True, 62 | sanitize=True, 63 | verbose=True, 64 | ): 65 | if filename is None: 66 | filename = "Lyrics_" + self.name.replace(" ", "") 67 | 68 | return super().save_lyrics( 69 | filename=filename, 70 | extension=extension, 71 | overwrite=overwrite, 72 | ensure_ascii=ensure_ascii, 73 | sanitize=sanitize, 74 | verbose=verbose, 75 | ) 76 | 77 | 78 | class Track(BaseEntity): 79 | """docstring for Track""" 80 | 81 | def __init__(self, client, json_dict, lyrics): 82 | warnings.warn( 83 | ( 84 | "The 'Track' class is deprecated and will be removed in a future version. " 85 | "Its functionality will be incorporated into the 'Song' class." 86 | ), 87 | DeprecationWarning, 88 | stacklevel=2, 89 | ) 90 | from .song import Song 91 | 92 | body = json_dict 93 | super().__init__(body["song"]["id"]) 94 | self._body = body 95 | self.song = Song(client, body["song"], lyrics) 96 | 97 | self.number = body["number"] 98 | 99 | def to_dict(self): 100 | body = super().to_dict() 101 | body["song"] = self.song.to_dict() 102 | return body 103 | 104 | def to_json(self, filename=None, sanitize=True, ensure_ascii=True): 105 | data = self.to_dict() 106 | 107 | return super().to_json( 108 | data=data, filename=filename, sanitize=sanitize, ensure_ascii=ensure_ascii 109 | ) 110 | 111 | def to_text(self, filename=None, sanitize=True): 112 | data = self.song.lyrics 113 | 114 | return super().to_text(data=data, filename=filename, sanitize=sanitize) 115 | 116 | def save_lyrics( 117 | self, 118 | filename=None, 119 | extension="json", 120 | overwrite=False, 121 | ensure_ascii=True, 122 | sanitize=True, 123 | verbose=True, 124 | ): 125 | if filename is None: 126 | filename = "Lyrics_{:02d}_{}".format(self.number, self.song.title) 127 | filename = filename.replace(" ", "") 128 | 129 | return super().save_lyrics( 130 | filename=filename, 131 | extension=extension, 132 | overwrite=overwrite, 133 | ensure_ascii=ensure_ascii, 134 | sanitize=sanitize, 135 | verbose=verbose, 136 | ) 137 | 138 | def __repr__(self): 139 | name = self.__class__.__name__ 140 | return "{}(number, song)".format(name) 141 | -------------------------------------------------------------------------------- /lyricsgenius/types/artist.py: -------------------------------------------------------------------------------- 1 | # LyricsGenius 2 | # copyright 2025 John W. R. Miller 3 | # See LICENSE for details. 4 | 5 | """Artist object""" 6 | 7 | import warnings 8 | 9 | from ..utils import safe_unicode 10 | from .base import BaseEntity 11 | 12 | 13 | class Artist(BaseEntity): 14 | """An artist with songs from the Genius.com database.""" 15 | 16 | def __init__(self, client, json_dict): 17 | warnings.warn( 18 | "The 'client' parameter and internal API client usage in the Artist class " 19 | "are deprecated and will be removed in a future version.", 20 | DeprecationWarning, 21 | stacklevel=2, 22 | ) 23 | # Artist Constructor 24 | body = json_dict 25 | super().__init__(body["id"]) 26 | self._body = body 27 | self._client = client 28 | self.songs = [] 29 | self.num_songs = len(self.songs) 30 | 31 | self.api_path = body["api_path"] 32 | self.header_image_url = body["header_image_url"] 33 | self.image_url = body["image_url"] 34 | # self.iq = body['iq'] 35 | self.is_meme_verified = body["is_meme_verified"] 36 | self.is_verified = body["is_verified"] 37 | self.name = body["name"] 38 | self.url = body["url"] 39 | 40 | def __len__(self): 41 | return len(self.songs) 42 | 43 | def add_song(self, new_song, verbose=True, include_features=False): 44 | warnings.warn( 45 | "The capability of `Artist.add_song` to fetch songs via API (when a string is provided) " 46 | "is deprecated and will be removed in a future version.", 47 | DeprecationWarning, 48 | stacklevel=2, 49 | ) 50 | """Adds a song to the Artist. 51 | 52 | This method adds a new song to the artist object. It checks 53 | if the song is already in artist's songs and whether the 54 | song's artist is the same as the `Artist` object. 55 | 56 | Args: 57 | new_song (:class:`Song `): Song to be added. 58 | verbose (:obj:`bool`, optional): prints operation result. 59 | include_features (:obj:`bool`, optional): If True, includes tracks 60 | featuring the artist. 61 | 62 | Returns: 63 | :obj:`int`: 0 for success and 1 for failure. 64 | 65 | Examples: 66 | .. code:: python 67 | 68 | genius = Genius(token) 69 | artist = genius.search_artist('Andy Shauf', max_songs=3) 70 | 71 | # Way 1 72 | song = genius.search_song('To You', artist.name) 73 | artist.add_song(song) 74 | 75 | # Way 2 76 | artist.add_song('To You') 77 | 78 | """ 79 | if isinstance(new_song, str): 80 | new_song = self._client.search_song(new_song) 81 | if new_song is None: 82 | return None 83 | if any([song.title == new_song.title for song in self.songs]): 84 | if verbose: 85 | print( 86 | "{s} already in {a}, not adding song.".format( 87 | s=safe_unicode(new_song.title), a=safe_unicode(self.name) 88 | ) 89 | ) 90 | return None 91 | if new_song.artist == self.name or ( 92 | include_features and any(new_song._body.get("featured_artists", [])) 93 | ): 94 | self.songs.append(new_song) 95 | self.num_songs += 1 96 | return new_song 97 | if verbose: 98 | print( 99 | "Can't add song by {b}, artist must be {a}.".format( 100 | b=safe_unicode(new_song.artist), a=safe_unicode(self.name) 101 | ) 102 | ) 103 | return None 104 | 105 | def song(self, song_name): 106 | warnings.warn( 107 | "The capability of `Artist.song` to fetch songs via API " 108 | "is deprecated and will be removed in a future version.", 109 | DeprecationWarning, 110 | stacklevel=2, 111 | ) 112 | """Gets the artist's song. 113 | 114 | If the song is in the artist's songs, returns the song. Otherwise searches 115 | Genius for the song and then returns the song. 116 | 117 | Args: 118 | song_name (:obj:`str`): name of the song. 119 | the result is returned as a string. 120 | 121 | Returns: 122 | :obj:`Song ` \\|‌ :obj:`None`: If it can't find the song, 123 | returns *None*. 124 | 125 | """ 126 | for song in self.songs: 127 | if song.title == song_name: 128 | return song 129 | 130 | song = self._client.search_song(song_name, self.name) 131 | return song 132 | 133 | def to_dict(self): 134 | body = super().to_dict() 135 | body["songs"] = [song.to_dict() for song in self.songs] 136 | return body 137 | 138 | def to_json(self, filename=None, sanitize=True, ensure_ascii=True): 139 | data = self.to_dict() 140 | return super().to_json( 141 | data=data, filename=filename, sanitize=sanitize, ensure_ascii=ensure_ascii 142 | ) 143 | 144 | def to_text(self, filename=None, sanitize=True): 145 | data = "\n\n".join( 146 | f"[Song {n}: {song.title}]\n{song.lyrics}" 147 | for n, song in enumerate(self.songs, start=1) 148 | ).strip() 149 | return super().to_text(data=data, filename=filename, sanitize=sanitize) 150 | 151 | def save_lyrics( 152 | self, 153 | filename=None, 154 | extension="json", 155 | overwrite=False, 156 | ensure_ascii=True, 157 | sanitize=True, 158 | verbose=True, 159 | ): 160 | # Determine the filename 161 | if filename is None: 162 | filename = "Lyrics_" + self.name.replace(" ", "") 163 | 164 | return super().save_lyrics( 165 | filename=filename, 166 | extension=extension, 167 | overwrite=overwrite, 168 | ensure_ascii=ensure_ascii, 169 | sanitize=sanitize, 170 | verbose=verbose, 171 | ) 172 | 173 | def __str__(self): 174 | """Return a string representation of the Artist object.""" 175 | msg = "{name}, {num} songs".format(name=self.name, num=self.num_songs) 176 | msg = msg[:-1] if self.num_songs == 1 else msg 177 | return msg 178 | -------------------------------------------------------------------------------- /lyricsgenius/types/base.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import warnings 4 | from abc import ABC, abstractmethod 5 | 6 | from ..utils import safe_unicode, sanitize_filename 7 | 8 | 9 | class BaseEntity(ABC): 10 | """Base class for types.""" 11 | 12 | def __init__(self, id): 13 | self.id = id 14 | 15 | @abstractmethod 16 | def save_lyrics( 17 | self, 18 | filename, 19 | extension="json", 20 | overwrite=False, 21 | ensure_ascii=True, 22 | sanitize=True, 23 | verbose=True, 24 | ): 25 | """Save Song(s) lyrics and metadata to a JSON or TXT file. 26 | 27 | If the extension is 'json' (the default), the lyrics will be saved 28 | alongside the song's information. Take a look at the example below. 29 | 30 | Args: 31 | filename (:obj:`str`, optional): Output filename, a string. 32 | If not specified, the result is returned as a string. 33 | extension (:obj:`str`, optional): Format of the file (`json` or `txt`). 34 | overwrite (:obj:`bool`, optional): Overwrites preexisting file if `True`. 35 | Otherwise prompts user for input. 36 | ensure_ascii (:obj:`bool`, optional): If ensure_ascii is true 37 | (the default), the output is guaranteed to have all incoming 38 | non-ASCII characters escaped. 39 | sanitize (:obj:`bool`, optional): Sanitizes the filename if `True`. 40 | verbose (:obj:`bool`, optional): prints operation result. 41 | 42 | Warning: 43 | If you set :obj:`sanitize` to `False`, the file name may contain 44 | invalid characters, and thefore cause the saving to fail. 45 | 46 | """ 47 | extension = extension.lstrip(".").lower() 48 | msg = "extension must be JSON or TXT" 49 | assert (extension == "json") or (extension == "txt"), msg 50 | 51 | # Determine the filename 52 | for ext in [".txt", ".TXT", ".json", ".JSON"]: 53 | if ext in filename: 54 | filename = filename.replace(ext, "") 55 | break 56 | filename += "." + extension 57 | filename = sanitize_filename(filename) if sanitize else filename 58 | 59 | # Check if file already exists 60 | write_file = False 61 | if overwrite or not os.path.isfile(filename): 62 | write_file = True 63 | elif verbose: 64 | msg = "{} already exists. Overwrite?\n(y/n): ".format(filename) 65 | if input(msg).lower() == "y": 66 | write_file = True 67 | 68 | # Exit if we won't be saving a file 69 | if not write_file: 70 | if verbose: 71 | print("Skipping file save.\n") 72 | return 73 | 74 | # Save the lyrics to a file 75 | if extension == "json": 76 | self.to_json(filename, ensure_ascii=ensure_ascii, sanitize=sanitize) 77 | else: 78 | self.to_text(filename, sanitize=sanitize) 79 | 80 | if verbose: 81 | print("Wrote {}.".format(safe_unicode(filename))) 82 | 83 | return None 84 | 85 | @abstractmethod 86 | def to_dict(self): 87 | """Converts the object to a dictionary.""" 88 | return self._body.copy() 89 | 90 | @abstractmethod 91 | def to_json(self, data, filename=None, sanitize=True, ensure_ascii=True): 92 | """Converts the object to a json string. 93 | 94 | Args: 95 | filename (:obj:`str`, optional): Output filename, a string. 96 | If not specified, the result is returned as a string. 97 | sanitize (:obj:`bool`, optional): Sanitizes the filename if `True`. 98 | ensure_ascii (:obj:`bool`, optional): If ensure_ascii is true 99 | (the default), the output is guaranteed to have all incoming 100 | non-ASCII characters escaped. 101 | 102 | Returns: 103 | :obj:`str` \\|‌ :obj:`None`: If :obj:`filename` is `None`, 104 | returns the lyrics as a plain string, otherwise `None`. 105 | 106 | Warning: 107 | If you set :obj:`sanitize` to `False`, the file name may contain 108 | invalid characters, and therefore cause the saving to fail. 109 | 110 | """ 111 | data = self.to_dict() 112 | 113 | # Return the json string if no output path was specified 114 | if not filename: 115 | return json.dumps(data, indent=1, ensure_ascii=ensure_ascii) 116 | 117 | # Save Song object to a json file 118 | filename = sanitize_filename(filename) if sanitize else filename 119 | with open(filename, "w", encoding="utf-8") as ff: 120 | json.dump(data, ff, indent=4, ensure_ascii=ensure_ascii) 121 | return None 122 | 123 | @abstractmethod 124 | def to_text(self, data, filename=None, sanitize=True): 125 | """Converts song(s) lyrics to a single string. 126 | 127 | Args: 128 | filename (:obj:`str`, optional): Output filename, a string. 129 | If not specified, the result is returned as a string. 130 | sanitize (:obj:`bool`, optional): Sanitizes the filename if `True`. 131 | 132 | Returns: 133 | :obj:`str` \\|‌ :obj:`None`: If :obj:`filename` is `None`, 134 | returns the lyrics as a plain string. Otherwise `None`. 135 | 136 | Warning: 137 | If you set :obj:`sanitize` to `False`, the file name may contain 138 | invalid characters, and therefore cause the saving to fail. 139 | 140 | """ 141 | # Return the lyrics as a string if no `filename` was specified 142 | if not filename: 143 | return data 144 | 145 | # Save song lyrics to a text file 146 | filename = sanitize_filename(filename) if sanitize else filename 147 | with open(filename, "w", encoding="utf-8") as ff: 148 | ff.write(data) 149 | return None 150 | 151 | def __repr__(self): 152 | name = self.__class__.__name__ 153 | attrs = [x for x in list(self.__dict__.keys()) if not x.startswith("_")] 154 | attrs = ", ".join(attrs[:2]) 155 | return "{}({}, ...)".format(name, attrs) 156 | 157 | 158 | class Stats(object): 159 | """Stats of an item. 160 | 161 | Note: 162 | The values passed to this class are inconsistent, 163 | and therefore need to be set dynamically. 164 | Use the built-in ``dir()`` function to 165 | see the available attributes. 166 | You could also access the stats by the dictionary 167 | annotation. For example: 168 | 169 | .. code:: python 170 | 171 | values = song.to_dict() 172 | print(values['stats']) 173 | 174 | """ 175 | 176 | def __init__(self, json_dict): 177 | warnings.warn( 178 | "The 'Stats' class is deprecated and will be removed in a future version. ", 179 | DeprecationWarning, 180 | stacklevel=2, 181 | ) 182 | for key, value in json_dict.items(): 183 | setattr(self, key, value) 184 | 185 | def __repr__(self): 186 | name = self.__class__.__name__ 187 | attrs = ", ".join(list(self.__dict__.keys())) 188 | return "{}({!r})".format(name, attrs) 189 | 190 | 191 | # class EntityWithLyrics(ABC, BaseEntity): 192 | # """Entity that has lyrics.""" 193 | # 194 | # def __init__(self, **kwargs): 195 | # super().__init__(**kwargs) 196 | # 197 | # @abstractmethod 198 | # def save_lyrics(self): 199 | # pass 200 | -------------------------------------------------------------------------------- /lyricsgenius/types/song.py: -------------------------------------------------------------------------------- 1 | # LyricsGenius 2 | # copyright 2025 John W. R. Miller 3 | # See LICENSE for details. 4 | 5 | import warnings 6 | from filecmp import cmp 7 | 8 | from .album import Album 9 | from .artist import Artist 10 | from .base import BaseEntity, Stats 11 | 12 | 13 | class Song(BaseEntity): 14 | """A song from the Genius.com database.""" 15 | 16 | def __init__(self, client, json_dict, lyrics=""): 17 | warnings.warn( 18 | "The 'client' parameter and internal API client usage in the Song class " 19 | "are deprecated and will be removed in a future version.", 20 | DeprecationWarning, 21 | stacklevel=2, 22 | ) 23 | warnings.warn( 24 | "The constructor signature will change in a future version. " 25 | "It will change to Song(lyrics, body) instead of Song(client, json_dict, lyrics).", 26 | FutureWarning, 27 | stacklevel=2, 28 | ) 29 | body = json_dict 30 | super().__init__(body["id"]) 31 | self._body = body 32 | self._client = client 33 | self.artist = body["primary_artist"]["name"] 34 | self.lyrics = lyrics if lyrics else "" 35 | self.primary_artist = Artist(client, body["primary_artist"]) 36 | self.stats = Stats(body["stats"]) 37 | self.album = Album(client, body["album"], []) if body.get("album") else None 38 | 39 | self.annotation_count = body["annotation_count"] 40 | self.api_path = body["api_path"] 41 | self.full_title = body["full_title"] 42 | self.header_image_thumbnail_url = body["header_image_thumbnail_url"] 43 | self.header_image_url = body["header_image_url"] 44 | self.lyrics_owner_id = body["lyrics_owner_id"] 45 | self.lyrics_state = body["lyrics_state"] 46 | self.path = body["path"] 47 | self.pyongs_count = body["pyongs_count"] 48 | self.song_art_image_thumbnail_url = body["song_art_image_thumbnail_url"] 49 | self.song_art_image_url = body["song_art_image_url"] 50 | self.title = body["title"] 51 | self.title_with_featured = body["title_with_featured"] 52 | self.url = body["url"] 53 | 54 | def to_dict(self): 55 | body = super().to_dict() 56 | body["artist"] = self.artist 57 | body["lyrics"] = self.lyrics 58 | return body 59 | 60 | def to_json(self, filename=None, sanitize=True, ensure_ascii=True): 61 | data = self.to_dict() 62 | return super().to_json( 63 | data=data, filename=filename, sanitize=sanitize, ensure_ascii=ensure_ascii 64 | ) 65 | 66 | def to_text(self, filename=None, sanitize=True): 67 | data = self.lyrics 68 | 69 | return super().to_text(data=data, filename=filename, sanitize=sanitize) 70 | 71 | def save_lyrics( 72 | self, 73 | filename=None, 74 | extension="json", 75 | overwrite=False, 76 | ensure_ascii=True, 77 | sanitize=True, 78 | verbose=True, 79 | ): 80 | if filename is None: 81 | filename = "Lyrics_{}_{}".format( 82 | self.artist.replace(" ", ""), self.title.replace(" ", "") 83 | ).lower() 84 | 85 | return super().save_lyrics( 86 | filename=filename, 87 | extension=extension, 88 | overwrite=overwrite, 89 | ensure_ascii=ensure_ascii, 90 | sanitize=sanitize, 91 | verbose=verbose, 92 | ) 93 | 94 | def __str__(self): 95 | """Return a string representation of the Song object.""" 96 | if len(self.lyrics) > 100: 97 | lyr = self.lyrics[:100] + "..." 98 | else: 99 | lyr = self.lyrics[:100] 100 | return '"{title}" by {artist}:\n {lyrics}'.format( 101 | title=self.title, artist=self.artist, lyrics=lyr.replace("\n", "\n ") 102 | ) 103 | 104 | def __cmp__(self, other): 105 | return ( 106 | cmp(self.title, other.title) 107 | and cmp(self.artist, other.artist) 108 | and cmp(self.lyrics, other.lyrics) 109 | ) 110 | -------------------------------------------------------------------------------- /lyricsgenius/utils.py: -------------------------------------------------------------------------------- 1 | """utility functions""" 2 | 3 | import os 4 | import re 5 | import sys 6 | import unicodedata 7 | from datetime import datetime 8 | from string import punctuation 9 | from urllib.parse import parse_qs, urlparse 10 | 11 | 12 | def auth_from_environment(): 13 | """Gets credentials from environment variables. 14 | 15 | Uses the following env vars: ``GENIUS_CLIENT_ID``, 16 | ``GENIUS_REDIRECT_URI`` and ``GENIUS_CLIENT_SECRET``. 17 | 18 | Returns: 19 | :obj:`tuple`: client ID, redirect URI and client secret. 20 | Replaces variables that are not present with :obj:`None`. 21 | 22 | """ 23 | client_id = os.environ.get("GENIUS_CLIENT_ID") 24 | redirect_uri = os.environ.get("GENIUS_REDIRECT_URI") 25 | client_secret = os.environ.get("GENIUS_CLIENT_SECRET") 26 | return client_id, redirect_uri, client_secret 27 | 28 | 29 | def convert_to_datetime(f): 30 | """Converts argument to a datetime object. 31 | 32 | Args: 33 | f (:obj:`str`| :obj:`dict`): string or dictionary containing 34 | date components. 35 | 36 | Returns: 37 | :class:`datetime`: datetime object. 38 | """ 39 | if f is None: 40 | return None 41 | 42 | if isinstance(f, dict): 43 | year = f.get("year") 44 | month = f.get("month") 45 | day = f.get("day") 46 | if year and month: 47 | date = "{year}-{month:02}".format(year=year, month=month) 48 | if day: 49 | date += "-{day:02}".format(day=day) 50 | elif year: 51 | date = str(year) 52 | else: 53 | return None 54 | f = date 55 | 56 | if f.count("-") == 2: 57 | date_format = "%Y-%m-%d" 58 | elif f.count("-") == 1: 59 | date_format = "%Y-%m" 60 | elif "," in f: 61 | date_format = "%B %d, %Y" 62 | elif f.isdigit(): 63 | date_format = "%Y" 64 | else: 65 | date_format = "%B %Y" 66 | 67 | return datetime.strptime(f, date_format) 68 | 69 | 70 | def clean_str(s): 71 | """Cleans a string to help with string comparison. 72 | 73 | Removes punctuation and returns 74 | a stripped, NFKC normalized string in lowercase. 75 | 76 | Args: 77 | s (:obj:`str`): A string. 78 | 79 | Returns: 80 | :obj:`str`: Cleaned string. 81 | 82 | """ 83 | punctuation_ = punctuation + "'" + "\u200b" 84 | string = s.translate(str.maketrans("", "", punctuation_)).strip().lower() 85 | return unicodedata.normalize("NFKC", string) 86 | 87 | 88 | def parse_redirected_url(url, flow): 89 | """Parse a URL for parameter 'code'/'token'. 90 | 91 | Args: 92 | url (:obj:`str`): The redirect URL. 93 | flow (:obj:`str`): authorization flow ('code' or 'token') 94 | 95 | Returns: 96 | :obj:`str`: value of 'code'/'token'. 97 | 98 | Raises: 99 | KeyError: if 'code'/'token' is not available or has multiple values. 100 | 101 | """ 102 | if flow == "code": 103 | query = urlparse(url).query 104 | elif flow == "token": 105 | query = re.sub(r".*#access_", "", url) 106 | parameters = parse_qs(query) 107 | code = parameters.get(flow, None) 108 | 109 | if code is None: 110 | raise KeyError("Parameter {} not available!".format(flow)) 111 | elif len(code) > 1: 112 | raise KeyError("Multiple values for {}!".format(flow)) 113 | 114 | return code[0] 115 | 116 | 117 | def safe_unicode(s): 118 | """Encodes and decodes string based on user's STDOUT. 119 | 120 | Encodes string to ``utf-8`` and then decodes it based 121 | on the user's STDOUT's encoding, replacing errors in the process. 122 | 123 | Args: 124 | s (:obj:`str`): a string. 125 | 126 | Returns: 127 | :obj:`str` 128 | 129 | """ 130 | return s.encode("utf-8").decode(sys.stdout.encoding, errors="replace") 131 | 132 | 133 | def sanitize_filename(f): 134 | """Removes invalid characters from file name. 135 | 136 | Args: 137 | f (:obj:`str`): file name to sanitize. 138 | 139 | Returns: 140 | :obj:`str`: sanitized file name including only alphanumeric 141 | characters, spaces, dots or underlines. 142 | 143 | """ 144 | keepchars = (" ", ".", "_") 145 | return "".join(c for c in f if c.isalnum() or c in keepchars).rstrip() 146 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "lyricsgenius" 7 | version = "3.6.4" 8 | dependencies = ["beautifulsoup4>=4.12.3", "requests>=2.27.1"] 9 | requires-python = ">=3.10" 10 | authors = [{ name = "John W. R. Miller", email = "john.w.millr+lg@gmail.com" }] 11 | description = "Download lyrics and metadata from Genius.com" 12 | readme = "README.md" 13 | license = "MIT" 14 | keywords = [ 15 | "lyrics", 16 | "lyricsgenius", 17 | "genius", 18 | "genius-api", 19 | "genius-lyrics", 20 | "download-lyrics", 21 | "download-song-lyrics", 22 | "scraping-lyrics", 23 | "song-lyrics", 24 | ] 25 | 26 | [project.optional-dependencies] 27 | docs = ["sphinx>=4.3.2", "sphinx-rtd-theme>=1.3.0"] 28 | checks = [ 29 | "doc8>=0.11.2", 30 | "flake8>=4.0.1", 31 | "flake8-bugbear>=22.9.23", 32 | "pygments>=2.14.0", 33 | "tox>=3.28.0", 34 | ] 35 | 36 | [project.urls] 37 | Homepage = "https://www.johnwmillr.com/scraping-genius-lyrics/" 38 | Documentation = "https://lyricsgenius.readthedocs.io/en/master/" 39 | Repository = "https://github.com/johnwmillr/LyricsGenius" 40 | Issues = "https://github.com/johnwmillr/LyricsGenius/issues" 41 | 42 | [dependency-groups] 43 | dev = [ 44 | "doc8>=0.11.2", 45 | "flake8>=4.0.1", 46 | "flake8-bugbear>=22.9.23", 47 | "pre-commit>=4.2.0", 48 | "pygments>=2.14.0", 49 | "ruff>=0.11.4", 50 | "sphinx>=4.3.2", 51 | "sphinx-rtd-theme>=1.3.0", 52 | "tox>=3.28.0", 53 | ] 54 | 55 | [[tool.uv.index]] 56 | name = "testpypi" 57 | url = "https://test.pypi.org/p/lyricsgenius/" 58 | publish-url = "https://test.pypi.org/legacy/" 59 | explicit = true 60 | 61 | [tool.pyright] 62 | typeCheckingMode = "off" 63 | ignore = ["*"] 64 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from lyricsgenius import Genius 4 | 5 | # Import client access token from environment variable 6 | access_token = os.environ.get("GENIUS_ACCESS_TOKEN", None) 7 | assert access_token is not None, ( 8 | "Must declare environment variable: GENIUS_ACCESS_TOKEN" 9 | ) 10 | 11 | # Genius client 12 | genius = Genius(access_token, sleep_time=1.0, timeout=15, retries=3) 13 | -------------------------------------------------------------------------------- /tests/test_album.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | import warnings 4 | 5 | from lyricsgenius.types import Album 6 | from tests import genius 7 | 8 | 9 | class TestAlbum(unittest.TestCase): 10 | @classmethod 11 | def setUpClass(cls): 12 | print("\n---------------------\nSetting up Album tests...\n") 13 | warnings.simplefilter("ignore", ResourceWarning) 14 | cls.album_name = "The Party" 15 | cls.artist_name = "Andy Shauf" 16 | cls.num_tracks = 10 17 | cls.album = genius.search_album(cls.album_name, cls.artist_name) 18 | 19 | def test_type(self): 20 | self.assertIsInstance(self.album, Album) 21 | 22 | def test_album_name(self): 23 | self.assertEqual(self.album.name, self.album_name) 24 | 25 | def test_album_artist(self): 26 | self.assertEqual(self.album.artist.name, self.artist_name) 27 | 28 | def test_tracks(self): 29 | self.assertEqual(len(self.album.tracks), self.num_tracks) 30 | 31 | def test_saving_json_file(self): 32 | print("\n") 33 | extension = "json" 34 | msg = "Could not save {} file.".format(extension) 35 | expected_filename = ( 36 | "Lyrics_" + self.album.name.replace(" ", "") + "." + extension 37 | ) 38 | 39 | # Remove the test file if it already exists 40 | if os.path.isfile(expected_filename): 41 | os.remove(expected_filename) 42 | 43 | # Test saving json file 44 | self.album.save_lyrics(extension=extension, overwrite=True) 45 | self.assertTrue(os.path.isfile(expected_filename), msg) 46 | 47 | # Test overwriting json file (now that file is written) 48 | try: 49 | self.album.save_lyrics(extension=extension, overwrite=True) 50 | except Exception: 51 | self.fail("Failed {} overwrite test".format(extension)) 52 | os.remove(expected_filename) 53 | 54 | def test_saving_txt_file(self): 55 | print("\n") 56 | extension = "txt" 57 | msg = "Could not save {} file.".format(extension) 58 | expected_filename = ( 59 | "Lyrics_" + self.album.name.replace(" ", "") + "." + extension 60 | ) 61 | 62 | # Remove the test file if it already exists 63 | if os.path.isfile(expected_filename): 64 | os.remove(expected_filename) 65 | 66 | # Test saving txt file 67 | self.album.save_lyrics(extension=extension, overwrite=True) 68 | self.assertTrue(os.path.isfile(expected_filename), msg) 69 | 70 | # Test overwriting txt file (now that file is written) 71 | try: 72 | self.album.save_lyrics(extension=extension, overwrite=True) 73 | except Exception: 74 | self.fail("Failed {} overwrite test".format(extension)) 75 | os.remove(expected_filename) 76 | -------------------------------------------------------------------------------- /tests/test_api.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from tests import genius 4 | 5 | 6 | class TestAPI(unittest.TestCase): 7 | @classmethod 8 | def setUpClass(cls): 9 | print("\n---------------------\nSetting up API tests...\n") 10 | 11 | def test_account(self): 12 | msg = ( 13 | "No user detail was returned. " 14 | "Are you sure you're using a user access token?" 15 | ) 16 | r = genius.account() 17 | self.assertTrue("user" in r, msg) 18 | 19 | def test_annotation(self): 20 | msg = "Returned annotation API path is different than expected." 21 | id_ = 10225840 22 | r = genius.annotation(id_) 23 | real = r["annotation"]["api_path"] 24 | expected = "/annotations/10225840" 25 | self.assertEqual(real, expected, msg) 26 | 27 | def test_manage_annotation(self): 28 | example_text = "The annotation" 29 | new_annotation = genius.create_annotation( 30 | example_text, "https://example.com", "illustrative examples", title="test" 31 | )["annotation"] 32 | msg = "Annotation text did not match the one that was passed." 33 | self.assertEqual(new_annotation["body"]["plain"], example_text, msg) 34 | 35 | try: 36 | example_text_two = "Updated annotation" 37 | r = genius.update_annotation( 38 | new_annotation["id"], 39 | example_text_two, 40 | "https://example.com", 41 | "illustrative examples", 42 | title="test", 43 | )["annotation"] 44 | msg = "Updated annotation text did not match the one that was passed." 45 | self.assertEqual(r["body"]["plain"], example_text_two, msg) 46 | 47 | r = genius.upvote_annotation(11828417) 48 | msg = "Upvote was not registered." 49 | self.assertTrue(r is not None, msg) 50 | 51 | r = genius.downvote_annotation(11828417) 52 | msg = "Downvote was not registered." 53 | self.assertTrue(r is not None, msg) 54 | 55 | r = genius.unvote_annotation(11828417) 56 | msg = "Vote was not removed." 57 | self.assertTrue(r is not None, msg) 58 | finally: 59 | msg = "Annotation was not deleted." 60 | r = genius.delete_annotation(new_annotation["id"]) 61 | self.assertEqual(r, 204, msg) 62 | 63 | def test_referents_web_page(self): 64 | msg = "Returned referent API path is different than expected." 65 | id_ = 10347 66 | r = genius.referents(web_page_id=id_) 67 | real = r["referents"][0]["api_path"] 68 | expected = "/referents/11828416" 69 | self.assertTrue(real == expected, msg) 70 | 71 | def test_referents_no_inputs(self): 72 | # Must supply `song_id`, `web_page_id`, or `created_by_id`. 73 | with self.assertRaises(AssertionError): 74 | genius.referents() 75 | 76 | def test_referents_invalid_input(self): 77 | # Method should prevent inputs for both song and web_pag ID. 78 | with self.assertRaises(AssertionError): 79 | genius.referents(song_id=1, web_page_id=1) 80 | 81 | def test_web_page(self): 82 | msg = "Returned web page API path is different than expected." 83 | url = "https://docs.genius.com" 84 | r = genius.web_page(raw_annotatable_url=url) 85 | real = r["web_page"]["api_path"] 86 | expected = "/web_pages/10347" 87 | self.assertEqual(real, expected, msg) 88 | -------------------------------------------------------------------------------- /tests/test_artist.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from lyricsgenius.types import Artist 5 | from lyricsgenius.utils import sanitize_filename 6 | from tests import genius 7 | 8 | 9 | class TestArtist(unittest.TestCase): 10 | @classmethod 11 | def setUpClass(cls): 12 | print("\n---------------------\nSetting up Artist tests...\n") 13 | 14 | cls.artist_name = "The Beatles" 15 | cls.new_song = "Paperback Writer" 16 | cls.max_songs = 2 17 | cls.artist = genius.search_artist(cls.artist_name, max_songs=cls.max_songs) 18 | 19 | def test_artist(self): 20 | msg = "The returned object is not an instance of the Artist class." 21 | self.assertIsInstance(self.artist, Artist, msg) 22 | 23 | def test_correct_artist_name(self): 24 | msg = "Returned artist name does not match searched artist." 25 | name = "Queen" 26 | result = genius.search_artist(name, max_songs=1).name 27 | self.assertEqual(name, result, msg) 28 | 29 | def test_zero_songs(self): 30 | msg = "Songs were downloaded even though 0 songs was requested." 31 | name = "Queen" 32 | result = genius.search_artist(name, max_songs=0).songs 33 | self.assertEqual(0, len(result), msg) 34 | 35 | def test_name(self): 36 | msg = "The artist object name does not match the requested artist name." 37 | self.assertEqual(self.artist.name, self.artist_name, msg) 38 | 39 | def test_add_song_from_same_artist(self): 40 | msg = "The new song was not added to the artist object." 41 | self.artist.add_song(genius.search_song(self.new_song, self.artist_name)) 42 | self.assertEqual(self.artist.num_songs, self.max_songs + 1, msg) 43 | 44 | def test_song(self): 45 | msg = "Song was not in artist's songs." 46 | song = self.artist.song(self.new_song) 47 | self.assertIsNotNone(song, msg) 48 | 49 | def test_add_song_from_different_artist(self): 50 | msg = "A song from a different artist was incorrectly allowed to be added." 51 | self.artist.add_song(genius.search_song("These Days", "Jackson Browne")) 52 | self.assertEqual(self.artist.num_songs, self.max_songs, msg) 53 | 54 | def test_artist_with_includes_features(self): 55 | # The artist did not get songs returned that they were featured in. 56 | name = "Swae Lee" 57 | result = genius.search_artist(name, max_songs=1, include_features=True) 58 | result = result.songs[0].artist 59 | self.assertNotEqual(result, name) 60 | 61 | def determine_filenames(self, extension): 62 | expected_filenames = [] 63 | for song in self.artist.songs: 64 | fn = "lyrics_{name}_{song}.{ext}".format( 65 | name=self.artist.name, song=song.title, ext=extension 66 | ) 67 | fn = sanitize_filename(fn.lower().replace(" ", "")) 68 | expected_filenames.append(fn) 69 | return expected_filenames 70 | 71 | def test_saving_json_file(self): 72 | print("\n") 73 | extension = "json" 74 | msg = "Could not save {} file.".format(extension) 75 | expected_filename = ( 76 | "Lyrics_" + self.artist.name.replace(" ", "") + "." + extension 77 | ) 78 | 79 | # Remove the test file if it already exists 80 | if os.path.isfile(expected_filename): 81 | os.remove(expected_filename) 82 | 83 | # Test saving json file 84 | self.artist.save_lyrics(extension=extension, overwrite=True) 85 | self.assertTrue(os.path.isfile(expected_filename), msg) 86 | 87 | # Test overwriting json file (now that file is written) 88 | try: 89 | self.artist.save_lyrics(extension=extension, overwrite=True) 90 | except Exception: 91 | self.fail("Failed {} overwrite test".format(extension)) 92 | os.remove(expected_filename) 93 | 94 | def test_saving_txt_file(self): 95 | print("\n") 96 | extension = "txt" 97 | msg = "Could not save {} file.".format(extension) 98 | expected_filename = ( 99 | "Lyrics_" + self.artist.name.replace(" ", "") + "." + extension 100 | ) 101 | 102 | # Remove the test file if it already exists 103 | if os.path.isfile(expected_filename): 104 | os.remove(expected_filename) 105 | 106 | # Test saving txt file 107 | self.artist.save_lyrics(extension=extension, overwrite=True) 108 | self.assertTrue(os.path.isfile(expected_filename), msg) 109 | 110 | # Test overwriting txt file (now that file is written) 111 | try: 112 | self.artist.save_lyrics(extension=extension, overwrite=True) 113 | except Exception: 114 | self.fail("Failed {} overwrite test".format(extension)) 115 | os.remove(expected_filename) 116 | -------------------------------------------------------------------------------- /tests/test_auth.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from unittest.mock import MagicMock, patch 4 | 5 | from lyricsgenius import OAuth2 6 | from lyricsgenius.errors import InvalidStateError 7 | 8 | client_id = os.environ["GENIUS_CLIENT_ID"] 9 | client_secret = os.environ["GENIUS_CLIENT_SECRET"] 10 | redirect_uri = os.environ["GENIUS_REDIRECT_URI"] 11 | 12 | 13 | def mocked_requests_post(*args, **kwargs): 14 | class MockResponse: 15 | def __init__(self, json_data, status_code): 16 | self.json_data = json_data 17 | self.status_code = status_code 18 | 19 | def json(self): 20 | return self.json_data 21 | 22 | def raise_for_status(self): 23 | if self.status_code > 300: 24 | raise ConnectionError 25 | 26 | method, url = args[0], args[1] 27 | data = kwargs["data"] 28 | code = data.get("code") 29 | data_client_id = data.get("client_id") 30 | data_client_secret = data.get("client_secret") 31 | data_redirect_uri = data.get("redirect_uri") 32 | data_grant_type = data.get("grant_type") 33 | data_response_type = data.get("response_type") 34 | 35 | if ( 36 | method == "POST" 37 | and url == OAuth2.token_url 38 | and code == "some_code" 39 | and data_client_id == client_id 40 | and data_client_secret == client_secret 41 | and data_redirect_uri == redirect_uri 42 | and data_grant_type == "authorization_code" 43 | and data_response_type == "code" 44 | ): 45 | return MockResponse({"access_token": "test"}, 200) 46 | 47 | return MockResponse(None, 403) 48 | 49 | 50 | class TestOAuth2(unittest.TestCase): 51 | @classmethod 52 | def setUpClass(cls): 53 | print("\n---------------------\nSetting up OAuth2 tests...\n") 54 | 55 | def test_init(self): 56 | with self.assertRaises(AssertionError): 57 | OAuth2(client_id, redirect_uri) 58 | 59 | scope = ("me", "create_annotation", "manage_annotation", "vote") 60 | auth = OAuth2(client_id, redirect_uri, client_secret, scope="all") 61 | self.assertEqual(auth.scope, scope) 62 | 63 | @patch("requests.Session.request", side_effect=mocked_requests_post) 64 | def test_get_user_token_code_flow(self, mock_post): 65 | # full code exchange flow 66 | 67 | state = "some_state" 68 | code = "some_code" 69 | code_flow_token = "test" 70 | 71 | auth = OAuth2.full_code_exchange( 72 | client_id, redirect_uri, client_secret, scope="all", state=state 73 | ) 74 | 75 | r = auth.get_user_token(code=code, state=state) 76 | self.assertEqual(r, code_flow_token) 77 | 78 | def test_get_user_token_token_flow(self): 79 | state = "some_state" 80 | token_flow_token = "test" 81 | redirected_url = "{}#access_token=test".format(redirect_uri) 82 | 83 | auth = OAuth2.client_only_app(client_id, redirect_uri, scope="all", state=state) 84 | 85 | r = auth.get_user_token(url=redirected_url) 86 | self.assertEqual(r, token_flow_token) 87 | 88 | def test_get_user_token_invalid_state(self): 89 | state = "state_1" 90 | auth = OAuth2.full_code_exchange( 91 | client_id, redirect_uri, client_secret, scope="all", state=state 92 | ) 93 | 94 | returned_code = "some_code" 95 | returned_state = "state_2" 96 | with self.assertRaises(InvalidStateError): 97 | auth.get_user_token(code=returned_code, state=returned_state) 98 | 99 | def test_get_user_token_no_parameter(self): 100 | state = "some_state" 101 | auth = OAuth2.full_code_exchange( 102 | client_id, redirect_uri, client_secret, scope="all", state=state 103 | ) 104 | 105 | with self.assertRaises(AssertionError): 106 | auth.get_user_token() 107 | 108 | def test_prompt_user(self): 109 | auth = OAuth2(client_id, redirect_uri, client_secret, scope="all") 110 | token = "test" 111 | current_module = "lyricsgenius.auth" 112 | 113 | input_ = MagicMock(return_value="http://example.com?code=some_code") 114 | with ( 115 | patch(current_module + ".webbrowser", MagicMock()), 116 | patch(current_module + ".input", input_), 117 | patch(current_module + ".print", MagicMock()), 118 | patch("requests.Session.request", side_effect=mocked_requests_post), 119 | ): 120 | r = auth.prompt_user() 121 | 122 | self.assertEqual(r, token) 123 | -------------------------------------------------------------------------------- /tests/test_base.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from requests.exceptions import HTTPError 4 | 5 | from tests import genius 6 | 7 | 8 | class TestAPIBase(unittest.TestCase): 9 | @classmethod 10 | def setUpClass(cls): 11 | print("\n---------------------\nSetting up API base tests...\n") 12 | 13 | def test_http_error_handler(self): 14 | status_code = None 15 | try: 16 | genius.annotation(0) 17 | except HTTPError as e: 18 | status_code = e.args[0] 19 | 20 | self.assertEqual(status_code, 404) 21 | -------------------------------------------------------------------------------- /tests/test_genius.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from tests import genius 4 | 5 | 6 | class TestEndpoints(unittest.TestCase): 7 | @classmethod 8 | def setUpClass(cls): 9 | print("\n---------------------\nSetting up Endpoint tests...\n") 10 | 11 | cls.search_term = "Ezra Furman" 12 | cls.song_title_only = "99 Problems" 13 | cls.tag = genius.tag("pop") 14 | 15 | def test_search_song(self): 16 | artist = "Jay-Z" 17 | # Empty response 18 | response = genius.search_song("") 19 | self.assertIsNone(response) 20 | 21 | # Pass no title and ID 22 | with self.assertRaises(AssertionError): 23 | genius.search_song() 24 | 25 | # Search by song ID 26 | response = genius.search_song(song_id=1) 27 | self.assertIsNotNone(response) 28 | 29 | # Exact match exact search 30 | response = genius.search_song(self.song_title_only) 31 | self.assertTrue(response.title.lower() == self.song_title_only.lower()) 32 | 33 | # Song with artist name 34 | response = genius.search_song(self.song_title_only, artist) 35 | self.assertTrue(response.title.lower() == self.song_title_only.lower()) 36 | 37 | # Spaced out search 38 | response = genius.search_song(" \t 99 \t \t\tProblems ", artist) 39 | self.assertTrue(response.title.lower() == self.song_title_only.lower()) 40 | 41 | # No title match because of artist 42 | response = genius.search_song(self.song_title_only, artist="Drake") 43 | self.assertFalse(response.title.lower() == self.song_title_only.lower()) 44 | 45 | def test_song_annotations(self): 46 | msg = "Incorrect song annotation response." 47 | r = sorted(genius.song_annotations(1)) 48 | real = r[0][0] 49 | expected = "(I'm at bat)" 50 | self.assertEqual(real, expected, msg) 51 | 52 | def test_tag_results(self): 53 | r = self.tag 54 | 55 | self.assertEqual(r["next_page"], 2) 56 | self.assertEqual(len(r["hits"]), 20) 57 | 58 | def test_tag_first_result(self): 59 | artists = ["Luis Fonsi", "Daddy Yankee"] 60 | featured_artists = ["Justin Bieber"] 61 | song_title = "Despacito (Remix)" 62 | title_with_artists = ( 63 | "Despacito (Remix) by Luis Fonsi & Daddy Yankee (Ft. Justin Bieber)" 64 | ) 65 | url = "https://genius.com/Luis-fonsi-and-daddy-yankee-despacito-remix-lyrics" 66 | 67 | first_song = self.tag["hits"][0] 68 | 69 | self.assertEqual(artists, first_song["artists"]) 70 | self.assertEqual(featured_artists, first_song["featured_artists"]) 71 | self.assertEqual(song_title, first_song["title"]) 72 | self.assertEqual(title_with_artists, first_song["title_with_artists"]) 73 | self.assertEqual(url, first_song["url"]) 74 | 75 | 76 | class TestLyrics(unittest.TestCase): 77 | @classmethod 78 | def setUpClass(cls): 79 | print("\n---------------------\nSetting up lyrics tests...\n") 80 | 81 | cls.song_url = "https://genius.com/Andy-shauf-begin-again-lyrics" 82 | cls.song_id = 2885745 83 | cls.lyrics_ending = ( 84 | "[Outro]" 85 | "\nNow I'm kicking leaves" 86 | "\nCursing the one that I love and the one I don't" 87 | "\nI wonder who you're thinking of" 88 | ) 89 | 90 | def test_lyrics_with_url(self): 91 | lyrics = genius.lyrics(song_url=self.song_url) 92 | self.assertTrue(lyrics.endswith(self.lyrics_ending)) 93 | 94 | def test_lyrics_with_id(self): 95 | lyrics = genius.lyrics(self.song_id) 96 | self.assertTrue(lyrics.endswith(self.lyrics_ending)) 97 | -------------------------------------------------------------------------------- /tests/test_song.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from lyricsgenius.types import Song 5 | from lyricsgenius.utils import clean_str 6 | from tests import genius 7 | 8 | 9 | class TestSong(unittest.TestCase): 10 | @classmethod 11 | def setUpClass(cls): 12 | print("\n---------------------\nSetting up Song tests...\n") 13 | 14 | cls.artist_name = "Andy Shauf" 15 | cls.song_title = "begin again" # Lowercase is intentional 16 | cls.album = "The Party" 17 | cls.year = "2016-05-20" 18 | cls.song = genius.search_song(cls.song_title, cls.artist_name) 19 | genius.remove_section_headers = True 20 | cls.song_trimmed = genius.search_song(cls.song_title, cls.artist_name) 21 | 22 | def test_song(self): 23 | msg = "The returned object is not an instance of the Song class." 24 | self.assertIsInstance(self.song, Song, msg) 25 | 26 | def test_title(self): 27 | msg = "The returned song title does not match the title of the requested song." 28 | self.assertEqual(clean_str(self.song.title), clean_str(self.song_title), msg) 29 | 30 | def test_artist(self): 31 | # The returned artist name does not match the artist of the requested song. 32 | self.assertEqual(self.song.artist, self.artist_name) 33 | 34 | def test_lyrics_raw(self): 35 | lyrics = "[Verse 1]" 36 | self.assertTrue(self.song.lyrics.startswith(lyrics)) 37 | 38 | def test_lyrics_no_section_headers(self): 39 | lyrics = "Begin again\nThis time you should take a bow at the" 40 | self.assertTrue(self.song_trimmed.lyrics.startswith(lyrics)) 41 | 42 | def test_result_is_lyrics(self): 43 | self.assertTrue(genius._result_is_lyrics(self.song.to_dict())) 44 | 45 | def test_saving_json_file(self): 46 | print("\n") 47 | extension = "json" 48 | msg = "Could not save {} file.".format(extension) 49 | expected_filename = "lyrics_save_test_file." + extension 50 | filename = expected_filename.split(".")[0] 51 | 52 | # Remove the test file if it already exists 53 | if os.path.isfile(expected_filename): 54 | os.remove(expected_filename) 55 | 56 | # Test saving json file 57 | self.song.save_lyrics(filename=filename, extension=extension, overwrite=True) 58 | self.assertTrue(os.path.isfile(expected_filename), msg) 59 | 60 | # Test overwriting json file (now that file is written) 61 | try: 62 | self.song.save_lyrics( 63 | filename=expected_filename, extension=extension, overwrite=True 64 | ) 65 | os.remove(expected_filename) 66 | except Exception: 67 | self.fail("Failed {} overwrite test".format(extension)) 68 | os.remove(expected_filename) 69 | 70 | def test_saving_txt_file(self): 71 | print("\n") 72 | extension = "txt" 73 | msg = "Could not save {} file.".format(extension) 74 | expected_filename = "lyrics_save_test_file." + extension 75 | filename = expected_filename.split(".")[0] 76 | 77 | # Remove the test file if it already exists 78 | if os.path.isfile(expected_filename): 79 | os.remove(expected_filename) 80 | 81 | # Test saving txt file 82 | self.song.save_lyrics(filename=filename, extension=extension, overwrite=True) 83 | self.assertTrue(os.path.isfile(expected_filename), msg) 84 | 85 | # Test overwriting txt file (now that file is written) 86 | try: 87 | self.song.save_lyrics( 88 | filename=filename, extension=extension, overwrite=True 89 | ) 90 | except Exception: 91 | self.fail("Failed {} overwrite test".format(extension)) 92 | os.remove(expected_filename) 93 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from lyricsgenius.utils import ( 4 | auth_from_environment, 5 | parse_redirected_url, 6 | sanitize_filename, 7 | ) 8 | from tests import genius 9 | 10 | 11 | class TestUtils(unittest.TestCase): 12 | @classmethod 13 | def setUpClass(cls): 14 | print("\n---------------------\nSetting up utils tests...\n") 15 | 16 | def test_sanitize_filename(self): 17 | raw = "B@ad File#_name" 18 | cleaned = "Bad File_name" 19 | r = sanitize_filename(raw) 20 | self.assertEqual(r, cleaned) 21 | 22 | def test_parse_redirected_url(self): 23 | redirected = "https://example.com/callback?code=test" 24 | flow = "code" 25 | code = "test" 26 | r = parse_redirected_url(redirected, flow) 27 | self.assertEqual(r, code) 28 | 29 | redirected = "https://example.com/callback#access_token=test" 30 | flow = "token" 31 | code = "test" 32 | r = parse_redirected_url(redirected, flow) 33 | self.assertEqual(r, code) 34 | 35 | def test_auth_from_environment(self): 36 | credentials = auth_from_environment() 37 | self.assertTrue(len(credentials) == 3) 38 | self.assertTrue(all(credentials)) 39 | 40 | @classmethod 41 | def tearDownClass(cls): 42 | genius._session.close() 43 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = test,flake8,doc8,docs 3 | 4 | [flake8] 5 | select = C,E,F,W,B,B9 6 | ignore = B305,B950,E402,E501,E722,F401,W503 7 | 8 | [doc8] 9 | ignore = D002,D004 10 | max-line-length = 80 11 | 12 | [test] 13 | python_files = *.py 14 | testpaths = tests 15 | 16 | [testenv] 17 | description = Run test suite with pytest 18 | extras = test 19 | commands = python -m unittest discover 20 | allowlist_externals = python 21 | passenv = GENIUS* 22 | 23 | [testenv:test] 24 | ; Inherit everything from testenv 25 | 26 | [testenv:docs] 27 | description = Build Sphinx HTML documentation 28 | extras = docs 29 | changedir = docs 30 | allowlist_externals = sphinx-build 31 | commands = sphinx-build -W -b html src build 32 | 33 | [testenv:doc8] 34 | description = Check documentation .rst files 35 | extras = checks 36 | allowlist_externals = doc8 37 | commands = doc8 docs/src 38 | 39 | [testenv:flake8] 40 | description = Check code style 41 | extras = checks 42 | allowlist_externals = flake8 43 | commands = flake8 44 | 45 | [testenv:lint] 46 | ; Duplication needed https://github.com/tox-dev/tox/issues/647 47 | description = Run all static checks 48 | extras = checks 49 | allowlist_externals = 50 | doc8 51 | flake8 52 | commands = 53 | flake8 54 | doc8 docs/src 55 | --------------------------------------------------------------------------------