Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PICARD-2607: Add a “_genre” variable #2545

Open
wants to merge 11 commits into
base: master
Choose a base branch
from

Conversation

rakim0
Copy link

@rakim0 rakim0 commented Oct 19, 2024

Summary

  • This is a…
    • Bug fix
    • Feature addition
    • Refactoring
    • Minor / simple change (like a typo)
    • Other
  • Describe this change in 1-2 sentences:
    Adds a custom field for folksonomy tags and genre.

Problem

There was no custom field for folksonomy tags. User was forced to either set all genre as genre tags or all folksonomy tags as Genre.
This PR aims to resolve it by introducing two new tags _folksonomy_tag and _genre.

Solution

To get both _genres and _folksonomy tags from the MusicBrainz server, I removed the 'use_folksonomy` check in picard/items.py and stored the tags in respective variables. Then for writing the metadata, I added two new functions in picard/track.py that would update the metadata with _genre and _folksonomy_tag. And before writing genre into the file, check whether genre should be set to _folksonomy_tag or _genre itself.

Check for 'Use only my genre' option is still done in picard/items.py

Action

Additional actions required:

  • Update Picard documentation (please include a reference to this PR)
  • Other (please specify below)

The check has been moved here from picard/mbjson.py as we are
loading _genres and _folksonomy_tags regardless of user setting.
Thus for filling in the actual genre tag, we are checking the user
preference.
@rakim0
Copy link
Author

rakim0 commented Oct 19, 2024

I tried to follow the Contribution Guidelines as much as possible but was a bit unsure about having multiple commits but couldn't figure a good commit message so broke the commit into 4 parts.

@Sophist-UK
Copy link
Contributor

I am really unclear whether this is actually a bug fix. And if it is an enhancement instead, then if it is not going to do something much more generalised, then it least needs to make sure it will be compatible with a future generalised function.

The issue with Genres is that there are so many competing sources of variable quality - many are free-form rather than a fixed list, those that are fixed lists have different fixed lists, and even when there are identical names, the definitions may be different or the allocation subjective. So if you are trying to fill a genre tag from multiple sources, you need to make sense of the mess you may often be given. In addition, there really needs to be needs to be a hierarchy i.e. there are many sub-forms of "Rock", and indeed these need to be many-to-many because there are also fusion genres which are sub-categories of more than one parent category.

So Picard IMO really needs to provide several generalised functions...

  1. Putting downloaded genres into separate metadata variables
  2. Providing functionality to clean-up these multiple lists of genres
  3. Providing functionality to consolidate them down into a single genre list for a track.
  4. Providing a plugin category for genre downloading - various types (artist, album, track) from various sources (MB being only one possible source of genre data).

I therefore encourage the authors and reviewers of this PR to think about the above and do their best to at least try to guide this PR into a form that is in the same general direction and which will at a minimum not make delivery of comprehensive genre functionality more difficult and ideally at least takes us a step in that direction.

(See also: Lyric handling which has similar (but simpler) plugin-based requirements.)

Copy link
Member

@phw phw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for this PR. I initially thought we can handle it more simple inside mbjson and directly write the values for _genres and _folksonomy_tags in the metadadata object there. But your implementation of storing the list with counts is more complex but needed to run the proper filtering later on.

Also tests should be fixed.

picard/item.py Outdated Show resolved Hide resolved
picard/mbjson.py Outdated Show resolved Hide resolved
picard/track.py Outdated
config = get_config()
tags = Counter(self._folksonomy_tags)
tags += self.album._folksonomy_tags
self.metadata['_folksonomy_tags'] = self._genres_to_metadata(tags, join_with=config.setting['join_genres'])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To my understanding we can do this simpler and don't need to filter the values for _folksonomy_tags and _genres. We just include all the loaded values in either variable. Only the final genre variable gets filtered.

But I asked UltimateRiff on PICARD-2607 for clarification also.

What I'm also unsure about if _folksonomy_tags should strictly only contain the non-genre tags. In that case we would filter out everything that is in _genres.

Copy link
Author

@rakim0 rakim0 Oct 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I think _folksonomy_tags should only have the non-genre tags. But well I'll just wait to see what UltimateRiff expects before I implement it.

For the implementation though, is iterating through _genres.keys() and deleting them from _folksonomy_tags going to be good enough or is there a more efficient method?

Copy link
Author

@rakim0 rakim0 Oct 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@phw

  1. Would you expect the _genres and _folksonomy_tags variables to hold all genres / folksonomy tags, or them to be filtered according to the genre filter rules? I assumed the first.
  2. Would you expect the _folksonomy_tags variable to hold all folksoonomy tags (of which the genres are just a part of) or strictly only the non-genre tags?

UltimateRiff replied:

on the first expectation, I'm honestly not sure what my preference would be
on the second tho, I think I'd expect _folksonomy_tags to be only non-genre tags

I think for first, including everything is the right way to go for now.
For the deleting genres from folksonomy tags, is my earlier approach fine?

Is iterating through _genres.keys() and deleting them from _folksonomy_tags going to be good enough or is there a more efficient method?

Copy link
Author

@rakim0 rakim0 Nov 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@phw Implemented deleting genres from tags in commit: 58286b7

picard/track.py Outdated Show resolved Hide resolved
@phw
Copy link
Member

phw commented Oct 20, 2024

I therefore encourage the authors and reviewers of this PR to think about the above and do their best to at least try to guide this PR into a form that is in the same general direction and which will at a minimum not make delivery of comprehensive genre functionality more difficult and ideally at least takes us a step in that direction.

@rakim-0 @Sophist-UK We have a separate ticket PICARD-2054 for this, but this needs additional considerations. But it has no effect on this story, which is only about adding the _genres and _folksonomy_tags variables with data loaded from MB.

