This repository has been archived by the owner on Feb 9, 2024. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
First cut at periodic jams for lb local. Not a bad start!
- Loading branch information
Showing
11 changed files
with
249 additions
and
51 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,3 +15,6 @@ mp3 | |
/build/ | ||
/dist/ | ||
config.py | ||
*.jspf | ||
*.m3u | ||
.eggs |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
from datetime import datetime, timedelta | ||
|
||
import troi.listenbrainz.recs | ||
import troi.musicbrainz.recording_lookup | ||
from troi import Playlist | ||
from troi.playlist import PlaylistMakerElement | ||
|
||
from lb_content_resolver.troi.recording_resolver import RecordingResolverElement | ||
from lb_content_resolver.model.database import db | ||
|
||
DAYS_OF_RECENT_LISTENS_TO_EXCLUDE = 60 # Exclude tracks listened in last X days from the daily jams playlist | ||
DAILY_JAMS_MIN_RECORDINGS = 25 # the minimum number of recordings we aspire to have in a daily jam, this is not a hard limit | ||
BATCH_SIZE_RECS = 1000 # the number of recommendations fetched in 1 go | ||
MAX_RECS_LIMIT = 1000 # the maximum of recommendations available in LB | ||
|
||
class LocalPeriodicJamsPatch(troi.patch.Patch): | ||
""" | ||
""" | ||
|
||
|
||
def __init__(self, args, debug=False): | ||
super().__init__(args, debug) | ||
|
||
@staticmethod | ||
def inputs(): | ||
""" | ||
Generate a periodic playlist from the ListenBrainz recommended recordings. | ||
\b | ||
USER_NAME is a MusicBrainz user name that has an account on ListenBrainz. | ||
TYPE Must be one of "daily-jams", "weekly-jams" or "weekly-exploration". | ||
JAM_DATE is the date for which the jam is created (this is needed to account for the fact different timezones | ||
can be on different dates). Required formatting for the date is 'YYYY-MM-DD'. | ||
""" | ||
return [{ | ||
"type": "argument", | ||
"args": ["user_name"] | ||
}, { | ||
"type": "argument", | ||
"args": ["type"], | ||
"kwargs": { | ||
"required": False | ||
} | ||
}] | ||
|
||
@staticmethod | ||
def outputs(): | ||
return [Playlist] | ||
|
||
@staticmethod | ||
def slug(): | ||
return "local-periodic-jams" | ||
|
||
@staticmethod | ||
def description(): | ||
return "Generate a localized periodic playlist from the ListenBrainz recommended recordings." | ||
|
||
def create(self, inputs): | ||
user_name = inputs['user_name'] | ||
|
||
recs = troi.listenbrainz.recs.UserRecordingRecommendationsElement(user_name, | ||
"raw", | ||
count=1000) | ||
recs_lookup = troi.musicbrainz.recording_lookup.RecordingLookupElement() | ||
recs_lookup.set_sources(recs) | ||
|
||
resolve = RecordingResolverElement(db, .8) | ||
resolve.set_sources(recs_lookup) | ||
|
||
pl_maker = PlaylistMakerElement(name="Local Periodic Jams for %s" % (user_name), | ||
desc="test playlist!", | ||
patch_slug="periodic-jams", | ||
max_num_recordings=50, | ||
max_artist_occurrence=2, | ||
shuffle=True, | ||
expires_at=datetime.utcnow() + timedelta(weeks=2)) | ||
pl_maker.set_sources(resolve) | ||
|
||
return pl_maker |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
from lb_content_resolver.lb_radio import ListenBrainzRadioLocal | ||
from lb_content_resolver.troi.patches.periodic_jams import LocalPeriodicJamsPatch | ||
|
||
|
||
class LocalPeriodicJams(ListenBrainzRadioLocal): | ||
''' | ||
Generate local playlists against a music collection available via subsonic. | ||
''' | ||
|
||
# TODO: Make this an argument | ||
MATCH_THRESHOLD = .8 | ||
|
||
def __init__(self, db, user_name): | ||
ListenBrainzRadioLocal.__init__(self, db) | ||
self.user_name = user_name | ||
|
||
def generate(self): | ||
""" | ||
Generate a periodic jams playlist | ||
""" | ||
|
||
self.db.open_db() | ||
|
||
patch = LocalPeriodicJamsPatch({"user_name": self.user_name, "echo": True, "debug": True, "min_recordings": 1}) | ||
|
||
# Now generate the playlist | ||
try: | ||
playlist = patch.generate_playlist() | ||
except RuntimeError as err: | ||
print(f"LB Radio generation failed: {err}") | ||
return None | ||
|
||
if playlist == None: | ||
print("Your prompt generated an empty playlist.") | ||
self.sanity_check() | ||
|
||
# Resolve any tracks that have not been resolved to a subsonic_id or a local file | ||
self.resolve_playlist(self.MATCH_THRESHOLD, playlist) | ||
|
||
return playlist.get_jspf() if playlist is not None else {"playlist": {"track": []}} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
#from troi.musicbrainz.recording_lookup import RecordingLookupElement | ||
from troi import Element | ||
|
||
from lb_content_resolver.content_resolver import ContentResolver | ||
from lb_content_resolver.model.subsonic import RecordingSubsonic | ||
from lb_content_resolver.model.recording import Recording | ||
from troi import Recording | ||
|
||
|
||
class RecordingResolverElement(Element): | ||
|
||
def __init__(self, db, match_threshold): | ||
Element.__init__(self) | ||
self.db = db | ||
self.match_threshold = match_threshold | ||
self.resolve = ContentResolver(db) | ||
|
||
@staticmethod | ||
def inputs(): | ||
return [] | ||
|
||
@staticmethod | ||
def outputs(): | ||
return [Recording] | ||
|
||
def read(self, inputs): | ||
|
||
# TODO: Add a check to make sure that metadata is present. | ||
|
||
# Build the fuzzy index | ||
lookup_data = [] | ||
for recording in inputs[0]: | ||
lookup_data.append({"artist_name": recording.artist.name, "recording_name": recording.name}) | ||
|
||
self.resolve.build_index() | ||
|
||
# Resolve the recordings | ||
resolved = self.resolve.resolve_recordings(lookup_data, self.match_threshold) | ||
recording_ids = [result["recording_id"] for result in resolved] | ||
|
||
# Fetch the recordings to lookup subsonic ids | ||
recordings = RecordingSubsonic \ | ||
.select() \ | ||
.where(RecordingSubsonic.recording_id.in_(recording_ids)) \ | ||
.dicts() | ||
|
||
# Build a subsonic index | ||
subsonic_index = {} | ||
matched = [] | ||
for recording in recordings: | ||
matched.append(recording["recording"]) | ||
subsonic_index[recording["recording"]] = recording["subsonic_id"] | ||
|
||
# Set the subsonic ids into the recordings and only return recordings with an ID | ||
results = [] | ||
for r in resolved: | ||
try: | ||
recording = inputs[0][r["index"]] | ||
recording.musicbrainz["subsonic_id"] = subsonic_index[r["recording_id"]] | ||
except KeyError: | ||
continue | ||
|
||
results.append(recording) | ||
|
||
return results |
Oops, something went wrong.