diff --git a/.gitignore b/.gitignore index 56d1b61..f64f121 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,7 @@ -# Created by .ignore support plugin (hsz.mobi) - # Project tmp/ coverage/ BEETSDIR/* -!BEETSDIR/config.yaml ### Python template diff --git a/BEETSDIR/config.yaml b/BEETSDIR/config.yaml deleted file mode 100644 index a4819fd..0000000 --- a/BEETSDIR/config.yaml +++ /dev/null @@ -1,60 +0,0 @@ -# DEVELOPMENT CONFIGURATION FILE -# USAGE: export environment variable BEETSDIR=/path/to/BEETSDIR -# then: `beets config -p` should list this file - -directory: /Volumes/J/Music/ -library: ~/.config/beets/real_library.db -#directory: Music -#library: library.db - -asciify_paths: yes -id3v23: yes - -pluginpath: - - ~/Documents/Projects/Python/BeetsPluginXtractor/beetsplug - -plugins: - - info - - xtractor - -format_item: "[format:$format][bpm:$bpm][gender:$gender] ::: $artist:$title" - -import: - copy: yes - autotag: no - -xtractor: - auto: no - dry-run: no - write: yes - threads: 1 - force: no - quiet: no - items_per_run: 1 - keep_output: yes - keep_profiles: no - output_path: /Users/jackisback/Documents/Projects/Python/BeetsPluginXtractor/BEETSDIR/xtraction - low_level_extractor: /Users/jackisback/Documents/Projects/Other/extractors/beta5/essentia_streaming_extractor_music - high_level_extractor: /Users/jackisback/Documents/Projects/Other/extractors/beta5/essentia_streaming_extractor_music_svm - low_level_profile: - outputFormat: json - outputFrames: 0 - high_level_profile: - outputFormat: json - highlevel: - compute: 1 - svm_models: - - /Users/jackisback/Documents/Projects/Other/extractors/svm_models_beta5/danceability.history - - /Users/jackisback/Documents/Projects/Other/extractors/svm_models_beta5/gender.history - - /Users/jackisback/Documents/Projects/Other/extractors/svm_models_beta5/genre_rosamerica.history - - /Users/jackisback/Documents/Projects/Other/extractors/svm_models_beta5/mood_acoustic.history - - /Users/jackisback/Documents/Projects/Other/extractors/svm_models_beta5/mood_aggressive.history - - /Users/jackisback/Documents/Projects/Other/extractors/svm_models_beta5/mood_electronic.history - - /Users/jackisback/Documents/Projects/Other/extractors/svm_models_beta5/mood_happy.history - - /Users/jackisback/Documents/Projects/Other/extractors/svm_models_beta5/mood_party.history - - /Users/jackisback/Documents/Projects/Other/extractors/svm_models_beta5/mood_relaxed.history - - /Users/jackisback/Documents/Projects/Other/extractors/svm_models_beta5/mood_sad.history - - /Users/jackisback/Documents/Projects/Other/extractors/svm_models_beta5/voice_instrumental.history - chromaprint: - compute: 0 - diff --git a/MANIFEST.in b/MANIFEST.in index 8b46fe1..733239c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,4 @@ prune test include LICENSE.txt include README.md -include beetsplug/xtractor/version.py -include beetsplug/xtractor/config_default.yml \ No newline at end of file +include beetsplug/xtractor/config_default.yml diff --git a/README.md b/README.md index e69bb3e..282a181 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ [![Build Status](https://travis-ci.org/adamjakab/BeetsPluginXtractor.svg?branch=master)](https://travis-ci.org/adamjakab/BeetsPluginXtractor) [![Coverage Status](https://coveralls.io/repos/github/adamjakab/BeetsPluginXtractor/badge.svg?branch=master)](https://coveralls.io/github/adamjakab/BeetsPluginXtractor?branch=master) [![PyPi](https://img.shields.io/pypi/v/beets-xtractor.svg)](https://pypi.org/project/beets-xtractor/) +[![PyPI pyversions](https://img.shields.io/pypi/pyversions/beets-xtractor.svg)](https://pypi.org/project/beets-xtractor/) +[![MIT license](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE.txt) +# Xtractor (Beets Plugin) -# Xtractor (beets plugin) +The *beets-xtractor* plugin lets you, through the use of the [Essentia](https://essentia.upf.edu/index.html) extractors, to obtain low and high level musical information about your songs. -The *beets-xtractor* plugin lets you use the extractors of the [Essentia](https://essentia.upf.edu/index.html) project developed by the Music Technology Group. - - -*NOTE: This plugin is highly unstable and not at all documented! Use it at your own risk* +Currently, the following attributes are extracted for each library item: `average_loudness`, `bpm`, `danceable`, `gender`, `genre_rosamerica`, `voice_instrumental`, `mood_acoustic`, `mood_aggressive`, `mood_electronic`, `mood_happy`, `mood_party`, `mood_relaxed`, `mood_sad` (some more to come soon) ## Installation @@ -17,16 +17,138 @@ The plugin can be installed via: ```shell script $ pip install beets-xtractor ``` +and activated the usual way by adding `xtractor` to the list of plugins in your configuration: + +```yaml +plugins: + - xtractor +``` +### Install the Essentia extractors +You will also need the two binary extractors from the [Essentia project](#credits). They are called: -## References -[Essentia](https://essentia.upf.edu/index.html) +- streaming_extractor_music +- streaming_extractor_music_svm + +Unfortunately, only the first extractor is readily available for download whilst to have the second one you will need to compile it yourself. The [official installation documentation](https://essentia.upf.edu/installing.html) is somewhat complex but with some cross searching on internet you will make it. If you are stuck you can use the [Issue tracker](https://github.com/adamjakab/BeetsPluginXtractor/issues). Make sure you compile with Gaia support (`--with-gaia`) otherwise your second `streaming_extractor_music_svm` will not be built. + +### Download the SVM models +The second extractor uses prebuilt trained models for prediction. You need to download these from here: [SVM Models](https://essentia.upf.edu/svm_models/) I suggest that you download the more recent beta5 version. This means that your binaries must match this version. Put the downloaded models in any folder from which they can be accessed. + + +## Configuration +All your configuration will need to go under the `xtractor` key. This is what your configuration should look like: + +```yaml +xtractor: + auto: no + dry-run: no + write: yes + threads: 1 + force: no + quiet: no + items_per_run: 0 + keep_output: yes + keep_profiles: no + output_path: /mnt/data/xtraction_data + low_level_extractor: /mnt/data/extractors/beta5/streaming_extractor_music + high_level_extractor: /mnt/data/extractors/beta5/streaming_extractor_music_svm + high_level_profile: + highlevel: + svm_models: + - /mnt/data/extractors/beta5/svm_models/danceability.history + - /mnt/data/extractors/beta5/svm_models/gender.history + - /mnt/data/extractors/beta5/svm_models/genre_rosamerica.history + - /mnt/data/extractors/beta5/svm_models/mood_acoustic.history + - /mnt/data/extractors/beta5/svm_models/mood_aggressive.history + - /mnt/data/extractors/beta5/svm_models/mood_electronic.history + - /mnt/data/extractors/beta5/svm_models/mood_happy.history + - /mnt/data/extractors/beta5/svm_models/mood_party.history + - /mnt/data/extractors/beta5/svm_models/mood_relaxed.history + - /mnt/data/extractors/beta5/svm_models/mood_sad.history + - /mnt/data/extractors/beta5/svm_models/voice_instrumental.history +``` + +First of all, you will need adjust all paths. Put the paths of the extractor binaries in `low_level_extractor`and `high_level_extractor`, substitute the location of the SVM models with your local path under the `svm_models` desction. And finally, set the `output_path` to indicate where the extracted data files will be stored. I you do not set this, a temporary path will be used. + +By default both `keep_output` and `keep_profile` options are set to `no`. This means that after extraction (and the storage of the important information) the profile files used to pass to the extractors and the json files created by the extractors will be deleted. There are various reasons you might want to keep these files. One is for debugging purposes. Another is to see what else is in these files (there is a lot) and maybe to use them with some other projects of yours. Lastly, you might want to keep these because the plugin only extracts data if these files are not present. If you store them, on a successive extraction, the plugin will skip the extraction and use these files (they are named by `mb_trackid`) - speeding up the process a lot. + +The `items_per_run` set to 0 will execute on all items. If you want to limit the number of items per execution (maybe because you want to run a nightly cron job in a limited timeframe) you can use this. + +The `force` option instructs the plugin to execute on items which already have the required properties. + +The `threads` option sets the number of concurrent executions. If you remove this option the number of cores present on your machine will be used. The extraction is quite a CPU intensive process so there might be cases when you want to limit it to just 1. + +The `write` option instructs the plugin to write the extracted attributes to the media file right away. Note that only `bpm` is actually written to the media file, all the other attributes are flex attributes and are only stored in the database. + +The `dry-run` option shows what would be done without actually doing it. + +**NOTE**: Please note that the `auto` option is not yet implemented. For now you will have to call the xtractor plugin manually. + +## Usage + +Invoke the plugin as: -[SVM Models](https://essentia.upf.edu/svm_models/) + $ beet xtractor [options] [QUERY...] + +For a more verbose reporting use the `-v` flag on `beet`: -[Essentia Licensing](https://essentia.upf.edu/licensing_information.html) + $ beet -v xtractor [options] [QUERY...] + +The plugin has also got a shorthand `xt` so you can also invoke it like this: + + $ beet xt [options] [QUERY...] + + +The following command line options are available: + +**--dry-run [-d]**: Only show what would be done - displays the extracted values but does not store them in the library. + +**--write [-w]**: Write the values (bpm only) to the media files. + +**--threads=THREADS [-t THREADS]**: The number of concurrently running executions. + +**--force [-f]**: Force the analysis of all items (skip attribute checks). + +**--count-only [-c]**: Show the number of items to be processed and exit. Extraction will not be executed. + +**--quiet [-q]**: Run without any output. + +**--version [-v]**: Display the version number of the plugin. Useful when you need to report some issue and you have to state the version of the plugin you are using. + +These command line options will override those specified in the configuration file. + + +## Issues +- If something is not working as expected please use the Issue tracker. +- If the documentation is not clear please use the Issue tracker. +- If you have a feature request please use the Issue tracker. +- In any other situation please use the Issue tracker. + + +## Other plugins by the same author + +- [beets-goingrunning](https://github.com/adamjakab/BeetsPluginGoingRunning) +- [beets-xtractor](https://github.com/adamjakab/BeetsPluginXtractor) +- [beets-yearfixer](https://github.com/adamjakab/BeetsPluginYearFixer) +- [beets-autofix](https://github.com/adamjakab/BeetsPluginAutofix) +- [beets-describe](https://github.com/adamjakab/BeetsPluginDescribe) +- [beets-bpmanalyser](https://github.com/adamjakab/BeetsPluginBpmAnalyser) +- [beets-template](https://github.com/adamjakab/BeetsPluginTemplate) + + +## Credits +Essentia is an open-source C++ library with Python bindings for audio analysis and audio-based music information retrieval. It is released under the Affero GPLv3 license and is also available under proprietary license upon request. This plugin is just a mere wrapper around this library. [Learn more about the Essentia project](http://essentia.upf.edu) + + +## References +- [Essentia](https://essentia.upf.edu/index.html) +- [SVM Models](https://essentia.upf.edu/svm_models/) +- [Essentia Licensing](https://essentia.upf.edu/licensing_information.html) +- [MTG Github - Music Technology Group](https://github.com/MTG) +- [Acousticbrainz Downloads](https://acousticbrainz.org/download) -[MTG Github - Music Technology Group](https://github.com/MTG) -[Acousticbrainz Downloads](https://acousticbrainz.org/download) +## Final Remarks +Enjoy! diff --git a/beetsplug/xtractor/about.py b/beetsplug/xtractor/about.py new file mode 100644 index 0000000..81163f5 --- /dev/null +++ b/beetsplug/xtractor/about.py @@ -0,0 +1,19 @@ +# Copyright: Copyright (c) 2020., Adam Jakab +# Author: Adam Jakab +# License: See LICENSE.txt + +__author__ = u'Adam Jakab' +__email__ = u'adam@jakab.pro' +__copyright__ = u'Copyright (c) 2020, {} <{}>'.format(__author__, __email__) +__license__ = u'License :: OSI Approved :: MIT License' +__version__ = u'0.2.3' +__status__ = u'Kickstarted' + +__PACKAGE_TITLE__ = u'Xtractor' +__PACKAGE_NAME__ = u'beets-xtractor' +__PACKAGE_DESCRIPTION__ = u'A beets plugin that extracts music descriptors ' \ + u'from your audio files', +__PACKAGE_URL__ = u'https://github.com/adamjakab/BeetsPluginXtractor' +__PLUGIN_NAME__ = u'xtractor' +__PLUGIN_ALIAS__ = u'xt' +__PLUGIN_SHORT_DESCRIPTION__ = u'get more out of your music...' diff --git a/beetsplug/xtractor/command.py b/beetsplug/xtractor/command.py index c49dca2..143dd85 100644 --- a/beetsplug/xtractor/command.py +++ b/beetsplug/xtractor/command.py @@ -1,12 +1,9 @@ # Copyright: Copyright (c) 2020., Adam Jakab -# # Author: Adam Jakab -# Created: 3/13/20, 12:17 AM # License: See LICENSE.txt import hashlib import json -import logging import os import tempfile from concurrent import futures @@ -17,18 +14,9 @@ from beets import dbcore from beets.library import Library, Item, parse_query_string from beets.ui import Subcommand, decargs -from confuse import Subview - +from beets.util.confit import Subview from beetsplug.xtractor import helper -# Module methods -log = logging.getLogger('beets.xtractor') - -# The plugin -__PLUGIN_NAME__ = u'xtractor' -__PLUGIN_SHORT_NAME__ = u'xt' -__PLUGIN_SHORT_DESCRIPTION__ = u'get more out of your music...' - class XtractorCommand(Subcommand): config: Subview = None @@ -61,19 +49,24 @@ def __init__(self, config): self.cfg_quiet = cfg.get("quiet") self.cfg_items_per_run = cfg.get("items_per_run") - self.parser = OptionParser(usage='beet xtractor [options] [QUERY...]') + self.parser = OptionParser( + usage='beet {plg} [options] [QUERY...]'.format( + plg=helper.plg_ns['__PLUGIN_NAME__'] + )) self.parser.add_option( '-d', '--dry-run', action='store_true', dest='dryrun', default=self.cfg_dry_run, - help=u'[default: {}] display the bpm values but do not update the library items'.format( + help=u'[default: {}] only show what would be done' + u'library items'.format( self.cfg_dry_run) ) self.parser.add_option( '-w', '--write', action='store_true', dest='write', default=self.cfg_write, - help=u'[default: {}] write the bpm values to the media files'.format( + help=u'[default: {}] write the extracted values (bpm) to the media ' + u'files'.format( self.cfg_write) ) @@ -112,9 +105,10 @@ def __init__(self, config): # Keep this at the end super(XtractorCommand, self).__init__( parser=self.parser, - name=__PLUGIN_NAME__, - help=__PLUGIN_SHORT_DESCRIPTION__, - aliases=[__PLUGIN_SHORT_NAME__] + name=helper.plg_ns['__PLUGIN_NAME__'], + aliases=[helper.plg_ns['__PLUGIN_ALIAS__']] if + helper.plg_ns['__PLUGIN_ALIAS__'] else [], + help=helper.plg_ns['__PLUGIN_SHORT_DESCRIPTION__'] ) def func(self, lib: Library, options, arguments): @@ -177,7 +171,7 @@ def find_items_to_analyse(self): unprocessed_items_query = dbcore.query.OrQuery(subqueries) combined_query = dbcore.query.AndQuery([parsed_query, unprocessed_items_query]) - log.debug("Combined query: {}".format(combined_query)) + self._say("Combined query: {}".format(combined_query)) # Get the library items self.items_to_analyse = self.lib.items(combined_query, parsed_sort) @@ -226,7 +220,7 @@ def _run_analysis_high_level(self, item): try: target_map = self.config["high_level_targets"] audiodata = helper.extract_from_output(output_path, target_map) - log.debug("Audiodata(High): {}".format(audiodata)) + self._say("Audiodata(High): {}".format(audiodata)) except FileNotFoundError as e: self._say("File not found: {0}".format(e)) return @@ -259,7 +253,7 @@ def _run_analysis_low_level(self, item): try: target_map = self.config["low_level_targets"] audiodata = helper.extract_from_output(output_path, target_map) - log.debug("Audiodata(Low): {}".format(audiodata)) + self._say("Audiodata(Low): {}".format(audiodata)) except FileNotFoundError as e: self._say("File not found: {0}".format(e)) return @@ -272,22 +266,24 @@ def _run_analysis_low_level(self, item): def _run_essentia_extractor(self, extractor_path, input_path, output_path, profile_path): if os.path.isfile(output_path): - log.debug("Output exists: {0}".format(output_path)) + self._say("Output exists: {0}".format(output_path)) return - log.debug("Extractor: {0}".format(extractor_path)) - log.debug("Input: {0}".format(input_path)) - log.debug("Output: {0}".format(output_path)) - log.debug("Profile: {0}".format(profile_path)) + self._say("Extractor: {0}".format(extractor_path)) + self._say("Input: {0}".format(input_path)) + self._say("Output: {0}".format(output_path)) + self._say("Profile: {0}".format(profile_path)) - proc = Popen([extractor_path, input_path, output_path, profile_path], stdout=PIPE, stderr=PIPE) + proc = Popen([extractor_path, input_path, output_path, profile_path], + stdout=PIPE, stderr=PIPE) stdout, stderr = proc.communicate() - log.debug("The process exited with code: {0}".format(proc.returncode)) - log.debug("Process stdout: {0}".format(stdout.decode())) - log.debug("Process stderr: {0}".format(stderr.decode())) + self._say("The process exited with code: {0}".format(proc.returncode)) + self._say("Process stdout: {0}".format(stdout.decode())) + self._say("Process stderr: {0}".format(stderr.decode())) - # Make sure file is encoded correctly (sometimes media files have funky tags) + # Make sure file is encoded correctly (sometimes media files have + # funky tags) helper.asciify_file_content(output_path) def _execute_on_each_items(self, items, func): @@ -328,7 +324,8 @@ def _get_extraction_output_path(self): output_path = self.config["output_path"].as_filename() else: - output_path = os.path.join(tempfile.gettempdir(), __PLUGIN_NAME__) + output_path = os.path.join(tempfile.gettempdir(), + helper.plg_ns['__PLUGIN_NAME__']) if not os.path.isdir(output_path): os.makedirs(output_path) @@ -371,17 +368,18 @@ def _get_extractor_path(self, level): extractor_path = self.config[extractor_key].as_filename() if not os.path.isfile(extractor_path): - raise FileNotFoundError("Extractor({}) is not found!".format(extractor_path)) + raise FileNotFoundError("Extractor({}) is not found!".format( + extractor_path)) return extractor_path def show_version_information(self): - from beetsplug.xtractor.version import __version__ - self._say( - "Xtractor(beets-{0}) plugin for Beets: v{1}".format(__PLUGIN_NAME__, __version__)) - - def _say(self, msg): - if not self.cfg_quiet: - log.info(msg) - else: - log.debug(msg) + self._say("{pt}({pn}) plugin for Beets: v{ver}".format( + pt=helper.plg_ns['__PACKAGE_TITLE__'], + pn=helper.plg_ns['__PACKAGE_NAME__'], + ver=helper.plg_ns['__version__'] + ), log_only=False) + + @staticmethod + def _say(msg, log_only=True, is_error=False): + helper.say(msg, log_only, is_error) diff --git a/beetsplug/xtractor/config_default.yml b/beetsplug/xtractor/config_default.yml index f39f3c1..3d2d193 100644 --- a/beetsplug/xtractor/config_default.yml +++ b/beetsplug/xtractor/config_default.yml @@ -11,6 +11,7 @@ low_level_targets: average_loudness: path: "lowlevel.average_loudness" type: float + required: yes bpm: path: "rhythm.bpm" type: integer @@ -78,8 +79,8 @@ high_level_targets: path: "highlevel.mood_sad.all.sad" type: float required: yes -low_level_extractor: /usr/lib/extractors/beta5/essentia_streaming_extractor_music -high_level_extractor: /usr/lib/extractors/beta5/essentia_streaming_extractor_music_svm +low_level_extractor: /your/path/to/streaming_extractor_music +high_level_extractor: /your/path/to/streaming_extractor_music_svm low_level_profile: outputFormat: json outputFrames: 0 @@ -88,16 +89,16 @@ high_level_profile: highlevel: compute: 1 svm_models: - - /usr/lib/extractors/svm_models_beta5/danceability.history - - /usr/lib/extractors/svm_models_beta5/gender.history - - /usr/lib/extractors/svm_models_beta5/genre_rosamerica.history - - /usr/lib/extractors/svm_models_beta5/mood_acoustic.history - - /usr/lib/extractors/svm_models_beta5/mood_aggressive.history - - /usr/lib/extractors/svm_models_beta5/mood_electronic.history - - /usr/lib/extractors/svm_models_beta5/mood_happy.history - - /usr/lib/extractors/svm_models_beta5/mood_party.history - - /usr/lib/extractors/svm_models_beta5/mood_relaxed.history - - /usr/lib/extractors/svm_models_beta5/mood_sad.history - - /usr/lib/extractors/svm_models_beta5/voice_instrumental.history + - /your/path/to/danceability.history + - /your/path/to/gender.history + - /your/path/to/genre_rosamerica.history + - /your/path/to/mood_acoustic.history + - /your/path/to/mood_aggressive.history + - /your/path/to/mood_electronic.history + - /your/path/to/mood_happy.history + - /your/path/to/mood_party.history + - /your/path/to/mood_relaxed.history + - /your/path/to/mood_sad.history + - /your/path/to/voice_instrumental.history chromaprint: compute: 0 \ No newline at end of file diff --git a/beetsplug/xtractor/helper.py b/beetsplug/xtractor/helper.py index 8d0d87d..47741fb 100644 --- a/beetsplug/xtractor/helper.py +++ b/beetsplug/xtractor/helper.py @@ -1,17 +1,26 @@ # Copyright: Copyright (c) 2020., Adam Jakab -# # Author: Adam Jakab -# Created: 3/13/20, 12:17 AM # License: See LICENSE.txt import json +import logging import os from beets.util.confit import Subview +# Get values as: plg_ns['__PLUGIN_NAME__'] +plg_ns = {} +about_path = os.path.join(os.path.dirname(__file__), u'about.py') +with open(about_path) as about_file: + exec(about_file.read(), plg_ns) + +__logger__ = logging.getLogger( + 'beets.{plg}'.format(plg=plg_ns['__PLUGIN_NAME__'])) + def extract_from_output(output_path, target_map: Subview): - """extracts data from the low level json file as mapped out in the `low_level_targets` configuration key + """extracts data from the low level json file as mapped out in the + `low_level_targets` configuration key """ data = {} @@ -61,3 +70,10 @@ def asciify_file_content(file_path): if content_orig != content_enc: with open(file_path, 'w', encoding="ascii") as content_file: content_file.write(content_enc) + + +def say(msg, log_only=True, is_error=False): + _level = logging.DEBUG + _level = _level if log_only else logging.INFO + _level = _level if not is_error else logging.ERROR + __logger__.log(level=_level, msg=msg) diff --git a/beetsplug/xtractor/version.py b/beetsplug/xtractor/version.py deleted file mode 100644 index e8bb6d4..0000000 --- a/beetsplug/xtractor/version.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright: Copyright (c) 2020., Adam Jakab -# -# Author: Adam Jakab -# Created: 3/13/20, 12:17 AM -# License: See LICENSE.txt - -__version__ = '0.2.2' diff --git a/setup.py b/setup.py index 2282f1d..525bab2 100644 --- a/setup.py +++ b/setup.py @@ -15,19 +15,19 @@ # The text of the README file README = (HERE / "README.md").read_text() -main_ns = {} -ver_path = convert_path('beetsplug/xtractor/version.py') -with open(ver_path) as ver_file: - exec(ver_file.read(), main_ns) +plg_ns = {} +about_path = convert_path('beetsplug/xtractor/about.py') +with open(about_path) as about_file: + exec(about_file.read(), plg_ns) # Setup setup( - name='beets-xtractor', - version=main_ns['__version__'], - description='A beets plugin for getting something more out of your music...', - author='Adam Jakab', - author_email='adam@jakab.pro', - url='https://github.com/adamjakab/BeetsPluginEssentiaExtractor', + name=plg_ns['__PACKAGE_NAME__'], + version=plg_ns['__version__'], + description=plg_ns['__PACKAGE_DESCRIPTION__'], + author=plg_ns['__author__'], + author_email=plg_ns['__email__'], + url=plg_ns['__PACKAGE_URL__'], license='MIT', long_description=README, long_description_content_type='text/markdown', @@ -35,28 +35,32 @@ include_package_data=True, test_suite='test', - packages=['beetsplug.xtractor'], + python_requires='>=3.6', + install_requires=[ 'beets>=1.4.9', - 'confuse', 'PyYAML' ], tests_require=[ - 'pytest', - 'nose', - 'coverage', - 'mock', - 'six' + 'pytest', 'nose', 'coverage', + 'mock', 'six', 'yaml', ], + # Extras needed during testing + extras_require={ + 'tests': [], + }, + classifiers=[ 'Topic :: Multimedia :: Sound/Audio', 'License :: OSI Approved :: MIT License', 'Environment :: Console', 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', ], ) diff --git a/test/00_completion_test.py b/test/00_completion_test.py index 462bbb4..b8072b9 100644 --- a/test/00_completion_test.py +++ b/test/00_completion_test.py @@ -3,31 +3,57 @@ # Author: Adam Jakab # Created: 3/12/20, 11:42 PM # License: See LICENSE.txt -import re -from test.helper import TestHelper, Assertions, PLUGIN_NAME, PLUGIN_SHORT_NAME, PLUGIN_SHORT_DESCRIPTION, capture_stdout +from beetsplug.xtractor import about + +from test.helper import TestHelper, Assertions, \ + PLUGIN_NAME, PLUGIN_SHORT_DESCRIPTION, \ + PACKAGE_NAME, PACKAGE_TITLE, PLUGIN_VERSION, \ + capture_log + +plg_log_ns = 'beets.{}'.format(PLUGIN_NAME) class CompletionTest(TestHelper, Assertions): - """Test invocation of ``beet goingrunning`` with this plugin. - Only ensures that command does not fail. + """Test invocation of the plugin and basic package health. """ - def test_application(self): - with capture_stdout() as out: - self.runcli() - - expected = "{0} \\({1}\\) +?{2}".format( - re.escape(PLUGIN_NAME), - re.escape(PLUGIN_SHORT_NAME), - re.escape(PLUGIN_SHORT_DESCRIPTION) - ) + def test_about_descriptor_file(self): + self.assertTrue(hasattr(about, "__author__")) + self.assertTrue(hasattr(about, "__email__")) + self.assertTrue(hasattr(about, "__copyright__")) + self.assertTrue(hasattr(about, "__license__")) + self.assertTrue(hasattr(about, "__version__")) + self.assertTrue(hasattr(about, "__status__")) + self.assertTrue(hasattr(about, "__PACKAGE_TITLE__")) + self.assertTrue(hasattr(about, "__PACKAGE_NAME__")) + self.assertTrue(hasattr(about, "__PACKAGE_DESCRIPTION__")) + self.assertTrue(hasattr(about, "__PACKAGE_URL__")) + self.assertTrue(hasattr(about, "__PLUGIN_NAME__")) + self.assertTrue(hasattr(about, "__PLUGIN_ALIAS__")) + self.assertTrue(hasattr(about, "__PLUGIN_SHORT_DESCRIPTION__")) - self.assertRegex(out.getvalue(), expected) + def test_application(self): + output = self.runcli() + self.assertIn(PLUGIN_NAME, output) + self.assertIn(PLUGIN_SHORT_DESCRIPTION, output) def test_application_plugin_list(self): - with capture_stdout() as out: - self.runcli("version") - - self.assertIn("plugins: {0}".format(PLUGIN_NAME), out.getvalue()) - + output = self.runcli("version") + self.assertIn("plugins: {0}".format(PLUGIN_NAME), output) + + def test_run_plugin(self): + with capture_log(plg_log_ns) as logs: + self.runcli(PLUGIN_NAME) + self.assertIn("xtractor: No items to process", "\n".join(logs)) + + def test_plugin_version(self): + with capture_log(plg_log_ns) as logs: + self.runcli(PLUGIN_NAME, "--version") + + versioninfo = "{pt}({pn}) plugin for Beets: v{ver}".format( + pt=PACKAGE_TITLE, + pn=PACKAGE_NAME, + ver=PLUGIN_VERSION + ) + self.assertIn(versioninfo, "\n".join(logs)) diff --git a/test/helper.py b/test/helper.py index 5c58547..e5e523f 100644 --- a/test/helper.py +++ b/test/helper.py @@ -1,11 +1,7 @@ # Copyright: Copyright (c) 2020., Adam Jakab -# # Author: Adam Jakab -# Created: 3/13/20, 12:17 AM # License: See LICENSE.txt -# -# References: https://docs.python.org/3/library/unittest.html -# + import os import shutil import sys @@ -30,13 +26,16 @@ from six import StringIO from beetsplug import xtractor +from beetsplug.xtractor import helper logging.getLogger('beets').propagate = True # Values -PLUGIN_NAME = 'xtractor' -PLUGIN_SHORT_NAME = 'xt' -PLUGIN_SHORT_DESCRIPTION = 'get more out of your music...' +PLUGIN_NAME = helper.plg_ns['__PLUGIN_NAME__'] +PLUGIN_SHORT_DESCRIPTION = helper.plg_ns['__PLUGIN_SHORT_DESCRIPTION__'] +PLUGIN_VERSION = helper.plg_ns['__version__'] +PACKAGE_NAME = helper.plg_ns['__PACKAGE_NAME__'] +PACKAGE_TITLE = helper.plg_ns['__PACKAGE_TITLE__'] class LogCapture(logging.Handler):