Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 629ea88388 | |||
| 05922fc9a7 | |||
| c7cabe8d63 | |||
| 5b24952e1e | |||
| 511986a131 | |||
| 21472e8100 | |||
| 4e0091f80f | |||
| cd8117343b | |||
| 8dc88f6361 | |||
| 306eb82237 | |||
| 0a77fa34fd | |||
| 5aefcc2a2d | |||
| 0034340d63 | |||
| 02cb79c4b2 | |||
| 26b346d359 | |||
| 1dae95735f | |||
| 78544673b4 | |||
| e8ce4b59f8 | |||
| e09728a7c7 | |||
| ae6c2bf985 | |||
| fd144abff0 | |||
| d499ee175e | |||
| fadcb98d81 | |||
| aa98102d6a | |||
| a10426f043 | |||
| 78ac63ca36 | |||
| 2d8f8dc63f |
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
@@ -1,3 +1,8 @@
|
|||||||
{
|
{
|
||||||
"python.pythonPath": "/Users/dtomlinson/.virtualenvs/musicbrainzapi/bin/python"
|
"python.pythonPath": "/Users/dtomlinson/.virtualenvs/musicbrainzapi/bin/python",
|
||||||
|
"restructuredtext.confPath": "${workspaceFolder}/docs/source",
|
||||||
|
"restructuredtext.linter.executablePath": "/Users/dtomlinson/.virtualenvs/utility-doc8/bin/doc8",
|
||||||
|
"files.trimTrailingWhitespace": true,
|
||||||
|
"restructuredtext.languageServer.trace.server": "messages",
|
||||||
|
"editor.fontSize": 13,
|
||||||
}
|
}
|
||||||
22
README.rst
22
README.rst
@@ -10,7 +10,6 @@ Introduction
|
|||||||
.. image:: https://img.shields.io/github/languages/code-size/dtomlinson91/musicbrainzapi-cv-airelogic?style=for-the-badge
|
.. image:: https://img.shields.io/github/languages/code-size/dtomlinson91/musicbrainzapi-cv-airelogic?style=for-the-badge
|
||||||
.. image:: https://img.shields.io/github/languages/top/dtomlinson91/musicbrainzapi-cv-airelogic?style=for-the-badge
|
.. image:: https://img.shields.io/github/languages/top/dtomlinson91/musicbrainzapi-cv-airelogic?style=for-the-badge
|
||||||
.. image:: https://img.shields.io/requires/github/dtomlinson91/musicbrainzapi-cv-airelogic?style=for-the-badge
|
.. image:: https://img.shields.io/requires/github/dtomlinson91/musicbrainzapi-cv-airelogic?style=for-the-badge
|
||||||
.. image:: https://img.shields.io/codacy/grade/f9517450400d48b0a7222a383c2e8fe2?style=for-the-badge
|
|
||||||
|
|
||||||
Summary
|
Summary
|
||||||
========
|
========
|
||||||
@@ -71,6 +70,27 @@ In the root of the repo in a virtual environment run:
|
|||||||
|
|
||||||
python ./setup.py install
|
python ./setup.py install
|
||||||
|
|
||||||
|
poetry
|
||||||
|
------
|
||||||
|
|
||||||
|
Clone the repo:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
git clone https://github.com/dtomlinson91/musicbrainzapi-cv-airelogic.git
|
||||||
|
|
||||||
|
In a virtual environment install poetry:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
pip install poetry
|
||||||
|
|
||||||
|
In the root of the repo in a virtual environment run:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
poetry install --no-dev
|
||||||
|
|
||||||
Docker
|
Docker
|
||||||
------
|
------
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ Code restructure
|
|||||||
|
|
||||||
The :class:`musicbrainzapi.api.lyrics.concrete_builder.LyricsConcreteBuilder` class could be improved. Many of the methods defined in here no longer need to be present. Some of the functionality (url checking for example) could be removed and implemented in other ways (a Mixin is one solution).
|
The :class:`musicbrainzapi.api.lyrics.concrete_builder.LyricsConcreteBuilder` class could be improved. Many of the methods defined in here no longer need to be present. Some of the functionality (url checking for example) could be removed and implemented in other ways (a Mixin is one solution).
|
||||||
|
|
||||||
If other ways of filtering were to be added (as opposed to the current default of just Albums) this class would be useful in constructing our :class:`musicbrainzapi.api.lyrics.Lyrics` objects consistently.
|
If other ways of filtering were to be added (as opposed to the current default of just Albums) then this class would be useful to build our :class:`musicbrainzapi.api.lyrics.Lyrics` objects consistently.
|
||||||
|
|
||||||
Additional functionality to the lyrics command
|
Additional functionality to the lyrics command
|
||||||
-----------------------------------------------
|
-----------------------------------------------
|
||||||
@@ -68,7 +68,7 @@ The ability for the user to specify something other than album or year to group
|
|||||||
Multiple artists
|
Multiple artists
|
||||||
^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
Searching for multiple artists and comparing is certainly possible in the current iteration (click provides a nice way to accept multiple artists and then we create our ``Lyrics`` objects from these) this wasn't implemented. There are rate limiting factors which may slow down the program and increase runtime considerably.
|
Searching for multiple artists and comparing is certainly possible in the current iteration (click provides a nice way to accept multiple artists and then we create our ``Lyrics`` objects from these) this wasn't implemented. There are rate limiting factors which may slow down the program and in the current implementation it could increase runtime considerably.
|
||||||
|
|
||||||
Speed improvements
|
Speed improvements
|
||||||
-------------------
|
-------------------
|
||||||
@@ -79,7 +79,7 @@ One solution would be to implement threading - as we are waiting on HTTP request
|
|||||||
|
|
||||||
This wasn't implemented primarily because of time - but threading could be implemented on each call we make to the API.
|
This wasn't implemented primarily because of time - but threading could be implemented on each call we make to the API.
|
||||||
|
|
||||||
An alternative, and I beleive an interesting solution, would be to use AWS Lambda (serverless).
|
An alternative, and I believe an interesting solution, would be to use AWS Lambda (serverless).
|
||||||
|
|
||||||
There is a caveat to this solution and it is cost - threading is free but adds development time and increases complexity. AWS isn't free but allows you to scale the requests out.
|
There is a caveat to this solution and it is cost - threading is free but adds development time and increases complexity. AWS isn't free but allows you to scale the requests out.
|
||||||
|
|
||||||
@@ -98,4 +98,29 @@ If more control was needed one solution could be:
|
|||||||
|
|
||||||
This requires the user to have an internet connection - which is a current requirement. Requests to the api could be made simultaneously - without adding the complexity that comes with threading. This would not solve any API rate limiting - we are required to provide an application user_agent to the api to identify the app.
|
This requires the user to have an internet connection - which is a current requirement. Requests to the api could be made simultaneously - without adding the complexity that comes with threading. This would not solve any API rate limiting - we are required to provide an application user_agent to the api to identify the app.
|
||||||
|
|
||||||
|
An interesting solution, and one I did consider, was to have the program run entirely in lambda, requiring no depdencies and just a simple front end that sends requests, and uses ``boto3`` to retrieve. The simplicity of this, and the fact that AWS provide an SDK for many languages, means the cient code could run in any language.
|
||||||
|
|
||||||
|
An interface to AWS API Gateway would provide the entry point to the lambda.
|
||||||
|
|
||||||
|
Writing it in this manner (with an api backend) would mean a webapp of the program could be possible, with the frontend served with something like ``Vuejs`` or ``React``.
|
||||||
|
|
||||||
.. _Zappa: https://github.com/Miserlou/Zappa
|
.. _Zappa: https://github.com/Miserlou/Zappa
|
||||||
|
|
||||||
|
Error catching
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Handling missing data from both APIs is done with error catching (namely ``ValueError`` and ``TypeError``).
|
||||||
|
|
||||||
|
Although inelegant, and not guaranteed to capture the specific behaviour we want to catch (missing data etc.) it is a solution and appears to work quite well.
|
||||||
|
|
||||||
|
Musicbrainz provides a schema for their api. If this were to be placed in a production environment then readdressing this should be a priority - we should be checking the values returned, using the schema as a guide, and replacing missing values accordingly. We should not rely on ``try except`` blocks to do this as it can be unreliable and is prone to raise other errors.
|
||||||
|
|
||||||
|
Further statistical analysis
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
Standard descriptive statistics are provided. I did consider including a more deeper analysis but opted not to for several reasons:
|
||||||
|
|
||||||
|
- Without a specific problem or question to answer - explorative work can take a lot of time and may not yield satisfactory results. Questions I did consider are:
|
||||||
|
|
||||||
|
+ `For active artists, based on their previous lyrics count what is the predicition of their next album?` Although a sensible question I'm not sure how useful the predicition would be - I am sure for some artists they would follow a pattern over time, but I'm not convinced all artists would and I imagine the results would be mixed.
|
||||||
|
+ `Anomaly detection - for artists with large releases, what albums stood out as larger than usual and what feature (or track) caused this anomaly?` - This would be a good question to answer and we have many tools available. As we have numeric data - clustering could be a candidate (DBSCAN or even K-MEANS). I opted not to because of time and the fact it would bloat the requirements up. Feature flags are an option when handling extra packages, ``pip install musicbrainzapi[analysis]`` for example, but nonetheless this would be an interesting question to answer and I beleive one of the easier ones to implement if it was desired.
|
||||||
1
docs/about.md
Normal file
1
docs/about.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Some about page
|
||||||
17
docs/index.md
Normal file
17
docs/index.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Welcome to MkDocs
|
||||||
|
|
||||||
|
For full documentation visit [mkdocs.org](https://www.mkdocs.org).
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
* `mkdocs new [dir-name]` - Create a new project.
|
||||||
|
* `mkdocs serve` - Start the live-reloading docs server.
|
||||||
|
* `mkdocs build` - Build the documentation site.
|
||||||
|
* `mkdocs -h` - Print help message and exit.
|
||||||
|
|
||||||
|
## Project layout
|
||||||
|
|
||||||
|
mkdocs.yml # The configuration file.
|
||||||
|
docs/
|
||||||
|
index.md # The documentation homepage.
|
||||||
|
... # Other markdown pages, images and other files.
|
||||||
20
docs/python.md
Normal file
20
docs/python.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Some python
|
||||||
|
|
||||||
|
## Some test python
|
||||||
|
|
||||||
|
!!! warning "Important warning"
|
||||||
|
This isn't yet complete.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _generate_word_cloud(self) -> None:
|
||||||
|
"""Generates a word cloud
|
||||||
|
"""
|
||||||
|
self.wc = WordCloud(
|
||||||
|
max_words=150,
|
||||||
|
width=1500,
|
||||||
|
height=1500,
|
||||||
|
mask=self.char_mask,
|
||||||
|
random_state=1,
|
||||||
|
).generate_from_frequencies(self.freq)
|
||||||
|
return self
|
||||||
|
```
|
||||||
83
docs/test.md
Normal file
83
docs/test.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# musicbrainzapi.wordcloud package
|
||||||
|
|
||||||
|
Wordcloud from lyrics.
|
||||||
|
|
||||||
|
|
||||||
|
### class musicbrainzapi.wordcloud.LyricsWordcloud(pillow_img: PIL.PngImagePlugin.PngImageFile, all_albums_lyrics_count: Lyrics.all_albums_lyrics_count)
|
||||||
|
Bases: `object`
|
||||||
|
|
||||||
|
Create a word cloud from Lyrics.
|
||||||
|
|
||||||
|
|
||||||
|
* **Variables**
|
||||||
|
|
||||||
|
|
||||||
|
* **all_albums_lyrics_count** (*list*) – List of all albums + track lyrics counted by each word
|
||||||
|
|
||||||
|
|
||||||
|
* **char_mask** (*np.array*) – numpy array containing data for the word cloud image
|
||||||
|
|
||||||
|
|
||||||
|
* **freq** (*collections.Counter*) – Counter object containing counts for all words across all tracks
|
||||||
|
|
||||||
|
|
||||||
|
* **lyrics_list** (*list*) – List of all words from all lyrics across all tracks.
|
||||||
|
|
||||||
|
|
||||||
|
* **pillow_img** (*PIL.PngImagePlugin.PngImageFile*) – pillow image of the word cloud base
|
||||||
|
|
||||||
|
|
||||||
|
* **wc** (*wordcloud.WordCloud*) – WordCloud object
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### \__init__(pillow_img: PIL.PngImagePlugin.PngImageFile, all_albums_lyrics_count: Lyrics.all_albums_lyrics_count)
|
||||||
|
Create a worcloud object.
|
||||||
|
|
||||||
|
|
||||||
|
* **Parameters**
|
||||||
|
|
||||||
|
|
||||||
|
* **pillow_img** (*PIL.PngImagePlugin.PngImageFile*) – pillow image of the word cloud base
|
||||||
|
|
||||||
|
|
||||||
|
* **all_albums_lyrics_count** (*Lyrics.all_albums_lyrics_count*) – List of all albums + track lyrics counted by each word
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### classmethod use_microphone(all_albums_lyrics_count: Lyrics.all_albums_lyrics_count)
|
||||||
|
Class method to instantiate with a microphone base image.
|
||||||
|
|
||||||
|
|
||||||
|
* **Parameters**
|
||||||
|
|
||||||
|
**all_albums_lyrics_count** (*Lyrics.all_albums_lyrics_count*) – List of all albums + track lyrics counted by each word
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### static generate_grey_colours(\*args, \*\*kwargs)
|
||||||
|
Static method to generate a random grey colour.
|
||||||
|
|
||||||
|
|
||||||
|
#### _get_lyrics_list()
|
||||||
|
Gets all words from lyrics in a single list + cleans them.
|
||||||
|
|
||||||
|
|
||||||
|
#### _get_frequencies()
|
||||||
|
Get frequencies of words from a list.
|
||||||
|
|
||||||
|
|
||||||
|
#### _get_char_mask()
|
||||||
|
Gets a numpy array for the image file.
|
||||||
|
|
||||||
|
|
||||||
|
#### _generate_word_cloud()
|
||||||
|
Generates a word cloud
|
||||||
|
|
||||||
|
|
||||||
|
#### _generate_plot()
|
||||||
|
Plots the wordcloud and sets matplotlib options.
|
||||||
|
|
||||||
|
|
||||||
|
#### create_word_cloud()
|
||||||
|
Creates a word cloud
|
||||||
5
docs/testdoc.md
Normal file
5
docs/testdoc.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Test documentation using mkdocstrings
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
::: musicbrainzapi.api.lyrics.builder
|
||||||
39
mkdocs.yml
Normal file
39
mkdocs.yml
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
site_name: Musicbrainzapi
|
||||||
|
|
||||||
|
nav:
|
||||||
|
- Welcome:
|
||||||
|
- Home: index.md
|
||||||
|
- About: about.md
|
||||||
|
- API:
|
||||||
|
- API: test.md
|
||||||
|
- Code:
|
||||||
|
- python.md
|
||||||
|
- testdoc.md
|
||||||
|
|
||||||
|
# theme: material
|
||||||
|
theme:
|
||||||
|
name: "material"
|
||||||
|
palette:
|
||||||
|
primary: "yellow"
|
||||||
|
accent: "red"
|
||||||
|
feature:
|
||||||
|
tabs: true
|
||||||
|
|
||||||
|
markdown_extensions:
|
||||||
|
- admonition
|
||||||
|
- codehilite:
|
||||||
|
guess_lang: true
|
||||||
|
- toc:
|
||||||
|
permalink: true
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
- search
|
||||||
|
- mkdocstrings
|
||||||
|
|
||||||
|
repo_name: "dtomlinson91/musicbrainzapi"
|
||||||
|
repo_url: "https://github.com/dtomlinson91/musicbrainzapi-cv-airelogic"
|
||||||
|
|
||||||
|
extra:
|
||||||
|
social:
|
||||||
|
- type: "github"
|
||||||
|
link: "https://github.com/dtomlinson91/musicbrainzapi"
|
||||||
821
poetry.lock
generated
821
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -20,20 +20,18 @@ click = "^7.0"
|
|||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
pytest = "^5.2"
|
pytest = "^5.2"
|
||||||
python-language-server = "^0.31.8"
|
|
||||||
Rope = "^0.16.0"
|
|
||||||
Pyflakes = "^2.1.1"
|
|
||||||
McCabe = "^0.6.1"
|
|
||||||
pycodestyle = "^2.5.0"
|
|
||||||
pydocstyle = "^5.0.2"
|
|
||||||
autopep8 = "^1.5"
|
|
||||||
YAPF = "^0.29.0"
|
|
||||||
pudb = "^2019.2"
|
pudb = "^2019.2"
|
||||||
pyls-black = "^0.4.4"
|
|
||||||
sphinx = "^2.4.4"
|
sphinx = "^2.4.4"
|
||||||
sphinx_rtd_theme = "^0.4.3"
|
sphinx_rtd_theme = "^0.4.3"
|
||||||
sphinx-click = "^2.3.1"
|
sphinx-click = "^2.3.1"
|
||||||
coverage = "^5.0.3"
|
prospector = "^1.2.0"
|
||||||
|
pylint = "^2.4.4"
|
||||||
|
pydoc-markdown = {git = "https://github.com/NiklasRosenstein/pydoc-markdown.git", rev = "develop"}
|
||||||
|
mkdocs = "^1.1"
|
||||||
|
mkdocs-material = "^4.6.3"
|
||||||
|
pymdown-extensions = "^6.3"
|
||||||
|
mkdocstrings = "^0.8.0"
|
||||||
|
beeprint = "^2.4.10"
|
||||||
|
|
||||||
[tool.poetry.plugins."console_scripts"]
|
[tool.poetry.plugins."console_scripts"]
|
||||||
"musicbrainzapi" = "musicbrainzapi.cli.cli:cli"
|
"musicbrainzapi" = "musicbrainzapi.cli.cli:cli"
|
||||||
|
|||||||
2
setup.py
2
setup.py
@@ -33,10 +33,8 @@ entry_points = \
|
|||||||
setup_kwargs = {
|
setup_kwargs = {
|
||||||
'name': 'musicbrainzapi',
|
'name': 'musicbrainzapi',
|
||||||
'version': '1.0.0',
|
'version': '1.0.0',
|
||||||
'description': '',
|
|
||||||
'long_description': None,
|
'long_description': None,
|
||||||
'description': 'Python module to calculate statistics and generate a wordcloud for a given artist. Uses the Musicbrainz API and the lyrics.ovh API.',
|
'description': 'Python module to calculate statistics and generate a wordcloud for a given artist. Uses the Musicbrainz API and the lyrics.ovh API.',
|
||||||
'long_description': '',
|
|
||||||
'author': 'dtomlinson',
|
'author': 'dtomlinson',
|
||||||
'author_email': 'dtomlinson@panaetius.co.uk',
|
'author_email': 'dtomlinson@panaetius.co.uk',
|
||||||
'maintainer': None,
|
'maintainer': None,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""
|
"""
|
||||||
musicbrainzapi: A CLI lyrics searcher
|
musicbrainzapi: A CLI lyrics searcher.
|
||||||
=====================================
|
======================================
|
||||||
|
|
||||||
This module was written by dtomlinson <dtomlinson@panaetius.co.uk> for Aire Logic
|
This module was written by dtomlinson <dtomlinson@panaetius.co.uk> for Aire Logic
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
"""
|
||||||
|
Lyrics object with statistics.
|
||||||
|
===============================
|
||||||
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Union, Dict, List
|
from typing import Union, Dict, List
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@@ -10,8 +14,7 @@ import numpy as np
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Lyrics:
|
class Lyrics:
|
||||||
"""Lyrics object for an artist.
|
"""Lyrics object for an artist."""
|
||||||
"""
|
|
||||||
|
|
||||||
artist_id: str
|
artist_id: str
|
||||||
artist: str
|
artist: str
|
||||||
|
|||||||
@@ -1,53 +1,49 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from collections import Counter
|
|
||||||
import html
|
import html
|
||||||
import json
|
import json
|
||||||
import math
|
import math
|
||||||
import string
|
import string
|
||||||
from typing import Union, Dict
|
from collections import Counter
|
||||||
|
from typing import Dict, Union
|
||||||
|
|
||||||
import addict
|
import addict
|
||||||
import click
|
import click
|
||||||
import musicbrainzngs
|
import musicbrainzngs
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import requests
|
import requests
|
||||||
|
from beeprint import pp
|
||||||
|
|
||||||
from musicbrainzapi.api.lyrics.concrete_builder import LyricsConcreteBuilder
|
|
||||||
from musicbrainzapi.api.lyrics import Lyrics
|
|
||||||
from musicbrainzapi.api import authenticate
|
from musicbrainzapi.api import authenticate
|
||||||
|
from musicbrainzapi.api.lyrics import Lyrics
|
||||||
|
from musicbrainzapi.api.lyrics.concrete_builder import LyricsConcreteBuilder
|
||||||
|
|
||||||
|
|
||||||
class LyricsBuilder(LyricsConcreteBuilder):
|
class LyricsBuilder(LyricsConcreteBuilder):
|
||||||
"""docstring for LyricsBuilder
|
"""
|
||||||
|
This interface will build a Lyrics object.
|
||||||
|
|
||||||
Attributes
|
!!! info "Attributes"
|
||||||
----------
|
- musicbrainz_artists (addict.Dict): A dict response from the Musicbrainz api for all artists.
|
||||||
album_statistics : addict.Dict
|
- release_group_ids (addict.Dict): : A dict response from the Musicbrainz api for all artists.
|
||||||
Dictionary containing album statistics
|
- all_albums (list): : A dict response from the Musicbrainz api for all artists.
|
||||||
all_albums : list
|
- total_track_count (list): : A dict response from the Musicbrainz api for all artists.
|
||||||
List of all albums + track titles
|
- all_albums_lyrics_url (addict.Dict): : A dict response from the Musicbrainz api for all artists.
|
||||||
all_albums_lyrics : list
|
- all_albums_lyrics (addict.Dict): : A dict response from the Musicbrainz api for all artists.
|
||||||
List of all albums + track lyrics
|
- all_albums_lyrics_count (addict.Dict): : A dict response from the Musicbrainz api for all artists.
|
||||||
all_albums_lyrics_count : list
|
- all_albums_lyrics_sum (addict.Dict): : A dict response from the Musicbrainz api for all artists.
|
||||||
List of all albums + track lyrics counted by each word
|
- album_statistics (addict.Dict): : A dict response from the Musicbrainz api for all artists.
|
||||||
all_albums_lyrics_sum : list
|
- album_statistics (addict.Dict): : A dict response from the Musicbrainz api for all artists.
|
||||||
List of all albums + track lyrics counted and summed up.
|
- year_statistics (addict.Dict): : A dict response from the Musicbrainz api for all artists.
|
||||||
all_albums_lyrics_url : list
|
- year_statistics (addict.Dict): : A dict response from the Musicbrainz api for all artists.
|
||||||
List of all albums + link to lyrics api for each track.
|
|
||||||
musicbrainz_artists : addict.Dict
|
Example:
|
||||||
Dictionary of response from Musicbrainzapi
|
A test example.
|
||||||
release_group_ids : addict.Dict
|
|
||||||
Dictionary of Musicbrainz release-group ids
|
|
||||||
total_track_count : int
|
|
||||||
Total number of tracks across all albums
|
|
||||||
year_statistics : addict.Dict
|
|
||||||
Dictionary containing album statistics
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def product(self) -> Lyrics:
|
def product(self) -> Lyrics:
|
||||||
product = self._product
|
return self._product
|
||||||
return product
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def artist(self) -> str:
|
def artist(self) -> str:
|
||||||
@@ -95,17 +91,13 @@ class LyricsBuilder(LyricsConcreteBuilder):
|
|||||||
def construct_lyrics_url(artist: str, song: str) -> str:
|
def construct_lyrics_url(artist: str, song: str) -> str:
|
||||||
"""Builds the URL for the lyrics api.
|
"""Builds the URL for the lyrics api.
|
||||||
|
|
||||||
Parameters
|
Args:
|
||||||
----------
|
artist (str): Your chosen artist.
|
||||||
artist : str
|
song (str): A song to find a lyrics url for.
|
||||||
Artist
|
|
||||||
song : str
|
Returns:
|
||||||
Track title
|
str: The url of the lyrics api for chosen song.
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
str
|
|
||||||
URL for lyrics from the lyrics api.
|
|
||||||
"""
|
"""
|
||||||
lyrics_api_base = 'https://api.lyrics.ovh/v1'
|
lyrics_api_base = 'https://api.lyrics.ovh/v1'
|
||||||
lyrics_api_url = html.escape(f'{lyrics_api_base}/{artist}/{song}')
|
lyrics_api_url = html.escape(f'{lyrics_api_base}/{artist}/{song}')
|
||||||
@@ -123,7 +115,8 @@ class LyricsBuilder(LyricsConcreteBuilder):
|
|||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
str
|
str
|
||||||
Lyrics of the trakc
|
Lyrics of the track.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
resp = requests.get(url)
|
resp = requests.get(url)
|
||||||
|
|
||||||
@@ -165,7 +158,7 @@ class LyricsBuilder(LyricsConcreteBuilder):
|
|||||||
Dict[str, int]
|
Dict[str, int]
|
||||||
Dictionary of statistic and value.
|
Dictionary of statistic and value.
|
||||||
"""
|
"""
|
||||||
if len(nums) == 0:
|
if not nums:
|
||||||
return
|
return
|
||||||
avg = math.ceil(np.mean(nums))
|
avg = math.ceil(np.mean(nums))
|
||||||
median = math.ceil(np.median(nums))
|
median = math.ceil(np.median(nums))
|
||||||
@@ -177,7 +170,7 @@ class LyricsBuilder(LyricsConcreteBuilder):
|
|||||||
p_75 = math.ceil(np.percentile(nums, 75))
|
p_75 = math.ceil(np.percentile(nums, 75))
|
||||||
p_90 = math.ceil(np.percentile(nums, 90))
|
p_90 = math.ceil(np.percentile(nums, 90))
|
||||||
count = len(nums)
|
count = len(nums)
|
||||||
_d = addict.Dict(
|
return addict.Dict(
|
||||||
('avg', avg),
|
('avg', avg),
|
||||||
('median', median),
|
('median', median),
|
||||||
('std', std),
|
('std', std),
|
||||||
@@ -189,9 +182,10 @@ class LyricsBuilder(LyricsConcreteBuilder):
|
|||||||
('p_90', p_90),
|
('p_90', p_90),
|
||||||
('count', count),
|
('count', count),
|
||||||
)
|
)
|
||||||
return _d
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
|
"""Create a `LyricsBuilder`.
|
||||||
|
"""
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
def reset(self) -> None:
|
def reset(self) -> None:
|
||||||
@@ -208,8 +202,7 @@ class LyricsBuilder(LyricsConcreteBuilder):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
def sort_artists(self) -> None:
|
def sort_artists(self) -> None:
|
||||||
"""Sort the artists from the Musicbrainzapi
|
"""Sort the artists from the Musicbrainzapi."""
|
||||||
"""
|
|
||||||
self._sort_names = dict(
|
self._sort_names = dict(
|
||||||
(i.get('id'), f'{i.get("name")} | {i.get("disambiguation")}')
|
(i.get('id'), f'{i.get("name")} | {i.get("disambiguation")}')
|
||||||
if i.get('disambiguation') is not None
|
if i.get('disambiguation') is not None
|
||||||
@@ -241,8 +234,7 @@ class LyricsBuilder(LyricsConcreteBuilder):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
def find_all_albums(self) -> None:
|
def find_all_albums(self) -> None:
|
||||||
"""Find all albums for the chosen artist
|
"""Find all albums for the chosen artist."""
|
||||||
"""
|
|
||||||
limit, offset, page = (100, 0, 1)
|
limit, offset, page = (100, 0, 1)
|
||||||
|
|
||||||
resp_0 = addict.Dict(
|
resp_0 = addict.Dict(
|
||||||
@@ -365,8 +357,7 @@ class LyricsBuilder(LyricsConcreteBuilder):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
def find_lyrics_urls(self) -> None:
|
def find_lyrics_urls(self) -> None:
|
||||||
"""Construct the URL for the lyrics api.
|
"""Construct the URL for the lyrics api."""
|
||||||
"""
|
|
||||||
self.all_albums_lyrics_url = list()
|
self.all_albums_lyrics_url = list()
|
||||||
for x in self.all_albums:
|
for x in self.all_albums:
|
||||||
for alb, tracks in x.items():
|
for alb, tracks in x.items():
|
||||||
@@ -518,3 +509,6 @@ class LyricsBuilder(LyricsConcreteBuilder):
|
|||||||
self.year_statistics = addict.Dict(
|
self.year_statistics = addict.Dict(
|
||||||
**self.year_statistics, **addict.Dict((year, _d))
|
**self.year_statistics, **addict.Dict((year, _d))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pp(LyricsBuilder)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class LyricsClickDirector:
|
|||||||
"""Director for Lyrics builder."""
|
"""Director for Lyrics builder."""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
|
"""Create a Director to orchestrate the builder."""
|
||||||
self._builder = None
|
self._builder = None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -62,6 +63,7 @@ class LyricsClickDirector:
|
|||||||
------
|
------
|
||||||
SystemExit
|
SystemExit
|
||||||
If no artist is found will cleanly quit.
|
If no artist is found will cleanly quit.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
artist_meta = None
|
artist_meta = None
|
||||||
for i, j in self.builder._top_five_results.items():
|
for i, j in self.builder._top_five_results.items():
|
||||||
@@ -111,8 +113,7 @@ class LyricsClickDirector:
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
def _query_for_data(self) -> None:
|
def _query_for_data(self) -> None:
|
||||||
"""Query Musicbrainz api for albums + track data.
|
"""Query Musicbrainz api for albums + track data."""
|
||||||
"""
|
|
||||||
self.builder.find_all_albums()
|
self.builder.find_all_albums()
|
||||||
self.builder.find_all_tracks()
|
self.builder.find_all_tracks()
|
||||||
self.builder._product.all_albums_with_tracks = self.builder.all_albums
|
self.builder._product.all_albums_with_tracks = self.builder.all_albums
|
||||||
|
|||||||
@@ -6,55 +6,64 @@ import click
|
|||||||
from musicbrainzapi.__version__ import __version__
|
from musicbrainzapi.__version__ import __version__
|
||||||
from musicbrainzapi.__header__ import __header__
|
from musicbrainzapi.__header__ import __header__
|
||||||
|
|
||||||
CONTEXT_SETTINGS = dict(auto_envvar_prefix='COMPLEX')
|
# pylint:disable=invalid-name
|
||||||
|
|
||||||
|
CONTEXT_SETTINGS = dict(auto_envvar_prefix="COMPLEX")
|
||||||
|
|
||||||
|
|
||||||
class Environment(object):
|
class Environment:
|
||||||
|
"""Environment class to house shared parameters between all subcommands."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.verbose = False
|
self.verbose = False
|
||||||
self.home = os.getcwd()
|
self.home = os.getcwd()
|
||||||
|
|
||||||
|
|
||||||
pass_environment = click.make_pass_decorator(Environment, ensure=True)
|
pass_environment = click.make_pass_decorator(
|
||||||
|
Environment, ensure=True
|
||||||
|
)
|
||||||
cmd_folder = os.path.abspath(
|
cmd_folder = os.path.abspath(
|
||||||
os.path.join(os.path.dirname(__file__), 'commands')
|
os.path.join(os.path.dirname(__file__), "commands")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ComplexCLI(click.MultiCommand):
|
class ComplexCLI(click.MultiCommand):
|
||||||
|
"""Access and run subcommands."""
|
||||||
|
|
||||||
def list_commands(self, ctx):
|
def list_commands(self, ctx):
|
||||||
rv = []
|
"""List all subcommands."""
|
||||||
for filename in os.listdir(cmd_folder):
|
rv = [
|
||||||
if filename.endswith('.py') and filename.startswith('cmd_'):
|
filename[4:-3]
|
||||||
rv.append(filename[4:-3])
|
for filename in os.listdir(cmd_folder)
|
||||||
|
if filename.endswith(".py") and filename.startswith("cmd_")
|
||||||
|
]
|
||||||
rv.sort()
|
rv.sort()
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
def get_command(self, ctx, cmd_name):
|
def get_command(self, ctx, cmd_name):
|
||||||
mod = import_module(f'musicbrainzapi.cli.commands.cmd_{cmd_name}')
|
"""Get chosen subcummands."""
|
||||||
|
mod = import_module(f"musicbrainzapi.cli.commands.cmd_{cmd_name}")
|
||||||
return getattr(mod, cmd_name)
|
return getattr(mod, cmd_name)
|
||||||
|
|
||||||
|
|
||||||
@click.command(cls=ComplexCLI, context_settings=CONTEXT_SETTINGS)
|
@click.command(cls=ComplexCLI, context_settings=CONTEXT_SETTINGS)
|
||||||
@click.option(
|
@click.option(
|
||||||
'-p',
|
"-p",
|
||||||
'--path',
|
"--path",
|
||||||
type=click.Path(
|
type=click.Path(exists=True, file_okay=False, resolve_path=True, writable=True),
|
||||||
exists=True, file_okay=False, resolve_path=True, writable=True
|
help="Local path to save any output files.",
|
||||||
),
|
default=os.getcwd(),
|
||||||
help='Local path to save any output files.',
|
|
||||||
default=os.getcwd()
|
|
||||||
)
|
)
|
||||||
# @click.option('-v', '--verbose', is_flag=True, help='Enables verbose mode.')
|
@click.option("-v", "--verbose", is_flag=True, help="Enables verbose mode.")
|
||||||
@click.version_option(
|
@click.version_option(
|
||||||
version=__version__,
|
version=__version__,
|
||||||
prog_name=__header__,
|
prog_name=__header__,
|
||||||
message=f'{__header__} version {__version__} 🎤',
|
message=f"{__header__} version {__version__} 🎤",
|
||||||
)
|
)
|
||||||
@pass_environment
|
@pass_environment
|
||||||
def cli(ctx, path):
|
def cli(ctx, verbose, path):
|
||||||
"""Base command for the musicbrainzapi program."""
|
"""Display base command for the musicbrainzapi program."""
|
||||||
# ctx.verbose = verbose
|
ctx.verbose = verbose
|
||||||
if path is not None:
|
if path is not None:
|
||||||
click.echo(f'Path set to {os.path.expanduser(path)}')
|
click.echo(f"Path set to {os.path.expanduser(path)}")
|
||||||
ctx.path = os.path.expanduser(path)
|
ctx.path = os.path.expanduser(path)
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
"""
|
||||||
|
Wordcloud from lyrics.
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import collections
|
import collections
|
||||||
from importlib import resources
|
from importlib import resources
|
||||||
@@ -16,55 +20,57 @@ if typing.TYPE_CHECKING:
|
|||||||
import PIL.PngImagePlugin.PngImageFile
|
import PIL.PngImagePlugin.PngImageFile
|
||||||
|
|
||||||
|
|
||||||
|
# pylint:disable=line-too-long
|
||||||
class LyricsWordcloud:
|
class LyricsWordcloud:
|
||||||
"""Create a word cloud from Lyrics.
|
"""
|
||||||
|
Create a Wordcloud from Lyrics.
|
||||||
|
|
||||||
|
The docstring continues here.
|
||||||
|
|
||||||
|
It should contain:
|
||||||
|
|
||||||
|
- something
|
||||||
|
- something else
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pillow_img (PIL.PngImagePlugin.PngImageFile): pillow image of the word
|
||||||
|
cloud base
|
||||||
|
all_albums_lyrics_count (dict): A dictionary containing the lyrics from
|
||||||
|
a whole album.
|
||||||
|
|
||||||
|
!!! Attributes
|
||||||
|
|
||||||
|
- `pillow_img` (pillow): A pillow image.
|
||||||
|
|
||||||
|
Anything else can go here.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
Here is how you can use it
|
||||||
|
|
||||||
Attributes
|
|
||||||
----------
|
|
||||||
all_albums_lyrics_count : list
|
|
||||||
List of all albums + track lyrics counted by each word
|
|
||||||
char_mask : np.array
|
|
||||||
numpy array containing data for the word cloud image
|
|
||||||
freq : collections.Counter
|
|
||||||
Counter object containing counts for all words across all tracks
|
|
||||||
lyrics_list : list
|
|
||||||
List of all words from all lyrics across all tracks.
|
|
||||||
pillow_img : PIL.PngImagePlugin.PngImageFile
|
|
||||||
pillow image of the word cloud base
|
|
||||||
wc : wordcloud.WordCloud
|
|
||||||
WordCloud object
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
pillow_img: 'PIL.PngImagePlugin.PngImageFile',
|
pillow_img: "PIL.PngImagePlugin.PngImageFile",
|
||||||
all_albums_lyrics_count: 'Lyrics.all_albums_lyrics_count',
|
all_albums_lyrics_count: "Lyrics.all_albums_lyrics_count",
|
||||||
):
|
):
|
||||||
"""
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
pillow_img : PIL.PngImagePlugin.PngImageFile
|
|
||||||
pillow image of the word cloud base
|
|
||||||
all_albums_lyrics_count : Lyrics.all_albums_lyrics_count
|
|
||||||
List of all albums + track lyrics counted by each word
|
|
||||||
"""
|
|
||||||
self.pillow_img = pillow_img
|
self.pillow_img = pillow_img
|
||||||
self.all_albums_lyrics_count = all_albums_lyrics_count
|
self.all_albums_lyrics_count = all_albums_lyrics_count
|
||||||
|
self.test = []
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def use_microphone(
|
def use_microphone(
|
||||||
cls, all_albums_lyrics_count: 'Lyrics.all_albums_lyrics_count',
|
cls, all_albums_lyrics_count: "Lyrics.all_albums_lyrics_count",
|
||||||
) -> LyricsWordcloud:
|
) -> LyricsWordcloud:
|
||||||
"""Class method to instantiate with a microphone base image.
|
"""Create a LyricsWordcloud using a microphone as a base image.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
all_albums_lyrics_count (dict): A dictionary containing the lyrics from a whole album.
|
||||||
|
Returns:
|
||||||
|
LyricsWordcloud: Instance of itself with a micrphone image loaded in.
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
all_albums_lyrics_count : Lyrics.all_albums_lyrics_count
|
|
||||||
List of all albums + track lyrics counted by each word
|
|
||||||
"""
|
"""
|
||||||
mic_resource = resources.path(
|
mic_resource = resources.path("musicbrainzapi.wordcloud.resources", "mic.png")
|
||||||
'musicbrainzapi.wordcloud.resources', 'mic.png'
|
|
||||||
)
|
|
||||||
with mic_resource as m:
|
with mic_resource as m:
|
||||||
mic_img = Image.open(m)
|
mic_img = Image.open(m)
|
||||||
|
|
||||||
@@ -78,9 +84,14 @@ class LyricsWordcloud:
|
|||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Static method to generate a random grey colour"""
|
"""Static method to return a random grey color.
|
||||||
colour = f'hsl(0, 0%, {random.randint(60, 100)}%)'
|
|
||||||
return colour
|
Returns:
|
||||||
|
str: A random grey colour in `hsl` form.
|
||||||
|
|
||||||
|
Can be any grey colour.
|
||||||
|
"""
|
||||||
|
return f"hsl(0, 0%, {random.randint(60, 100)}%)"
|
||||||
|
|
||||||
def _get_lyrics_list(self) -> None:
|
def _get_lyrics_list(self) -> None:
|
||||||
"""Gets all words from lyrics in a single list + cleans them.
|
"""Gets all words from lyrics in a single list + cleans them.
|
||||||
@@ -93,12 +104,8 @@ class LyricsWordcloud:
|
|||||||
for word in track:
|
for word in track:
|
||||||
for _ in range(1, word[1]):
|
for _ in range(1, word[1]):
|
||||||
cleaned = word[0]
|
cleaned = word[0]
|
||||||
cleaned = re.sub(
|
cleaned = re.sub(r"[\(\[].*?[\)\]]", " ", cleaned)
|
||||||
r'[\(\[].*?[\)\]]', ' ', cleaned
|
cleaned = re.sub(r"[^a-zA-Z0-9\s]", "", cleaned)
|
||||||
)
|
|
||||||
cleaned = re.sub(
|
|
||||||
r'[^a-zA-Z0-9\s]', '', cleaned
|
|
||||||
)
|
|
||||||
cleaned = cleaned.lower()
|
cleaned = cleaned.lower()
|
||||||
if cleaned in STOPWORDS:
|
if cleaned in STOPWORDS:
|
||||||
continue
|
continue
|
||||||
@@ -121,11 +128,7 @@ class LyricsWordcloud:
|
|||||||
"""Generates a word cloud
|
"""Generates a word cloud
|
||||||
"""
|
"""
|
||||||
self.wc = WordCloud(
|
self.wc = WordCloud(
|
||||||
max_words=150,
|
max_words=150, width=1500, height=1500, mask=self.char_mask, random_state=1,
|
||||||
width=1500,
|
|
||||||
height=1500,
|
|
||||||
mask=self.char_mask,
|
|
||||||
random_state=1,
|
|
||||||
).generate_from_frequencies(self.freq)
|
).generate_from_frequencies(self.freq)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@@ -133,12 +136,10 @@ class LyricsWordcloud:
|
|||||||
"""Plots the wordcloud and sets matplotlib options.
|
"""Plots the wordcloud and sets matplotlib options.
|
||||||
"""
|
"""
|
||||||
plt.imshow(
|
plt.imshow(
|
||||||
self.wc.recolor(
|
self.wc.recolor(color_func=self.generate_grey_colours, random_state=3),
|
||||||
color_func=self.generate_grey_colours, random_state=3
|
interpolation="bilinear",
|
||||||
),
|
|
||||||
interpolation='bilinear',
|
|
||||||
)
|
)
|
||||||
plt.axis('off')
|
plt.axis("off")
|
||||||
return self
|
return self
|
||||||
|
|
||||||
# def show_word_cloud(self):
|
# def show_word_cloud(self):
|
||||||
|
|||||||
BIN
src/musicbrainzapi/wordcloud/resources/.DS_Store
vendored
BIN
src/musicbrainzapi/wordcloud/resources/.DS_Store
vendored
Binary file not shown.
Reference in New Issue
Block a user