Genres for separate sources would be handled by the genre handling functions in item.py. Generally we have most functionality available for this, and probably we could change plugins to use MetadataItem.add_genre / MetadataItem.add_folksonomy tags feature. But for full implementation we should have genre providers similar to cover art providers that can be enabled / disabled. If we do this correctly we could also solve additional tickets, e.g. we could have providers separately for recordings, releases, artist etc. and users could choose.

It's not unlikely that if we implement this the code added here will need some refactoring as well, but that needs to be done when PICARD-2054 actually gets implemented.

@rakim0
Copy link
Author

rakim0 commented Oct 20, 2024

I'll squash the smaller commits after I fix the tests.

Check for _genres and _folksonomy_tags individually as both are filled in separately
now.
@rakim0
Copy link
Author

rakim0 commented Oct 22, 2024

Fixed tests and resolved most comments.
I'm just unsure about this one: #2545 (comment) (filtering out genres from the folksonomy tags).

@zas zas requested a review from phw November 5, 2024 19:49
picard/track.py Outdated Show resolved Hide resolved
picard/track.py Outdated Show resolved Hide resolved
phw
phw previously approved these changes Nov 12, 2024
Copy link
Member

@phw phw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for the updates. This looks good to me and should work.

zas
zas previously approved these changes Nov 12, 2024
Copy link
Collaborator

@zas zas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks for your contribution.

@rdswift
Copy link
Collaborator

rdswift commented Nov 13, 2024

If you don't mind, can you please enter a ticket to update the Picard documentation to include this new variable? I suspect that the page that needs to be updated is https://github.com/metabrainz/picard-docs/blob/master/variables/variables_basic.rst

Thanks.

This function would remove the tags that are present in genre.
@rakim0 rakim0 dismissed stale reviews from zas and phw via 58286b7 November 17, 2024 10:29
picard/track.py Outdated Show resolved Hide resolved
Copy link
Member

@phw phw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you again. For me this looks great. Waiting for last review from @zas

@phw phw requested a review from zas November 18, 2024 07:11
Copy link
Collaborator

@zas zas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declaring attributes as "protected" (prefixing them with underscore) is good, but then they aren't meant to be used outside the class, apart its subclasses.

For _genres and _folksonomy_tags, please check carefully, and use the getter method if not in the class itself or a subclass.

genres += self.album.genres
use_folksonomy = config.setting['folksonomy_tags']
if use_folksonomy:
genres += self.album._folksonomy_tags
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
genres += self.album._folksonomy_tags
genres += self.album.folksonomy_tags

if use_folksonomy:
genres += self.album._folksonomy_tags
else:
genres += self.album._genres
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
genres += self.album._genres
genres += self.album.genres

self.assertEqual(a.genres, {
'genre1': 6, 'genre2': 3,
'tag1': 6, 'tag2': 3})
self.assertEqual(a._genres, {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.assertEqual(a._genres, {
self.assertEqual(a.genres, {

self.assertEqual(a._genres, {
'genre1': 6, 'genre2': 3
})
self.assertEqual(a._folksonomy_tags, {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.assertEqual(a._folksonomy_tags, {
self.assertEqual(a.folksonomy_tags, {

for artist in a._album_artists:
self.assertEqual(artist.genres, {
self.assertEqual(artist._folksonomy_tags, {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.assertEqual(artist._folksonomy_tags, {
self.assertEqual(artist.folksonomy_tags, {

'blue-eyed soul': 1,
'pop': 3})
for artist in t._track_artists:
self.assertEqual(artist.genres, {
self.assertEqual(artist._folksonomy_tags, {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.assertEqual(artist._folksonomy_tags, {
self.assertEqual(artist.folksonomy_tags, {

@@ -770,7 +776,7 @@ def test_release_group(self):
self.assertEqual(m['releasetype'], 'album')
self.assertEqual(m['~primaryreleasetype'], 'album')
self.assertEqual(m['~releasegroup'], 'The Dark Side of the Moon')
self.assertEqual(r.genres, {'test2': 3, 'test': 6})
self.assertEqual(r._folksonomy_tags, {'test2': 3, 'test': 6})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.assertEqual(r._folksonomy_tags, {'test2': 3, 'test': 6})
self.assertEqual(r.folksonomy_tags, {'test2': 3, 'test': 6})


def _add_folksonomy_tags(self):
tags = Counter(self._folksonomy_tags)
tags += self.album._folksonomy_tags
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
tags += self.album._folksonomy_tags
tags += self.album.folksonomy_tags


def _add_genres(self):
genre = Counter(self._genres)
genre += self.album._genres
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
genre += self.album._genres
genre += self.album.genres

genre += self.album._genres
self._add_tags(genre, '_genres')

def _delete_genres_from_tags(self):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we're in Track and delete stuff in Album, what about moving this method to Album instead and calling it from here?

@Sophist-UK
Copy link
Contributor

Declaring attributes as "protected" (prefixing them with underscore) is good, but then they aren't meant to be used outside the class, apart its subclasses.

Aside from in tests, are these used outside the class?

Because I wonder whether using protected attributes in tests is actually a good thing or not. (And funny enough about 20mins before reading this I was considering exactly this question but for PHP - except in PHP protected attributes are not actually accessible outside a class except through reflection manipulation).

The reason I wonder is because the idea behind protecting them is to prevent their use in genuine application code that shouldn't have access - and tests are IMO a special case. So on the one hand, making them non-protected fits the language intent and avoids e.g. codacy flags, but then makes them available to other genuine code when they probably should be hidden - and on the other hand using protected attributes outside the class in tests may lead to issues if Python ever decides that protected really means protected.

On the whole, my personal opinion leans towards continuing to have them as protected attributes and using them outside the class only in tests - and I am wondering how to achieve this in php tests using reflection.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants