From 080680c950432898a47979e96f64a50622ddf50e Mon Sep 17 00:00:00 2001 From: Dorian Soergel Date: Fri, 31 May 2019 14:35:51 +0200 Subject: [PATCH 01/27] add parentwork plugin, first try --- beetsplug/parentwork.py | 193 +++++++++++++++++++++++++++++++++++++ docs/plugins/index.rst | 2 + docs/plugins/parentwork.py | 60 ++++++++++++ test/test_parentwork.py | 93 ++++++++++++++++++ 4 files changed, 348 insertions(+) create mode 100644 beetsplug/parentwork.py create mode 100644 docs/plugins/parentwork.py create mode 100644 test/test_parentwork.py diff --git a/beetsplug/parentwork.py b/beetsplug/parentwork.py new file mode 100644 index 0000000000..8a8c0c12a8 --- /dev/null +++ b/beetsplug/parentwork.py @@ -0,0 +1,193 @@ +# -*- coding: utf-8 -*- +# This file is part of beets. +# Copyright 2017, Dorian Soergel. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +"""Gets work title, disambiguation, parent work and its disambiguation, +composer, composer sort name and performers +""" + +from __future__ import division, absolute_import, print_function + +from beets import ui +from beets.plugins import BeetsPlugin + +import musicbrainzngs + + +def work_father(mb_workid, work_date=None): + """ This function finds the id of the father work given its id""" + work_info = musicbrainzngs.get_work_by_id(mb_workid, + includes=["work-rels", + "artist-rels"]) + if 'artist-relation-list' in work_info['work'] and work_date is None: + for artist in work_info['work']['artist-relation-list']: + if artist['type'] == 'composer': + if 'end' in artist.keys(): + work_date = artist['end'] + + if 'work-relation-list' in work_info['work']: + for work_father in work_info['work']['work-relation-list']: + if work_father['type'] == 'parts' \ + and work_father.get('direction') == 'backward': + father_id = work_father['work']['id'] + return father_id, work_date + return None, work_date + + +def work_parent(mb_workid): + """This function finds the parentwork id of a work given its id. """ + work_date = None + while True: + (new_mb_workid, work_date) = work_father(mb_workid, work_date) + if not new_mb_workid: + return mb_workid, work_date + mb_workid = new_mb_workid + return mb_workid, work_date + + +def find_parentwork(mb_workid): + """This function gives the work relationships (dict) of a parentwork + given the id of the work""" + parent_id, work_date = work_parent(mb_workid) + work_info = musicbrainzngs.get_work_by_id(parent_id, + includes=["artist-rels"]) + return work_info, work_date + + +class ParentWorkPlugin(BeetsPlugin): + def __init__(self): + super(ParentWorkPlugin, self).__init__() + + self.config.add({ + 'auto': False, + 'force': False, + }) + + self._command = ui.Subcommand( + 'parentwork', + help=u'Fetches parent works, composers and dates') + + self._command.parser.add_option( + u'-f', u'--force', dest='force', + action='store_true', default=None, + help=u'Re-fetches all parent works') + + if self.config['auto']: + self.import_stages = [self.imported] + + def commands(self): + + def func(lib, opts, args): + self.config.set_args(opts) + force_parent = self.config['force'].get(bool) + write = ui.should_write() + + for item in lib.items(ui.decargs(args)): + self.find_work(item, force_parent) + item.store() + if write: + item.try_write() + + self._command.func = func + return [self._command] + + def imported(self, session, task): + """Import hook for fetching parent works automatically. + """ + force_parent = self.config['force'].get(bool) + + for item in task.imported_items(): + self.find_work(item, force_parent) + item.store() + + def get_info(self, item, work_info): + """Given the parentwork info dict, this function fetches + parent_composer, parent_composer_sort, parentwork, + parentwork_disambig, mb_workid and composer_ids""" + + parent_composer = [] + parent_composer_sort = [] + + composer_exists = False + if 'artist-relation-list' in work_info['work']: + for artist in work_info['work']['artist-relation-list']: + if artist['type'] == 'composer': + parent_composer.append(artist['artist']['name']) + parent_composer_sort.append(artist['artist']['sort-name']) + if not composer_exists: + self._log.info(item.artist + ' - ' + item.title) + self._log.debug( + "no composer, add one at https://musicbrainz.org/work/" + + work_info['work']['id']) + parentwork = work_info['work']['title'] + mb_parentworkid = work_info['work']['id'] + if 'disambiguation' in work_info['work']: + parentwork_disambig = work_info['work']['disambiguation'] + else: + parentwork_disambig.append('') + return parentwork, mb_parentworkid, parentwork_disambig, + parent_composer, parent_composer_sort + + def find_work(self, item, force): + + recording_id = item.mb_trackid + try: + item.parentwork + hasparent = True + except AttributeError: + hasparent = False + hasawork = True + if not item.mb_workid: + self._log.info("No work attached, recording id: " + + recording_id) + self._log.info(item.artist + ' - ' + item.title) + self._log.info("add one at https://musicbrainz.org" + + "/recording/" + recording_id) + hasawork = False + found = False + + if hasawork and (force or (not hasparent)): + try: + work_info, work_date = find_parentwork(item.mb_workid) + (parentwork, mb_parentworkid, parentwork_disambig, + parent_composer, + parent_composer_sort) = self.get_info(item, work_info) + found = True + except musicbrainzngs.musicbrainz.WebServiceError: + self._log.debug("Work unreachable") + found = False + elif parentwork: + self._log.debug("Work already in library, not necessary fetching") + return + + if found: + self._log.debug("Finished searching work for: " + + item.artist + ' - ' + item.title) + self._log.debug("Work fetched: " + parentwork + + ' - ' + u', '.join(parent_composer)) + item['parentwork'] = parentwork + item['parentwork_disambig'] = parentwork_disambig + item['mb_parentworkid'] = mb_parentworkid + item['parent_composer'] = u'' + item['parent_composer'] = u', '.join(parent_composer) + item['parent_composer_sort'] = u'' + item['parent_composer_sort'] = u', '.join(parent_composer_sort) + if work_date: + item['work_date'] = work_date + ui.show_model_changes( + item, fields=['parentwork', 'parentwork_disambig', + 'mb_parentworkid', 'parent_composer', + 'parent_composer_sort', 'work_date']) + + item.store() diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index e75e2f810f..b962f7a100 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -80,6 +80,7 @@ like this:: missing mpdstats mpdupdate + parentwork permissions play playlist @@ -131,6 +132,7 @@ Metadata * :doc:`metasync`: Fetch metadata from local or remote sources * :doc:`mpdstats`: Connect to `MPD`_ and update the beets library with play statistics (last_played, play_count, skip_count, rating). +* :doc:`parentwork`: Fetch work titles and works they are part of. * :doc:`replaygain`: Calculate volume normalization for players that support it. * :doc:`scrub`: Clean extraneous metadata from music files. * :doc:`zero`: Nullify fields by pattern or unconditionally. diff --git a/docs/plugins/parentwork.py b/docs/plugins/parentwork.py new file mode 100644 index 0000000000..d64934b887 --- /dev/null +++ b/docs/plugins/parentwork.py @@ -0,0 +1,60 @@ +Parentwork Plugin +================= + +The ``parentwork`` plugin fetches the work title, parentwork title and +parentwork composer. + +In the MusicBrainz database, a recording can be associated with a work. A +work can itself be associated with another work, for example one being part +of the other (what I call the father work). This plugin looks the work id +from the library and then looks up the father, then the father of the father +and so on until it reaches the top. The work at the top is what I call the +parentwork. This plugin is especially designed for classical music. For +classical music, just fetching the work title as in MusicBrainz is not +satisfying, because MusicBrainz has separate works for, for example, all the +movements of a symphony. This plugin aims to solve this problem by not only +fetching the work itself from MusicBrainz but also its parentwork which would +be, in this case, the whole symphony. + +This plugin adds five tags: + +- **parentwork**: The title of the parentwork. +- **mb_parentworkid**: The musicbrainz id of the parentwork. +- **parentwork_disambig**: The disambiguation of the parentwork title. +- **parent_composer**: The composer of the parentwork. +- **parent_composer_sort**: The sort name of the parentwork composer. +- **work_date**: THe composition date of the work, or the first parent work + that has a composition date. Format: yyyy-mm-dd. + +To fill in the parentwork tag and the associated parent** tags, in case there +are several works on the recording, it fills it with the results of the first +work and then appends the results of the second work only if they differ from +the ones already there. This is to care for cases of, for example, an opera +recording that contains several scenes of the opera: neither the parentwork +nor all the associated tags will be duplicated. +If there are several works linked to a recording, they all get a +disambiguation (empty as default) and if all disambiguations are empty, the +disambiguation field is left empty, else the disambiguation field can look +like ``,disambig,,`` (if there are four works and only the second has a +disambiguation) if only the second work has a disambiguation. This may +seem clumsy but it allows to identify which of the four works the +disambiguation belongs to. + +To use the ``parentwork`` plugin, enable it in your configuration (see +:ref:`using-plugins`). + +Configuration +------------- + +To configure the plugin, make a ``parentwork:`` section in your +configuration file. The available options are: + +- **force**: As a default, ``parentwork`` only fetches work info for + recordings that do not already have a ``parentwork`` tag. If ``force`` + is enabled, it fetches it for all recordings. + Default: ``no`` + +- **auto**: If enabled, automatically fetches works at import. It takes quite + some time, because beets is restricted to one musicbrainz query per second. + Default: ``no`` + diff --git a/test/test_parentwork.py b/test/test_parentwork.py new file mode 100644 index 0000000000..44545c63e3 --- /dev/null +++ b/test/test_parentwork.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +# This file is part of beets. +# Copyright 2017, Dorian Soergel +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +"""Tests for the 'parentwork' plugin.""" + +from __future__ import division, absolute_import, print_function + +from mock import patch +import unittest +from test.helper import TestHelper + +from beets.library import Item +from beetsplug import parentwork + + +@patch('beets.util.command_output') +class ParentWorkTest(unittest.TestCase, TestHelper): + def setUp(self): + """Set up configuration""" + self.setup_beets() + self.load_plugins('parentwork') + + def tearDown(self): + self.unload_plugins() + self.teardown_beets() + + def test_normal_case(self, command_output): + item = Item(path='/file', + mb_workid=u'e27bda6e-531e-36d3-9cd7-b8ebc18e8c53') + item.add(self.lib) + + command_output.return_value = u'32c8943f-1b27-3a23-8660-4567f4847c94' + self.run_command('parentwork') + + item.load() + self.assertEqual(item['mb_parentworkid'], + u'32c8943f-1b27-3a23-8660-4567f4847c94') + + def test_force(self, command_output): + self.config['parentwork']['force'] = True + item = Item(path='/file', + mb_workid=u'e27bda6e-531e-36d3-9cd7-b8ebc18e8c53', + mb_parentworkid=u'XXX') + item.add(self.lib) + + command_output.return_value = u'32c8943f-1b27-3a23-8660-4567f4847c94' + self.run_command('parentwork') + + item.load() + self.assertEqual(item['mb_parentworkid'], + u'32c8943f-1b27-3a23-8660-4567f4847c94') + + def test_no_force(self, command_output): + self.config['parentwork']['force'] = True + item = Item(path='/file', mb_workid=u'e27bda6e-531e-36d3-9cd7-\ + b8ebc18e8c53', mb_parentworkid=u'XXX') + item.add(self.lib) + + command_output.return_value = u'32c8943f-1b27-3a23-8660-4567f4847c94' + self.run_command('parentwork') + + item.load() + self.assertEqual(item['mb_parentworkid'], u'XXX') + + # test different cases, still with Matthew Passion Ouverture or Mozart + # requiem + + def test_father_work(self, command_output): + mb_workid = u'2e4a3668-458d-3b2a-8be2-0b08e0d8243a' + self.assertEqual(u'f04b42df-7251-4d86-a5ee-67cfa49580d1', + parentwork.work_father(mb_workid)[0]) + self.assertEqual(u'45afb3b2-18ac-4187-bc72-beb1b1c194ba', + parentwork.work_parent(mb_workid)[0]) + + +def suite(): + return unittest.TestLoader().loadTestsFromName(__name__) + + +if __name__ == '__main__': + unittest.main(defaultTest='suite') From 6d6c1a16473212be6f28dccec4711de515e6ee6b Mon Sep 17 00:00:00 2001 From: Dorian Soergel Date: Fri, 31 May 2019 15:04:00 +0200 Subject: [PATCH 02/27] fixes for disambiguation --- beetsplug/parentwork.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/beetsplug/parentwork.py b/beetsplug/parentwork.py index 8a8c0c12a8..869e8ea96c 100644 --- a/beetsplug/parentwork.py +++ b/beetsplug/parentwork.py @@ -134,10 +134,11 @@ def get_info(self, item, work_info): mb_parentworkid = work_info['work']['id'] if 'disambiguation' in work_info['work']: parentwork_disambig = work_info['work']['disambiguation'] + return [parentwork, mb_parentworkid, parent_composer, + parent_composer_sort, parentwork_disambig] else: - parentwork_disambig.append('') - return parentwork, mb_parentworkid, parentwork_disambig, - parent_composer, parent_composer_sort + return [parentwork, mb_parentworkid, parent_composer, + parent_composer_sort, None] def find_work(self, item, force): @@ -147,27 +148,29 @@ def find_work(self, item, force): hasparent = True except AttributeError: hasparent = False - hasawork = True if not item.mb_workid: self._log.info("No work attached, recording id: " + recording_id) self._log.info(item.artist + ' - ' + item.title) self._log.info("add one at https://musicbrainz.org" + "/recording/" + recording_id) - hasawork = False + return found = False - - if hasawork and (force or (not hasparent)): + if force or (not hasparent): try: work_info, work_date = find_parentwork(item.mb_workid) - (parentwork, mb_parentworkid, parentwork_disambig, - parent_composer, - parent_composer_sort) = self.get_info(item, work_info) + parent_info = self.get_info(item, work_info) + parentwork = parent_info[0] + mb_parentworkid = parent_info[1] + parent_composer = parent_info[2] + parent_composer_sort = parent_info[3] + parentwork_disambig = parent_info[4] + found = True except musicbrainzngs.musicbrainz.WebServiceError: self._log.debug("Work unreachable") found = False - elif parentwork: + elif hasparent: self._log.debug("Work already in library, not necessary fetching") return @@ -177,7 +180,8 @@ def find_work(self, item, force): self._log.debug("Work fetched: " + parentwork + ' - ' + u', '.join(parent_composer)) item['parentwork'] = parentwork - item['parentwork_disambig'] = parentwork_disambig + if parentwork_disambig: + item['parentwork_disambig'] = parentwork_disambig item['mb_parentworkid'] = mb_parentworkid item['parent_composer'] = u'' item['parent_composer'] = u', '.join(parent_composer) From b28d6850596009466be20a1b3c202bf4ee9fff03 Mon Sep 17 00:00:00 2001 From: Dorian Soergel Date: Fri, 31 May 2019 15:13:55 +0200 Subject: [PATCH 03/27] wrong file name for parentwork documentation --- docs/plugins/{parentwork.py => parentwork.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/plugins/{parentwork.py => parentwork.rst} (100%) diff --git a/docs/plugins/parentwork.py b/docs/plugins/parentwork.rst similarity index 100% rename from docs/plugins/parentwork.py rename to docs/plugins/parentwork.rst From 638e9d5dc866d1765a5e9f91628ad3589c8f8e4f Mon Sep 17 00:00:00 2001 From: Dorian Soergel Date: Fri, 31 May 2019 17:17:06 +0200 Subject: [PATCH 04/27] style changes, docstrings --- beetsplug/parentwork.py | 113 ++++++++++++++++++++-------------------- 1 file changed, 57 insertions(+), 56 deletions(-) diff --git a/beetsplug/parentwork.py b/beetsplug/parentwork.py index 869e8ea96c..4520edea28 100644 --- a/beetsplug/parentwork.py +++ b/beetsplug/parentwork.py @@ -25,8 +25,8 @@ import musicbrainzngs -def work_father(mb_workid, work_date=None): - """ This function finds the id of the father work given its id""" +def work_father_id(mb_workid, work_date=None): + """ Given a mb_workid, find the id one of the works the work is part of""" work_info = musicbrainzngs.get_work_by_id(mb_workid, includes=["work-rels", "artist-rels"]) @@ -45,21 +45,21 @@ def work_father(mb_workid, work_date=None): return None, work_date -def work_parent(mb_workid): - """This function finds the parentwork id of a work given its id. """ +def work_parent_id(mb_workid): + """Find the parentwork id of a work given its id. """ work_date = None while True: - (new_mb_workid, work_date) = work_father(mb_workid, work_date) + new_mb_workid, work_date = work_father_id(mb_workid, work_date) if not new_mb_workid: return mb_workid, work_date mb_workid = new_mb_workid return mb_workid, work_date -def find_parentwork(mb_workid): - """This function gives the work relationships (dict) of a parentwork - given the id of the work""" - parent_id, work_date = work_parent(mb_workid) +def find_parentwork_info(mb_workid): + """Return the work relationships (dict) of a parentwork given the id of + the work""" + parent_id, work_date = work_parent_id(mb_workid) work_info = musicbrainzngs.get_work_by_id(parent_id, includes=["artist-rels"]) return work_info, work_date @@ -112,12 +112,13 @@ def imported(self, session, task): item.store() def get_info(self, item, work_info): - """Given the parentwork info dict, this function fetches - parent_composer, parent_composer_sort, parentwork, - parentwork_disambig, mb_workid and composer_ids""" + """Given the parentwork info dict, fetch parent_composer, + parent_composer_sort, parentwork, parentwork_disambig, mb_workid and + composer_ids. """ parent_composer = [] parent_composer_sort = [] + parentwork_info = {} composer_exists = False if 'artist-relation-list' in work_info['work']: @@ -125,73 +126,73 @@ def get_info(self, item, work_info): if artist['type'] == 'composer': parent_composer.append(artist['artist']['name']) parent_composer_sort.append(artist['artist']['sort-name']) + + parentwork_info['parent_composer'] = u', '.join(parent_composer) + parentwork_info['parent_composer_sort'] = u', '.join( + parent_composer_sort) + if not composer_exists: self._log.info(item.artist + ' - ' + item.title) self._log.debug( "no composer, add one at https://musicbrainz.org/work/" + work_info['work']['id']) - parentwork = work_info['work']['title'] - mb_parentworkid = work_info['work']['id'] + + parentwork_info['parentwork'] = work_info['work']['title'] + parentwork_info['mb_parentworkid'] = work_info['work']['id'] + if 'disambiguation' in work_info['work']: - parentwork_disambig = work_info['work']['disambiguation'] - return [parentwork, mb_parentworkid, parent_composer, - parent_composer_sort, parentwork_disambig] + parentwork_info['parentwork_disambig'] = work_info[ + 'work']['disambiguation'] + else: - return [parentwork, mb_parentworkid, parent_composer, - parent_composer_sort, None] + parentwork_info['parentwork_disambig'] = None + + return parentwork_info def find_work(self, item, force): + """ Finds the parentwork of a recording and populates the tags + accordingly. - recording_id = item.mb_trackid - try: - item.parentwork + Namely, the tags parentwork, parentwork_disambig, mb_parentworkid, + parent_composer, parent_composer_sort and work_date are populated. """ + + if hasattr(item, 'parentwork'): hasparent = True - except AttributeError: + else: hasparent = False if not item.mb_workid: self._log.info("No work attached, recording id: " + - recording_id) + item.mb_trackid) self._log.info(item.artist + ' - ' + item.title) self._log.info("add one at https://musicbrainz.org" + - "/recording/" + recording_id) + "/recording/" + item.mb_trackid) return - found = False if force or (not hasparent): try: - work_info, work_date = find_parentwork(item.mb_workid) - parent_info = self.get_info(item, work_info) - parentwork = parent_info[0] - mb_parentworkid = parent_info[1] - parent_composer = parent_info[2] - parent_composer_sort = parent_info[3] - parentwork_disambig = parent_info[4] - - found = True + work_info, work_date = find_parentwork_info(item.mb_workid) except musicbrainzngs.musicbrainz.WebServiceError: self._log.debug("Work unreachable") - found = False + return + parent_info = self.get_info(item, work_info) + elif hasparent: self._log.debug("Work already in library, not necessary fetching") return - if found: - self._log.debug("Finished searching work for: " + - item.artist + ' - ' + item.title) - self._log.debug("Work fetched: " + parentwork + - ' - ' + u', '.join(parent_composer)) - item['parentwork'] = parentwork - if parentwork_disambig: - item['parentwork_disambig'] = parentwork_disambig - item['mb_parentworkid'] = mb_parentworkid - item['parent_composer'] = u'' - item['parent_composer'] = u', '.join(parent_composer) - item['parent_composer_sort'] = u'' - item['parent_composer_sort'] = u', '.join(parent_composer_sort) - if work_date: - item['work_date'] = work_date - ui.show_model_changes( - item, fields=['parentwork', 'parentwork_disambig', - 'mb_parentworkid', 'parent_composer', - 'parent_composer_sort', 'work_date']) + self._log.debug("Finished searching work for: " + + item.artist + ' - ' + item.title) + self._log.debug("Work fetched: " + parent_info['parentwork'] + + ' - ' + parent_info['parent_composer']) - item.store() + for key, value in parent_info.items(): + if value: + item[key] = value + + if work_date: + item['work_date'] = work_date + ui.show_model_changes( + item, fields=['parentwork', 'parentwork_disambig', + 'mb_parentworkid', 'parent_composer', + 'parent_composer_sort', 'work_date']) + + item.store() From acf447b4b01f8c9296c05245975e954d612966a7 Mon Sep 17 00:00:00 2001 From: Dorian Soergel Date: Fri, 31 May 2019 17:52:39 +0200 Subject: [PATCH 05/27] adapt tests, correct docstrings --- beetsplug/parentwork.py | 9 +++++---- test/test_parentwork.py | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/beetsplug/parentwork.py b/beetsplug/parentwork.py index 4520edea28..e03a142d4e 100644 --- a/beetsplug/parentwork.py +++ b/beetsplug/parentwork.py @@ -26,7 +26,8 @@ def work_father_id(mb_workid, work_date=None): - """ Given a mb_workid, find the id one of the works the work is part of""" + """ Given a mb_workid, find the id one of the works the work is part of + and the first composition date it encounters. """ work_info = musicbrainzngs.get_work_by_id(mb_workid, includes=["work-rels", "artist-rels"]) @@ -46,7 +47,7 @@ def work_father_id(mb_workid, work_date=None): def work_parent_id(mb_workid): - """Find the parentwork id of a work given its id. """ + """Find the parentwork id and composition date of a work given its id. """ work_date = None while True: new_mb_workid, work_date = work_father_id(mb_workid, work_date) @@ -57,8 +58,8 @@ def work_parent_id(mb_workid): def find_parentwork_info(mb_workid): - """Return the work relationships (dict) of a parentwork given the id of - the work""" + """Return the work relationships (dict) and composition date of a + parentwork given the id of the work""" parent_id, work_date = work_parent_id(mb_workid) work_info = musicbrainzngs.get_work_by_id(parent_id, includes=["artist-rels"]) diff --git a/test/test_parentwork.py b/test/test_parentwork.py index 44545c63e3..985723fb5f 100644 --- a/test/test_parentwork.py +++ b/test/test_parentwork.py @@ -80,9 +80,9 @@ def test_no_force(self, command_output): def test_father_work(self, command_output): mb_workid = u'2e4a3668-458d-3b2a-8be2-0b08e0d8243a' self.assertEqual(u'f04b42df-7251-4d86-a5ee-67cfa49580d1', - parentwork.work_father(mb_workid)[0]) + parentwork.work_father_id(mb_workid)[0]) self.assertEqual(u'45afb3b2-18ac-4187-bc72-beb1b1c194ba', - parentwork.work_parent(mb_workid)[0]) + parentwork.work_parent_id(mb_workid)[0]) def suite(): From e6da3e149815b93d99b3a7fbf400f0845b55cd3c Mon Sep 17 00:00:00 2001 From: Dorian Soergel Date: Fri, 31 May 2019 18:28:53 +0200 Subject: [PATCH 06/27] move _command into command --- beetsplug/parentwork.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/beetsplug/parentwork.py b/beetsplug/parentwork.py index e03a142d4e..94d32f41ec 100644 --- a/beetsplug/parentwork.py +++ b/beetsplug/parentwork.py @@ -75,15 +75,6 @@ def __init__(self): 'force': False, }) - self._command = ui.Subcommand( - 'parentwork', - help=u'Fetches parent works, composers and dates') - - self._command.parser.add_option( - u'-f', u'--force', dest='force', - action='store_true', default=None, - help=u'Re-fetches all parent works') - if self.config['auto']: self.import_stages = [self.imported] @@ -99,9 +90,17 @@ def func(lib, opts, args): item.store() if write: item.try_write() + command = ui.Subcommand( + 'parentwork', + help=u'Fetches parent works, composers and dates') + + command.parser.add_option( + u'-f', u'--force', dest='force', + action='store_true', default=None, + help=u'Re-fetches all parent works') - self._command.func = func - return [self._command] + command.func = func + return [command] def imported(self, session, task): """Import hook for fetching parent works automatically. From a10ad548c9d5acf84c29c5bb33fef24247f44120 Mon Sep 17 00:00:00 2001 From: Dorian Soergel Date: Fri, 31 May 2019 20:54:15 +0200 Subject: [PATCH 07/27] logging if no parent composer --- beetsplug/parentwork.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/beetsplug/parentwork.py b/beetsplug/parentwork.py index 94d32f41ec..6350a72007 100644 --- a/beetsplug/parentwork.py +++ b/beetsplug/parentwork.py @@ -181,8 +181,11 @@ def find_work(self, item, force): self._log.debug("Finished searching work for: " + item.artist + ' - ' + item.title) - self._log.debug("Work fetched: " + parent_info['parentwork'] + - ' - ' + parent_info['parent_composer']) + if parent_info['parent_composer']: + self._log.debug("Work fetched: " + parent_info['parentwork'] + + ' - ' + parent_info['parent_composer']) + else: + self._log.debug("Work fetched: " + parent_info['parentwork']) for key, value in parent_info.items(): if value: From feafc66f96d51c0e26687abf150ee39b79a2990e Mon Sep 17 00:00:00 2001 From: Dorian Soergel Date: Mon, 3 Jun 2019 16:58:07 +0200 Subject: [PATCH 08/27] fixing parentwork but no parent composer --- beetsplug/parentwork.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/parentwork.py b/beetsplug/parentwork.py index 6350a72007..41b4fa4619 100644 --- a/beetsplug/parentwork.py +++ b/beetsplug/parentwork.py @@ -181,7 +181,7 @@ def find_work(self, item, force): self._log.debug("Finished searching work for: " + item.artist + ' - ' + item.title) - if parent_info['parent_composer']: + if hasattr(item, 'parentwork'): self._log.debug("Work fetched: " + parent_info['parentwork'] + ' - ' + parent_info['parent_composer']) else: From 369629bea5059c7955ffaaeb4214be5aac0885e8 Mon Sep 17 00:00:00 2001 From: Dorian Soergel Date: Mon, 3 Jun 2019 17:20:36 +0200 Subject: [PATCH 09/27] clarifying docstrings --- beetsplug/parentwork.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/beetsplug/parentwork.py b/beetsplug/parentwork.py index 41b4fa4619..00c6635db8 100644 --- a/beetsplug/parentwork.py +++ b/beetsplug/parentwork.py @@ -13,8 +13,8 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -"""Gets work title, disambiguation, parent work and its disambiguation, -composer, composer sort name and performers +"""Gets parent work, its disambiguation and id, composer, composer sort name +and work composition date """ from __future__ import division, absolute_import, print_function @@ -27,7 +27,10 @@ def work_father_id(mb_workid, work_date=None): """ Given a mb_workid, find the id one of the works the work is part of - and the first composition date it encounters. """ + and the first composition date it encounters. + + For a give work, hat we call father_work is the work it is part of. + The parent_work is the furthest ancestor.""" work_info = musicbrainzngs.get_work_by_id(mb_workid, includes=["work-rels", "artist-rels"]) From 1177222c6f4c5660afa52276ec9c559dbf6d6bca Mon Sep 17 00:00:00 2001 From: Dorian Soergel Date: Mon, 3 Jun 2019 17:45:57 +0200 Subject: [PATCH 10/27] flake8 --- beetsplug/parentwork.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/parentwork.py b/beetsplug/parentwork.py index 00c6635db8..78b7e129a3 100644 --- a/beetsplug/parentwork.py +++ b/beetsplug/parentwork.py @@ -29,7 +29,7 @@ def work_father_id(mb_workid, work_date=None): """ Given a mb_workid, find the id one of the works the work is part of and the first composition date it encounters. - For a give work, hat we call father_work is the work it is part of. + For a give work, hat we call father_work is the work it is part of. The parent_work is the furthest ancestor.""" work_info = musicbrainzngs.get_work_by_id(mb_workid, includes=["work-rels", From 380003a2fb23ea9cbfb1f148dd1db2af18209950 Mon Sep 17 00:00:00 2001 From: Dorian Soergel Date: Mon, 3 Jun 2019 20:01:02 +0200 Subject: [PATCH 11/27] fix documentation --- docs/plugins/parentwork.rst | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/docs/plugins/parentwork.rst b/docs/plugins/parentwork.rst index d64934b887..d76070b332 100644 --- a/docs/plugins/parentwork.rst +++ b/docs/plugins/parentwork.rst @@ -26,20 +26,6 @@ This plugin adds five tags: - **work_date**: THe composition date of the work, or the first parent work that has a composition date. Format: yyyy-mm-dd. -To fill in the parentwork tag and the associated parent** tags, in case there -are several works on the recording, it fills it with the results of the first -work and then appends the results of the second work only if they differ from -the ones already there. This is to care for cases of, for example, an opera -recording that contains several scenes of the opera: neither the parentwork -nor all the associated tags will be duplicated. -If there are several works linked to a recording, they all get a -disambiguation (empty as default) and if all disambiguations are empty, the -disambiguation field is left empty, else the disambiguation field can look -like ``,disambig,,`` (if there are four works and only the second has a -disambiguation) if only the second work has a disambiguation. This may -seem clumsy but it allows to identify which of the four works the -disambiguation belongs to. - To use the ``parentwork`` plugin, enable it in your configuration (see :ref:`using-plugins`). From 92d005ab30c9351745bffb8d5f37079dba04acf0 Mon Sep 17 00:00:00 2001 From: Dorian Soergel Date: Mon, 3 Jun 2019 20:05:34 +0200 Subject: [PATCH 12/27] renaming functions --- beetsplug/parentwork.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/beetsplug/parentwork.py b/beetsplug/parentwork.py index 78b7e129a3..753f1fa5fb 100644 --- a/beetsplug/parentwork.py +++ b/beetsplug/parentwork.py @@ -25,12 +25,9 @@ import musicbrainzngs -def work_father_id(mb_workid, work_date=None): +def direct_parent_id(mb_workid, work_date=None): """ Given a mb_workid, find the id one of the works the work is part of - and the first composition date it encounters. - - For a give work, hat we call father_work is the work it is part of. - The parent_work is the furthest ancestor.""" + and the first composition date it encounters.""" work_info = musicbrainzngs.get_work_by_id(mb_workid, includes=["work-rels", "artist-rels"]) @@ -53,7 +50,7 @@ def work_parent_id(mb_workid): """Find the parentwork id and composition date of a work given its id. """ work_date = None while True: - new_mb_workid, work_date = work_father_id(mb_workid, work_date) + new_mb_workid, work_date = direct_parent_id(mb_workid, work_date) if not new_mb_workid: return mb_workid, work_date mb_workid = new_mb_workid From b3b59f8452004694c64d9769123bbb693d4bd391 Mon Sep 17 00:00:00 2001 From: Dorian Soergel Date: Mon, 3 Jun 2019 20:14:43 +0200 Subject: [PATCH 13/27] rename functions in test --- test/test_parentwork.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_parentwork.py b/test/test_parentwork.py index 985723fb5f..8857739068 100644 --- a/test/test_parentwork.py +++ b/test/test_parentwork.py @@ -80,9 +80,9 @@ def test_no_force(self, command_output): def test_father_work(self, command_output): mb_workid = u'2e4a3668-458d-3b2a-8be2-0b08e0d8243a' self.assertEqual(u'f04b42df-7251-4d86-a5ee-67cfa49580d1', - parentwork.work_father_id(mb_workid)[0]) + parentwork.direct_parent_id(mb_workid)[0]) self.assertEqual(u'45afb3b2-18ac-4187-bc72-beb1b1c194ba', - parentwork.work_parent_id(mb_workid)[0]) + parentwork.direct_parent_id(mb_workid)[0]) def suite(): From a71c381bb5f800522e961d02a80a8994448357e8 Mon Sep 17 00:00:00 2001 From: Dorian Soergel Date: Mon, 3 Jun 2019 20:33:49 +0200 Subject: [PATCH 14/27] rename functions in test --- test/test_parentwork.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_parentwork.py b/test/test_parentwork.py index 8857739068..8447fcdd1c 100644 --- a/test/test_parentwork.py +++ b/test/test_parentwork.py @@ -82,7 +82,7 @@ def test_father_work(self, command_output): self.assertEqual(u'f04b42df-7251-4d86-a5ee-67cfa49580d1', parentwork.direct_parent_id(mb_workid)[0]) self.assertEqual(u'45afb3b2-18ac-4187-bc72-beb1b1c194ba', - parentwork.direct_parent_id(mb_workid)[0]) + parentwork.work_parent_id(mb_workid)[0]) def suite(): From 8363dedaebae23797abc5d2cddb0b41f8faf6158 Mon Sep 17 00:00:00 2001 From: Dorian Soergel Date: Wed, 5 Jun 2019 11:10:11 +0200 Subject: [PATCH 15/27] logging and minor comments --- beetsplug/parentwork.py | 60 +++++++++++++++++-------------------- docs/plugins/parentwork.rst | 34 ++++++++++----------- test/test_parentwork.py | 2 +- 3 files changed, 45 insertions(+), 51 deletions(-) diff --git a/beetsplug/parentwork.py b/beetsplug/parentwork.py index 753f1fa5fb..2ee6739d9b 100644 --- a/beetsplug/parentwork.py +++ b/beetsplug/parentwork.py @@ -26,8 +26,9 @@ def direct_parent_id(mb_workid, work_date=None): - """ Given a mb_workid, find the id one of the works the work is part of - and the first composition date it encounters.""" + """Given a mb_workid, find the id one of the works the work is part of + and the first composition date it encounters. + """ work_info = musicbrainzngs.get_work_by_id(mb_workid, includes=["work-rels", "artist-rels"]) @@ -38,16 +39,17 @@ def direct_parent_id(mb_workid, work_date=None): work_date = artist['end'] if 'work-relation-list' in work_info['work']: - for work_father in work_info['work']['work-relation-list']: - if work_father['type'] == 'parts' \ - and work_father.get('direction') == 'backward': - father_id = work_father['work']['id'] - return father_id, work_date + for direct_parent in work_info['work']['work-relation-list']: + if direct_parent['type'] == 'parts' \ + and direct_parent.get('direction') == 'backward': + direct_id = direct_parent['work']['id'] + return direct_id, work_date return None, work_date def work_parent_id(mb_workid): - """Find the parentwork id and composition date of a work given its id. """ + """Find the parent work id and composition date of a work given its id. + """ work_date = None while True: new_mb_workid, work_date = direct_parent_id(mb_workid, work_date) @@ -59,7 +61,8 @@ def work_parent_id(mb_workid): def find_parentwork_info(mb_workid): """Return the work relationships (dict) and composition date of a - parentwork given the id of the work""" + parent work given the id of the work + """ parent_id, work_date = work_parent_id(mb_workid) work_info = musicbrainzngs.get_work_by_id(parent_id, includes=["artist-rels"]) @@ -112,9 +115,10 @@ def imported(self, session, task): item.store() def get_info(self, item, work_info): - """Given the parentwork info dict, fetch parent_composer, + """Given the parent work info dict, fetch parent_composer, parent_composer_sort, parentwork, parentwork_disambig, mb_workid and - composer_ids. """ + composer_ids. + """ parent_composer = [] parent_composer_sort = [] @@ -132,10 +136,8 @@ def get_info(self, item, work_info): parent_composer_sort) if not composer_exists: - self._log.info(item.artist + ' - ' + item.title) - self._log.debug( - "no composer, add one at https://musicbrainz.org/work/" + - work_info['work']['id']) + self._log.debug('no composer for {}; add one at \ +https://musicbrainz.org/work/{}', item, work_info['work']['id']) parentwork_info['parentwork'] = work_info['work']['title'] parentwork_info['mb_parentworkid'] = work_info['work']['id'] @@ -150,43 +152,35 @@ def get_info(self, item, work_info): return parentwork_info def find_work(self, item, force): - """ Finds the parentwork of a recording and populates the tags + """Finds the parent work of a recording and populates the tags accordingly. Namely, the tags parentwork, parentwork_disambig, mb_parentworkid, - parent_composer, parent_composer_sort and work_date are populated. """ + parent_composer, parent_composer_sort and work_date are populated. + """ if hasattr(item, 'parentwork'): hasparent = True else: hasparent = False if not item.mb_workid: - self._log.info("No work attached, recording id: " + - item.mb_trackid) - self._log.info(item.artist + ' - ' + item.title) - self._log.info("add one at https://musicbrainz.org" + - "/recording/" + item.mb_trackid) + self._log.info('No work for {}, \ +add one at https://musicbrainz.org/recording/{}', item, item.mb_trackid) return if force or (not hasparent): try: work_info, work_date = find_parentwork_info(item.mb_workid) - except musicbrainzngs.musicbrainz.WebServiceError: - self._log.debug("Work unreachable") + except musicbrainzngs.musicbrainz.WebServiceError as e: + self._log.debug("error fetching work: {}", e) return parent_info = self.get_info(item, work_info) + self._log.debug("Work fetched: {} - {}", parent_info['parentwork'], + parent_info['parent_composer']) elif hasparent: - self._log.debug("Work already in library, not necessary fetching") + self._log.debug("{} : Work present, skipping", item) return - self._log.debug("Finished searching work for: " + - item.artist + ' - ' + item.title) - if hasattr(item, 'parentwork'): - self._log.debug("Work fetched: " + parent_info['parentwork'] + - ' - ' + parent_info['parent_composer']) - else: - self._log.debug("Work fetched: " + parent_info['parentwork']) - for key, value in parent_info.items(): if value: item[key] = value diff --git a/docs/plugins/parentwork.rst b/docs/plugins/parentwork.rst index d76070b332..cb586cce1c 100644 --- a/docs/plugins/parentwork.rst +++ b/docs/plugins/parentwork.rst @@ -1,29 +1,29 @@ Parentwork Plugin ================= -The ``parentwork`` plugin fetches the work title, parentwork title and -parentwork composer. +The ``parentwork`` plugin fetches the work title, parent work title and +parent work composer. In the MusicBrainz database, a recording can be associated with a work. A work can itself be associated with another work, for example one being part -of the other (what I call the father work). This plugin looks the work id -from the library and then looks up the father, then the father of the father -and so on until it reaches the top. The work at the top is what I call the -parentwork. This plugin is especially designed for classical music. For -classical music, just fetching the work title as in MusicBrainz is not -satisfying, because MusicBrainz has separate works for, for example, all the -movements of a symphony. This plugin aims to solve this problem by not only -fetching the work itself from MusicBrainz but also its parentwork which would -be, in this case, the whole symphony. +of the other (what I call the direct parent). This plugin looks the work id +from the library and then looks up the direct parent, then the direct parent +of the direct parent and so on until it reaches the top. The work at the top +is what I call the parent work. This plugin is especially designed for +classical music. For classical music, just fetching the work title as in +MusicBrainz is not satisfying, because MusicBrainz has separate works for, for +example, all the movements of a symphony. This plugin aims to solve this +problem by not only fetching the work itself from MusicBrainz but also its +parent work which would be, in this case, the whole symphony. This plugin adds five tags: -- **parentwork**: The title of the parentwork. -- **mb_parentworkid**: The musicbrainz id of the parentwork. -- **parentwork_disambig**: The disambiguation of the parentwork title. -- **parent_composer**: The composer of the parentwork. -- **parent_composer_sort**: The sort name of the parentwork composer. -- **work_date**: THe composition date of the work, or the first parent work +- **parentwork**: The title of the parent work. +- **mb_parentworkid**: The musicbrainz id of the parent work. +- **parentwork_disambig**: The disambiguation of the parent work title. +- **parent_composer**: The composer of the parent work. +- **parent_composer_sort**: The sort name of the parent work composer. +- **work_date**: The composition date of the work, or the first parent work that has a composition date. Format: yyyy-mm-dd. To use the ``parentwork`` plugin, enable it in your configuration (see diff --git a/test/test_parentwork.py b/test/test_parentwork.py index 8447fcdd1c..dfebc66023 100644 --- a/test/test_parentwork.py +++ b/test/test_parentwork.py @@ -77,7 +77,7 @@ def test_no_force(self, command_output): # test different cases, still with Matthew Passion Ouverture or Mozart # requiem - def test_father_work(self, command_output): + def test_direct_parent_work(self, command_output): mb_workid = u'2e4a3668-458d-3b2a-8be2-0b08e0d8243a' self.assertEqual(u'f04b42df-7251-4d86-a5ee-67cfa49580d1', parentwork.direct_parent_id(mb_workid)[0]) From fae065693576593c2f86cdcd349db98a51339333 Mon Sep 17 00:00:00 2001 From: Dorian Soergel Date: Wed, 5 Jun 2019 13:39:13 +0200 Subject: [PATCH 16/27] still dealing with cases where no parent composer --- beetsplug/parentwork.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/beetsplug/parentwork.py b/beetsplug/parentwork.py index 2ee6739d9b..76a98fc087 100644 --- a/beetsplug/parentwork.py +++ b/beetsplug/parentwork.py @@ -174,8 +174,13 @@ def find_work(self, item, force): self._log.debug("error fetching work: {}", e) return parent_info = self.get_info(item, work_info) - self._log.debug("Work fetched: {} - {}", parent_info['parentwork'], - parent_info['parent_composer']) + if 'parent_composer' in parent_info: + self._log.debug("Work fetched: {} - {}", + parent_info['parentwork'], + parent_info['parent_composer']) + else: + self._log.debug("Work fetched: {} - no parent composer", + parent_info['parentwork']) elif hasparent: self._log.debug("{} : Work present, skipping", item) From 1d9e42567b9dfc4238a861ae030911080a1a2e9d Mon Sep 17 00:00:00 2001 From: Dorian Soergel Date: Wed, 5 Jun 2019 13:40:04 +0200 Subject: [PATCH 17/27] flake8 --- beetsplug/parentwork.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/parentwork.py b/beetsplug/parentwork.py index 76a98fc087..2c5af03d16 100644 --- a/beetsplug/parentwork.py +++ b/beetsplug/parentwork.py @@ -178,7 +178,7 @@ def find_work(self, item, force): self._log.debug("Work fetched: {} - {}", parent_info['parentwork'], parent_info['parent_composer']) - else: + else: self._log.debug("Work fetched: {} - no parent composer", parent_info['parentwork']) From 765f7dc12d6732a9536df715cc3ac4f0d63aa57e Mon Sep 17 00:00:00 2001 From: Dorian Soergel Date: Fri, 7 Jun 2019 14:57:38 +0200 Subject: [PATCH 18/27] first try to implement event handler --- beetsplug/parentwork.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/beetsplug/parentwork.py b/beetsplug/parentwork.py index 2c5af03d16..ee892e8254 100644 --- a/beetsplug/parentwork.py +++ b/beetsplug/parentwork.py @@ -77,9 +77,14 @@ def __init__(self): 'auto': False, 'force': False, }) + if self.config['auto']: self.import_stages = [self.imported] + self.register_listener('database_change', self.find_work2) + + def find_work2(self, lib, model): + self.find_work(model, True) def commands(self): From 9c3c538dfb3940275148193f0011c107f632c523 Mon Sep 17 00:00:00 2001 From: Dorian Soergel Date: Fri, 7 Jun 2019 16:51:33 +0200 Subject: [PATCH 19/27] alternative way to refetch parent works --- beetsplug/parentwork.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/beetsplug/parentwork.py b/beetsplug/parentwork.py index ee892e8254..2c04fb5e9e 100644 --- a/beetsplug/parentwork.py +++ b/beetsplug/parentwork.py @@ -77,14 +77,9 @@ def __init__(self): 'auto': False, 'force': False, }) - if self.config['auto']: self.import_stages = [self.imported] - self.register_listener('database_change', self.find_work2) - - def find_work2(self, lib, model): - self.find_work(model, True) def commands(self): @@ -161,7 +156,8 @@ def find_work(self, item, force): accordingly. Namely, the tags parentwork, parentwork_disambig, mb_parentworkid, - parent_composer, parent_composer_sort and work_date are populated. + parent_composer, parent_composer_sort, mb_workid_current and work_date + are populated. """ if hasattr(item, 'parentwork'): @@ -172,13 +168,17 @@ def find_work(self, item, force): self._log.info('No work for {}, \ add one at https://musicbrainz.org/recording/{}', item, item.mb_trackid) return - if force or (not hasparent): + workcorrect=True + if hasattr(item, 'mb_workid_current'): + workcorrect=item.mb_workid_current==item.mb_workid + if force or (not hasparent) or (not workcorrect): try: work_info, work_date = find_parentwork_info(item.mb_workid) except musicbrainzngs.musicbrainz.WebServiceError as e: self._log.debug("error fetching work: {}", e) return parent_info = self.get_info(item, work_info) + parent_info['mb_workid_current']=item.mb_workid if 'parent_composer' in parent_info: self._log.debug("Work fetched: {} - {}", parent_info['parentwork'], From eacdb0d0e4334fe8a84f54b0779cbb2d3c4b232f Mon Sep 17 00:00:00 2001 From: Dorian Soergel Date: Fri, 7 Jun 2019 17:15:04 +0200 Subject: [PATCH 20/27] refetching works moved to new PR --- beetsplug/parentwork.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/beetsplug/parentwork.py b/beetsplug/parentwork.py index 2c04fb5e9e..681cff226c 100644 --- a/beetsplug/parentwork.py +++ b/beetsplug/parentwork.py @@ -168,17 +168,13 @@ def find_work(self, item, force): self._log.info('No work for {}, \ add one at https://musicbrainz.org/recording/{}', item, item.mb_trackid) return - workcorrect=True - if hasattr(item, 'mb_workid_current'): - workcorrect=item.mb_workid_current==item.mb_workid - if force or (not hasparent) or (not workcorrect): + if force or (not hasparent): try: work_info, work_date = find_parentwork_info(item.mb_workid) except musicbrainzngs.musicbrainz.WebServiceError as e: self._log.debug("error fetching work: {}", e) return parent_info = self.get_info(item, work_info) - parent_info['mb_workid_current']=item.mb_workid if 'parent_composer' in parent_info: self._log.debug("Work fetched: {} - {}", parent_info['parentwork'], From 070f50e1e7b9299bc39e61a1993f725a7634008e Mon Sep 17 00:00:00 2001 From: Dorian Soergel Date: Fri, 7 Jun 2019 17:16:03 +0200 Subject: [PATCH 21/27] docstring --- beetsplug/parentwork.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/beetsplug/parentwork.py b/beetsplug/parentwork.py index 681cff226c..2c5af03d16 100644 --- a/beetsplug/parentwork.py +++ b/beetsplug/parentwork.py @@ -156,8 +156,7 @@ def find_work(self, item, force): accordingly. Namely, the tags parentwork, parentwork_disambig, mb_parentworkid, - parent_composer, parent_composer_sort, mb_workid_current and work_date - are populated. + parent_composer, parent_composer_sort and work_date are populated. """ if hasattr(item, 'parentwork'): From c96dcfffb65d31243491699838cf4bd51d1f0432 Mon Sep 17 00:00:00 2001 From: Dorian Soergel Date: Sat, 8 Jun 2019 22:44:33 +0200 Subject: [PATCH 22/27] docstrings and style --- beetsplug/parentwork.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/beetsplug/parentwork.py b/beetsplug/parentwork.py index 2c5af03d16..3b8e272d42 100644 --- a/beetsplug/parentwork.py +++ b/beetsplug/parentwork.py @@ -60,8 +60,8 @@ def work_parent_id(mb_workid): def find_parentwork_info(mb_workid): - """Return the work relationships (dict) and composition date of a - parent work given the id of the work + """Get the MusicBrainz information dict about a parent work, including + the artist relations, and the composition date for a work's parent work. """ parent_id, work_date = work_parent_id(mb_workid) work_info = musicbrainzngs.get_work_by_id(parent_id, @@ -95,12 +95,12 @@ def func(lib, opts, args): item.try_write() command = ui.Subcommand( 'parentwork', - help=u'Fetches parent works, composers and dates') + help=u'fetche parent works, composers and dates') command.parser.add_option( u'-f', u'--force', dest='force', action='store_true', default=None, - help=u'Re-fetches all parent works') + help=u're-fetch when parent work is already present') command.func = func return [command] @@ -155,19 +155,21 @@ def find_work(self, item, force): """Finds the parent work of a recording and populates the tags accordingly. + The parent work is found recursively, by finding the direct parent + repeatedly until there are no more links in the chain. We return the + final, topmost work in the chain. + Namely, the tags parentwork, parentwork_disambig, mb_parentworkid, parent_composer, parent_composer_sort and work_date are populated. """ - if hasattr(item, 'parentwork'): - hasparent = True - else: - hasparent = False if not item.mb_workid: self._log.info('No work for {}, \ add one at https://musicbrainz.org/recording/{}', item, item.mb_trackid) return - if force or (not hasparent): + + hasparent = hasattr(item, 'parentwork') + if force or not hasparent: try: work_info, work_date = find_parentwork_info(item.mb_workid) except musicbrainzngs.musicbrainz.WebServiceError as e: @@ -183,9 +185,10 @@ def find_work(self, item, force): parent_info['parentwork']) elif hasparent: - self._log.debug("{} : Work present, skipping", item) + self._log.debug("{}: Work present, skipping", item) return + # apply all non-null values to the item for key, value in parent_info.items(): if value: item[key] = value @@ -196,5 +199,3 @@ def find_work(self, item, force): item, fields=['parentwork', 'parentwork_disambig', 'mb_parentworkid', 'parent_composer', 'parent_composer_sort', 'work_date']) - - item.store() From 022e3d44ead6ba688d5c08cf2a1092a84b837a54 Mon Sep 17 00:00:00 2001 From: Dorian Soergel Date: Sat, 8 Jun 2019 22:46:17 +0200 Subject: [PATCH 23/27] Update docs/plugins/parentwork.rst Co-Authored-By: Adrian Sampson --- docs/plugins/parentwork.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/parentwork.rst b/docs/plugins/parentwork.rst index cb586cce1c..188826e7e8 100644 --- a/docs/plugins/parentwork.rst +++ b/docs/plugins/parentwork.rst @@ -2,7 +2,7 @@ Parentwork Plugin ================= The ``parentwork`` plugin fetches the work title, parent work title and -parent work composer. +parent work composer from MusicBrainz. In the MusicBrainz database, a recording can be associated with a work. A work can itself be associated with another work, for example one being part From c8c206f19e3ec6ec15c264861b960864239d8803 Mon Sep 17 00:00:00 2001 From: Dorian Soergel Date: Sat, 8 Jun 2019 22:46:32 +0200 Subject: [PATCH 24/27] Update docs/plugins/parentwork.rst Co-Authored-By: Adrian Sampson --- docs/plugins/parentwork.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/parentwork.rst b/docs/plugins/parentwork.rst index 188826e7e8..74d22320f4 100644 --- a/docs/plugins/parentwork.rst +++ b/docs/plugins/parentwork.rst @@ -9,7 +9,7 @@ work can itself be associated with another work, for example one being part of the other (what I call the direct parent). This plugin looks the work id from the library and then looks up the direct parent, then the direct parent of the direct parent and so on until it reaches the top. The work at the top -is what I call the parent work. This plugin is especially designed for +is what we call the *parent work*. This plugin is especially designed for classical music. For classical music, just fetching the work title as in MusicBrainz is not satisfying, because MusicBrainz has separate works for, for example, all the movements of a symphony. This plugin aims to solve this From 2c3389beae9d1cf6b45ffe4567a7ad929a79747d Mon Sep 17 00:00:00 2001 From: Dorian Soergel Date: Sat, 8 Jun 2019 22:46:47 +0200 Subject: [PATCH 25/27] Update docs/plugins/parentwork.rst Co-Authored-By: Adrian Sampson --- docs/plugins/parentwork.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/parentwork.rst b/docs/plugins/parentwork.rst index 74d22320f4..e4469ae907 100644 --- a/docs/plugins/parentwork.rst +++ b/docs/plugins/parentwork.rst @@ -6,7 +6,7 @@ parent work composer from MusicBrainz. In the MusicBrainz database, a recording can be associated with a work. A work can itself be associated with another work, for example one being part -of the other (what I call the direct parent). This plugin looks the work id +of the other (what we call the *direct parent*). This plugin looks the work id from the library and then looks up the direct parent, then the direct parent of the direct parent and so on until it reaches the top. The work at the top is what we call the *parent work*. This plugin is especially designed for From fd14b5b64927c1bbe3a6a39ed000a41c01a5fd98 Mon Sep 17 00:00:00 2001 From: Dorian Soergel Date: Sat, 8 Jun 2019 22:55:05 +0200 Subject: [PATCH 26/27] docstrings and style --- beetsplug/parentwork.py | 2 +- docs/plugins/parentwork.rst | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/beetsplug/parentwork.py b/beetsplug/parentwork.py index 3b8e272d42..8e3dff32f2 100644 --- a/beetsplug/parentwork.py +++ b/beetsplug/parentwork.py @@ -26,7 +26,7 @@ def direct_parent_id(mb_workid, work_date=None): - """Given a mb_workid, find the id one of the works the work is part of + """Given a Musicbrainz id, find the id one of the works the work is part of and the first composition date it encounters. """ work_info = musicbrainzngs.get_work_by_id(mb_workid, diff --git a/docs/plugins/parentwork.rst b/docs/plugins/parentwork.rst index e4469ae907..a221e19b54 100644 --- a/docs/plugins/parentwork.rst +++ b/docs/plugins/parentwork.rst @@ -43,4 +43,3 @@ configuration file. The available options are: - **auto**: If enabled, automatically fetches works at import. It takes quite some time, because beets is restricted to one musicbrainz query per second. Default: ``no`` - From 9d184e3cade99e9a6529230f0f1ac095d05602ba Mon Sep 17 00:00:00 2001 From: Dorian Soergel Date: Sat, 8 Jun 2019 22:58:05 +0200 Subject: [PATCH 27/27] docstrings and style --- beetsplug/parentwork.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beetsplug/parentwork.py b/beetsplug/parentwork.py index 8e3dff32f2..63ef0102cd 100644 --- a/beetsplug/parentwork.py +++ b/beetsplug/parentwork.py @@ -26,8 +26,8 @@ def direct_parent_id(mb_workid, work_date=None): - """Given a Musicbrainz id, find the id one of the works the work is part of - and the first composition date it encounters. + """Given a Musicbrainz work id, find the id one of the works the work is + part of and the first composition date it encounters. """ work_info = musicbrainzngs.get_work_by_id(mb_workid, includes=["work-rels",