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

Integration fixes #135

Merged
merged 35 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
6282720
Start the process of updating troi in lb-server
mayhem Feb 16, 2024
55556b7
Interim check in
mayhem Feb 19, 2024
6d6ce41
Start fixing the artist search to be compatible to the new endpoint
mayhem Feb 20, 2024
010c4c1
Interim checkin
mayhem Feb 21, 2024
021128d
New artist element is finally workin, but the server side needs
mayhem Feb 21, 2024
de7b842
Interim check in
mayhem Mar 14, 2024
141ba33
Change begin/end percent to pop_begin/end
mayhem Mar 15, 2024
c554743
Merge branch 'main' into integration-fixes
mayhem Apr 8, 2024
735969c
Since artist search is now merged, update this.
mayhem Apr 8, 2024
cb39231
Update for using beta's tag search
mayhem Apr 8, 2024
2970bc6
Merge branch 'main' into integration-fixes
mayhem Apr 12, 2024
88918a1
LB artist has gotten pretty trivial now that most of the logic is in PG!
mayhem Apr 12, 2024
240de74
Interim check-in
mayhem Apr 15, 2024
e441dd0
Fix specifying artist mbid for an artist
mayhem Apr 16, 2024
7b77315
Rename splitter to plist
mayhem Apr 16, 2024
7c67ffc
Meh
mayhem Apr 16, 2024
b80c37f
Finish plist cleanup
mayhem Apr 16, 2024
fa5c1a1
Make sure that plist random_items does not return duplicates
mayhem Apr 16, 2024
5f9e8cf
Fix the artist limiter and turn it on!
mayhem Apr 16, 2024
00908d2
Start on tag element cleanup
mayhem Apr 16, 2024
9341a17
Start work on the improved tag element -- things are looking promising
mayhem Apr 17, 2024
fd42859
Tweak
mayhem Apr 17, 2024
b030d0a
Use prod, since new tag search was merged
mayhem Apr 17, 2024
815e697
Add country element
mayhem Apr 22, 2024
adf63fe
Country and tag improvements.
mayhem Apr 23, 2024
06ad291
Allow area_mbids
mayhem Apr 23, 2024
8b5f56a
More minor fixes
mayhem Apr 23, 2024
23a128c
Shuffle tag radio results
mayhem Apr 25, 2024
d5682b5
Make syntax more consistent
mayhem Apr 25, 2024
6da8e98
More debugging, updating docs and testing all the examples
mayhem Apr 25, 2024
2477f5d
Improve error handling
mayhem Apr 25, 2024
3b22cc7
Minor error message improvement
mayhem Apr 25, 2024
99d5096
Add missing requirements file
mayhem Apr 25, 2024
921cc71
Add docs for country element
mayhem Apr 25, 2024
5b27377
PR feedback fixes
mayhem Apr 25, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions docs/lb_radio.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ The LB Radio supports the following entities:
#. **playlist**: Use a ListenBrainz playlist as a source of recordings. (mode also does not apply to playlists)
#. **stats**: Use a ListenBrainz user's statistics as a source of recordings.
#. **recs**: Use a ListenBrainz user's recommended recordings as a source of recordings.
#. **country**: Select recordings from artists who are from the given country.

Options
-------
Expand Down Expand Up @@ -165,21 +166,21 @@ If LB-radio does not find your artist, you can specify an artist using an Artist

::

artist:8f6bd1e4-fbe1-4f50-aa9b-94c450ec0f11
artist:(8f6bd1e4-fbe1-4f50-aa9b-94c450ec0f11)

LB-radio also supports MusicBrainz collections as sources:

::

collection:8be1a919-a386-45f3-8cc2-0d9249b02aa4
collection:(8be1a919-a386-45f3-8cc2-0d9249b02aa4)

