PICARD-2422: Remove all AcousticBrainz extraction and submission code

This commit is contained in:
Philipp Wolfer
2022-02-16 12:00:05 +01:00
parent 5e3fa405af
commit aeb1a7d5a8
31 changed files with 11285 additions and 14298 deletions

View File

@@ -1,290 +0,0 @@
# -*- coding: utf-8 -*-
#
# Picard, the next-generation MusicBrainz tagger
#
# Copyright (C) 2014 Music Technology Group - Universitat Pompeu Fabra
# Copyright (C) 2020-2021 Gabriel Ferreira
# Copyright (C) 2021 Laurent Monin
# Copyright (C) 2021 Philipp Wolfer
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
from collections import (
defaultdict,
namedtuple,
)
from concurrent.futures import Future
from functools import partial
import json
import os
from tempfile import NamedTemporaryFile
from picard import log
from picard.acousticbrainz.extractor import (
check_extractor_version,
precompute_extractor_sha,
)
from picard.config import get_config
from picard.const import (
ACOUSTICBRAINZ_HOST,
ACOUSTICBRAINZ_PORT,
EXTRACTOR_NAMES,
)
from picard.util import (
find_executable,
load_json,
run_executable,
)
from picard.util.thread import run_task
from picard.webservice import ratecontrol
ratecontrol.set_minimum_delay((ACOUSTICBRAINZ_HOST, ACOUSTICBRAINZ_PORT), 1000)
ABExtractorProperties = namedtuple('ABExtractorProperties', ('path', 'version', 'sha', 'mtime_ns'))
class ABExtractor:
def __init__(self):
self._init_cache()
def _init_cache(self):
self.cache = defaultdict(lambda: None)
def get(self, config=None):
if not config:
config = get_config()
if not config.setting["use_acousticbrainz"]:
log.debug('ABExtractor: AcousticBrainz disabled')
return None
extractor_path = config.setting["acousticbrainz_extractor"]
if not extractor_path:
extractor_path = find_extractor()
else:
extractor_path = find_executable(extractor_path)
if not extractor_path:
log.debug('ABExtractor: cannot find a path to extractor binary')
return None
try:
statinfo = os.stat(extractor_path)
mtime_ns = statinfo.st_mtime_ns
except OSError as exc:
log.warning('ABExtractor: cannot stat extractor: %s', exc)
return None
# check if we have this in cache already
cached = self.cache[(extractor_path, mtime_ns)]
if cached is not None:
log.debug('ABExtractor: cached: %r', cached)
return cached
# create a new cache entry
try:
version = check_extractor_version(extractor_path)
if version:
sha = precompute_extractor_sha(extractor_path)
result = ABExtractorProperties(
path=extractor_path,
version=version,
sha=sha,
mtime_ns=mtime_ns
)
# clear the cache, we keep only one entry
self._init_cache()
self.cache[(result.path, result.mtime_ns)] = result
log.debug('ABExtractor: caching: %r', result)
return result
else:
raise Exception("check_extractor_version(%r) returned None" % extractor_path)
except Exception as exc:
log.warning('ABExtractor: failed to get version or sha: %s', exc)
return None
def path(self, config=None):
result = self.get(config)
return result.path if result else None
def version(self, config=None):
result = self.get(config)
return result.version if result else None
def sha(self, config=None):
result = self.get(config)
return result.sha if result else None
def available(self, config=None):
return self.get(config) is not None
def find_extractor():
return find_executable(*EXTRACTOR_NAMES)
def ab_check_version(extractor):
extractor_path = find_executable(extractor)
return check_extractor_version(extractor_path)
def ab_feature_extraction(tagger, recording_id, input_path, extractor_callback):
# Fetch existing features from AB server to check for duplicates before extracting
tagger.webservice.get(
host=ACOUSTICBRAINZ_HOST,
port=ACOUSTICBRAINZ_PORT,
path="/%s/low-level" % recording_id,
handler=partial(run_extractor, tagger, input_path, extractor_callback),
priority=True,
important=False,
parse_response_type=None
)
def ab_extractor_callback(tagger, file, result, error):
file.clear_pending()
ab_metadata_file, result, error = result.result() if isinstance(result, Future) else result
if "Writing results" in error and ab_metadata_file:
file.acousticbrainz_features_file = ab_metadata_file
log.debug("AcousticBrainz extracted features of recording %s: %s" %
(file.metadata["musicbrainz_recordingid"], file.filename,))
# Submit results
ab_submit_features(tagger, file)
elif 'Duplicate' in error:
log.debug("AcousticBrainz already has an entry for recording %s: %s" %
(file.metadata["musicbrainz_recordingid"], file.filename,))
file.acousticbrainz_is_duplicate = True
else:
# Something went wrong
log.debug("AcousticBrainz extraction failed with error: %s" % error)
file.acousticbrainz_error = True
if result == 1:
log.warning("AcousticBrainz extraction failed: %s" % file.filename)
elif result == 2:
log.warning(
"AcousticBrainz extraction failed due to missing a MusicBrainz Recording ID: %s" % file.filename)
else:
log.warning("AcousticBrainz extraction failed due to an unknown error: %s" % file.filename)
file.update()
def run_extractor(tagger, input_path, extractor_callback, response, reply, error, main_thread=False):
duplicate = False
# Check if AcousticBrainz server answered with the json file for the recording id
if not error:
# If it did, mark file as duplicate and skip extraction
try:
load_json(response)
duplicate = True
except json.JSONDecodeError:
pass
if duplicate:
# If an entry for the same recording ID already exists
# using the same extractor version exists, skip extraction
results = (None, 0, "Duplicate")
extractor_callback(results, None)
else:
if main_thread:
# Run extractor on main thread, used for testing
extractor_callback(extractor(tagger, input_path), None)
else:
# Run extractor on a different thread and call the callback when done
run_task(partial(extractor, tagger, input_path), extractor_callback)
def extractor(tagger, input_path):
# Create a temporary file with AcousticBrainz output
output_file = NamedTemporaryFile("w", suffix=".json")
output_file.close() # close file to ensure other processes can write to it
extractor = tagger.ab_extractor.get()
if not extractor:
return (output_file.name, -1, "no extractor found")
# Call the features extractor and wait for it to finish
try:
return_code, stdout, stderr = run_executable(extractor.path, input_path, output_file.name)
results = (output_file.name, return_code, stdout+stderr)
except (FileNotFoundError, PermissionError) as e:
# this can happen if AcousticBrainz extractor was removed or its permissions changed
return (output_file.name, -1, str(e))
# Add feature extractor sha to the output features file
try:
with open(output_file.name, "r+", encoding="utf-8") as f:
features = json.load(f)
features["metadata"]["version"]["essentia_build_sha"] = extractor.sha
f.seek(0)
json.dump(features, f, indent=4)
except FileNotFoundError:
pass
# Return task results to the main thread (search for extractor_callback/ab_extractor_callback)
return results
def ab_submit_features(tagger, file):
# If file is not a duplicate and was previously extracted, we now load the features file
with open(file.acousticbrainz_features_file, "r", encoding="utf-8") as f:
features = json.load(f)
try:
musicbrainz_recordingid = features['metadata']['tags']['musicbrainz_trackid'][0]
except KeyError:
musicbrainz_recordingid = None
# Check if extracted recording id exists and matches the current file (recording ID may have been merged with others)
if not musicbrainz_recordingid or musicbrainz_recordingid != file.metadata['musicbrainz_recordingid']:
# If it doesn't, skip the submission
log.debug("AcousticBrainz features recording ID does not match the file metadata: %s" % file.filename)
submit_features_callback(file, None, None, True)
return
# Set the filename. Especially on Windows the feature extractor fails to set the file basename only.
# As Picard already handles this just add the correct data ourselves.
try:
features['metadata']['tags']['file_name'] = file.base_filename
except KeyError:
log.warning('AcousticBrainz: Failed to set file_name in features data')
# Submit features to the server
tagger.webservice.post(
host=ACOUSTICBRAINZ_HOST,
port=ACOUSTICBRAINZ_PORT,
path="/%s/low-level" % musicbrainz_recordingid,
data=json.dumps(features),
handler=partial(submit_features_callback, file),
priority=True,
important=False,
mblogin=False,
parse_response_type="json"
)
def submit_features_callback(file, data, http, error):
# Check if features submission succeeded or not
if not error:
file.acousticbrainz_is_duplicate = True
os.remove(file.acousticbrainz_features_file)
file.acousticbrainz_features_file = None
log.debug("AcousticBrainz features were successfully submitted: %s" % file.filename)
else:
file.acousticbrainz_error = True
log.debug("AcousticBrainz features were not submitted: %s" % file.filename)
file.update()

