PICARD-2422: Remove all AcousticBrainz extraction and submission code
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -58,6 +58,3 @@ class MIDIFile(File):
|
||||
|
||||
def can_analyze(self):
|
||||
return False
|
||||
|
||||
def can_extract(self):
|
||||
return False
|
||||
|
||||
23737
picard/resources.py
@@ -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
|
||||
# =======================================================================
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
]),
|
||||
]
|
||||
|
||||
|
||||
@@ -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..."))
|
||||
|
Before Width: | Height: | Size: 591 B |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 545 B |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 670 B |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 604 B |
|
Before Width: | Height: | Size: 3.9 KiB |
@@ -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="&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 |
@@ -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 |
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
)
|
||||
@@ -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>
|
||||