Repository: beetbox/beets

Issue:
Add parentwork plugin
Add a plugin that is able to fetch a performer, his sort name, the work, the work it is part of ( as explained in https://github.com/beetbox/beets/issues/2452 ) and the respective disambiguations, composer names and sort names. 
This plugin doesn't work in its current form, I don't understand why (there is certainly a valuable reason and an easy fix, I'm just completely helpless when confronted with SQL). 
I would be interested in additional featues or options for this plugin. 

What it should do so far is: 
fetch the performers of the recording
fetch the work title and disambiguation
look if the work is part of a bigger one, if yes take the work it is part of, repeat
fetch the parent work (which is part of no work) and disambiguation
fetch its composer (sort)name

Afaict the fetch part works fine, it's just the writing part that doesn't. 
So far, I would only put the tags into the library, but if there is an actual tag out there where it can be written in the file itself I would be interested. 



Discussion:
When I try to import something with this plugin enabled, I get: 

Traceback (most recent call last):
  File "/usr/bin/beet", line 11, in <module>
    load_entry_point('beets==1.4.4', 'console_scripts', 'beet')()
  File "/usr/lib/python3.6/site-packages/beets/ui/__init__.py", line 1228, in main
    _raw_main(args)
  File "/usr/lib/python3.6/site-packages/beets/ui/__init__.py", line 1215, in _raw_main
    subcommand.func(lib, suboptions, subargs)
  File "/usr/lib/python3.6/site-packages/beets/ui/commands.py", line 933, in import_func
    import_files(lib, paths, query)
  File "/usr/lib/python3.6/site-packages/beets/ui/commands.py", line 910, in import_files
    session.run()
  File "/usr/lib/python3.6/site-packages/beets/importer.py", line 325, in run
    pl.run_parallel(QUEUE_SIZE)
  File "/usr/lib/python3.6/site-packages/beets/util/pipeline.py", line 445, in run_parallel
    six.reraise(exc_info[0], exc_info[1], exc_info[2])
  File "/usr/lib/python3.6/site-packages/six.py", line 686, in reraise
    raise value
  File "/usr/lib/python3.6/site-packages/beets/util/pipeline.py", line 312, in run
    out = self.coro.send(msg)
  File "/usr/lib/python3.6/site-packages/beets/util/pipeline.py", line 194, in coro
    func(*(args + (task,)))
  File "/usr/lib/python3.6/site-packages/beets/importer.py", line 1398, in plugin_stage
    func(session, task)
  File "/usr/lib/python3.6/site-packages/beets/plugins.py", line 124, in wrapper
    return func(*args, **kwargs)
  File "/usr/lib/python3.6/site-packages/beetsplug/parentwork.py", line 59, in imported
    self.find_work(task.items)        
  File "/usr/lib/python3.6/site-packages/beetsplug/parentwork.py", line 135, in find_work
    item.store(fields=['parent_work','parent_work_disambig','work','work_disambig','performer','performer_sort','parent_composer','parent_composer_sort'])
  File "/usr/lib/python3.6/site-packages/beets/library.py", line 326, in store
    super(LibModel, self).store(fields)
  File "/usr/lib/python3.6/site-packages/beets/dbcore/db.py", line 381, in store
    tx.mutate(query, subvars)
  File "/usr/lib/python3.6/site-packages/beets/dbcore/db.py", line 693, in mutate
    cursor = self.db._connection().execute(statement, subvals)
sqlite3.InterfaceError: Error binding parameter 0 - probably unsupported type.

Now there is probably quite some work with the style, but at least I would like to get it working. 
Now it works on my laptop. I guess the problem with sql was a typing problem, I was trying to give him a list when the tag was marked as string. 
I have a question: now that I added tags, do I need to add fields (for file naming) or is this automatic (and with which name)?
I removed the multiple tries because it needed 2 indentation blocks and space is tight. If there is a tighter way to put it in I would be interested, but for now I will try to survive with the 8 tries of musicbrainzngs. However, for me this is usually not enough and I expect errors to be raised or works not to be fetched. 
So I removed the while-try-except loop that cares for the cases the standart amount of tries in musicbrainzngs is not enough and handle the corresponding error. However, I get problems with it: if the standart amount of tries is not enough an error is raised and the probram stops. I would like to put it back, but that would mean to add two indentation blocks, which I don't have enough space for if I have a max of 79 characters per line. 
I just did a try with the the supplementary tries and it is really slow: I let him go for 10 hours and he processed about 5000 recordings. 
Now the multiple tries are back, because it simply works. If the maximum number of tries is used, there is an error message. 
Hi! The plugin works fine, but due to the multiple calls to the MB server, it's really slow (a few seconds per track, so for a big collection it quickly becomes days or even weeks) so I use a copy of the server on localhost and it works fine. 
Does anyone have an opinion about what would be good optional parameters? 
I don't get how the tests work, they are complaining about indentation and whitespace but I don't understand how to correct it (as you see on the many 'style' commits), is there a style guide somewhere? The main problem seems to be on how to indent continuated lines or multiplely continuated lines. 
This plugin makes sense for classical music only, is there a way to segregate between classical music and non-classical? I tried the lastgenre plugin without success, would there be a way to do it by hand (something like making a list of non-classical albums or artists)?
I don't agree that it makes sense for classical works only.  In fact, this
plugin seems to pull the Performers tag, which is something i've been
hoping for since in started using Beets years ago.  For non-classical
entries, the other tags won't be there anyway.

On Wed, Jun 7, 2017 at 9:37 AM dosoe <notifications@github.com> wrote:

> This plugin makes sense for classical music only, is there a way to
> segregate between classical music and non-classical? I tried the lastgenre
> plugin without success, would there be a way to do it by hand (something
> like making a list of non-classical albums or artists)?
>
> —
> You are receiving this because you are subscribed to this thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/beetbox/beets/pull/2580#issuecomment-306833722>, or mute
> the thread
> <https://github.com/notifications/unsubscribe-auth/AADgP0HL-gVUVtmcCiDk6cp5nKPHWHb4ks5sBsOAgaJpZM4Nr2Ce>
> .
>
-- 

Bearcat M. Şándor, CEO
Feline Soul Systems LLC
Voice: 872.CAT.SOUL (872.228.7685 <(872)%20228-7685>)
Fax: 406.235.7070 <(406)%20235-7070>

True, maybe the "performer" tag should actually be implemented in the program itself, not in a plugin. However, I already have a pull request pending with some additional tags (arranger_sort and lyricist_sort) so I can do this once they have been accepted. 
I listen mainly to classical music so maybe I'm not the best to tell, but afaik the "parentwork" tag makes only sense in classical music. 
Maybe it would make sense to make a separate plugin with performers and instruments?
That would be great. I'm not s python programmer (yet) but I'd use such a plug-in to create more of them. Beets calls itself a teaser for those with OCD but it doesn't even import the producer, mixer, or mastering fields.
My plugin works fine, it just have some whitespace problems. Starting from it it should be easy to make one that just get the performer fields. Just add some printing to see how the recording info dictionaries look like. 
Once the problems with the arranger_sort and lyricist_sort are solved, I could use it as a template to add a performer tag, as well as a performer_sort and maybe a performer_instrument (or how should this one be called?). As sampsyo noticed, they are not at their right place in this plugin, they should move into the main program (or into a specific plugin) since they are not related to the work or the parentwork. 
I know how to read a dict and how to make trial and error, I don't know how to write good/clean/readable code, sorry for that. 
It's no problem at all! We're here to help.
I will remove the 'performer'-fetching part and put it into the main code
Ok, I made the documentation. Where can I see how it looks like? 
Woohoo! The docs are built with Sphinx. You can render them as HTML by going into the `docs` directory and running `make` (after installing Sphinx, which you can get from pip and most package managers).
Ok, so the documentation kinda works now, I just get this error (among other warnings): 
```checking consistency... /home/dorian/beets2/docs/plugins/parentwork.rst: WARNING: document isn't included in any toctree```
A question about the logging: when I try ```self._log.info()``` in a function outside of the parentworkplugin class, I get an error message when it gets applied. How can I go around it? I get that the ```self``` is only well defined inside the class, but how could I get around this outside of the class? 
Now I want to class my music using this plugin, but I want to class only the classical music. Is there a way to tell it: 'if the artist/albumartist is in this list, then move it according to this scheme, else use this other scheme' ? The lastgenre plugin is not working well enough for me, not all my albums are on last.fm. 
Should I add the new tags on https://beets.readthedocs.io/en/v1.4.5/reference/pathformat.html as well?
> A question about the logging: when I try self._log.info() in a function outside of the parentworkplugin class, I get an error message when it gets applied. How can I go around it? I get that the self is only well defined inside the class, but how could I get around this outside of the class?

One common technique is to pass the log object through as a parameter to helper functions. Another is to take those external functions and turn them into methods on the plugin class.

> Now I want to class my music using this plugin, but I want to class only the classical music. Is there a way to tell it: 'if the artist/albumartist is in this list, then move it according to this scheme, else use this other scheme' ?

Sure; you can use a beets query with an "or" operator. For example, the query `artist:foo, artist:bar` matches either of those two artists. Or you can use the `modify` command to manually set the `genre` field for classical music and use that.

> Should I add the new tags on https://beets.readthedocs.io/en/v1.4.5/reference/pathformat.html as well?

I'd say no—we generally use that for fields provided by the core, not plugins.
I just saw that there is a tag for "work" here: https://picard.musicbrainz.org/docs/mappings/ . Should I implement it and if yes, how?
Sure! You can add a new MediaField in the same way you did previously for `composer_sort`: https://github.com/beetbox/beets/pull/2529/files#diff-9f32ae8136422d861cc3215f657f1e1c
Yes, but now it's in a plugin, so I can't hardwire it in beets/mediafile.py , or can I?
Ah, I see. With one weird exception, no, fields with MediaFile storage need to be built-in. Of course, it would be fine to add these as built-in fields and then populate their values with a plugin.
Ok I tried to fill it in, but I'm not entirely sure about the values that should be in MP4StorageStyle and ASFStorageStyle . 
> One common technique is to pass the log object through as a parameter to helper functions. Another is to take those external functions and turn them into methods on the plugin class.

What do you mean? Would the function spit out a string and then it would be turned into a log inside of the class?
A little simpler than that: you'd just add a parameter to the function called `log` or something, and then the caller would provide `self._log` for that parameter. Then the function can just use `log.debug()` or whatever as normal.
Is there a condition for this parameter (type...)?
I have another question: one of the tests keep failing because of some style stuff. Where can I find the guidelines for this? Especially for expressions that span over several lines. 
> Is there a condition for this parameter (type...)?

Hmm; I don't quite understand… what do you mean by "condition" or "type"? (Python doesn't have type annotations.)

> I have another question: one of the tests keep failing because of some style stuff. Where can I find the guidelines for this? Especially for expressions that span over several lines.

Yep! Please take a look at the [Hacking](https://github.com/beetbox/beets/wiki/Hacking) page on the wiki. It describes our adherence to PEP 8 and how we check it with `flake8`. You can install and run `flake8` locally if you like.
So if I want something like "if albumartist_sort:'Amstrong, Louis' or albumartist_sort:'Aznavour, Charles', then do this, else do that" I have to write: ```%if{ albumartist_sort:'Amstrong, Louis', albumartist_sort:'Aznavour, Charles', this, that}``` ? I am wondering because the comma seems to mean either ```or```, ```then``` and ```else```. 
Ok, I got the style stuff done but I have an error with the documentation file that I don't understand. 
> /home/travis/build/beetbox/beets/docs/plugins/parentwork.rst:document isn't included in any toctree

You need to reference your plugin in the plugins index page <<https://github.com/beetbox/beets/blob/master/docs/plugins/index.rst>> to make that warning go away.
I tried various stuff to get a logical 'or' in my path format configuration, none of them worked (including ```%if{ albumartist_sort:'Amstrong, Louis', albumartist_sort:'Aznavour, Charles', this, that}```, ```%if{ albumartist_sort::'Amstrong, Louis', albumartist_sort::'Aznavour, Charles', this, that}``` and ```%if{ albumartist_sort::(Amstrong, Louis|Aznavour, Charles), this, that}```). Is there a proper way to do it? 
Thanks! I included it and the warning disappeared. 
One more thing I would like to do: implement a -f or --force option in the command line. I already have a force parameter in the configuration. 
> I tried various stuff to get a logical 'or' in my path format configuration

Unfortunately, %if doesn't work that way. You'll want to use conditional path formats, like the `albumtype:soundtrack` path described [in the docs](http://docs.beets.io/en/v1.4.5/reference/config.html#path-format-configuration).
Thanks, you just made it work. A little commit to correct the logs when the string is spread out over several lines. 
Ok I still have the logging problem I had there: 

> A question about the logging: when I try self._log.info() in a function outside of the parentworkplugin class, I get an error message when it gets applied. How can I go around it? I get that the self is only well defined inside the class, but how could I get around this outside of the class?

> One common technique is to pass the log object through as a parameter to helper functions. Another is to take those external functions and turn them into methods on the plugin class.

but then I don't understand your answer... If I need to pass a supplementary argument, let's call him "log", what type should he have? Should it be a string, an int? 
Something doesn't work related to the 'force' option as well, I have to look at it a bit. 
> If I need to pass a supplementary argument, let's call him "log", what type should he have? Should it be a string, an int?

It should be a Logger object—i.e., what `self._log` is.
Ok I tried to understand the other plugins, but I failed. How can I add an option ```force``` that would be a boolean and defined in the config file (```force: yes```) and in the command (```beet parentwork -f``` or something similar)? 
Ok I tried to understand the other plugins, but I failed. How can I add an option ```force``` that would be a boolean and defined in the config file (```force: yes```) and in the command (```beet parentwork -f``` or something similar)? 
 @bearcatsandor The performer fetching got moved there: https://github.com/beetbox/beets/pull/2563 , feel free to discuss it there. 
One straightforward way to get the "force from config or CLI" thing is just to use something like `opts.force or config['force']`, like this: https://github.com/beetbox/beets/blob/master/beetsplug/lyrics.py#L674
Logging seems to work, testing the cases where there is a work but no composer. 
Ok, I still don't get how to create a logger object. I tried out this: https://docs.python.org/3/howto/logging.html but it doesn't produce any output. I need one for the cases where there is a parent_work but no parent_composer, in the function get_info. 
In general, you don't need to create a logger yourself—you can just get `self._log` from your plugin and use that. For example, use `self._log.debug('...')` to log messages at the debug level.
afaict, the plugin now works, it should be ready for merging. I guess the content and informative value of the logs is always subject to debate, but the plugin gets the job done. 
ok, that seems to work. BTW, is there a shortcut in the terminal syntax for the beets directory (`directory: `in the config file)?
> ok, that seems to work. BTW, is there a shortcut in the terminal syntax for the beets directory (directory:in the config file)?

Hmm, I don't quite understand… what do you mean my "terminal syntax"? It's possible to get that value, if you're interested, from the `Library` object (i.e., `lib.directory`).
Well, it's a bit long to write down `/media/windows/Users/Dorian/Musik/beets/ZZZ-nowork`, is there a built-in alias for the library (that would transform it into something like `lib/ZZZ-nowork`)?
Aha, no—there's no shortcut exactly like that.
Do you have any more requests concerning this plugin or this other pull request https://github.com/beetbox/beets/pull/2563 ?
dosoe, what are you using as an editor? If it's vim, check out
https://github.com/Valloric/YouCompleteMe or
https://github.com/Shougo/deoplete.nvim for nvim.  They will give you
completion to insert that via an auto-dropdown.

On Tue, Jul 18, 2017 at 8:49 AM, dosoe <notifications@github.com> wrote:

> Do you have any more requests concerning this plugin or this other pull
> request #2563 <https://github.com/beetbox/beets/pull/2563> ?
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/beetbox/beets/pull/2580#issuecomment-316088414>, or mute
> the thread
> <https://github.com/notifications/unsubscribe-auth/AADgPx4uol_Z7Xk3rjU-DdjfdJfJA7saks5sPMX4gaJpZM4Nr2Ce>
> .
>



-- 
Bearcat M. Şándor
Feline Soul Systems LLC
Voice: 872.CAT.SOUL (872.228.7685)
Fax: 406.235.7070

I'm using... gedit. Yes, I'm not a programmer... But I will have to choose between emacs and vim at some point. 
but I'm asking for the terminal, not for an editor. 
Btw I did a pull request concerning fetching the performers, I can add other fields from MB if you want others. 
After the discussion in https://github.com/beetbox/beets/pull/2619 I moved the fetching of the `work` and `work_disambig` tags into the core and added the `work_id` (also in the core), and modified the plugin accordingly (no longer necessary to lookup the work using the recording id, checking if there is one because it is already done in core). This makes it faster because then the plugin needs one less API query per recording. However, I discovered a problem that I didn't notice before about the `work_disambig` tag: sometimes a recording contains multiple works, and some of them have a disambiguation, others don't. If I just create a plain list where the disambiguations, if they are present, are put in, then there will be an issue because it will be impossible to tell which disambiguation corresponds to which work (for example, if the first one has no disambiguation and the second has one, then `work` would look like: `[work1, work2]` and `work_disambig` would look like: `[disambig]` , however, the disambiguation corresponds to the second work). If I create a list with disambiguations and add empty strings for each work that doesn't have one, it will remove this problem but create another: if I make my paths like `$work ($work_disambig)` then if a work has no disambiguation, the path will look like: `work ()` which is not satisfying either. Nevertheless, that is the solution I chose, but there could be a discussion about it. 
I didn't have this problem with the `parent_work_disambig`, because I assume that a given recording has only one `parent_work``so the first of the methods explained above works fine. 
Ok I added a `parent_work_id` tag, and got stuck into another problem: I said before that 

> I didn't have this problem with the `parent_work_disambig`, because I assume that a given recording has only one `parent_work``so the first of the methods explained above works fine. 

However this is not true, because if a work doesn't have a father, it is its own parentwork and with that you can have exactly the same problems as with `work_disambig`. Is there a way that in the path configuration you tell beets that if a string is empty he should not print it? 
I will change the code to adapt the way disambiguations are fetched for parentworks. 
@mhendu : The parts of the code that do the work title, id and disambiguation fetching are all in the core part of beets, the plugin part (beetplug.parentwork.py) deals with the parentwork. 

I have to arrange the documentation and the changelog, I will do that tomorrow if there is no issue with the pull request so far. 
Documentation and changelog are done, I think this is ready to merge. 
Hi! I have a completely unrelated question: what is the job if a package manager, exactly? Do you have to test all pull requests by yourself? 
By the way, I see that this PR has much more commits than other ones. On my first PR I got a link to learn to make atomic commits (https://github.com/beetbox/beets/pull/2529 ), are they fine or are they too atomic now?
Hi @sampsyo I'm going through Hacking and I found: 

> Add tests. If you've fixed a bug, write a test to ensure that you've actually fixed it. If there's a new feature or plugin, please contribute tests that show that your code does what it says.

How could I do that? Just upload music files? 
I think lots of commits are fine! There’s no sin in that.

As for the tests, we actually have a separate wiki page all about testing. You can see lots of existing tests under the `test` directory in the repo.

As for the role of maintainer, that’s a very philosophical question! These days I see myself as having two roles: reviewing pull requests and setting the direction of the project. I also contribute code myself when I can. But it turns out that just carefully giving feedback on PRs is a full-time job...
a little merge to keep my fork up to date
Would you have a template or a similar plugin for me to create tests for this plugin? I have no idea how to make them from scratch. 
A good place to start might be the tests for an existing simple plugin, like this one: https://github.com/beetbox/beets/blob/master/test/test_ftintitle.py

In general, a good testing philosophy is to try to break your code into pieces (i.e., functions) that are as small an independent as possible. Then you can test them in isolation without too much effort.
So basically to test I have to let the functions run on examples where I know what the true solution is and then compare the true solution with what the result on the example is, and if it is the same the test is passed? Then we have to keep our true solution up to date, how do we do that? 
Yep, that’s the idea!

Sadly, the need to keep the tests and the implementation in sync is a necessary evil. It’s a little more work as the software evolves, but we often find it “worth it” to help lock in changes and avoid creating bugs accidentally.
Thanks for the example, but I don't understand what's happening in there. What are all these classes and functions doing?
The classes are basically just containers for the `test_*` methods, which do the actual work by setting things up and then calling `self.assert*` functions. It might be useful to start with some general background on the Python unittest library. There are some really great introductions out there:
http://python-guide-pt-br.readthedocs.io/en/latest/writing/tests/
http://www.diveintopython.net/unit_testing/index.html

The module documentation itself is pretty good:
https://docs.python.org/3/library/unittest.html
I hacked something together in the first class, why do you need the second one? commit incoming
By the way, how are the tests tested?
The tests are complaining about my `path` tag, it's strange because it works fine on the ftintitle test. 
Question on this - if you're able to pull all the data on the below pages, is it also possible to pull the recording date of a recording and the composition date of a work, going from the end date?

https://musicbrainz.org/release/876abe23-656e-4d65-8bc2-2e6513030be2

https://musicbrainz.org/release/7e77cba7-5584-4f58-9da2-ee24af146eb7

Beets doesn't currently store these fields (I think) but the recording date is referenced in another plugin that doesn't seem to be functional.

https://github.com/beetbox/beets/issues/1498

Similar to the folks on that thread, I like to tag my music to include when it was recorded (or, in the case of classical music, when it was composed) - this allows me to create playlists for my favorite songs from the '90s, for instance. Would need I think a separate issue to describe this but seems like the work done on this issue could apply in some ways.
Hi! Thanks for your input. Yes, getting the composition date is fairly easy, getting the first recording could be more tricky: do you want the release date of the first album containing it or the date of the first recording (i. e. the date it was actually recorded and not the date it was published)? Depending on the works only one of them (or even none) might be available. I expect the release date of the first album to exist on most works (especially for non-classical stuff) but this will be tricky to get: from the work page you get all the recordings of a work, from there you get to all the releases containing the said recordings and get the oldest one... I expect a lot of MB queries (we are capped at 1 query per second) to implement this. Additionally, if a work has parts, I expect people to record the whole work as a recording (a track) and others to record the work in several pieces (so several tracks) for the same work, which date do you take? It should be possible if we search the composition/first recording/first publishing date of a given work without caring about parentworks but if we get into the complications of split up works and having to deal with the corresponding dates for parentworks will be very complicated. 
btw the only problem so far with this plugin is that the test I wrote doesn't work (all the others do), would you have any idea why? 
I could get the composition year, but it will be much more complicated to get the year a work was first recorded. 
I have no clue why the tests keep failing, I believe that there are errors in test_parentwork.py but I don't find them, @sampsyo maybe you know? The plugin itself is working fine, with a funny exception: it doesn't work for very big releases (around 100 CDs) . For those, the work_id is not fetched, therefor the plugin cannot act. Could there be a limitation of the size of a query? For the kind of releases I speak about an album query (the release dictionary) represents a few MB of text. 
I would like to get it working before adding additional features like composition/first recording year
Now it seems that without external input I am not able to test it as extensively as I would like to, now I'm just testing if the functions that grab the father work and the parent work id work as they should on two examples. Unsurprisingly, the tests now start working. 
I tried to look deeper into the testing and the tests for the parentwork do work, there is just a test deep in test_ui.py that fails, I have no idea why and how, namely this one, line 805: 
```
def test_multiple_replacements_parsed(self):
        with self.write_config_file() as config:
            config.write("replace: {'[xy]': z, foo: bar}")
        self.run_command('test', lib=None)
        replacements = self.test_cmd.lib.replacements
        self.assertEqual(replacements, [
            (re.compile(u'[xy]'), u'z'),
            (re.compile(u'foo'), u'bar'),
        ])
```
with this error: 
```
======================================================================
FAIL: test_multiple_replacements_parsed (test.test_ui.ConfigTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/travis/build/beetbox/beets/test/test_ui.py", line 812, in test_multiple_replacements_parsed
    (re.compile(u'foo'), u'bar'),
AssertionError: Lists differ: [(<_sre.SRE_Pattern object at ... != [(<_sre.SRE_Pattern object at ...
First differing element 0:
(<_sre.SRE_Pattern object at 0x7f0e5513aae0>, u'z')
(<_sre.SRE_Pattern object at 0x7f0e5633cd40>, u'z')
- [(<_sre.SRE_Pattern object at 0x7f0e5513aae0>, u'z'),
?                                      ^^ ^^^
+ [(<_sre.SRE_Pattern object at 0x7f0e5633cd40>, u'z'),
?                                      ^ ^^^^
   (<_sre.SRE_Pattern object at 0x7f0e57eaf378>, u'bar')]
```
It seems not at all related to what I'm doing. 
By the way, I tried to run ```python3 setup.py test``` and got the following errors: 
```
======================================================================
FAIL: test_asciify_variable (test_library.DestinationFunctionTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/soergeld/beets/test/test_library.py", line 594, in test_asciify_variable
    self._assert_dest(b'/base/abC_ 1_2 d')
  File "/home/soergeld/beets/test/test_library.py", line 565, in _assert_dest
    self.assertEqual(actual, dest)
AssertionError: b'/base/abC_1_2d' != b'/base/abC_ 1_2 d'

======================================================================
FAIL: test_asciify_character_expanding_to_slash (test_library.DestinationTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/soergeld/beets/test/test_library.py", line 430, in test_asciify_character_expanding_to_slash
    self.assertEqual(self.i.destination(), np('lib/abC_ 1_2 d'))
AssertionError: b'/home/soergeld/beets/test/lib/abC_1_2d' != b'/home/soergeld/beets/test/lib/abC_ 1_2 d'

----------------------------------------------------------------------
```
that do not appear in the automated test and are equally not related to what I'm doing. Is it normal?
Those are caused by an old version of `unidecode`. Try updating that library and you should be good.
There was a requirement to add somewhere the composition date, I'm not sure how to do that. There would be a field ```work_date```. How could I catch it? Would it be just plainly on the work page (which would apply for most songs) or should I try to go higher, like go up the parentwork tree and take the first date I can find? Another issue: Musicbrainz doesn't think in dates, it thinks in intervals. When entering a composer to a work, I can add a begin and an end date for the composition, should I then take the end date? @mhendu , maybe you have an opinion about that? 
Additionally, now that I can fetch the works, the nest step would be to fetch the performers and instruments. However, it seems like it would be better to rework entirely the way the tags are implemented (https://github.com/beetbox/beets/issues/1547) . I tried but it seems I just don't have the programming skill and experience to do it. Is there any news from that?
I usually use the end date for the composition date. On occasion you'll find that there are child works that don't have composition dates but the parent work does, but that would I think require a separate API call to get the parent work's composition date.
I can retrieve the composition dates in the same api call that I use to get the parent work (or to check that there is none), that's not an issue. 
Here is a first try, that works on an album picked at random. It adds a tag ```work_date``` that contains the date (```yyyy-mm-dd``` if all is available, ```yyyy-mm``` if only the month and so on). Should I split it up into year, month and day? What do you think? I can always split it at the end. 
On the other hand, if you're interested in modern music (pop, rock and so on) what you might want to know is not the composition date but rather the date of the first recording. That however will be far trickier to get: should I get the oldest recording of the work, or the oldest recording of any of the work/father/parent? It shouldn't need more api calls, but it will fetch much more data. 
For me personally I look for composition date for classical works, and recording date for other recordings. In other words, if it's Rodrigo's Concierto de Aranjuez as performed by Narciso Yepes (https://musicbrainz.org/recording/0504986f-cd2d-4a1f-be9c-e21bf6206d0f) I'd look for the composition end date (1939). If it's Landslide as recorded by the Smashing Pumpkins (https://musicbrainz.org/recording/b35c227f-9840-4e0e-acd2-d9cebd1d041e) I'd look for the recording date or performance date (in this case, recording date of 1993-09-12), but not for the composition date of the original work.
I can get the oldest date there a work has been recorded. However, I feel like sometimes the first release date is better, but it's something I cannot get here. 
Wait, do you look for the recording time of the actual recording or for the first recording ever? If it's just the date of the recording, it's pretty easy to do (since it doesn't need more API calls , it could be in beets.core), if it's the oldest recording then it has its place here. 
So if I sum it up: you would like to get either the composition date (for classical stuff) or the date where the music was recorded. You do not want to know the date the recording was first released in any form or the first time a recording of the work was made. 
If that's it, I can get the recording date in the beets.core, except for recordings that are part of compilations/are a compilation, they would need special treatment. Should I just skip on those so far? As far as I know, it only concerns a small minority of recordings (but maybe in modern stuff it is different). If you don't want to skip it, an extra plugin would be required. Are you sure that the plugin on https://github.com/beetbox/beets/issues/1498 doesn't work? I didn't try it, but it looks fine. I feel like this PR is more about works than about recordings, but it doesn't harm. While you're at it, @mhendu maybe you can find the reason why one of the tests keeps failing. 
I think it has to be approved by the package maintainer, but thanks! 
@sampsyo I have a question for the tests: For them, I assume that I'm connected to the internet (and more specifically the musicbrainz server since I use their api). Could that be a reason the tests I wrote for the plugin are systematically failing? Could it be that for whatever reason, it cannot connect to the api and fails horribly because of that?
the standart test looks line this: 
```
def test_normal_case(self, command_output):
        item = Item(path='/file',
                    work_id=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['parent_work_id'],
                         u'32c8943f-1b27-3a23-8660-4567f4847c94')
```
Do you have any clue what could go wrong there? 
I’m not sure, but as a policy, our tests don’t use external services for this reason. It makes the tests flakier—for example, they will just fail if MB is down or your WiFi goes out. If you look at the MB tests we have, for example, they use mocking to keep things local.

I don’t see anything obviously wrong with that test. Any clues about what’s going wrong?
Hi! 
The thing I don't understand is that the test failing is not ```test_parentwork.py``` but ```test_ui.py```: 
```
Traceback (most recent call last):
  File "/home/travis/build/beetbox/beets/test/test_ui.py", line 812, in test_multiple_replacements_parsed
    (re.compile(u'foo'), u'bar'),
AssertionError: Lists differ: [(<_sre.SRE_Pattern object at ... != [(<_sre.SRE_Pattern object at ...
```

About not using external service, it's the integration of an external service that I'm testing (whether I read and process the results of the calls to musicbrainz correctly) so I don't know how to do without. 

**Edit: **
It turns out that even with the normal case, the test_ui.py is failing.
@sampsyo it seems like the tests are finally running fine. However, each time I want to edit the changelog I get a merging conflict, do you know why, or is it just that I didn't touch it for so long that I don't know how to use git anymore? Once this is cleared, could this be the moment to merge it? 
Hi! Thanks for the feedback! I'm not familiar with git or github, so I tried rebasing, but without success I think. Could an easy solution be for me to restart from scratch and decompose it accordingly into different PRs? Splitting between changes to the core and the plugin might be tricky considering that the plugin relies on the changes to the core. 
New PRs would work just fine! You could even start with the core fields PR and base the plugin PR on that branch to encode the fact that the latter depends on the former.

Resolving patch:
diff --git a/beetsplug/parentwork.py b/beetsplug/parentwork.py
new file mode 100644
--- /dev/null
+++ b/beetsplug/parentwork.py
@@ -0,0 +1,201 @@
+# -*- 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 parent work, its disambiguation and id, composer, composer sort name
+and work composition date
+"""
+
+from __future__ import division, absolute_import, print_function
+
+from beets import ui
+from beets.plugins import BeetsPlugin
+
+import musicbrainzngs
+
+
+def direct_parent_id(mb_workid, work_date=None):
+    """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",
+                                                        "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 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 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)
+        if not new_mb_workid:
+            return mb_workid, work_date
+        mb_workid = new_mb_workid
+    return mb_workid, work_date
+
+
+def find_parentwork_info(mb_workid):
+    """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,
+                                              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,
+        })
+
+        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()
+        command = ui.Subcommand(
+            'parentwork',
+            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-fetch when parent work is already present')
+
+        command.func = func
+        return [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 parent work 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']:
+            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'])
+
+            parentwork_info['parent_composer'] = u', '.join(parent_composer)
+            parentwork_info['parent_composer_sort'] = u', '.join(
+                    parent_composer_sort)
+
+        if not composer_exists:
+            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']
+
+        if 'disambiguation' in work_info['work']:
+            parentwork_info['parentwork_disambig'] = work_info[
+                    'work']['disambiguation']
+
+        else:
+            parentwork_info['parentwork_disambig'] = None
+
+        return parentwork_info
+
+    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 not item.mb_workid:
+            self._log.info('No work for {}, \
+add one at https://musicbrainz.org/recording/{}', item, item.mb_trackid)
+            return
+
+        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:
+                self._log.debug("error fetching work: {}", e)
+                return
+            parent_info = self.get_info(item, work_info)
+            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)
+            return
+
+        # apply all non-null values to the item
+        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'])


Test patch:
diff --git a/test/test_parentwork.py b/test/test_parentwork.py
new file mode 100644
--- /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_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])
+        self.assertEqual(u'45afb3b2-18ac-4187-bc72-beb1b1c194ba',
+                         parentwork.work_parent_id(mb_workid)[0])
+
+
+def suite():
+    return unittest.TestLoader().loadTestsFromName(__name__)
+
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')