View File

@@ -1,60 +0,0 @@
# -*- coding: utf-8 -*-
#
# Picard, the next-generation MusicBrainz tagger
#
# Copyright (C) 2020-2021 Gabriel Ferreira
# Copyright (C) 2021 Laurent Monin
# Copyright (C) 2022 Philipp Wolfer
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import hashlib
import re
from picard import log
from picard.util import run_executable
def precompute_extractor_sha(essentia_path):
# Precompute extractor sha1
h = hashlib.sha1() # nosec
h.update(open(essentia_path, "rb").read())
return h.hexdigest()
def check_extractor_version(essentia_path):
# returns something like this (at least on windows)
# 'Error: wrong number of arguments\r\n
# Usage: streaming_extractor_music.exe input_audiofile output_textfile [profile]\r\n
# \r\n
# Music extractor version 'music 1.0'\r\n
# built with Essentia version v2.1_beta2-1-ge3940c0\r\n
# \r\n
# '
if not essentia_path:
return None
version = None
try:
return_code, stdout, stderr = run_executable(essentia_path, timeout=10)
version_regex = re.compile(r"Essentia version (.*[^ \r\n])")
version = version_regex.findall(stdout)[0]
except IndexError:
log.error("Failed to extract AcousticBrainz feature extractor version")
except Exception as e:
log.error("AcousticBrainz extractor failed with error: %s" % e)
return version

View File

@@ -722,9 +722,6 @@ class Album(DataObject, Item):
def can_view_info(self):
return self.loaded or bool(self.errors)
def can_extract(self):
return any(track.can_extract() for track in self.tracks)
def is_album_like(self):
return True

View File

@@ -50,12 +50,6 @@ USER_PLUGIN_DIR = appdirs.plugin_folder()
# Network Cache default settings
CACHE_SIZE_IN_BYTES = 100*1000*1000
# AcousticBrainz
ACOUSTICBRAINZ_HOST = 'acousticbrainz.org'
ACOUSTICBRAINZ_PORT = 443
ACOUSTICBRAINZ_DOWNLOAD_URL = 'https://acousticbrainz.org/download'
EXTRACTOR_NAMES = ['streaming_extractor_music']
# AcoustID client API key
ACOUSTID_KEY = 'v8pQ6oyB'
ACOUSTID_HOST = 'api.acoustid.org'

View File

@@ -157,10 +157,6 @@ class File(QtCore.QObject, Item):
self.acoustid_length = 0
self.match_recordingid = None
self.acousticbrainz_is_duplicate = False
self.acousticbrainz_features_file = None
self.acousticbrainz_error = False
def __repr__(self):
return '<%s %r>' % (type(self).__name__, self.base_filename)
@@ -732,14 +728,6 @@ class File(QtCore.QObject, Item):
def can_view_info(self):
return True
def can_extract(self):
from picard.track import Track
return (
isinstance(self.parent, Track)
and self.is_saved()
and bool(self.metadata["musicbrainz_recordingid"])
)
def _info(self, metadata, file):
if hasattr(file.info, 'length'):
metadata.length = int(file.info.length * 1000)

View File

@@ -58,6 +58,3 @@ class MIDIFile(File):
def can_analyze(self):
return False
def can_extract(self):
return False

File diff suppressed because it is too large Load Diff

View File

