mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-05 04:53:01 +00:00
document and add image to mix
This commit is contained in:
@@ -31,7 +31,7 @@ def get_track_mix():
|
|||||||
@api.post("/artist")
|
@api.post("/artist")
|
||||||
def get_artist_mix():
|
def get_artist_mix():
|
||||||
mixes = MixesPlugin()
|
mixes = MixesPlugin()
|
||||||
return mixes.get_artists()
|
return mixes.create_artist_mixes()
|
||||||
# tracks = mixes.get_artist_mix("09306be8039b98ad")
|
# tracks = mixes.get_artist_mix("09306be8039b98ad")
|
||||||
|
|
||||||
# return {
|
# return {
|
||||||
@@ -54,7 +54,7 @@ def get_mix(query: MixQuery):
|
|||||||
case "a":
|
case "a":
|
||||||
mixtype = "artist_mixes"
|
mixtype = "artist_mixes"
|
||||||
case _:
|
case _:
|
||||||
raise ValueError(f"Invalid mix ID: {query.mixid}")
|
return {"msg": "Invalid mix ID"}, 400
|
||||||
|
|
||||||
mix = HomepageStore.get_mix(mixtype, query.mixid[1:])
|
mix = HomepageStore.get_mix(mixtype, query.mixid[1:])
|
||||||
if mix:
|
if mix:
|
||||||
|
|||||||
+1
-1
@@ -16,6 +16,6 @@ class Mixes(CronJob):
|
|||||||
"""
|
"""
|
||||||
print("⭐⭐⭐⭐ Mixes cron job running")
|
print("⭐⭐⭐⭐ Mixes cron job running")
|
||||||
mixes = MixesPlugin()
|
mixes = MixesPlugin()
|
||||||
artist_mixes = mixes.get_artists()
|
artist_mixes = mixes.create_artist_mixes()
|
||||||
|
|
||||||
HomepageStore.set_artist_mixes(artist_mixes)
|
HomepageStore.set_artist_mixes(artist_mixes)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class Mix:
|
|||||||
title: str
|
title: str
|
||||||
description: str
|
description: str
|
||||||
tracks: list[str]
|
tracks: list[str]
|
||||||
|
image: dict
|
||||||
|
|
||||||
timestamp: int = field(default_factory=lambda: int(time.time()))
|
timestamp: int = field(default_factory=lambda: int(time.time()))
|
||||||
extra: dict = field(default_factory=dict)
|
extra: dict = field(default_factory=dict)
|
||||||
|
|||||||
+50
-7
@@ -32,7 +32,19 @@ class MixesPlugin(Plugin):
|
|||||||
|
|
||||||
@plugin_method
|
@plugin_method
|
||||||
def get_track_mix(self, tracks: list[Track], with_help: bool = False):
|
def get_track_mix(self, tracks: list[Track], with_help: bool = False):
|
||||||
# query = f"{track.title} - {','.join(a['name'] for a in track.artists)}"
|
"""
|
||||||
|
Given a list of tracks, creates a mix by fetching data from the
|
||||||
|
Swing Music Cloud recommendation server.
|
||||||
|
|
||||||
|
The server returns a list of weak trackhashes. We use these to fetch
|
||||||
|
the matching track data from our library database. Found tracks are
|
||||||
|
then balanced and returned as the final mix tracklist.
|
||||||
|
|
||||||
|
:param with_help: Whether to include the help flag in the query.
|
||||||
|
The flag tells the server to find more data using other tracks from the same album.
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
queries = [
|
queries = [
|
||||||
{
|
{
|
||||||
"query": f"{track.title} - {','.join(a['name'] for a in track.artists)}",
|
"query": f"{track.title} - {','.join(a['name'] for a in track.artists)}",
|
||||||
@@ -60,15 +72,23 @@ class MixesPlugin(Plugin):
|
|||||||
for track in trackmatches:
|
for track in trackmatches:
|
||||||
grouped.setdefault(track.weakhash, []).append(track)
|
grouped.setdefault(track.weakhash, []).append(track)
|
||||||
|
|
||||||
trackmatches = [max(group, key=lambda x: x.bitrate) for group in grouped.values()]
|
trackmatches = [
|
||||||
|
max(group, key=lambda x: x.bitrate) for group in grouped.values()
|
||||||
|
]
|
||||||
|
|
||||||
# sort by trackhash order
|
# sort by trackhash order
|
||||||
trackmatches = sorted(trackmatches, key=lambda x: trackhashes.index(x.weakhash))
|
trackmatches = sorted(trackmatches, key=lambda x: trackhashes.index(x.weakhash))
|
||||||
|
|
||||||
|
# try to balance the mix
|
||||||
trackmatches = balance_mix(trackmatches)
|
trackmatches = balance_mix(trackmatches)
|
||||||
return trackmatches
|
return trackmatches
|
||||||
|
|
||||||
@plugin_method
|
@plugin_method
|
||||||
def get_artist_mix(self, artisthash: str):
|
def get_artist_mix(self, artisthash: str):
|
||||||
|
"""
|
||||||
|
Given an artisthash, creates an artist mix using the
|
||||||
|
self.MAX_TRACKS_TO_FETCH most listened to tracks.
|
||||||
|
"""
|
||||||
artist = ArtistStore.artistmap[artisthash]
|
artist = ArtistStore.artistmap[artisthash]
|
||||||
tracks = TrackStore.get_tracks_by_trackhashes(artist.trackhashes)
|
tracks = TrackStore.get_tracks_by_trackhashes(artist.trackhashes)
|
||||||
|
|
||||||
@@ -76,7 +96,7 @@ class MixesPlugin(Plugin):
|
|||||||
return self.get_track_mix(tracks[: self.MAX_TRACKS_TO_FETCH])
|
return self.get_track_mix(tracks[: self.MAX_TRACKS_TO_FETCH])
|
||||||
|
|
||||||
@plugin_method
|
@plugin_method
|
||||||
def get_artists(self, limit: int = 10):
|
def create_artist_mixes(self, limit: int = 10):
|
||||||
mixes: list[Mix] = []
|
mixes: list[Mix] = []
|
||||||
indexed = set()
|
indexed = set()
|
||||||
|
|
||||||
@@ -130,6 +150,10 @@ class MixesPlugin(Plugin):
|
|||||||
return mixes
|
return mixes
|
||||||
|
|
||||||
def get_mix_description(self, tracks: list[Track], artishash: str):
|
def get_mix_description(self, tracks: list[Track], artishash: str):
|
||||||
|
"""
|
||||||
|
Constructs a description for a mix by putting together the first n=4
|
||||||
|
artists in the mix tracklist.
|
||||||
|
"""
|
||||||
first_4_artists = []
|
first_4_artists = []
|
||||||
indexed = set()
|
indexed = set()
|
||||||
|
|
||||||
@@ -151,13 +175,22 @@ class MixesPlugin(Plugin):
|
|||||||
return f"Featuring {tracks[0].artists[0]['name']}"
|
return f"Featuring {tracks[0].artists[0]['name']}"
|
||||||
|
|
||||||
def create_artist_mix(self, artist: dict[str, str]):
|
def create_artist_mix(self, artist: dict[str, str]):
|
||||||
|
"""
|
||||||
|
Given an artist dict, creates an artist mix.
|
||||||
|
"""
|
||||||
mix_tracks = self.get_artist_mix(artist["artisthash"])
|
mix_tracks = self.get_artist_mix(artist["artisthash"])
|
||||||
|
|
||||||
if len(mix_tracks) < self.MIN_TRACK_MIX_LENGTH:
|
if len(mix_tracks) < self.MIN_TRACK_MIX_LENGTH:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
_artist = ArtistStore.get_artist_by_hash(artist["artisthash"])
|
||||||
|
|
||||||
|
if not _artist:
|
||||||
|
return None
|
||||||
|
|
||||||
return Mix(
|
return Mix(
|
||||||
id=artist["artisthash"],
|
# the a prefix indicates that this is an artist mix
|
||||||
|
id=f"a{artist['artisthash']}",
|
||||||
title=artist["artist"],
|
title=artist["artist"],
|
||||||
description=self.get_mix_description(mix_tracks, artist["artisthash"]),
|
description=self.get_mix_description(mix_tracks, artist["artisthash"]),
|
||||||
tracks=[t.trackhash for t in mix_tracks],
|
tracks=[t.trackhash for t in mix_tracks],
|
||||||
@@ -165,10 +198,19 @@ class MixesPlugin(Plugin):
|
|||||||
"type": "artist",
|
"type": "artist",
|
||||||
"artisthash": artist["artisthash"],
|
"artisthash": artist["artisthash"],
|
||||||
},
|
},
|
||||||
|
image={
|
||||||
|
"image": _artist.image,
|
||||||
|
"color": _artist.color,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def fallback_create_artist_mix(
|
||||||
def fallback_create_artist_mix(self, artist: dict[str, str], similar_artists: list[str], trackhashes: set[str], limit: int):
|
self,
|
||||||
|
artist: dict[str, str],
|
||||||
|
similar_artists: list[str],
|
||||||
|
trackhashes: set[str],
|
||||||
|
limit: int,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Creates an artist mix by selecting random tracks from similar artists.
|
Creates an artist mix by selecting random tracks from similar artists.
|
||||||
|
|
||||||
@@ -200,4 +242,5 @@ class MixesPlugin(Plugin):
|
|||||||
|
|
||||||
# Since the artisthashes are ordered by similarity score, we iterate from the start
|
# Since the artisthashes are ordered by similarity score, we iterate from the start
|
||||||
# and go forward collecting tracks that aren't in the mix yet.
|
# and go forward collecting tracks that aren't in the mix yet.
|
||||||
#
|
#
|
||||||
|
|
||||||
+37
-19
@@ -3,20 +3,31 @@ from typing import List, Dict, Tuple
|
|||||||
from collections import Counter
|
from collections import Counter
|
||||||
|
|
||||||
|
|
||||||
def violates_gap_rule(balanced_mix: Dict[int, Track], position: int, track: Track, gap: int = 3) -> bool:
|
def violates_gap_rule(
|
||||||
|
balanced_mix: Dict[int, Track], position: int, track: Track, gap: int = 3
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if placing the track at the given position violates the gap rule.
|
Check if placing the track at the given position violates the gap rule.
|
||||||
|
|
||||||
|
The gap rule is violated if the track has an artist in common with any
|
||||||
|
track within the gap range (default = 3).
|
||||||
"""
|
"""
|
||||||
track_artists = set(artist['artisthash'] for artist in track.artists)
|
track_artists = set(artist["artisthash"] for artist in track.artists)
|
||||||
|
|
||||||
for i in range(max(0, position - gap), position):
|
for i in range(max(0, position - gap), position):
|
||||||
if i in balanced_mix:
|
if i in balanced_mix:
|
||||||
existing_artists = set(artist['artisthash'] for artist in balanced_mix[i].artists)
|
existing_artists = set(
|
||||||
|
artist["artisthash"] for artist in balanced_mix[i].artists
|
||||||
|
)
|
||||||
if track_artists.intersection(existing_artists):
|
if track_artists.intersection(existing_artists):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def find_next_position(balanced_mix: Dict[int, Track], start: int, track: Track, total_tracks: int) -> int:
|
|
||||||
|
def find_next_position(
|
||||||
|
balanced_mix: Dict[int, Track], start: int, track: Track, total_tracks: int
|
||||||
|
) -> int:
|
||||||
"""
|
"""
|
||||||
Find the next available position for the track, starting from 'start' and wrapping around.
|
Find the next available position for the track, starting from 'start' and wrapping around.
|
||||||
"""
|
"""
|
||||||
@@ -28,59 +39,66 @@ def find_next_position(balanced_mix: Dict[int, Track], start: int, track: Track,
|
|||||||
return i
|
return i
|
||||||
return start # If no better position is found, return the original position
|
return start # If no better position is found, return the original position
|
||||||
|
|
||||||
|
|
||||||
def is_tracklist_balanced(tracks: List[Track], gap: int = 3) -> Tuple[bool, bool]:
|
def is_tracklist_balanced(tracks: List[Track], gap: int = 3) -> Tuple[bool, bool]:
|
||||||
"""
|
"""
|
||||||
Checks if a tracklist is balanced or can be balanced.
|
Checks if a tracklist is balanced or can be balanced.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
- tracks: List of Track objects
|
- tracks: List of Track objects
|
||||||
- gap: Minimum number of tracks between songs by the same artist (default 3)
|
- gap: Minimum number of tracks between songs by the same artist (default 3)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
- A tuple (can_be_balanced, is_currently_balanced)
|
- A tuple (can_be_balanced, is_currently_balanced)
|
||||||
"""
|
"""
|
||||||
total_tracks = len(tracks)
|
total_tracks = len(tracks)
|
||||||
|
|
||||||
# Count tracks per artist (considering only the first artist)
|
# Count tracks per artist (considering only the first artist)
|
||||||
artist_counts = Counter(track.artists[0]['artisthash'] for track in tracks)
|
artist_counts = Counter(track.artists[0]["artisthash"] for track in tracks)
|
||||||
|
|
||||||
# Calculate the maximum number of tracks an artist can have in a balanced list
|
# Calculate the maximum number of tracks an artist can have in a balanced list
|
||||||
max_tracks_per_artist = (total_tracks + gap) // (gap + 1)
|
max_tracks_per_artist = (total_tracks + gap) // (gap + 1)
|
||||||
|
|
||||||
# Check if it's mathematically possible to balance the tracklist
|
# Check if it's mathematically possible to balance the tracklist
|
||||||
can_be_balanced = all(count <= max_tracks_per_artist for count in artist_counts.values())
|
can_be_balanced = all(
|
||||||
|
count <= max_tracks_per_artist for count in artist_counts.values()
|
||||||
|
)
|
||||||
|
|
||||||
if not can_be_balanced:
|
if not can_be_balanced:
|
||||||
return False, False
|
return False, False
|
||||||
|
|
||||||
# Check if the current arrangement is balanced
|
# Check if the current arrangement is balanced
|
||||||
is_currently_balanced = True
|
is_currently_balanced = True
|
||||||
artist_last_positions = {}
|
artist_last_positions = {}
|
||||||
|
|
||||||
for i, track in enumerate(tracks):
|
for i, track in enumerate(tracks):
|
||||||
artist = track.artists[0]['artisthash']
|
artist = track.artists[0]["artisthash"]
|
||||||
if artist in artist_last_positions:
|
if artist in artist_last_positions:
|
||||||
if i - artist_last_positions[artist] <= gap:
|
if i - artist_last_positions[artist] <= gap:
|
||||||
is_currently_balanced = False
|
is_currently_balanced = False
|
||||||
break
|
break
|
||||||
artist_last_positions[artist] = i
|
artist_last_positions[artist] = i
|
||||||
|
|
||||||
return can_be_balanced, is_currently_balanced
|
return can_be_balanced, is_currently_balanced
|
||||||
|
|
||||||
|
|
||||||
def balance_mix(tracks: List[Track]) -> List[Track]:
|
def balance_mix(tracks: List[Track]) -> List[Track]:
|
||||||
"""
|
"""
|
||||||
Balances the mix by ensuring that the tracks in a mix are distributed evenly.
|
Balances the mix by ensuring that the tracks in a mix are distributed evenly.
|
||||||
Preserves the overall rating order of tracks while minimizing disruption.
|
Preserves the overall rating order of tracks while minimizing disruption.
|
||||||
|
|
||||||
|
Tracks that need to be moved are moved down the tracklist until they no longer
|
||||||
|
violate the gap rule.
|
||||||
"""
|
"""
|
||||||
can_be_balanced, is_balanced = is_tracklist_balanced(tracks)
|
can_be_balanced, is_balanced = is_tracklist_balanced(tracks)
|
||||||
|
|
||||||
if not can_be_balanced:
|
if not can_be_balanced:
|
||||||
print("Warning: This tracklist cannot be perfectly balanced.")
|
print("Warning: This tracklist cannot be perfectly balanced.")
|
||||||
# Proceed with best-effort balancing
|
# Proceed with best-effort balancing
|
||||||
|
|
||||||
if is_balanced:
|
if is_balanced:
|
||||||
return tracks # Already balanced, no need to modify
|
return tracks # Already balanced, no need to modify
|
||||||
|
|
||||||
balanced_mix: Dict[int, Track] = {}
|
balanced_mix: Dict[int, Track] = {}
|
||||||
total_tracks = len(tracks)
|
total_tracks = len(tracks)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user