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

Add sonicallySimilar method to Audio class #1288

Merged
merged 9 commits into from
Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
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
36 changes: 36 additions & 0 deletions plexapi/audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from pathlib import Path
from urllib.parse import quote_plus

from typing import List, Optional, TypeVar

from plexapi import media, utils
from plexapi.base import Playable, PlexPartialObject, PlexHistory, PlexSession
from plexapi.exceptions import BadRequest
Expand All @@ -14,6 +16,9 @@
from plexapi.playlist import Playlist


TAudio = TypeVar("TAudio", bound="Audio")


class Audio(PlexPartialObject, PlayedUnplayedMixin):
""" Base class for all audio objects including :class:`~plexapi.audio.Artist`,
:class:`~plexapi.audio.Album`, and :class:`~plexapi.audio.Track`.
Expand All @@ -22,6 +27,7 @@ class Audio(PlexPartialObject, PlayedUnplayedMixin):
addedAt (datetime): Datetime the item was added to the library.
art (str): URL to artwork image (/library/metadata/<ratingKey>/art/<artid>).
artBlurHash (str): BlurHash string for artwork image.
distance (float): Sonic Distance of the item from the seed item.
fields (List<:class:`~plexapi.media.Field`>): List of field objects.
guid (str): Plex GUID for the artist, album, or track (plex://artist/5d07bcb0403c64029053ac4c).
index (int): Plex index number (often the track number).
Expand Down Expand Up @@ -65,6 +71,7 @@ def _loadData(self, data):
self.listType = 'audio'
self.moods = self.findItems(data, media.Mood)
self.musicAnalysisVersion = utils.cast(int, data.attrib.get('musicAnalysisVersion'))
self.distance = utils.cast(float, data.attrib.get('distance'))
Dr-Blank marked this conversation as resolved.
Show resolved Hide resolved
self.ratingKey = utils.cast(int, data.attrib.get('ratingKey'))
self.summary = data.attrib.get('summary')
self.thumb = data.attrib.get('thumb')
Expand Down Expand Up @@ -125,6 +132,35 @@ def sync(self, bitrate, client=None, clientId=None, limit=None, title=None):

return myplex.sync(sync_item, client=client, clientId=clientId)

def sonicallySimilar(
self: TAudio,
limit: Optional[int] = None,
maxDistance: Optional[float] = None,
**kwargs,
) -> List[TAudio]:
"""Returns a list of sonically similar audio items.

Parameters:
limit (int): Maximum count of items to return. Default 50 (server default)
maxDistance (float): Maximum distance between tracks, 0.0 - 1.0. Default 0.25 (server default).
**kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.fetchItems`.

Returns:
List[:class:`~plexapi.audio.Audio`]: list of sonically similar audio items.
"""

key = f"{self.key}/nearest"
params = {"maxDistance": maxDistance, "limit": limit}
key += utils.joinArgs(
{k: v for k, v in params.items() if v is not None}
)
Copy link
Collaborator

Choose a reason for hiding this comment

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

It is more clear to add params instead of removing params.

Suggested change
params = {"maxDistance": maxDistance, "limit": limit}
key += utils.joinArgs(
{k: v for k, v in params.items() if v is not None}
)
params = {}
if limit is not None:
params['limit'] = limit
if maxDistance is not None:
params['maxDistance'] = maxDistance
key += utils.joinArgs(params)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have a way to maintain the adding of param as well as make it add programmatically so that it is scalable and avoid code repetition of if statements for every key

        all_args = {"limit": limit, "maxDistance": maxDistance}
        params = {k: v for k, v in all_args.items() if v is not None}  # add if not None


return self.fetchItems(
key,
cls=self.__class__,
**kwargs,
)


@utils.registerPlexObject
class Artist(
Expand Down
6 changes: 6 additions & 0 deletions tests/test_audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,12 @@ def test_audio_Audio_section(artist, album, track):
assert track.section().key == album.section().key == artist.section().key


def test_audio_Audio_sonicallySimilar(artist):
Dr-Blank marked this conversation as resolved.
Show resolved Hide resolved
similar_audio = artist.sonicallySimilar()
assert isinstance(similar_audio, list)
assert all(isinstance(i, type(artist)) for i in similar_audio)


def test_audio_Artist_download(monkeydownload, tmpdir, artist):
total = len(artist.tracks())
filepaths = artist.download(savepath=str(tmpdir))
Expand Down