@@ -70,11 +70,6 @@ from picard import (
acoustid,
log,
)
from picard.acousticbrainz import (
ABExtractor,
ab_extractor_callback,
ab_feature_extraction,
)
from picard.acoustid.manager import AcoustIDManager
from picard.album import (
Album,
@@ -275,9 +270,6 @@ class Tagger(QtWidgets.QApplication):
self._acoustid.init()
self.acoustidmanager = AcoustIDManager(acoustid_api)
# Setup AcousticBrainz extraction
self.ab_extractor = ABExtractor()
self.enable_menu_icons(config.setting['show_menu_icons'])
# Load plugins
@@ -899,37 +891,6 @@ class Tagger(QtWidgets.QApplication):
file.set_pending()
self._acoustid.fingerprint(file, partial(finished, file))
def extract_and_submit_acousticbrainz_features(self, objs):
"""Extract AcousticBrainz features and submit them."""
if not self.ab_extractor.available():
return
for file in iter_files_from_objects(objs):
# Skip unmatched files
if not file.can_extract():
log.warning("AcousticBrainz requires a MusicBrainz Recording ID, but file does not have it: %s" % file.filename)
# And process matched ones
else:
file.set_pending()
# Check if file was either already processed or sent to the AcousticBrainz server
if file.acousticbrainz_features_file:
results = (file.acousticbrainz_features_file, 0, "Writing results")
ab_extractor_callback(self, file, results, False)
elif file.acousticbrainz_is_duplicate:
results = (None, 0, "Duplicate")
ab_extractor_callback(self, file, results, False)
else:
file.acousticbrainz_error = False
# Launch the acousticbrainz on a separate process
log.debug("Extracting AcousticBrainz features from %s" % file.filename)
ab_feature_extraction(
self,
file.metadata["musicbrainz_recordingid"],
file.filename,
partial(ab_extractor_callback, self, file)
)
# =======================================================================
# Metadata-based lookups
# =======================================================================

View File

@@ -237,10 +237,6 @@ class Track(DataObject, FileListItem):
def can_view_info(self):
return self.num_linked_files == 1 or bool(self.metadata.images)
def can_extract(self):
"""Return if this object has a linked file with a recordingId, required by the AcousticBrainz feature extractor."""
return any(file.can_extract() for file in self.files)
def column(self, column):
m = self.metadata
if column == 'title':

View File

@@ -63,10 +63,6 @@ class Item(object):
"""Return True if this object can be submitted to MusicBrainz.org."""
return False
def can_extract(self):
"""Return True if this object can have AcousticBrainz features extracted"""
return False
@property
def can_show_coverart(self):
"""Return if this object supports cover art."""

View File

@@ -168,7 +168,6 @@ class MainPanel(QtWidgets.QSplitter):
(N_('Media'), 'media'),
(N_('Genre'), 'genre'),
(N_('Fingerprint status'), '~fingerprint'),
(N_('AcousticBrainz status'), '~acousticbrainz'),
(N_('Date'), 'date'),
(N_('Original Release Date'), 'originaldate'),
(N_('Cover'), 'covercount'),
@@ -181,7 +180,6 @@ class MainPanel(QtWidgets.QSplitter):
DISCNUMBER_COLUMN = _column_indexes['discnumber']
LENGTH_COLUMN = _column_indexes['~length']
FINGERPRINT_COLUMN = _column_indexes['~fingerprint']
ACOUSTICBRAINZ_COLUMN = _column_indexes['~acousticbrainz']
NAT_SORT_COLUMNS = [
_column_indexes['title'],
@@ -263,7 +261,6 @@ class MainPanel(QtWidgets.QSplitter):
FileItem.icon_saved = QtGui.QIcon(":/images/track-saved.png")
FileItem.icon_fingerprint = icontheme.lookup('fingerprint', icontheme.ICON_SIZE_MENU)
FileItem.icon_fingerprint_gray = icontheme.lookup('fingerprint-gray', icontheme.ICON_SIZE_MENU)
FileItem.icon_acousticbrainz = icontheme.lookup('acousticbrainz', icontheme.ICON_SIZE_MENU)
FileItem.match_icons = [
QtGui.QIcon(":/images/match-50.png"),
QtGui.QIcon(":/images/match-60.png"),
@@ -365,7 +362,7 @@ class ConfigurableColumnsHeader(TristateSortHeaderView):
if self.sectionSize(column) == 0:
self.resizeSection(column, self.defaultSectionSize())
self._visible_columns.add(column)
if column in {MainPanel.FINGERPRINT_COLUMN, MainPanel.ACOUSTICBRAINZ_COLUMN}:
if column == MainPanel.FINGERPRINT_COLUMN:
self.setSectionResizeMode(column, QtWidgets.QHeaderView.ResizeMode.Fixed)
self.resizeSection(column, COLUMN_ICON_SIZE)
else:
@@ -415,23 +412,18 @@ class ConfigurableColumnsHeader(TristateSortHeaderView):
super().paintSection(painter, rect, index)
painter.restore()
paint_column_icon(painter, rect, FileItem.icon_fingerprint_gray)
elif index == MainPanel.ACOUSTICBRAINZ_COLUMN:
painter.save()
super().paintSection(painter, rect, index)
painter.restore()
paint_column_icon(painter, rect, FileItem.icon_acousticbrainz)
else:
super().paintSection(painter, rect, index)
def on_sort_indicator_changed(self, index, order):
if index in {MainPanel.FINGERPRINT_COLUMN, MainPanel.ACOUSTICBRAINZ_COLUMN}:
if index == MainPanel.FINGERPRINT_COLUMN:
self.setSortIndicator(-1, QtCore.Qt.SortOrder.AscendingOrder)
def lock(self, is_locked):
super().lock(is_locked)
for column_index in {MainPanel.FINGERPRINT_COLUMN, MainPanel.ACOUSTICBRAINZ_COLUMN}:
if not self.is_locked and self.count() > column_index:
self.setSectionResizeMode(column_index, QtWidgets.QHeaderView.ResizeMode.Fixed)
column_index = MainPanel.FINGERPRINT_COLUMN
if not self.is_locked and self.count() > column_index:
self.setSectionResizeMode(column_index, QtWidgets.QHeaderView.ResizeMode.Fixed)
class BaseTreeView(QtWidgets.QTreeWidget):
@@ -443,7 +435,7 @@ class BaseTreeView(QtWidgets.QTreeWidget):
self.panel = parent
# Should multiple files dropped be assigned to tracks sequentially?
self._move_to_multi_tracks = True
self.setHeaderLabels([_(h) if n not in {'~fingerprint', '~acousticbrainz'} else ''
self.setHeaderLabels([_(h) if n != '~fingerprint' else ''
for h, n in MainPanel.columns])
self.restore_state()
@@ -899,7 +891,6 @@ class TreeItem(QtWidgets.QTreeWidgetItem):
]:
self.setTextAlignment(column, QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter)
self.setSizeHint(MainPanel.FINGERPRINT_COLUMN, ICON_SIZE)
self.setSizeHint(MainPanel.ACOUSTICBRAINZ_COLUMN, ICON_SIZE)
def setText(self, column, text):
self._sortkeys[column] = None
@@ -1063,15 +1054,10 @@ class TrackItem(TreeItem):
fingerprint_icon, fingerprint_tooltip = FileItem.decide_fingerprint_icon_info(file)
self.setToolTip(MainPanel.FINGERPRINT_COLUMN, fingerprint_tooltip)
self.setIcon(MainPanel.FINGERPRINT_COLUMN, fingerprint_icon)
ab_icon, ab_tooltip = FileItem.decide_ab_icon_info(file)
self.setToolTip(MainPanel.ACOUSTICBRAINZ_COLUMN, ab_tooltip)
self.setIcon(MainPanel.ACOUSTICBRAINZ_COLUMN, ab_icon)
else:
self.setToolTip(MainPanel.TITLE_COLUMN, "")
self.setToolTip(MainPanel.FINGERPRINT_COLUMN, "")
self.setIcon(MainPanel.FINGERPRINT_COLUMN, QtGui.QIcon())
self.setToolTip(MainPanel.ACOUSTICBRAINZ_COLUMN, "")
self.setIcon(MainPanel.ACOUSTICBRAINZ_COLUMN, QtGui.QIcon())
if track.ignored_for_completeness():
color = TreeItem.text_color_secondary
else:
@@ -1127,9 +1113,6 @@ class FileItem(TreeItem):
fingerprint_icon, fingerprint_tooltip = FileItem.decide_fingerprint_icon_info(file)
self.setToolTip(MainPanel.FINGERPRINT_COLUMN, fingerprint_tooltip)
self.setIcon(MainPanel.FINGERPRINT_COLUMN, fingerprint_icon)
ab_icon, ab_tooltip = FileItem.decide_ab_icon_info(file)
self.setToolTip(MainPanel.ACOUSTICBRAINZ_COLUMN, ab_tooltip)
self.setIcon(MainPanel.ACOUSTICBRAINZ_COLUMN, ab_icon)
color = FileItem.file_colors[file.state]
bgcolor = get_match_color(file.similarity, TreeItem.base_color)
for i, column in enumerate(MainPanel.columns):
@@ -1186,22 +1169,3 @@ class FileItem(TreeItem):
icon = QtGui.QIcon()
tooltip = _('No fingerprint was calculated for this file, use "Scan" or "Generate AcoustID Fingerprints" to calculate the fingerprint.')
return (icon, tooltip)
@staticmethod
def decide_ab_icon_info(file):
if file.acousticbrainz_error:
icon = TrackItem.icon_error
tooltip = _('Extraction or submission of Acoustic features failed')
elif file.acousticbrainz_is_duplicate:
icon = FileItem.icon_saved
if file.acousticbrainz_features_file is None:
tooltip = _('The server already has the features of this file')
else:
tooltip = _('Features have already been submitted')
else:
icon = FileItem.icon_file_pending
if file.acousticbrainz_features_file is None:
tooltip = _('No acoustic features were extracted from this file. Use "Submit Acoustic features" to extract and submit them')
else:
tooltip = _('Unsubmitted features')
return icon, tooltip

View File

@@ -783,16 +783,6 @@ class MainWindow(QtWidgets.QMainWindow, PreserveGeometry):
action.triggered.connect(self.generate_fingerprints)
self.generate_fingerprints_action = action
@MainWindowActions.add()
def _create_extract_and_submit_acousticbrainz_features_action(self):
action = QtWidgets.QAction(icontheme.lookup('acousticbrainz-submit'), _("&Submit AcousticBrainz features"), self)
action.setIconText(_("Submit Acoustic features"))
action.setStatusTip(_("Submit the AcousticBrainz features for the selected files"))
action.setEnabled(False)
action.setToolTip(_("Submit the AcousticBrainz features for the selected files"))
action.triggered.connect(self.extract_and_submit_acousticbrainz_features)
self.extract_and_submit_acousticbrainz_features_action = action
@MainWindowActions.add()
def _create_cluster_action(self):
action = QtWidgets.QAction(icontheme.lookup('picard-cluster'), _("Cl&uster"), self)
@@ -1007,7 +997,6 @@ class MainWindow(QtWidgets.QMainWindow, PreserveGeometry):
menu.addSeparator()
menu.addAction(self.save_action)
menu.addAction(self.submit_acoustid_action)
menu.addAction(self.extract_and_submit_acousticbrainz_features_action)
menu.addSeparator()
menu.addAction(self.exit_action)
menu = self.menuBar().addMenu(_("&Edit"))
@@ -1056,7 +1045,6 @@ class MainWindow(QtWidgets.QMainWindow, PreserveGeometry):
menu.addAction(self.track_search_action)
menu.addSeparator()
menu.addAction(self.generate_fingerprints_action)
menu.addAction(self.extract_and_submit_acousticbrainz_features_action)
menu.addAction(self.tags_from_filenames_action)
menu.addAction(self.open_collection_in_browser_action)
self.menuBar().addSeparator()
@@ -1362,9 +1350,6 @@ class MainWindow(QtWidgets.QMainWindow, PreserveGeometry):
self.tagger.generate_fingerprints(self.selected_objects)
self._ensure_fingerprinting_configured(callback)
def extract_and_submit_acousticbrainz_features(self):
self.tagger.extract_and_submit_acousticbrainz_features(self.selected_objects)
def _openUrl(self, url):
return QtCore.QUrl.fromLocalFile(url)
@@ -1512,7 +1497,6 @@ class MainWindow(QtWidgets.QMainWindow, PreserveGeometry):
can_refresh = False
can_autotag = False
can_submit = False
can_extract = False
single = self.selected_objects[0] if len(self.selected_objects) == 1 else None
can_view_info = bool(single and single.can_view_info())
can_browser_lookup = bool(single and single.can_browser_lookup())
@@ -1534,7 +1518,6 @@ class MainWindow(QtWidgets.QMainWindow, PreserveGeometry):
# if x is already True
can_analyze = can_analyze or obj.can_analyze()
can_autotag = can_autotag or obj.can_autotag()
can_extract = can_extract or obj.can_extract()
can_refresh = can_refresh or obj.can_refresh()
can_remove = can_remove or obj.can_remove()
can_save = can_save or obj.can_save()
@@ -1543,20 +1526,17 @@ class MainWindow(QtWidgets.QMainWindow, PreserveGeometry):
if (
can_analyze
and can_autotag
and can_extract
and can_refresh
and can_remove
and can_save
and can_submit
):
break
can_extract = can_extract and self.tagger.ab_extractor.available()
self.remove_action.setEnabled(can_remove)
self.save_action.setEnabled(can_save)
self.view_info_action.setEnabled(can_view_info)
self.analyze_action.setEnabled(can_analyze)
self.generate_fingerprints_action.setEnabled(have_files)
self.extract_and_submit_acousticbrainz_features_action.setEnabled(can_extract)
self.refresh_action.setEnabled(can_refresh)
self.autotag_action.setEnabled(can_autotag)
self.browser_lookup_action.setEnabled(can_browser_lookup)

View File

@@ -1,149 +0,0 @@
# -*- coding: utf-8 -*-
#
# Picard, the next-generation MusicBrainz tagger
#
# Copyright (C) 2011-2012 Lukáš Lalinský
# Copyright (C) 2011-2013 Michael Wiencek
# Copyright (C) 2013, 2018, 2020-2021 Laurent Monin
# Copyright (C) 2015, 2020-2021 Philipp Wolfer
# Copyright (C) 2016-2017 Sambhav Kothari
# Copyright (C) 2021 Gabriel Ferreira
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import os
from PyQt5 import QtWidgets
from picard.acousticbrainz import (
ab_check_version,
find_extractor,
)
from picard.config import (
BoolOption,
TextOption,
get_config,
)
from picard.const import ACOUSTICBRAINZ_DOWNLOAD_URL
from picard.util import webbrowser2
from picard.ui.options import (
OptionsCheckError,
OptionsPage,
register_options_page,
)
from picard.ui.ui_options_acousticbrainz import Ui_AcousticBrainzOptionsPage
class AcousticBrainzOptionsPage(OptionsPage):
NAME = "acousticbrainz"
TITLE = N_("AcousticBrainz")
PARENT = None
SORT_ORDER = 45
ACTIVE = True
HELP_URL = '/config/options_acousticbrainz.html'
options = [
BoolOption("setting", "use_acousticbrainz", False),
TextOption("setting", "acousticbrainz_extractor", ""),
]
def __init__(self, parent=None):
super().__init__(parent)
self._extractor_valid = True
self.ui = Ui_AcousticBrainzOptionsPage()
self.ui.setupUi(self)
self.ui.use_acousticbrainz.toggled.connect(self._acousticbrainz_extractor_check)
self.ui.acousticbrainz_extractor.textEdited.connect(self._acousticbrainz_extractor_check)
self.ui.acousticbrainz_extractor_browse.clicked.connect(self.acousticbrainz_extractor_browse)
self.ui.acousticbrainz_extractor_download.clicked.connect(self.acousticbrainz_extractor_download)
self.ui.acousticbrainz_extractor_download.setToolTip(
_("Open AcousticBrainz website in browser to download extractor binary")
)
self._config = get_config()
def load(self):
self.ui.use_acousticbrainz.setChecked(self._config.setting["use_acousticbrainz"])
extractor_path = self._config.setting["acousticbrainz_extractor"]
if not extractor_path or not ab_check_version(extractor_path):
self.ui.acousticbrainz_extractor.clear()
else:
self.ui.acousticbrainz_extractor.setText(extractor_path)
self._acousticbrainz_extractor_check()
def save(self):
enabled = self.ui.use_acousticbrainz.isChecked()
changed = self._config.setting["use_acousticbrainz"] != enabled
if changed:
self._config.setting["use_acousticbrainz"] = enabled
self.tagger.window.update_actions()
if enabled:
self._config.setting["acousticbrainz_extractor"] = self.ui.acousticbrainz_extractor.text()
def acousticbrainz_extractor_browse(self):
path, _filter = QtWidgets.QFileDialog.getOpenFileName(self, "", self.ui.acousticbrainz_extractor.text())
if path:
path = os.path.normpath(path)
self.ui.acousticbrainz_extractor.setText(path)
self._acousticbrainz_extractor_check()
def acousticbrainz_extractor_download(self):
webbrowser2.open(ACOUSTICBRAINZ_DOWNLOAD_URL)
def _acousticbrainz_extractor_check(self):
enabled = self.ui.use_acousticbrainz.isChecked()
self.ui.acousticbrainz_extractor.setPlaceholderText(_("Path to streaming_extractor_music(.exe)"))
if not enabled:
self._acousticbrainz_extractor_set_success("")
return
extractor_path = self.ui.acousticbrainz_extractor.text()
try_find = not extractor_path
if try_find:
extractor_path = find_extractor()
if extractor_path:
version = ab_check_version(extractor_path)
if version:
if try_find:
# extractor path will not be saved to config file if it was auto-detected
self.ui.acousticbrainz_extractor.clear()
self.ui.acousticbrainz_extractor.setPlaceholderText(extractor_path)
self._acousticbrainz_extractor_set_success(_("Extractor version: %s") % version)
return
self._acousticbrainz_extractor_set_error()
def _acousticbrainz_extractor_set_success(self, version):
self._extractor_valid = True
self.ui.acousticbrainz_extractor_info.setStyleSheet("")
self.ui.acousticbrainz_extractor_info.setText(version)
def _acousticbrainz_extractor_set_error(self):
self._extractor_valid = False
self.ui.acousticbrainz_extractor_info.setStyleSheet(self.STYLESHEET_ERROR)
self.ui.acousticbrainz_extractor_info.setText(_("Please select a valid extractor executable."))
def check(self):
if not self._extractor_valid:
raise OptionsCheckError(_("Invalid extractor executable"), _("Please select a valid extractor executable."))
def display_error(self, error):
pass
register_options_page(AcousticBrainzOptionsPage)

View File

@@ -57,7 +57,6 @@ from picard.ui import (
from picard.ui.options import ( # noqa: F401 # pylint: disable=unused-import
OptionsCheckError,
_pages as page_classes,
acousticbrainz,
advanced,
cdlookup,
cover,

View File

@@ -117,10 +117,6 @@ class InterfaceOptionsPage(OptionsPage):
'label': N_('Submit AcoustIDs'),
'icon': 'acoustid-fingerprinter'
},
'extract_and_submit_acousticbrainz_features_action': {
'label': N_('Submit AcousticBrainz features'),
'icon': 'acousticbrainz-submit'
},
'generate_fingerprints_action': {
'label': N_("Generate Fingerprints"),
'icon': 'fingerprint'
@@ -169,7 +165,6 @@ class InterfaceOptionsPage(OptionsPage):
'cd_lookup_action',
'separator',
'submit_acoustid_action',
'extract_and_submit_acousticbrainz_features_action'
]),
]

View File

@@ -1,53 +0,0 @@
# -*- coding: utf-8 -*-
# Automatically generated - don't edit.
# Use `python setup.py build_ui` to update it.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_AcousticBrainzOptionsPage(object):
def setupUi(self, AcousticBrainzOptionsPage):
AcousticBrainzOptionsPage.setObjectName("AcousticBrainzOptionsPage")
AcousticBrainzOptionsPage.resize(515, 503)
self.verticalLayout = QtWidgets.QVBoxLayout(AcousticBrainzOptionsPage)
self.verticalLayout.setObjectName("verticalLayout")
self.use_acousticbrainz = QtWidgets.QGroupBox(AcousticBrainzOptionsPage)
self.use_acousticbrainz.setCheckable(True)
self.use_acousticbrainz.setChecked(False)
self.use_acousticbrainz.setObjectName("use_acousticbrainz")
self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.use_acousticbrainz)
self.verticalLayout_3.setObjectName("verticalLayout_3")
self.label = QtWidgets.QLabel(self.use_acousticbrainz)
self.label.setObjectName("label")
self.verticalLayout_3.addWidget(self.label)
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
self.acousticbrainz_extractor = QtWidgets.QLineEdit(self.use_acousticbrainz)
self.acousticbrainz_extractor.setObjectName("acousticbrainz_extractor")
self.horizontalLayout_2.addWidget(self.acousticbrainz_extractor)
self.acousticbrainz_extractor_browse = QtWidgets.QPushButton(self.use_acousticbrainz)
self.acousticbrainz_extractor_browse.setObjectName("acousticbrainz_extractor_browse")
self.horizontalLayout_2.addWidget(self.acousticbrainz_extractor_browse)
self.acousticbrainz_extractor_download = QtWidgets.QPushButton(self.use_acousticbrainz)
self.acousticbrainz_extractor_download.setObjectName("acousticbrainz_extractor_download")
self.horizontalLayout_2.addWidget(self.acousticbrainz_extractor_download)
self.verticalLayout_3.addLayout(self.horizontalLayout_2)
self.acousticbrainz_extractor_info = QtWidgets.QLabel(self.use_acousticbrainz)
self.acousticbrainz_extractor_info.setText("")
self.acousticbrainz_extractor_info.setObjectName("acousticbrainz_extractor_info")
self.verticalLayout_3.addWidget(self.acousticbrainz_extractor_info)
self.verticalLayout.addWidget(self.use_acousticbrainz)
spacerItem = QtWidgets.QSpacerItem(181, 21, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout.addItem(spacerItem)
self.retranslateUi(AcousticBrainzOptionsPage)
QtCore.QMetaObject.connectSlotsByName(AcousticBrainzOptionsPage)
def retranslateUi(self, AcousticBrainzOptionsPage):
_translate = QtCore.QCoreApplication.translate
self.use_acousticbrainz.setTitle(_("AcousticBrainz features extraction"))
self.label.setText(_("AcousticBrainz/Essentia feature extractor:"))
self.acousticbrainz_extractor_browse.setText(_("Browse..."))
self.acousticbrainz_extractor_download.setText(_("Download..."))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 591 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 545 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 670 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 604 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -1,61 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:ns="&amp;ns_sfw;"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
width="30"
height="30"
viewBox="0 0 30 30"
enable-background="new 0 0 27 30"
xml:space="preserve"
inkscape:version="0.91 r13725"
sodipodi:docname="AcousticBrainz_logo_icon.svg"><defs
id="defs3713" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1280"
inkscape:window-height="759"
id="namedview3711"
showgrid="false"
inkscape:zoom="17.633333"
inkscape:cx="12.649338"
inkscape:cy="15"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" /><metadata
id="metadata3699"><ns:sfw><ns:slices /><ns:sliceSourceBounds
height="28"
width="25"
y="-914"
x="2963"
bottomLeftOrigin="true" /></ns:sfw><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><g
id="g3701" /><g
id="g3703"
transform="translate(1.5,0)"><g
id="g3705"><polygon
points="13,1 1,8 1,22 13,29 "
id="polygon3707"
style="fill:#4e7ec2" /><polygon
points="14,1 26,8 26,22 14,29 "
id="polygon3709"
style="fill:#eb743b" /></g></g></svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -1,35 +0,0 @@
<?xml version="1.0"?>
<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" version="1.1">
<metadata/>
<g class="layer">
<title>Layer 1</title>
<g id="svg_1"/>
<g id="svg_18">
<polygon fill="#EB743B" id="svg_19" points="51,2 51,98 92,74 92,26 "/>
<polygon fill="#4E7EC2" id="svg_20" points="48,2 7,26 7,74 48,98 "/>
<rect fill="#FFFEDB" height="4" id="svg_21" width="6" x="39" y="16"/>
<rect fill="#FFFEDB" height="4" id="svg_22" width="6" x="39" y="23"/>
<rect fill="#FFFEDB" height="4" id="svg_23" width="6" x="39" y="30"/>
<rect fill="#FFFEDB" height="4" id="svg_24" width="6" x="39" y="37"/>
<rect fill="#FFFEDB" height="4" id="svg_25" width="6" x="39" y="44"/>
<rect fill="#8DB2E3" height="4" id="svg_26" width="6" x="39" y="51"/>
<rect fill="#8DB2E3" height="4" id="svg_27" width="6" x="39" y="58"/>
<rect fill="#8DB2E3" height="4" id="svg_28" width="6" x="39" y="65"/>
<rect fill="#8DB2E3" height="4" id="svg_29" width="6" x="39" y="72"/>
<rect fill="#8DB2E3" height="4" id="svg_30" width="6" x="39" y="79"/>
<rect fill="#FFFEDB" height="4" id="svg_31" width="6" x="30" y="30"/>
<rect fill="#FFFEDB" height="4" id="svg_32" width="6" x="30" y="37"/>
<rect fill="#FFFEDB" height="4" id="svg_33" width="6" x="30" y="44"/>
<rect fill="#8DB2E3" height="4" id="svg_34" width="6" x="30" y="51"/>
<rect fill="#8DB2E3" height="4" id="svg_35" width="6" x="30" y="58"/>
<rect fill="#8DB2E3" height="4" id="svg_36" width="6" x="30" y="65"/>
<rect fill="#FFFEDB" height="4" id="svg_37" width="6" x="21" y="44"/>
<rect fill="#8DB2E3" height="4" id="svg_38" width="6" x="21" y="51"/>
<rect fill="#FFFEDB" height="4" id="svg_39" width="6" x="12" y="37"/>
<rect fill="#FFFEDB" height="4" id="svg_40" width="6" x="12" y="44"/>
<rect fill="#8DB2E3" height="4" id="svg_41" width="6" x="12" y="51"/>
<rect fill="#8DB2E3" height="4" id="svg_42" width="6" x="12" y="58"/>
<path d="m83.281,58.087c-1.239,-1.97 -3.38,-3.148 -5.718,-3.148c-0.666,0 -1.324,0.103 -1.956,0.295c-2.632,-3.125 -5.819,-4.775 -8.603,-5.636c2.235,-1.714 4.208,-3.771 5.909,-6.153c1.74,0.394 3.63,0.127 5.162,-0.832c3.158,-2.006 4.103,-6.197 2.104,-9.348c-1.249,-1.975 -3.39,-3.152 -5.724,-3.152c-1.283,0 -2.533,0.363 -3.616,1.05c-3.146,1.991 -4.093,6.181 -2.105,9.336c0.13,0.207 0.271,0.404 0.422,0.596c-2.514,3.442 -5.708,6.056 -9.519,7.785l-0.014,0.004c-3.53,1.603 -5.623,2.116 -8.623,2.116l0,5c4,0 5.931,-1.125 10.016,-2.898c1.366,-0.023 6.962,0.185 10.991,4.747c-1.507,2.164 -1.655,5.108 -0.16,7.471c1.241,1.97 3.382,3.15 5.721,3.15c1.278,0 2.524,-0.36 3.607,-1.044c1.538,-0.975 2.599,-2.482 2.995,-4.252c0.396,-1.761 0.078,-3.568 -0.889,-5.087zm-10.22,-23.417c0.423,-0.266 0.901,-0.405 1.396,-0.405c0.903,0 1.731,0.456 2.219,1.224c0.771,1.214 0.403,2.836 -0.818,3.61c-0.41,0.261 -0.896,0.398 -1.387,0.398c-0.493,0 -0.974,-0.134 -1.39,-0.391c-0.343,-0.213 -0.625,-0.489 -0.832,-0.817c-0.771,-1.225 -0.407,-2.846 0.812,-3.619zm7.058,27.6c-0.154,0.687 -0.564,1.27 -1.161,1.645c-0.413,0.264 -0.896,0.406 -1.391,0.406c-0.906,0 -1.731,-0.457 -2.21,-1.218c-0.745,-1.18 -0.441,-2.727 0.691,-3.528l0.117,-0.078c1.208,-0.766 2.866,-0.367 3.608,0.808c0.378,0.591 0.498,1.286 0.346,1.965z" fill="#FFFEDB" id="svg_43"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -1,9 +1,5 @@
<!DOCTYPE RCC><RCC version="1.0">
<qresource>
<file>images/16x16/acousticbrainz-submit.png</file>
<file>images/16x16/acousticbrainz-submit@2x.png</file>
<file>images/16x16/acousticbrainz.png</file>
<file>images/16x16/acousticbrainz@2x.png</file>
<file>images/16x16/acoustid-fingerprinter.png</file>
<file>images/16x16/acoustid-fingerprinter@2x.png</file>
<file>images/16x16/action-go-down-16.png</file>
@@ -91,10 +87,6 @@
<file>images/16x16/speaker-100@2x.png</file>
<file>images/16x16/view-refresh.png</file>
<file>images/16x16/view-refresh@2x.png</file>
<file>images/22x22/acousticbrainz-submit.png</file>
<file>images/22x22/acousticbrainz-submit@2x.png</file>
<file>images/22x22/acousticbrainz.png</file>
<file>images/22x22/acousticbrainz@2x.png</file>
<file>images/22x22/acoustid-fingerprinter.png</file>
<file>images/22x22/acoustid-fingerprinter@2x.png</file>
<file>images/22x22/document-open.png</file>

File diff suppressed because one or more lines are too long

View File

@@ -1,69 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Picard, the next-generation MusicBrainz tagger
#
# Copyright (C) 2021 Gabriel Ferreira
# Copyright (C) 2021 Laurent Monin
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import json
import sys
# remove all arguments with 'py' until the first one without it is found, removing the interpreter and scripts
i = 0
for arg in sys.argv:
if "py" in arg:
i += 1
else:
break
sys.argv = sys.argv[i:]
if len(sys.argv) != 2:
message = """Error: wrong number of arguments
Usage: streaming_extractor_music.exe input_audiofile output_textfile [profile]
Music extractor version 'music 1.0'
built with Essentia version v2.1_beta2-1-ge3940c0
"""
retcode = 1
else:
if "test.mp3" in sys.argv[0]:
message = """Process step: Read metadata
Process step: Compute md5 audio hash and codec
Process step: Replay gain
Process step: Compute audio features
Process step: Compute aggregation
All done
Writing results to file out.txt
"""
retcode = 0
with open("./test/data/acousticbrainz/acousticbrainz_sample.json", "r", encoding="utf-8") as f:
ab_features = json.load(f)
with open(sys.argv[1], "w", encoding="utf-8") as f:
json.dump(ab_features, f)
elif "fail.mp3" in sys.argv[0]:
message = ""
retcode = 1
else:
message = ""
retcode = 2
print(message)
exit(retcode)

View File

@@ -1,213 +0,0 @@
# -*- coding: utf-8 -*-
#
# Picard, the next-generation MusicBrainz tagger
#
# Copyright (C) 2021 Gabriel Ferreira
# Copyright (C) 2021 Laurent Monin
# Copyright (C) 2021 Philipp Wolfer
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
from functools import partial
import json
import os
from unittest.mock import (
MagicMock,
Mock,
)
from test.picardtestcase import PicardTestCase
from picard.acousticbrainz import (
ABExtractor,
ab_extractor_callback,
ab_feature_extraction,
)
from picard.file import File
mock_extractor = os.path.abspath("./test/data/acousticbrainz/mock_acousticbrainz_extractor.py")
settings = {
"use_acousticbrainz": True,
"acousticbrainz_extractor": "",
"acousticbrainz_extractor_version": "",
"acousticbrainz_extractor_sha": "",
"clear_existing_tags": False,
"compare_ignore_tags": [],
}
class AcousticBrainzSetupTest(PicardTestCase):
def test_ab_setup_present(self):
settings['acousticbrainz_extractor'] = mock_extractor
self.set_config_values(settings)
# Try to setup AB
ab_extractor = ABExtractor()
# Extractor should be found
self.assertTrue(ab_extractor.available())
def test_ab_setup_not_present(self):
settings['acousticbrainz_extractor'] = "non_existing_extractor"
self.set_config_values(settings)
# Try to setup AB
ab_extractor = ABExtractor()
# Extractor should not be found
self.assertFalse(ab_extractor.available())
class AcousticBrainzFeatureExtractionTest(PicardTestCase):
ab_features_file = "test/data/acousticbrainz/acousticbrainz_sample.json"
singleton = None
def setUp(self):
super().setUp()
AcousticBrainzFeatureExtractionTest.singleton = self
# Setup mock extractor
settings['acousticbrainz_extractor'] = mock_extractor
self.set_config_values(settings)
self.tagger.ab_extractor = ABExtractor()
self.assertTrue(self.tagger.ab_extractor.available())
# Load an irrelevant test file
self.file = File("./test/data/test.mp3")
# Load the AB features sample and copy the recording ID
with open(self.ab_features_file, "r", encoding="utf-8") as f:
ab_features = json.load(f)
# Copy the MB recordingID to the file, as we only work with already matched files
self.file.metadata['musicbrainz_recordingid'] = ab_features['metadata']['tags']['musicbrainz_trackid']
self.tagger.webservice = MagicMock()
@staticmethod
def mock_ab_extractor_callback_duplicate(tagger, file, result, error):
AcousticBrainzFeatureExtractionTest.singleton.assertEqual(error, None)
ab_metadata_file, result, error = result
AcousticBrainzFeatureExtractionTest.singleton.assertTrue("Duplicate" in error)
@staticmethod
def mock_ab_extractor_callback_extraction_failed(tagger, file, result, error):
AcousticBrainzFeatureExtractionTest.singleton.assertEqual(error, None)
ab_metadata_file, result, error = result
AcousticBrainzFeatureExtractionTest.singleton.assertTrue("Duplicate" not in error or "Writing results" not in error)
@staticmethod
def mock_ab_extractor_callback_extraction_succeeded(tagger, file, result, error):
AcousticBrainzFeatureExtractionTest.singleton.assertEqual(error, None)
ab_metadata_file, result, error = result
AcousticBrainzFeatureExtractionTest.singleton.assertTrue("Writing results" in error)
def mock_get_succeed(self, host, port, path, handler, priority, important, parse_response_type):
# Return features in utf-8 straight from the web response
error = 0
with open(AcousticBrainzFeatureExtractionTest.ab_features_file, "rb") as f:
response = f.read()
reply = ""
handler(response, reply, error)
def mock_get_fail(self, host, port, path, handler, priority, important, parse_response_type):
error = 203
response = b"""{"message":"Not found"}\n"""
reply = ""
handler(response, reply, error, main_thread=True)
def test_check_duplicate(self):
self.tagger.webservice.get = Mock(wraps=self.mock_get_succeed)
ab_feature_extraction(
self.tagger,
self.file.metadata["musicbrainz_recordingid"],
self.file.filename,
partial(self.mock_ab_extractor_callback_duplicate, self.tagger, self.file)
)
def test_check_not_duplicate_and_fail_extraction(self):
self.tagger.webservice.get = Mock(wraps=self.mock_get_fail)
ab_feature_extraction(
self.tagger,
self.file.metadata["musicbrainz_recordingid"],
"fail.mp3",
partial(self.mock_ab_extractor_callback_extraction_failed, self.tagger, self.file)
)
def test_check_not_duplicate_and_succeed_extraction(self):
self.tagger.webservice.get = Mock(wraps=self.mock_get_fail)
ab_feature_extraction(
self.tagger,
self.file.metadata["musicbrainz_recordingid"],
"test.mp3",
partial(self.mock_ab_extractor_callback_extraction_succeeded, self.tagger, self.file)
)
class AcousticBrainzFeatureSubmissionTest(AcousticBrainzFeatureExtractionTest):
def setUp(self):
super().setUp()
self.tagger.webservice.get = Mock(wraps=self.mock_get_fail)
# Setup methods and settings required by file.set_pending and file.clear_pending
self.tagger.tagger_stats_changed = MagicMock()
self.tagger.tagger_stats_changed.emit = Mock(wraps=self.mock_emit)
self.set_config_values(settings)
@staticmethod
def mock_ab_submission_failed(tagger, file, result, error):
AcousticBrainzFeatureExtractionTest.singleton.assertEqual(error, None)
ab_metadata_file, result, error = result
AcousticBrainzFeatureExtractionTest.singleton.assertTrue("Duplicate" not in error or "Writing results" not in error)
@staticmethod
def mock_ab_submission_succeeded(tagger, file, result, error):
AcousticBrainzFeatureExtractionTest.singleton.assertEqual(error, None)
ab_metadata_file, result, error = result
AcousticBrainzFeatureExtractionTest.singleton.assertTrue("Writing results" in error)
def mock_emit(self):
pass
def mock_post_succeed(self, host, port, path, data, handler, parse_response_type, priority, important, mblogin, queryargs=None, request_mimetype=None):
handler("""{"message":"ok"}""", "", 0)
def mock_post_fail(self, host, port, path, data, handler, parse_response_type, priority, important, mblogin, queryargs=None, request_mimetype=None):
handler("""APIBadRequest""", "", 400)
def test_submit_failed(self):
self.file.set_pending()
self.tagger.webservice.post = Mock(wraps=self.mock_post_fail)
ab_feature_extraction(
self.tagger,
self.file.metadata["musicbrainz_recordingid"],
"test.mp3",
partial(ab_extractor_callback, self.tagger, self.file)
)
def test_submit_succeeded(self):
self.file.set_pending()
self.tagger.webservice.post = Mock(wraps=self.mock_post_succeed)
ab_feature_extraction(
self.tagger,
self.file.metadata["musicbrainz_recordingid"],
"test.mp3",
partial(ab_extractor_callback, self.tagger, self.file)
)

View File

@@ -1,81 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AcousticBrainzOptionsPage</class>
<widget class="QWidget" name="AcousticBrainzOptionsPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>515</width>
<height>503</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="use_acousticbrainz">
<property name="title">
<string>AcousticBrainz features extraction</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>false</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>AcousticBrainz/Essentia feature extractor:</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLineEdit" name="acousticbrainz_extractor"/>
</item>
<item>
<widget class="QPushButton" name="acousticbrainz_extractor_browse">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="acousticbrainz_extractor_download">
<property name="text">
<string>Download...</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="acousticbrainz_extractor_info">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>181</width>
<height>21</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>