#!/usr/bin/python3 import os import shutil from pydub import AudioSegment from pygame.mixer import Sound from tinytag import TinyTag from tinytag.tinytag import Images as TTImages from dataclasses import dataclass from ..types import Types @dataclass class TrackMetadata: path: str | None=None title: str | None=None artist: str | None=None album: str | None=None genre: str | None=None images: TTImages | None=None # tinytag images def add_missing(self): # Make the album be an empty string instead of "None" if self.title == "None": self.title = "" if self.artist == "None": self.artist = "" if self.album == "None": self.album = "" if self.genre == "None": self.genre = "" if self.path is None: # can't add missing information without a path return if self.title is None or self.artist is None or self.album is None or self.genre is None: tags = TinyTag.get(self.path, ignore_errors=True, duration=False) self.title = tags.title self.artist = tags.artist self.album = tags.album self.genre = tags.genre class Track: """ Class representing a track. """ def __init__(self, app, path: str, cache: bool=False, metadata: TrackMetadata=None): self.app = app self.path = path # add self to loaded tracks to make sure that no other track object is created for this track app.library.loaded_tracks[self.path] = self if metadata is None: # load metadata from audio file tags = TinyTag.get(path, ignore_errors=True, duration=False) self.metadata = TrackMetadata(path, tags.title, tags.artist, tags.album) else: self.metadata = metadata self.cached = False self.audio = None self.sound = None self.duration = 0 self.items = [] self.occurrences = {} # all occurrences in playlists categorized by playlist and id of the track widget if cache: self.cache() def __new__(cls, app, path: str, cache: bool=False, metadata: TrackMetadata=None): loaded_track = app.library.loaded_track(path) if loaded_track is not None: if cache: loaded_track.cache() return loaded_track else: return super().__new__(cls) def set_occurrences(self): # set track item for every occurrence of track in a playlist new_occurrences = {} for item in self.items: # create dict of item: item.index (actually the id of the item bc. the item can't be used as key) playlist_occurrences = new_occurrences.get(item.playlist, {}) playlist_occurrences[id(item)] = item.index new_occurrences[item.playlist] = playlist_occurrences self.correct_occurrences(new_occurrences) self.occurrences = new_occurrences def correct_occurrences(self, new_occurrences): """ If this track is the currently playing track, and it gets moved, this corrects the current playlist index. """ if self.app.player.current_playlist is not None: if self.app.player.current_playlist.current_track is self: for item in self.items: if ( item.playlist in self.occurrences and self.occurrences[item.playlist][id(item)] == self.app.player.current_playlist.current_track_index ): self.app.player.current_playlist.set_track(new_occurrences[item.playlist][id(item)]) def cache(self): self.load_audio() # audio = normalize(audio) wav = self.audio.export(format="wav") self.sound = Sound(wav) self.duration = len(self.audio) # track duration in milliseconds # metadata with images self.metadata.images = TinyTag.get(self.path, ignore_errors=True, duration=False, image=True).images self.cached = True def clear_cache(self): self.cached = False self.audio = None self.sound = None self.duration = 0 self.metadata.images = None def load_audio(self): self.audio = AudioSegment.from_file(self.path) def remaining(self, position: int): remaining_audio = self.audio[position:] wav = remaining_audio.export(format="wav") sound = Sound(wav) # return the remaining part of the track's audio and the duration of the remaining part return sound, len(remaining_audio) def delete_items(self, playlist): """ Deletes all QTreeWidgetItems that correspond to this track and the given playlist. """ for item in self.items: if id(item) in self.occurrences[playlist]: self.items.remove(item) self.occurrences.pop(playlist) def copy(self, dest: str, copy_type: int=Types.CopyType.symlink, moved: bool=True): match copy_type: case Types.CopyType.symlink: os.symlink(self.path, dest) case Types.CopyType.copy: shutil.copyfile(self.path, dest) case Types.CopyType.move: shutil.move(self.path, dest) if moved: # update path variables self.path = dest self.metadata.path = dest