basic importing functionality
This commit is contained in:
parent
51676044e3
commit
63a9b37312
4 changed files with 638 additions and 0 deletions
52
engine_sync/__main__.py
Normal file
52
engine_sync/__main__.py
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
from pathlib import Path
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
from engine_sync.engine_interface import EngineInterface
|
||||||
|
from engine_sync.track import Track
|
||||||
|
from engine_sync.playlist import Playlist
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
engine_prefix_path = Path(
|
||||||
|
"../../AppData/Local/AIR Music Technology/EnginePrime/Favorites/Music_Vault/"
|
||||||
|
)
|
||||||
|
|
||||||
|
junction_point = Path("/mnt/media/music/")
|
||||||
|
|
||||||
|
print("Hello from engine-sync!")
|
||||||
|
|
||||||
|
db_path = Path("/mnt/c/Users/Fabian/Music/Engine Library/Database2/m.db")
|
||||||
|
con = sqlite3.connect(db_path)
|
||||||
|
|
||||||
|
itf = EngineInterface(engine_prefix_path, junction_point)
|
||||||
|
|
||||||
|
filepath = Path(
|
||||||
|
"/mnt/media/music/astr0/nanobii/Rainbow Road/Disk 001/nanobii - Rainbow Road.flac"
|
||||||
|
)
|
||||||
|
|
||||||
|
track = Track(filepath)
|
||||||
|
track.load_from_metadata()
|
||||||
|
track.fill_empty()
|
||||||
|
|
||||||
|
print(track)
|
||||||
|
|
||||||
|
cur = con.cursor()
|
||||||
|
|
||||||
|
parent = itf.get_playlist_id(Path("container"), cur)
|
||||||
|
playlist = Playlist("First try", parent=parent.engine_id)
|
||||||
|
itf.check_track_in_database(track, cur)
|
||||||
|
# itf.add_track(track, cur)
|
||||||
|
# playlist = itf.get_playlist_id(Path("1/1.3"), cur)
|
||||||
|
# print("playlist", playlist)
|
||||||
|
uuid = itf.get_database_uuid(cur)
|
||||||
|
itf.add_playlist(playlist, cur)
|
||||||
|
itf.add_track_to_playlist(track, playlist, uuid, cur)
|
||||||
|
|
||||||
|
con.commit()
|
||||||
|
# print("Track ID is", track.engine_id)
|
||||||
|
print("Playlist ID is", playlist.engine_id)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
359
engine_sync/engine_interface.py
Normal file
359
engine_sync/engine_interface.py
Normal file
|
|
@ -0,0 +1,359 @@
|
||||||
|
import sqlite3
|
||||||
|
from pathlib import Path
|
||||||
|
from hashlib import sha1
|
||||||
|
from PIL import Image, ImageOps
|
||||||
|
from io import BytesIO
|
||||||
|
from datetime import datetime
|
||||||
|
from dateutil.parser import parse as dateparse
|
||||||
|
|
||||||
|
from engine_sync.track import Track
|
||||||
|
from engine_sync.playlist import Playlist
|
||||||
|
|
||||||
|
|
||||||
|
class EngineInterface:
|
||||||
|
def __init__(self, engine_prefix_path: Path, junction_point: Path) -> None:
|
||||||
|
self.engine_prefix_path = engine_prefix_path
|
||||||
|
self.junction_point = junction_point
|
||||||
|
|
||||||
|
def add_track(self, track: Track, cur: sqlite3.Cursor) -> None:
|
||||||
|
"""
|
||||||
|
Add a track to the Engine DJ library.
|
||||||
|
|
||||||
|
Track ID will be added to the track object
|
||||||
|
"""
|
||||||
|
|
||||||
|
album_art_id = None
|
||||||
|
|
||||||
|
if track.cover is not None:
|
||||||
|
# add the track cover to the database
|
||||||
|
album_art_id = self.add_album_cover(track.cover, cur)
|
||||||
|
|
||||||
|
# get the filename
|
||||||
|
filename = track.file.name
|
||||||
|
|
||||||
|
# generate the path Engine wants to see
|
||||||
|
fixed_path = self.engine_prefix_path / track.file.relative_to(
|
||||||
|
self.junction_point
|
||||||
|
)
|
||||||
|
|
||||||
|
# merge the artist string
|
||||||
|
artist_str = ", ".join(track.artists)
|
||||||
|
|
||||||
|
# get the current UNIX timestamp date
|
||||||
|
current_date = int(datetime.now().timestamp())
|
||||||
|
|
||||||
|
# translate data type for explicit lyrics
|
||||||
|
explicit_int = 1 if track.explicit else 0
|
||||||
|
|
||||||
|
# add the track to the database
|
||||||
|
res = cur.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO Track (
|
||||||
|
length,
|
||||||
|
bpm,
|
||||||
|
year,
|
||||||
|
path,
|
||||||
|
filename,
|
||||||
|
bitrate,
|
||||||
|
albumArtId,
|
||||||
|
fileBytes,
|
||||||
|
title,
|
||||||
|
artist,
|
||||||
|
album,
|
||||||
|
genre,
|
||||||
|
label,
|
||||||
|
fileType,
|
||||||
|
dateCreated,
|
||||||
|
dateAdded,
|
||||||
|
explicitLyrics,
|
||||||
|
lastEditTime,
|
||||||
|
playOrder,
|
||||||
|
rating,
|
||||||
|
albumArt,
|
||||||
|
isPlayed,
|
||||||
|
isAnalyzed,
|
||||||
|
isAvailable,
|
||||||
|
isMetadataOfPackedTrackChanged,
|
||||||
|
isPerfomanceDataOfPackedTrackChanged,
|
||||||
|
isMetadataImported,
|
||||||
|
pdbImportKey,
|
||||||
|
streamingFlags
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 0, 'image://planck/0', 0, 0, 1, 0, 0, 1, 0, 0)
|
||||||
|
RETURNING id
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
track.length,
|
||||||
|
track.bpm,
|
||||||
|
track.release_date.year,
|
||||||
|
str(fixed_path),
|
||||||
|
filename,
|
||||||
|
track.bitrate,
|
||||||
|
album_art_id,
|
||||||
|
track.file_size,
|
||||||
|
track.title,
|
||||||
|
artist_str,
|
||||||
|
track.album,
|
||||||
|
track.genre,
|
||||||
|
track.label,
|
||||||
|
track.file.suffix[1:],
|
||||||
|
track.creation_date,
|
||||||
|
current_date,
|
||||||
|
explicit_int,
|
||||||
|
current_date,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
track.engine_id = res.fetchone()[0]
|
||||||
|
|
||||||
|
def add_album_cover(self, cover: bytes, cur: sqlite3.Cursor) -> int:
|
||||||
|
"""
|
||||||
|
Add an album cover to the track database, skip if already in there.
|
||||||
|
Return the ID of the column in the database.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# if this is a duplicate, skip it
|
||||||
|
duplicate, hash, id = self.check_cover_in_database(cover, cur)
|
||||||
|
|
||||||
|
if duplicate:
|
||||||
|
return id
|
||||||
|
|
||||||
|
cover_im = Image.open(BytesIO(cover))
|
||||||
|
|
||||||
|
# resize the image to 256x256 (Engine requirement)
|
||||||
|
# keep the aspect ratio
|
||||||
|
cover_im = ImageOps.contain(cover_im, (256, 256))
|
||||||
|
|
||||||
|
to_db_bytes = b""
|
||||||
|
|
||||||
|
# convert to PNG
|
||||||
|
with BytesIO() as f:
|
||||||
|
cover_im.save(f, format="PNG")
|
||||||
|
f.seek(0)
|
||||||
|
to_db_bytes = f.getvalue()
|
||||||
|
|
||||||
|
# save to the database and readback ID
|
||||||
|
res = cur.execute(
|
||||||
|
"INSERT INTO AlbumArt (hash, albumArt) VALUES (?, ?) RETURNING id",
|
||||||
|
(
|
||||||
|
hash,
|
||||||
|
to_db_bytes,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# return the ID
|
||||||
|
return res.fetchone()[0]
|
||||||
|
|
||||||
|
def check_cover_in_database(
|
||||||
|
self, cover: bytes, cur: sqlite3.Cursor
|
||||||
|
) -> tuple[bool, str, int]:
|
||||||
|
"""
|
||||||
|
Check if the given image exists in the album art table.
|
||||||
|
|
||||||
|
While the exact way Engine calculates the image hash is unknown,
|
||||||
|
we can at least de-duplicate with tracks that have been imported
|
||||||
|
through this tool.
|
||||||
|
|
||||||
|
The hash is a simple sha1 sum of the original cover image.
|
||||||
|
"""
|
||||||
|
|
||||||
|
image_hash = sha1(cover).hexdigest()
|
||||||
|
|
||||||
|
res = cur.execute("SELECT id FROM AlbumArt WHERE hash=?", (image_hash,))
|
||||||
|
|
||||||
|
found = False
|
||||||
|
id = -1
|
||||||
|
|
||||||
|
data = res.fetchone()
|
||||||
|
|
||||||
|
if data is not None:
|
||||||
|
found = True
|
||||||
|
id = data[0]
|
||||||
|
|
||||||
|
return found, image_hash, id
|
||||||
|
|
||||||
|
def check_track_in_database(self, track: Track, cur: sqlite3.Cursor) -> bool:
|
||||||
|
"""
|
||||||
|
Check if a track is the Engine database / collection.
|
||||||
|
Track object only needs a file path.
|
||||||
|
If true is returned, the track object will have the Engine database ID filled.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# generate the path Engine wants to see
|
||||||
|
fixed_path = self.engine_prefix_path / track.file.relative_to(
|
||||||
|
self.junction_point
|
||||||
|
)
|
||||||
|
|
||||||
|
res = cur.execute(
|
||||||
|
"SELECT (id) FROM Track WHERE path=?",
|
||||||
|
(str(fixed_path),),
|
||||||
|
)
|
||||||
|
|
||||||
|
db_track = res.fetchone()
|
||||||
|
|
||||||
|
# check if the track was found
|
||||||
|
if db_track is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
track.engine_id = db_track[0]
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_database_uuid(self, cur: sqlite3.Cursor) -> str:
|
||||||
|
"""
|
||||||
|
Get the uuid of the Engine database
|
||||||
|
"""
|
||||||
|
res = cur.execute("SELECT uuid FROM Information WHERE id=1").fetchone()
|
||||||
|
|
||||||
|
if res is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return res[0]
|
||||||
|
|
||||||
|
def get_playlist(self, playlist_id: int, cur: sqlite3.Cursor) -> Playlist:
|
||||||
|
"""
|
||||||
|
Get a playlist object from an Engine database ID.
|
||||||
|
"""
|
||||||
|
|
||||||
|
res = cur.execute(
|
||||||
|
"SELECT title, lastEditTime, parentListId FROM Playlist WHERE id=?",
|
||||||
|
(playlist_id,),
|
||||||
|
)
|
||||||
|
|
||||||
|
pl_db_data = res.fetchone()
|
||||||
|
|
||||||
|
if pl_db_data is None:
|
||||||
|
# something has gone wrong
|
||||||
|
return None
|
||||||
|
|
||||||
|
pl_db_name = pl_db_data[0]
|
||||||
|
pl_db_last_edit = pl_db_data[1]
|
||||||
|
pl_db_parent = pl_db_data[2]
|
||||||
|
|
||||||
|
playlist = Playlist(pl_db_name, engine_id=playlist_id, parent=pl_db_parent)
|
||||||
|
playlist.last_edited = dateparse(pl_db_last_edit)
|
||||||
|
|
||||||
|
return playlist
|
||||||
|
|
||||||
|
def get_playlist_id(self, path: Path, cur: sqlite3.Cursor) -> Playlist:
|
||||||
|
"""
|
||||||
|
Get the Engine database ID of a given playlist in the tree.
|
||||||
|
"""
|
||||||
|
if len(path.parts) < 1:
|
||||||
|
print("no parts")
|
||||||
|
return None
|
||||||
|
|
||||||
|
db_path = ";".join(reversed(path.parts))
|
||||||
|
db_path += ";"
|
||||||
|
print("looking for path", db_path)
|
||||||
|
|
||||||
|
# check if the playlist exists
|
||||||
|
res = cur.execute(
|
||||||
|
"SELECT id FROM PlaylistPath WHERE path=?",
|
||||||
|
(db_path,),
|
||||||
|
)
|
||||||
|
|
||||||
|
pl_db_id = res.fetchone()
|
||||||
|
|
||||||
|
if pl_db_id is None:
|
||||||
|
print("no path")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# clean up the data
|
||||||
|
pl_db_id = pl_db_id[0]
|
||||||
|
|
||||||
|
return self.get_playlist(pl_db_id, cur)
|
||||||
|
|
||||||
|
def add_playlist(self, playlist: Playlist, cur: sqlite3.Cursor) -> None:
|
||||||
|
"""
|
||||||
|
Add a new playlist to the Engine database
|
||||||
|
|
||||||
|
Adds the playlist ID to the playlist object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# insert the new playlist
|
||||||
|
res = cur.execute(
|
||||||
|
"INSERT INTO Playlist (title, parentListId, isPersisted, nextListId, lastEditTime, isExplicitlyExported) VALUES (?, ?, 1, 0, ?, 1) RETURNING id",
|
||||||
|
(
|
||||||
|
playlist.name,
|
||||||
|
playlist.parent,
|
||||||
|
playlist.last_edited.isoformat(sep=" ", timespec="seconds"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
playlist.engine_id = res.fetchone()[0]
|
||||||
|
|
||||||
|
def add_track_to_playlist(
|
||||||
|
self,
|
||||||
|
track: Track,
|
||||||
|
playlist: Playlist,
|
||||||
|
database_uuid: str,
|
||||||
|
cur: sqlite3.Cursor,
|
||||||
|
true_member: bool = True,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Add a track to a given playlist.
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
membershipReference:
|
||||||
|
|
||||||
|
If the track is not in this playlist but propagates up from one of the child playlists,
|
||||||
|
the membership reference field represents how many direkt child playlists contain this track.
|
||||||
|
|
||||||
|
If the track is direct part of this playlist, the membership reference field is 0.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# check if the track is already in the playlist
|
||||||
|
res = cur.execute(
|
||||||
|
"SELECT id, membershipReference FROM PlaylistEntity WHERE listId=? AND trackId=?",
|
||||||
|
(playlist.engine_id, track.engine_id),
|
||||||
|
)
|
||||||
|
|
||||||
|
entity = res.fetchone()
|
||||||
|
|
||||||
|
if entity is None:
|
||||||
|
# track is not yet in playlist, add it
|
||||||
|
|
||||||
|
# if this is caused by propagation, set the membership reference to 1, otherwise 0
|
||||||
|
mem_ref = 0 if true_member else 1
|
||||||
|
|
||||||
|
# insert the track into the playlist
|
||||||
|
cur.execute(
|
||||||
|
"INSERT INTO PlaylistEntity (listId, trackId, databaseUuid, nextEntityId, membershipReference) VALUES (?, ?, ?, 0, ?)",
|
||||||
|
(playlist.engine_id, track.engine_id, database_uuid, mem_ref),
|
||||||
|
)
|
||||||
|
|
||||||
|
# if this is the root level, we're done
|
||||||
|
if playlist.parent == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
# if the playlist has a parent, propagate up
|
||||||
|
parent_pl = self.get_playlist(playlist.parent, cur)
|
||||||
|
self.add_track_to_playlist(
|
||||||
|
track, parent_pl, database_uuid, cur, true_member=False
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
# track is already in playlist, check if we need to update
|
||||||
|
|
||||||
|
entity_id = entity[0]
|
||||||
|
mem_ref = entity[1]
|
||||||
|
|
||||||
|
# if the membership reference is 0, we're done
|
||||||
|
if mem_ref == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
mem_ref = 0 if true_member else mem_ref + 1
|
||||||
|
|
||||||
|
# we only need to update this level
|
||||||
|
cur.execute(
|
||||||
|
"UPDATE PlaylistEntity SET membershipReference=? WHERE id=?",
|
||||||
|
(mem_ref, entity_id),
|
||||||
|
)
|
||||||
|
|
||||||
|
def check_track_in_playlist(
|
||||||
|
self, track: Track, playlist: Playlist, cur: sqlite3.Cursor
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Check if a track is in a given playlist.
|
||||||
|
"""
|
||||||
23
engine_sync/playlist.py
Normal file
23
engine_sync/playlist.py
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class Playlist:
|
||||||
|
def __init__(self, name: str, engine_id: int = None, parent: int = 0):
|
||||||
|
self.name = name
|
||||||
|
self.parent = parent
|
||||||
|
self.engine_id = engine_id
|
||||||
|
self.last_edited = datetime.now()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
|
||||||
|
st = "'" + self.name + "'"
|
||||||
|
|
||||||
|
if self.parent != 0:
|
||||||
|
st += ", parent " + str(self.parent)
|
||||||
|
|
||||||
|
if self.engine_id != 0:
|
||||||
|
st += ", Engine ID " + str(self.engine_id)
|
||||||
|
|
||||||
|
st += ", Last edited " + self.last_edited.isoformat(sep=" ", timespec="seconds")
|
||||||
|
|
||||||
|
return st
|
||||||
204
engine_sync/track.py
Normal file
204
engine_sync/track.py
Normal file
|
|
@ -0,0 +1,204 @@
|
||||||
|
import math
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from mutagen.mp3 import MP3
|
||||||
|
from mutagen.flac import FLAC
|
||||||
|
from datetime import datetime
|
||||||
|
from dateutil.parser import parse as dateparse
|
||||||
|
|
||||||
|
|
||||||
|
class Track:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
file: Path,
|
||||||
|
bitrate: int = None,
|
||||||
|
file_size: int = None,
|
||||||
|
creation_date: int = None,
|
||||||
|
modification_date: int = None,
|
||||||
|
length: int = None,
|
||||||
|
title: str = None,
|
||||||
|
artists: list[str] = None,
|
||||||
|
album: str = None,
|
||||||
|
genre: str = None,
|
||||||
|
bpm: int = None,
|
||||||
|
release_date: datetime = None,
|
||||||
|
label: str = None,
|
||||||
|
cover: bytes = None,
|
||||||
|
filetype: str = None,
|
||||||
|
explicit: bool = False,
|
||||||
|
engine_id: int = None,
|
||||||
|
):
|
||||||
|
self.file = file
|
||||||
|
self.bitrate = bitrate
|
||||||
|
self.file_size = file_size
|
||||||
|
self.creation_date = creation_date
|
||||||
|
self.modification_date = modification_date
|
||||||
|
self.length = length
|
||||||
|
self.title = title
|
||||||
|
self.artists = artists
|
||||||
|
self.album = album
|
||||||
|
self.genre = genre
|
||||||
|
self.bpm = bpm
|
||||||
|
self.release_date = release_date
|
||||||
|
self.label = label
|
||||||
|
self.cover = cover
|
||||||
|
self.filetype = filetype
|
||||||
|
self.explicit = explicit
|
||||||
|
self.engine_id = engine_id
|
||||||
|
|
||||||
|
def fill_empty(self):
|
||||||
|
|
||||||
|
if self.title is None:
|
||||||
|
self.title = ""
|
||||||
|
|
||||||
|
if self.artists is None:
|
||||||
|
self.artists = [""]
|
||||||
|
|
||||||
|
if self.album is None:
|
||||||
|
self.album = ""
|
||||||
|
|
||||||
|
if self.genre is None:
|
||||||
|
self.genre = ""
|
||||||
|
|
||||||
|
if self.release_date is None:
|
||||||
|
self.release_date = datetime.now()
|
||||||
|
|
||||||
|
if self.label is None:
|
||||||
|
self.label = ""
|
||||||
|
|
||||||
|
if self.explicit is None:
|
||||||
|
self.explicit = False
|
||||||
|
|
||||||
|
def load_from_metadata(self):
|
||||||
|
"""
|
||||||
|
Try to fill up non-defined fields from file metadata.
|
||||||
|
"""
|
||||||
|
if self.file.suffix == ".flac":
|
||||||
|
self.load_from_metadata_flac()
|
||||||
|
|
||||||
|
def load_from_metadata_flac(self):
|
||||||
|
"""
|
||||||
|
Try to fill up non-defined fields from file metadata.
|
||||||
|
Loads the fields from a FLAC file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
tags = FLAC(self.file)
|
||||||
|
|
||||||
|
if self.bitrate is None:
|
||||||
|
self.bitrate = int(tags.info.bitrate / 1000)
|
||||||
|
|
||||||
|
if self.file_size is None:
|
||||||
|
self.file_size = self.file.stat().st_size
|
||||||
|
|
||||||
|
if self.creation_date is None:
|
||||||
|
self.creation_date = int(self.file.stat().st_ctime)
|
||||||
|
|
||||||
|
if self.modification_date is None:
|
||||||
|
self.modification_date = int(self.file.stat().st_mtime)
|
||||||
|
|
||||||
|
if self.length is None:
|
||||||
|
self.length = int(math.ceil(tags.info.length))
|
||||||
|
|
||||||
|
if self.title is None:
|
||||||
|
try:
|
||||||
|
self.title = tags["title"][0]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if self.artists is None:
|
||||||
|
try:
|
||||||
|
self.artists = tags["artist"]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if self.album is None:
|
||||||
|
try:
|
||||||
|
self.album = tags["album"][0]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if self.genre is None:
|
||||||
|
try:
|
||||||
|
self.genre = tags["genre"][0]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if self.bpm is None:
|
||||||
|
try:
|
||||||
|
self.bpm = int(tags["bpm"][0])
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if self.release_date is None:
|
||||||
|
try:
|
||||||
|
self.release_date = dateparse(tags["date"][0])
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if self.label is None:
|
||||||
|
try:
|
||||||
|
self.label = tags["publisher"][0]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if self.cover is None:
|
||||||
|
pictures = tags.pictures
|
||||||
|
|
||||||
|
if pictures is not None and len(pictures) > 0:
|
||||||
|
self.cover = tags.pictures[0].data
|
||||||
|
|
||||||
|
if self.explicit is None:
|
||||||
|
try:
|
||||||
|
self.explicit = bool(tags["itunesadvisory"])
|
||||||
|
except KeyError:
|
||||||
|
self.explicit = False
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
st = (
|
||||||
|
"'"
|
||||||
|
+ self.title
|
||||||
|
+ "' by "
|
||||||
|
+ ", ".join(self.artists)
|
||||||
|
+ " in album '"
|
||||||
|
+ self.album
|
||||||
|
+ "', released "
|
||||||
|
+ str(self.release_date.date())
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.label is not None:
|
||||||
|
st += " by " + self.label + ". "
|
||||||
|
else:
|
||||||
|
st += ". "
|
||||||
|
|
||||||
|
st += "Song is "
|
||||||
|
|
||||||
|
if self.bpm is not None:
|
||||||
|
st += "at " + str(self.bpm) + " BPM, "
|
||||||
|
|
||||||
|
st += "Explicit " + str(self.explicit)
|
||||||
|
|
||||||
|
if self.genre is not None:
|
||||||
|
st += ", Genre " + self.genre
|
||||||
|
|
||||||
|
st += (
|
||||||
|
", Runtime "
|
||||||
|
+ str(self.length)
|
||||||
|
+ ", Bitrate "
|
||||||
|
+ str(self.bitrate)
|
||||||
|
+ ", File size "
|
||||||
|
+ str(self.file_size)
|
||||||
|
+ ", Created "
|
||||||
|
+ str(self.creation_date)
|
||||||
|
+ ", Last modified "
|
||||||
|
+ str(self.modification_date)
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.engine_id is not None:
|
||||||
|
st += ", Engine ID " + str(self.engine_id)
|
||||||
|
|
||||||
|
if self.cover is None:
|
||||||
|
st += ", No cover."
|
||||||
|
else:
|
||||||
|
st += ", Cover attached."
|
||||||
|
|
||||||
|
return st
|
||||||
Loading…
Reference in a new issue