Will select random recordings from a MusicBrainz recording collection -- the modes wont have any affect on collections, since
collections have no inherent ranking that could be used to select recordings according to mode. :(


::

playlist:8be1a919-a386-45f3-8cc2-0d9249b02aa4
playlist:(8be1a919-a386-45f3-8cc2-0d9249b02aa4)

Will select random recordings from a ListenBrainz playlist -- the modes wont have any affect on collections, since
plylists have no inherent ranking that could be used to select recordings according to mode. :(
Expand All @@ -198,6 +199,14 @@ Will select random recordings from the ListenBrainz user lucifer recordings stat

Will select random recordings from the ListenBrainz user mr_monkey's recommended recordings that mr_monkey hasn't listened to.

::

country:(Mali)

Will select random recordings from artists who are from the given country. While this features generally represents music from
that selected country, some artists leave their home country and don't perform music representative of their country, so
this element may not always be 100% on point. But it can still create some very interesting playlists!


More complex examples
---------------------
Expand Down
5 changes: 5 additions & 0 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Sphinx==7.2.6
sphinxcontrib-httpdomain==1.8.1
sphinx_rtd_theme==2.0.0
docutils==0.20.1
sphinx-click==5.1.0
31 changes: 22 additions & 9 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,21 @@ class TestParser(unittest.TestCase):

def test_basic_entities(self):
pp = PromptParser()
r = pp.parse("artist:57baa3c6-ee43-4db3-9e6a-50bbc9792ee4")
r = pp.parse("artist:(57baa3c6-ee43-4db3-9e6a-50bbc9792ee4)")
assert r[0] == {"entity": "artist", "values": [UUID("57baa3c6-ee43-4db3-9e6a-50bbc9792ee4")], "weight": 1, "opts": []}

r = pp.parse("artist:57baa3c6-ee43-4db3-9e6a-50bbc9792ee4")
r = pp.parse("artist:(57baa3c6-ee43-4db3-9e6a-50bbc9792ee4)")
assert r[0] == {"entity": "artist", "values": [UUID("57baa3c6-ee43-4db3-9e6a-50bbc9792ee4")], "weight": 1, "opts": []}

self.assertRaises(ParseError, pp.parse, "wrong:57baa3c6-ee43-4db3-9e6a-50bbc9792ee4")
self.assertRaises(ParseError, pp.parse, "artist:57baa3c6-ee43-4db3-9e6a-50bbc9792ee4")

r = pp.parse("artist:(the knife)")
assert r[0] == {"entity": "artist", "values": ["the knife"], "weight": 1, "opts": []}

self.assertRaises(ParseError, pp.parse, "artist:u2:nosim")
self.assertRaises(ParseError, pp.parse, "artists:u2:nosim")
self.assertRaises(ParseError, pp.parse, "country:andorra")

def test_tags(self):
pp = PromptParser()
Expand Down Expand Up @@ -52,6 +54,9 @@ def test_tags(self):
r = pp.parse("tag:(モーニング娘。)")
assert r[0] == {"entity": "tag", "values": ["モーニング娘。"], "weight": 1, "opts": []}

r = pp.parse("tag:(57baa3c6-ee43-4db3-9e6a-50bbc9792ee4)")
assert r[0] == {"entity": "tag", "values": ["57baa3c6-ee43-4db3-9e6a-50bbc9792ee4"], "weight": 1, "opts": []}

def test_tag_errors(self):
pp = PromptParser()
self.assertRaises(ParseError, pp.parse, "t:(abstract rock blues):bork")
Expand All @@ -73,25 +78,25 @@ def test_shortcuts(self):

def test_compound(self):
pp = PromptParser()
r = pp.parse('artist:05319f96-e409-4199-b94f-3cabe7cc188a:2 tag:(downtempo):1 tag:(trip hop, abstract):2')
r = pp.parse('artist:(05319f96-e409-4199-b94f-3cabe7cc188a):2 tag:(downtempo):1 tag:(trip hop, abstract):2')
assert r[0] == {"entity": "artist", "values": [UUID("05319f96-e409-4199-b94f-3cabe7cc188a")], "weight": 2, "opts": []}
assert r[1] == {"entity": "tag", "values": ["downtempo"], "weight": 1, "opts": []}
assert r[2] == {"entity": "tag", "values": ["trip hop", "abstract"], "weight": 2, "opts": []}

def test_weights(self):
pp = PromptParser()
r = pp.parse("artist:57baa3c6-ee43-4db3-9e6a-50bbc9792ee4:1 artist:f54ba4c6-12dd-4358-9136-c64ad89420c5:2")
r = pp.parse("artist:(57baa3c6-ee43-4db3-9e6a-50bbc9792ee4):1 artist:(f54ba4c6-12dd-4358-9136-c64ad89420c5):2")
assert r[0] == {"entity": "artist", "values": [UUID("57baa3c6-ee43-4db3-9e6a-50bbc9792ee4")], "weight": 1, "opts": []}
assert r[1] == {"entity": "artist", "values": [UUID("f54ba4c6-12dd-4358-9136-c64ad89420c5")], "weight": 2, "opts": []}

self.assertRaises(ParseError, pp.parse,
"artist:57baa3c6-ee43-4db3-9e6a-50bbc9792ee4:1 artist:f54ba4c6-12dd-4358-9136-c64ad89420c5:fussy")
self.assertRaises(ParseError, pp.parse, "artist:57baa3c6-ee43-4db3-9e6a-50bbc9792ee4:1 artist:f54ba4c6-12dd-4358-9136-c64ad89420c5:.5")

r = pp.parse("artist:portishead::easy")
r = pp.parse("artist:(portishead)::easy")
assert r[0] == {"entity": "artist", "values": ["portishead"], "weight": 1, "opts": ["easy"]}

r = pp.parse("artist:57baa3c6-ee43-4db3-9e6a-50bbc9792ee4::easy")
r = pp.parse("artist:(57baa3c6-ee43-4db3-9e6a-50bbc9792ee4)::easy")
assert r[0] == {"entity": "artist", "values": [UUID("57baa3c6-ee43-4db3-9e6a-50bbc9792ee4")], "weight": 1, "opts": ["easy"]}

def test_opts(self):
Expand All @@ -112,13 +117,13 @@ def test_parens(self):

def test_collection_playlist(self):
pp = PromptParser()
r = pp.parse("collection:57baa3c6-ee43-4db3-9e6a-50bbc9792ee4")
r = pp.parse("collection:(57baa3c6-ee43-4db3-9e6a-50bbc9792ee4)")
assert r[0] == {"entity": "collection", "values": [UUID("57baa3c6-ee43-4db3-9e6a-50bbc9792ee4")], "weight": 1, "opts": []}

r = pp.parse("playlist:57baa3c6-ee43-4db3-9e6a-50bbc9792ee4")
r = pp.parse("playlist:(57baa3c6-ee43-4db3-9e6a-50bbc9792ee4)")
assert r[0] == {"entity": "playlist", "values": [UUID("57baa3c6-ee43-4db3-9e6a-50bbc9792ee4")], "weight": 1, "opts": []}

r = pp.parse("playlist:57baa3c6-ee43-4db3-9e6a-50bbc9792ee4")
r = pp.parse("playlist:(57baa3c6-ee43-4db3-9e6a-50bbc9792ee4)")
assert r[0] == {"entity": "playlist", "values": [UUID("57baa3c6-ee43-4db3-9e6a-50bbc9792ee4")], "weight": 1, "opts": []}

def test_stats(self):
Expand Down Expand Up @@ -156,3 +161,11 @@ def test_recs(self):

r = pp.parse("recs:(rob zombie)")
assert r[0] == {"entity": "recs", "values": ["rob zombie"], "weight": 1, "opts": []}

def test_country(self):
pp = PromptParser()
r = pp.parse("country:(57baa3c6-ee43-4db3-9e6a-50bbc9792ee4)")
assert r[0] == {"entity": "country", "values": [UUID("57baa3c6-ee43-4db3-9e6a-50bbc9792ee4")], "weight": 1, "opts": []}

r = pp.parse("country:(mali)")
assert r[0] == {"entity": "country", "values": ["mali"], "weight": 1, "opts": []}
25 changes: 25 additions & 0 deletions tests/test_plist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import unittest

from troi.plist import plist


class TestSplitter(unittest.TestCase):

def test_plist(self):
pl = plist([0,1,2,3,4,5,6,7,8,9])

assert pl[0:10] == [0]
assert pl[0:20] == [0,1]
assert pl[50:100] == [5,6,7,8,9]
assert pl.uslice(0, 10) == [0]
assert pl.uslice(0, 20) == [0,1]
assert pl.uslice(50, 100) == [5,6,7,8,9]

assert pl.dslice(0, 2) == [0,1]

assert pl.random_item(50, 100) in [5,6,7,8,9]

def test_plist_unique(self):
pl = plist([0,1,2,3,4,5,6,7,8,9])
rlist = pl.random_item(count=9)
assert len(rlist) == len(set(rlist))
76 changes: 0 additions & 76 deletions tests/test_splitter.py

This file was deleted.

6 changes: 5 additions & 1 deletion troi/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,11 @@ def playlist(patch, quiet, save, token, upload, args, created_for, name, desc, m
"This is a local patch and should be invoked via the specific troi function, rather than the playlist function.")
return None

ret = patch.generate_playlist()
try:
ret = patch.generate_playlist()
except RuntimeError as err:
logger.error(err)
ret = 0

user_feedback = patch.user_feedback()
if len(user_feedback) > 0:
Expand Down
72 changes: 56 additions & 16 deletions troi/content_resolver/artist_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,21 @@
from troi.content_resolver.model.recording import Recording, RecordingMetadata
from troi.content_resolver.utils import select_recordings_on_popularity
from troi.recording_search_service import RecordingSearchByArtistService
from troi.splitter import plist
from troi.plist import plist

OVERHYPED_SIMILAR_ARTISTS = [
"b10bbbfc-cf9e-42e0-be17-e2c3e1d2600d", # The Beatles
"83d91898-7763-47d7-b03b-b92132375c47", # Pink Floyd
"a74b1b7f-71a5-4011-9441-d0b5e4122711", # Radiohead
"8bfac288-ccc5-448d-9573-c33ea2aa5c30", # Red Hot Chili Peppers
"9c9f1380-2516-4fc9-a3e6-f9f61941d090", # Muse
"cc197bad-dc9c-440d-a5b5-d52ba2e14234", # Coldplay
"65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab", # Metallica
"5b11f4ce-a62d-471e-81fc-a69a8278c7da", # Nirvana
"f59c5520-5f46-4d2c-b2c4-822eabf53419", # Linkin Park
"cc0b7089-c08d-4c10-b6b0-873582c17fd6", # System of a Down
"ebfc1398-8d96-47e3-82c3-f782abcdb13d", # Beach boys
]

class LocalRecordingSearchByArtistService(RecordingSearchByArtistService):
'''
Expand All @@ -21,23 +34,50 @@ class LocalRecordingSearchByArtistService(RecordingSearchByArtistService):
def __init__(self):
RecordingSearchByArtistService.__init__(self)

def search(self, artist_mbids, begin_percent, end_percent, num_recordings):
def get_similar_artists(self, artist_mbid):
""" Fetch similar artists, given an artist_mbid. Returns a sored plist of artists. """

r = requests.post("https://labs.api.listenbrainz.org/similar-artists/json",
json=[{
'artist_mbid':
artist_mbid,
'algorithm':
"session_based_days_7500_session_300_contribution_5_threshold_10_limit_100_filter_True_skip_30"
}])
if r.status_code != 200:
raise RuntimeError(f"Cannot fetch similar artists: {r.status_code} ({r.text})")

try:
artists = r.json()[3]["data"]
except IndexError:
return []

# Knock down super hyped artists
for artist in artists:
if artist["artist_mbid"] in OVERHYPED_SIMILAR_ARTISTS:
artist["score"] /= 3 # Chop!

return plist(sorted(artists, key=lambda a: a["score"], reverse=True))

def search(self, mode, artist_mbid, pop_begin, pop_end, max_recordings_per_artist, max_similar_artists):

"""
Perform an artist search. Parameters:

tags - a list of artist_mbids for which to search recordings
begin_percent - if many recordings match the above parameters, return only
recordings that have a minimum popularity percent score
of begin_percent.
end_percent - if many recordings match the above parameters, return only
recordings that have a maximum popularity percent score
of end_percent.
num_recordings - ideally return these many recordings

If only few recordings match, the begin_percent and end_percent are
ignored.
mode: the mode used for this artist search
pop_begin: if many recordings match the above parameters, return only
recordings that have a minimum popularity percent score
of pop_begin.
pop_end: if many recordings match the above parameters, return only
recordings that have a maximum popularity percent score
of pop_end.
max_recordings_per_artist: The number of recordings to collect for each artist.
max_similar_artists: The maximum number of similar artists to select.

If only few recordings match, the pop_begin and pop_end are ignored.
"""

similar_artists = self.get_similar_artists(artist_mbid)
query = """SELECT popularity
, recording_mbid
, artist_mbid
Expand All @@ -50,8 +90,8 @@ def search(self, artist_mbids, begin_percent, end_percent, num_recordings):
ORDER BY artist_mbid
, popularity"""

placeholders = ",".join(("?", ) * len(artist_mbids))
cursor = db.execute_sql(query % placeholders, params=tuple(artist_mbids))
placeholders = ",".join(("?", ) * len(similar_artists))
cursor = db.execute_sql(query % placeholders, params=tuple(similar_artists))

artists = defaultdict(list)
for rec in cursor.fetchall():
Expand All @@ -64,6 +104,6 @@ def search(self, artist_mbids, begin_percent, end_percent, num_recordings):
})

for artist in artists:
artists[artist] = select_recordings_on_popularity(artists[artist], begin_percent, end_percent, num_recordings)
artists[artist] = select_recordings_on_popularity(artists[artist], pop_begin, pop_end, num_recordings)

return artists
Loading
Loading