├── MANIFEST.in ├── beetsplug ├── __init__.py └── popularity.py ├── LICENSE.txt ├── setup.py ├── .gitignore └── README.md /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md -------------------------------------------------------------------------------- /beetsplug/__init__.py: -------------------------------------------------------------------------------- 1 | from pkgutil import extend_path 2 | __path__ = extend_path(__path__, __name__) 3 | __import__('pkg_resources').declare_namespace(__name__) 4 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 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 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from os import path 3 | 4 | directory = path.abspath(path.dirname(__file__)) 5 | readme_path = path.join(directory, 'README.md') 6 | 7 | try: 8 | import pypandoc 9 | long_description = pypandoc.convert(readme_path, 'rst') 10 | except (IOError, ImportError): 11 | long_description = open(readme_path, encoding='utf-8').read() 12 | 13 | setup( 14 | name='beets-popularity', 15 | version='1.0.2', 16 | description="Beets plugin to fetch and store popularity values as flexible item attributes", 17 | long_description=long_description, 18 | url='https://github.com/abba23/beets-popularity', 19 | download_url='https://github.com/abba23/beets-popularity.git', 20 | author='abba23', 21 | author_email='628208@gmail.com', 22 | license='MIT', 23 | platforms='ALL', 24 | classifiers=[ 25 | 'License :: OSI Approved :: MIT License', 26 | 'Programming Language :: Python :: 3.6', 27 | 'Topic :: Multimedia :: Sound/Audio', 28 | 'Environment :: Console', 29 | ], 30 | packages=['beetsplug'], 31 | namespace_packages=['beetsplug'], 32 | install_requires=['beets>=1.4.3', 'requests>=2.13.0'], 33 | ) 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # beets-popularity 2 | 3 | [Beets](http://beets.io/) plugin to store the popularity values from Deezer as flexible item attributes in the database 4 | 5 | ## Installation 6 | Using pip: 7 | 8 | $ pip install beets-popularity 9 | 10 | Manually: 11 | 12 | $ git clone https://github.com/abba23/beets-popularity.git 13 | $ cd beets-popularity 14 | $ python setup.py install 15 | 16 | You can then enable the plugin by [adding it to your `config.yaml`](https://beets.readthedocs.io/en/latest/plugins/index.html#using-plugins): 17 | 18 | ```yaml 19 | plugins: popularity 20 | ``` 21 | ## Usage 22 | $ beet popularity happy 23 | popularity: Bon Jovi - The Circle - Happy Now: 20 24 | popularity: The Doors - Strange Days - Unhappy Girl: 40 25 | popularity: Kygo - Cloud Nine - Happy Birthday: 59 26 | 27 | ## Options 28 | | Option | |Description | 29 | | ------ | ------ | ------ | 30 | | -a | \-\-album | match albums instead of tracks | 31 | | -n | \-\-nowrite | print the popularity values without storing them | 32 | 33 | ## Import 34 | All imported songs will automatically have a popularity attribute and value assigned to them if the plugin is enabled. 35 | 36 | ## Query 37 | As the popularity of a song is a value between 0 and 100, you could filter your library like this in order to list all tracks that have a popularity of at least 20: 38 | 39 | $ beet list -f '$artist - $title ($popularity)' popularity:20.. 40 | Bon Jovi - Happy Now (20) 41 | The Doors - Unhappy Girl (40) 42 | Kygo - Happy Birthday (59) 43 | 44 | This is especially useful in combination with the [Smart Playlist Plugin](https://beets.readthedocs.io/en/latest/plugins/smartplaylist.html). Adding this to your configuration would allow you to have continuously updated playlists of the most popular songs in your library: 45 | 46 | ```yaml 47 | smartplaylist: 48 | playlist_dir: ~/Music/Playlists 49 | playlists: 50 | - name: popular.m3u 51 | query: 'popularity:70..' 52 | 53 | - name: popular_rock.m3u 54 | query: 'popularity:60.. genre:Rock' 55 | ``` 56 | -------------------------------------------------------------------------------- /beetsplug/popularity.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from beets.plugins import BeetsPlugin 4 | from beets.dbcore import types 5 | import beets.ui as ui 6 | import json 7 | import requests 8 | 9 | 10 | class Popularity(BeetsPlugin): 11 | 12 | def __init__(self): 13 | super(Popularity, self).__init__() 14 | self.item_types = {'popularity': types.INTEGER} 15 | self.register_listener('write', self._on_write) 16 | self.API_URL = 'https://api.deezer.com/search' 17 | #self.API_URL = 'https://api.spotify.com/v1/search' 18 | 19 | def commands(self): 20 | command = ui.Subcommand('popularity', 21 | help='fetch popularity values', 22 | aliases=['pop']) 23 | command.func = self._command 24 | command.parser.add_album_option() 25 | command.parser.add_option( 26 | '-n', '--nowrite', action='store_true', 27 | dest='nowrite', default=False, 28 | help='print the popularity values without storing them') 29 | return [command] 30 | 31 | def _command(self, lib, opts, args): 32 | # search library for items matching the query 33 | items = [] 34 | if opts.album: 35 | albums = list(lib.albums(ui.decargs(args))) 36 | for album in albums: 37 | items += album.items() 38 | else: 39 | items = lib.items(ui.decargs(args)) 40 | 41 | # query and set popularity value for all matching items 42 | for item in items: 43 | self._set_popularity(item, opts.nowrite) 44 | 45 | def _on_write(self, item, path, tags): 46 | # query and set popularity value for the item that is to be imported 47 | self._set_popularity(item, False) 48 | 49 | def _set_popularity(self, item, nowrite): 50 | # query Spotify API 51 | query = 'artist:"' + item.artist + '" album:"' + item.album + '" track:"' + item.title + '"' 52 | #query = item.artist + ' ' + item.album + ' ' + item.title 53 | payload = {'q': query, 'order': 'RANKING'} 54 | #payload = {'q': query, 'type': 'track', 'limit': '1'} 55 | response = requests.get(self.API_URL, params=payload) 56 | 57 | try: 58 | # raise an exception for bad response status codes 59 | response.raise_for_status() 60 | 61 | # load response as json 62 | response_json = json.loads(response.text) 63 | tracks = response_json["data"] 64 | #tracks = response_json["tracks"]["items"] 65 | 66 | # raise an exception if the query returned no tracks 67 | if not tracks: 68 | raise EmptyResponseError() 69 | 70 | popularity = round(tracks[0]["rank"] / 10000) 71 | #popularity = tracks[0]["popularity"] 72 | self._log.info( 73 | u'{0.artist} - {0.album} - {0.title}: {1}', item, popularity) 74 | 75 | # store the popularity value as a flexible attribute 76 | if not nowrite: 77 | item.popularity = popularity 78 | item.store() 79 | 80 | except requests.exceptions.HTTPError: 81 | self._log.warning(u'Bad status code in API response') 82 | except EmptyResponseError: 83 | self._log.debug( 84 | u'{0.title} - {0.artist} not found', item) 85 | 86 | 87 | class EmptyResponseError(Exception): 88 | pass 89 | --------------------------------------------------------------------------------