music-kraken-core/src/music_kraken/objects/song.py

514 lines
16 KiB
Python
Raw Normal View History

2022-12-06 13:45:18 +00:00
import os
2023-02-23 22:52:41 +00:00
from typing import List, Optional, Type, Dict
2023-01-13 13:37:15 +00:00
import pycountry
2022-12-06 13:45:18 +00:00
2023-01-12 22:01:19 +00:00
from .metadata import (
Mapping as id3Mapping,
2023-01-30 13:41:02 +00:00
ID3Timestamp,
MetadataAttribute
2023-01-12 22:01:19 +00:00
)
from src.music_kraken.utils.shared import (
2022-12-06 13:45:18 +00:00
MUSIC_DIR,
2023-02-25 21:16:32 +00:00
DATABASE_LOGGER as LOGGER
2022-12-06 13:45:18 +00:00
)
2023-01-12 15:25:50 +00:00
from .parents import (
2022-12-06 13:45:18 +00:00
DatabaseObject,
2023-02-25 21:16:32 +00:00
MainObject
2022-12-06 13:45:18 +00:00
)
2023-01-20 22:05:15 +00:00
from .source import (
Source,
SourceTypes,
2023-01-27 11:56:59 +00:00
SourcePages,
SourceAttribute
2023-01-20 22:05:15 +00:00
)
2023-02-06 08:16:28 +00:00
from .formatted_text import FormattedText
2023-02-08 12:16:48 +00:00
from .collection import Collection
2023-02-22 16:56:52 +00:00
from .album import AlbumType, AlbumStatus
2023-02-25 21:16:32 +00:00
from .lyrics import Lyrics
from .target import Target
2022-12-06 13:45:18 +00:00
2022-12-07 14:51:38 +00:00
"""
All Objects dependent
"""
2023-02-22 16:56:52 +00:00
CountryTyping = type(list(pycountry.countries)[0])
2022-12-08 08:28:28 +00:00
2023-02-25 21:16:32 +00:00
class Song(MainObject, SourceAttribute, MetadataAttribute):
2023-02-13 22:05:16 +00:00
"""
Class representing a song object, with attributes id, mb_id, title, album_name, isrc, length,
tracksort, genre, source_list, target, lyrics_list, album, main_artist_list, and feature_artist_list.
Inherits from DatabaseObject, SourceAttribute, and MetadataAttribute classes.
"""
2022-12-06 13:45:18 +00:00
def __init__(
2022-12-06 22:44:42 +00:00
self,
2023-02-25 21:16:32 +00:00
_id: str = None,
dynamic: bool = False,
2022-12-06 22:44:42 +00:00
title: str = None,
isrc: str = None,
length: int = None,
2022-12-09 17:24:58 +00:00
tracksort: int = None,
2023-01-16 13:31:43 +00:00
genre: str = None,
2023-01-27 11:56:59 +00:00
source_list: List[Source] = None,
2023-02-25 21:16:32 +00:00
target_list: List[Target] = None,
2023-02-10 12:52:18 +00:00
lyrics_list: List[Lyrics] = None,
2023-02-22 16:56:52 +00:00
album_list: Type['Album'] = None,
main_artist_list: List[Type['Artist']] = None,
feature_artist_list: List[Type['Artist']] = None,
2023-02-10 12:52:18 +00:00
**kwargs
2022-12-06 22:44:42 +00:00
) -> None:
2022-12-06 13:45:18 +00:00
"""
2023-02-13 22:05:16 +00:00
Initializes the Song object with the following attributes:
2022-12-06 13:45:18 +00:00
"""
2023-02-25 21:16:32 +00:00
MainObject.__init__(self, _id=_id, dynamic=dynamic, **kwargs)
2022-12-06 13:45:18 +00:00
# attributes
2023-01-16 13:23:33 +00:00
self.title: str = title
self.isrc: str = isrc
self.length: int = length
2023-02-10 12:52:18 +00:00
self.tracksort: int = tracksort or 0
2023-01-16 13:31:43 +00:00
self.genre: str = genre
2023-02-10 12:52:18 +00:00
self.source_list = source_list or []
2022-12-06 13:45:18 +00:00
2023-02-23 22:52:41 +00:00
self.target_list = target_list or []
2022-12-06 13:45:18 +00:00
2023-02-23 22:52:41 +00:00
self.lyrics_collection: Collection = Collection(
data=lyrics_list or [],
map_attributes=[],
element_type=Lyrics
)
2023-02-22 16:56:52 +00:00
2023-02-23 22:52:41 +00:00
self.album_collection: Collection = Collection(
data=album_list or [],
map_attributes=["title"],
element_type=Album
)
2022-12-06 13:45:18 +00:00
2023-02-10 12:52:18 +00:00
self.main_artist_collection = Collection(
data=main_artist_list or [],
map_attributes=["title"],
element_type=Artist
)
self.feature_artist_collection = Collection(
data=feature_artist_list or [],
map_attributes=["title"],
element_type=Artist
)
2022-12-09 14:50:44 +00:00
2022-12-08 08:28:28 +00:00
def __eq__(self, other):
if type(other) != type(self):
return False
return self.id == other.id
2022-12-13 10:16:19 +00:00
def get_artist_credits(self) -> str:
2023-02-10 12:52:18 +00:00
main_artists = ", ".join([artist.name for artist in self.main_artist_collection])
feature_artists = ", ".join([artist.name for artist in self.feature_artist_collection])
2023-02-22 16:56:52 +00:00
2023-02-10 12:52:18 +00:00
if len(feature_artists) == 0:
return main_artists
return f"{main_artists} feat. {feature_artists}"
2022-12-13 10:16:19 +00:00
2022-12-06 13:45:18 +00:00
def __str__(self) -> str:
2022-12-13 10:16:19 +00:00
artist_credit_str = ""
artist_credits = self.get_artist_credits()
if artist_credits != "":
artist_credit_str = f" by {artist_credits}"
return f"\"{self.title}\"{artist_credit_str}"
2022-12-06 13:45:18 +00:00
def __repr__(self) -> str:
2023-02-10 12:52:18 +00:00
return f"Song(\"{self.title}\")"
2022-12-09 17:24:58 +00:00
2023-01-30 14:12:30 +00:00
def get_tracksort_str(self):
2023-02-10 12:52:18 +00:00
"""
if the album tracklist is empty, it sets it length to 1, this song has to be in the Album
:returns id3_tracksort: {song_position}/{album.length_of_tracklist}
"""
return f"{self.tracksort}/{len(self.album.tracklist) or 1}"
2023-01-30 14:12:30 +00:00
2023-01-30 13:41:02 +00:00
def get_metadata(self) -> MetadataAttribute.Metadata:
metadata = MetadataAttribute.Metadata({
id3Mapping.TITLE: [self.title],
id3Mapping.ISRC: [self.isrc],
2023-01-30 22:54:21 +00:00
id3Mapping.LENGTH: [self.length],
id3Mapping.GENRE: [self.genre],
id3Mapping.TRACKNUMBER: [self.tracksort_str]
2023-01-30 13:41:02 +00:00
})
2023-01-16 13:23:33 +00:00
2023-01-30 13:41:02 +00:00
metadata.merge_many([s.get_song_metadata() for s in self.source_list])
2023-02-23 22:52:41 +00:00
metadata.merge_many([a.metadata for a in self.album_collection])
metadata.merge_many([a.metadata for a in self.main_artist_collection])
metadata.merge_many([a.metadata for a in self.feature_artist_collection])
metadata.merge_many([lyrics.metadata for lyrics in self.lyrics_collection])
2023-01-16 13:23:33 +00:00
return metadata
2023-01-30 14:12:30 +00:00
2023-02-09 08:40:57 +00:00
def get_options(self) -> list:
2023-02-13 22:05:16 +00:00
"""
Return a list of related objects including the song object, album object, main artist objects, and feature artist objects.
:return: a list of objects that are related to the Song object
"""
2023-02-09 08:40:57 +00:00
options = self.main_artist_list.copy()
2023-02-23 22:52:41 +00:00
options.extend(self.feature_artist_collection)
options.extend(self.album_collection)
2023-02-09 08:40:57 +00:00
options.append(self)
return options
def get_option_string(self) -> str:
2023-02-10 12:52:18 +00:00
return f"Song({self.title}) of Album({self.album.title}) from Artists({self.get_artist_credits()})"
2023-02-09 08:40:57 +00:00
2023-02-22 16:56:52 +00:00
tracksort_str: List[Type['Album']] = property(fget=get_tracksort_str)
main_artist_list: List[Type['Artist']] = property(fget=lambda self: self.main_artist_collection.copy())
feature_artist_list: List[Type['Artist']] = property(fget=lambda self: self.feature_artist_collection.copy())
2023-01-11 01:25:17 +00:00
2023-02-23 22:52:41 +00:00
album_list: List[Type['Album']] = property(fget=lambda self: self.album_collection.copy())
lyrics_list: List[Type[Lyrics]] = property(fget=lambda self: self.lyrics_collection.copy())
2022-12-06 13:45:18 +00:00
2022-12-07 14:51:38 +00:00
"""
2022-12-09 17:24:58 +00:00
All objects dependent on Album
2022-12-07 14:51:38 +00:00
"""
2022-12-08 08:28:28 +00:00
2023-02-25 21:16:32 +00:00
class Album(MainObject, SourceAttribute, MetadataAttribute):
2022-12-07 14:51:38 +00:00
def __init__(
2022-12-08 08:28:28 +00:00
self,
2023-02-25 21:16:32 +00:00
_id: str = None,
2022-12-08 08:28:28 +00:00
title: str = None,
2023-01-13 13:37:15 +00:00
language: pycountry.Languages = None,
2023-01-14 13:41:40 +00:00
date: ID3Timestamp = None,
2022-12-08 08:28:28 +00:00
barcode: str = None,
2022-12-12 18:30:18 +00:00
is_split: bool = False,
albumsort: int = None,
2023-01-23 12:49:07 +00:00
dynamic: bool = False,
2023-01-27 11:56:59 +00:00
source_list: List[Source] = None,
2023-02-10 12:52:18 +00:00
artist_list: list = None,
2023-02-23 22:52:41 +00:00
song_list: List[Song] = None,
2023-02-22 16:56:52 +00:00
album_status: AlbumStatus = None,
album_type: AlbumType = None,
2023-02-23 22:52:41 +00:00
label_list: List[Type['Label']] = None,
2023-02-10 12:52:18 +00:00
**kwargs
2022-12-07 14:51:38 +00:00
) -> None:
2023-02-25 21:16:32 +00:00
MainObject.__init__(self, _id=_id, dynamic=dynamic, **kwargs)
2023-02-08 16:14:51 +00:00
2022-12-07 14:51:38 +00:00
self.title: str = title
2023-02-22 16:56:52 +00:00
self.album_status: AlbumStatus = album_status
2023-02-23 22:52:41 +00:00
self.album_type: AlbumType = album_type
2023-01-13 13:37:15 +00:00
self.language: pycountry.Languages = language
2023-02-10 12:52:18 +00:00
self.date: ID3Timestamp = date or ID3Timestamp()
2023-02-23 22:52:41 +00:00
2023-01-13 11:05:44 +00:00
"""
TODO
find out the id3 tag for barcode and implement it
2023-02-22 16:56:52 +00:00
maybe look at how mutagen does it with easy_id3
2023-01-13 11:05:44 +00:00
"""
2022-12-07 14:51:38 +00:00
self.barcode: str = barcode
2023-02-10 12:52:18 +00:00
"""
TODO
implement a function in the Artist class,
to set albumsort with help of the release year
"""
2023-02-22 16:56:52 +00:00
self.albumsort: Optional[int] = albumsort
2022-12-07 14:51:38 +00:00
2023-02-23 22:52:41 +00:00
self.song_collection: Collection = Collection(
data=song_list or [],
2023-02-08 12:16:48 +00:00
map_attributes=["title"],
element_type=Song
)
2023-02-23 22:52:41 +00:00
self.artist_collection: Collection = Collection(
2023-02-10 12:52:18 +00:00
data=artist_list or [],
map_attributes=["name"],
element_type=Artist
)
2023-01-23 12:49:07 +00:00
2023-02-23 22:52:41 +00:00
self.label_collection: Collection = Collection(
data=label_list,
map_attributes=["name"],
element_type=Label
)
self.source_list = source_list or []
2022-12-09 17:24:58 +00:00
2022-12-16 17:26:05 +00:00
def __repr__(self):
2023-02-08 12:29:01 +00:00
return f"Album(\"{self.title}\")"
2022-12-16 17:26:05 +00:00
2023-02-23 22:52:41 +00:00
def update_tracksort(self):
"""
This updates the tracksort attributes, of the songs in
`self.song_collection`, and sorts the songs, if possible.
2022-12-09 17:24:58 +00:00
2023-02-23 22:52:41 +00:00
It is advised to only call this function, once all the tracks are
added to the songs.
2022-12-07 14:51:38 +00:00
2023-02-23 22:52:41 +00:00
:return:
"""
tracksort_map: Dict[int, Song] = {song.tracksort: song for song in self.song_collection if
song.tracksort is not None}
# place the songs, with set tracksort attribute according to it
for tracksort, song in tracksort_map.items():
index = tracksort - 1
"""
I ONLY modify the `Collection._data` attribute directly,
to bypass the mapping of the attributes, because I will add the item in the next step
"""
self.song_collection._data.remove(song)
self.song_collection._data.insert(index, song)
# fill in the empty tracksort attributes
for i, song in enumerate(self.song_collection):
if song.tracksort is not None:
continue
song.tracksort = i + 1
2022-12-12 18:30:18 +00:00
2023-01-30 13:41:02 +00:00
def get_metadata(self) -> MetadataAttribute.Metadata:
return MetadataAttribute.Metadata({
id3Mapping.ALBUM: [self.title],
id3Mapping.COPYRIGHT: [self.copyright],
id3Mapping.LANGUAGE: [self.iso_639_2_language],
2023-02-23 22:52:41 +00:00
id3Mapping.ALBUM_ARTIST: [a.name for a in self.artist_collection],
id3Mapping.DATE: [self.date.timestamp]
2023-01-30 13:41:02 +00:00
})
2023-01-13 11:05:44 +00:00
2023-01-13 13:37:15 +00:00
def get_copyright(self) -> str:
2023-01-30 22:54:21 +00:00
if self.date is None:
2023-02-22 16:56:52 +00:00
return ""
2023-02-23 22:52:41 +00:00
if self.date.has_year or len(self.label_collection) == 0:
2023-02-22 16:56:52 +00:00
return ""
2023-01-13 13:37:15 +00:00
2023-02-23 22:52:41 +00:00
return f"{self.date.year} {self.label_collection[0].name}"
2023-01-13 13:37:15 +00:00
2023-02-27 15:51:55 +00:00
@property
def iso_639_2_lang(self) -> Optional[str]:
2023-01-13 13:37:15 +00:00
if self.language is None:
return None
return self.language.alpha_3
2023-02-28 00:13:53 +00:00
@property
def is_split(self) -> bool:
"""
A split Album is an Album from more than one Artists
usually half the songs are made by one Artist, the other half by the other one.
In this case split means either that or one artist featured by all songs.
:return:
"""
return len(self.artist_collection) > 1
2023-02-09 08:40:57 +00:00
def get_options(self) -> list:
2023-02-23 22:52:41 +00:00
options = self.artist_collection.copy()
2023-02-09 08:40:57 +00:00
options.append(self)
2023-02-23 22:52:41 +00:00
options.extend(self.song_collection)
2023-02-09 08:40:57 +00:00
return options
2023-02-22 16:56:52 +00:00
2023-02-09 08:40:57 +00:00
def get_option_string(self) -> str:
2023-02-23 22:52:41 +00:00
return f"Album: {self.title}; Artists {', '.join([i.name for i in self.artist_collection])}"
label_list: List[Type['Label']] = property(fget=lambda self: self.label_collection.copy())
artist_list: List[Type['Artist']] = property(fget=lambda self: self.artist_collection.copy())
song_list: List[Song] = property(fget=lambda self: self.song_collection.copy())
tracklist: List[Song] = property(fget=lambda self: self.song_collection.copy())
2023-02-09 08:40:57 +00:00
2023-01-13 13:37:15 +00:00
copyright = property(fget=get_copyright)
2023-01-13 11:05:44 +00:00
2022-12-12 18:30:18 +00:00
"""
All objects dependent on Artist
"""
2023-02-25 21:16:32 +00:00
class Artist(MainObject, SourceAttribute, MetadataAttribute):
2022-12-12 18:30:18 +00:00
def __init__(
self,
2023-02-25 21:16:32 +00:00
_id: str = None,
dynamic: bool = False,
2022-12-12 18:30:18 +00:00
name: str = None,
2023-01-27 11:56:59 +00:00
source_list: List[Source] = None,
2023-02-23 22:52:41 +00:00
feature_song_list: List[Song] = None,
main_album_list: List[Album] = None,
2023-02-06 08:16:28 +00:00
notes: FormattedText = None,
2023-02-01 13:26:54 +00:00
lyrical_themes: List[str] = None,
general_genre: str = "",
2023-02-22 16:56:52 +00:00
country: CountryTyping = None,
2023-02-23 22:52:41 +00:00
formed_in: ID3Timestamp = None,
label_list: List[Type['Label']] = None,
2023-02-25 21:16:32 +00:00
**kwargs
2022-12-12 18:30:18 +00:00
):
2023-02-25 21:16:32 +00:00
MainObject.__init__(self, _id=_id, dynamic=dynamic, **kwargs)
self.name: str = name
2022-12-12 18:30:18 +00:00
"""
TODO implement album type and notes
"""
2023-02-22 16:56:52 +00:00
self.country: CountryTyping = country
self.formed_in: ID3Timestamp = formed_in
2023-02-01 13:26:54 +00:00
"""
notes, generall genre, lyrics themes are attributes
which are meant to only use in outputs to describe the object
i mean do as you want but there aint no strict rule about em so good luck
"""
2023-02-09 14:49:22 +00:00
self.notes: FormattedText = notes or FormattedText()
2023-02-25 21:16:32 +00:00
"""
TODO
implement in db
"""
2023-02-09 14:49:22 +00:00
self.lyrical_themes: List[str] = lyrical_themes or []
2023-02-01 13:26:54 +00:00
self.general_genre = general_genre
2023-01-24 08:40:01 +00:00
2023-02-23 22:52:41 +00:00
self.feature_song_collection: Collection = Collection(
data=feature_song_list,
2023-02-08 16:14:51 +00:00
map_attributes=["title"],
element_type=Song
)
2023-02-23 22:52:41 +00:00
self.main_album_collection: Collection = Collection(
data=main_album_list,
2023-02-08 16:14:51 +00:00
map_attributes=["title"],
element_type=Album
)
2022-12-12 18:30:18 +00:00
2023-02-23 22:52:41 +00:00
self.label_collection: Collection = Collection(
data=label_list,
map_attributes=["name"],
element_type=Label
)
self.source_list = source_list or []
2023-01-20 10:43:35 +00:00
2022-12-12 18:30:18 +00:00
def __str__(self):
2023-02-06 08:16:28 +00:00
string = self.name or ""
plaintext_notes = self.notes.get_plaintext()
if plaintext_notes is not None:
string += "\n" + plaintext_notes
return string
2022-12-12 18:30:18 +00:00
2022-12-16 15:59:21 +00:00
def __repr__(self):
2023-02-23 22:52:41 +00:00
return f"Artist(\"{self.name}\")"
2023-02-27 15:51:55 +00:00
@property
def country_string(self):
return self.country.alpha_3
2023-02-23 22:52:41 +00:00
def update_albumsort(self):
"""
This updates the albumsort attributes, of the albums in
`self.main_album_collection`, and sorts the albums, if possible.
2022-12-12 18:30:18 +00:00
2023-02-23 22:52:41 +00:00
It is advised to only call this function, once all the albums are
added to the artist.
:return:
"""
self.main_album_collection.sort(key=lambda _album: _album.date)
for i, album in enumerate(self.main_album_collection):
if album.albumsort is None:
continue
album.albumsort = i + 1
2022-12-16 15:59:21 +00:00
def get_features(self) -> Album:
2022-12-12 18:30:18 +00:00
feature_release = Album(
title="features",
2023-02-22 16:56:52 +00:00
album_status=AlbumStatus.UNRELEASED,
album_type=AlbumType.COMPILATION_ALBUM,
2022-12-12 18:30:18 +00:00
is_split=True,
albumsort=666,
2023-02-23 22:52:41 +00:00
dynamic=True,
song_list=self.feature_song_collection.copy()
2022-12-12 18:30:18 +00:00
)
2022-12-16 15:59:21 +00:00
return feature_release
2022-12-12 18:30:18 +00:00
2023-02-23 22:52:41 +00:00
def get_metadata(self) -> MetadataAttribute.Metadata:
metadata = MetadataAttribute.Metadata({
id3Mapping.ARTIST: [self.name]
})
metadata.merge_many([s.get_artist_metadata() for s in self.source_list])
return metadata
def get_options(self) -> list:
options = [self]
options.extend(self.main_album_collection)
options.extend(self.feature_song_collection)
return options
def get_option_string(self) -> str:
return f"Artist: {self.name}"
2023-02-09 14:49:22 +00:00
def get_all_songs(self) -> List[Song]:
"""
returns a list of all Songs.
2023-02-22 16:56:52 +00:00
probably not that useful, because it is unsorted
2023-02-09 14:49:22 +00:00
"""
2023-02-23 22:52:41 +00:00
collection = self.feature_song_collection.copy()
2023-02-09 14:49:22 +00:00
for album in self.discography:
2023-02-23 22:52:41 +00:00
collection.extend(album.song_collection)
2023-02-09 14:49:22 +00:00
return collection
2022-12-12 18:30:18 +00:00
2022-12-16 15:59:21 +00:00
def get_discography(self) -> List[Album]:
2023-02-23 22:52:41 +00:00
flat_copy_discography = self.main_album_collection.copy()
2022-12-16 15:59:21 +00:00
flat_copy_discography.append(self.get_features())
2022-12-12 18:30:18 +00:00
2022-12-16 15:59:21 +00:00
return flat_copy_discography
2022-12-12 18:30:18 +00:00
2023-02-23 22:52:41 +00:00
album_list: List[Album] = property(fget=lambda self: self.album_collection.copy())
2023-01-20 10:43:35 +00:00
2023-02-23 22:52:41 +00:00
complete_album_list: List[Album] = property(fget=get_discography)
discography: List[Album] = property(fget=get_discography)
2023-01-16 13:23:33 +00:00
2023-02-23 22:52:41 +00:00
feature_album: Album = property(fget=get_features)
song_list: List[Song] = property(fget=get_all_songs)
label_list: List[Type['Label']] = property(fget=lambda self: self.label_collection.copy())
2023-02-09 08:40:57 +00:00
2023-02-23 22:52:41 +00:00
"""
Label
"""
2023-02-25 21:16:32 +00:00
class Label(MainObject, SourceAttribute, MetadataAttribute):
2023-02-23 22:52:41 +00:00
def __init__(
self,
2023-02-25 21:16:32 +00:00
_id: str = None,
dynamic: bool = False,
2023-02-23 22:52:41 +00:00
name: str = None,
album_list: List[Album] = None,
current_artist_list: List[Artist] = None,
source_list: List[Source] = None,
**kwargs
):
2023-02-25 21:16:32 +00:00
MainObject.__init__(self, _id=_id, dynamic=dynamic, **kwargs)
2023-02-23 22:52:41 +00:00
self.name: str = name
self.album_collection: Collection = Collection(
data=album_list,
map_attributes=["title"],
element_type=Album
)
self.current_artist_collection: Collection = Collection(
data=current_artist_list,
map_attributes=["name"],
element_type=Artist
)
self.source_list = source_list or []
2023-02-25 21:16:32 +00:00
@property
def album_list(self) -> List[Album]:
return self.album_collection.copy()
@property
def current_artist_list(self) -> List[Artist]:
self.current_artist_collection.copy()