2024-12-21 16:07:27 +01:00
|
|
|
#!/usr/bin/python3
|
|
|
|
|
|
|
|
from pydub import AudioSegment
|
|
|
|
from pygame.mixer import Sound
|
2024-12-29 13:50:19 +01:00
|
|
|
from tinytag import TinyTag
|
2025-03-04 17:24:24 +01:00
|
|
|
from tinytag.tinytag import Images as TTImages
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class TrackMetadata:
|
2025-03-04 19:29:24 +01:00
|
|
|
path: str | None=None
|
|
|
|
title: str | None=None
|
|
|
|
artist: str | None=None
|
|
|
|
album: str | None=None
|
2025-03-07 19:24:41 +01:00
|
|
|
genre: str | None=None
|
2025-03-04 17:24:24 +01:00
|
|
|
images: TTImages | None=None # tinytag images
|
2024-12-21 16:07:27 +01:00
|
|
|
|
2025-03-04 19:29:24 +01:00
|
|
|
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 = ""
|
|
|
|
|
2025-03-07 19:24:41 +01:00
|
|
|
if self.genre == "None":
|
|
|
|
self.genre = ""
|
|
|
|
|
2025-03-04 19:29:24 +01:00
|
|
|
if self.path is None: # can't add missing information without a path
|
|
|
|
return
|
|
|
|
|
2025-03-07 19:24:41 +01:00
|
|
|
if self.title is None or self.artist is None or self.album is None or self.genre is None:
|
2025-03-04 19:29:24 +01:00
|
|
|
tags = TinyTag.get(self.path, ignore_errors=True, duration=False)
|
|
|
|
|
|
|
|
self.title = tags.title
|
|
|
|
self.artist = tags.artist
|
|
|
|
self.album = tags.album
|
2025-03-07 19:24:41 +01:00
|
|
|
self.genre = tags.genre
|
2025-03-04 19:29:24 +01:00
|
|
|
|
2024-12-21 16:07:27 +01:00
|
|
|
|
|
|
|
class Track:
|
|
|
|
"""
|
2025-03-04 17:24:24 +01:00
|
|
|
Class representing a track.
|
2024-12-21 16:07:27 +01:00
|
|
|
"""
|
|
|
|
|
2025-03-04 17:24:24 +01:00
|
|
|
def __init__(self, app, path: str, cache: bool=False, metadata: TrackMetadata=None):
|
2024-12-24 17:22:30 +01:00
|
|
|
self.app = app
|
2024-12-21 16:07:27 +01:00
|
|
|
self.path = path
|
|
|
|
|
2025-03-06 16:35:13 +01:00
|
|
|
# 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
|
|
|
|
|
2025-03-04 17:24:24 +01:00
|
|
|
if metadata is None:
|
|
|
|
# load metadata from audio file
|
2025-03-04 19:29:24 +01:00
|
|
|
tags = TinyTag.get(path, ignore_errors=True, duration=False)
|
2025-03-04 17:24:24 +01:00
|
|
|
|
2025-03-04 19:29:24 +01:00
|
|
|
self.metadata = TrackMetadata(path, tags.title, tags.artist, tags.album)
|
2025-03-04 17:24:24 +01:00
|
|
|
|
|
|
|
else:
|
|
|
|
self.metadata = metadata
|
2024-12-29 13:50:19 +01:00
|
|
|
|
2024-12-29 14:31:21 +01:00
|
|
|
self.cached = False
|
2024-12-24 17:22:30 +01:00
|
|
|
self.audio = None
|
|
|
|
self.sound = None
|
|
|
|
self.duration = 0
|
|
|
|
|
2025-01-26 16:49:09 +01:00
|
|
|
self.items = []
|
2025-02-28 19:28:07 +01:00
|
|
|
self.occurrences = {} # all occurrences in playlists categorized by playlist and id of the track widget
|
2025-01-25 17:21:43 +01:00
|
|
|
|
2024-12-29 14:31:21 +01:00
|
|
|
if cache:
|
2024-12-24 17:22:30 +01:00
|
|
|
self.cache()
|
2024-12-21 16:07:27 +01:00
|
|
|
|
2025-03-06 16:35:13 +01:00
|
|
|
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)
|
|
|
|
|
2025-01-26 16:49:09 +01:00
|
|
|
def set_occurrences(self):
|
|
|
|
# set track item for every occurrence of track in a playlist
|
|
|
|
|
|
|
|
new_occurrences = {}
|
|
|
|
|
|
|
|
for item in self.items:
|
2025-02-02 14:56:06 +01:00
|
|
|
# create dict of item: item.index (actually the id of the item bc. the item can't be used as key)
|
2025-01-26 16:49:09 +01:00
|
|
|
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.
|
|
|
|
"""
|
|
|
|
|
2025-02-03 17:53:35 +01:00
|
|
|
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)])
|
2025-01-26 16:49:09 +01:00
|
|
|
|
2024-12-21 16:07:27 +01:00
|
|
|
def cache(self):
|
2024-12-29 20:10:06 +01:00
|
|
|
self.load_audio()
|
2024-12-21 21:06:10 +01:00
|
|
|
# audio = normalize(audio)
|
2024-12-21 16:07:27 +01:00
|
|
|
|
2024-12-24 17:22:30 +01:00
|
|
|
wav = self.audio.export(format="wav")
|
2024-12-21 16:07:27 +01:00
|
|
|
|
2024-12-24 17:22:30 +01:00
|
|
|
self.sound = Sound(wav)
|
2024-12-21 16:07:27 +01:00
|
|
|
|
2024-12-24 17:22:30 +01:00
|
|
|
self.duration = len(self.audio) # track duration in milliseconds
|
2024-12-21 16:07:27 +01:00
|
|
|
|
2025-03-04 17:24:24 +01:00
|
|
|
# metadata with images
|
|
|
|
self.metadata.images = TinyTag.get(self.path, ignore_errors=True, duration=False, image=True).images
|
2025-02-21 17:26:47 +01:00
|
|
|
|
2024-12-29 14:31:21 +01:00
|
|
|
self.cached = True
|
|
|
|
|
2025-01-25 18:29:27 +01:00
|
|
|
def clear_cache(self):
|
|
|
|
self.cached = False
|
|
|
|
|
|
|
|
self.audio = None
|
|
|
|
self.sound = None
|
|
|
|
self.duration = 0
|
|
|
|
|
2025-03-04 17:24:24 +01:00
|
|
|
self.metadata.images = None
|
2025-02-21 17:26:47 +01:00
|
|
|
|
2024-12-29 20:10:06 +01:00
|
|
|
def load_audio(self):
|
2025-01-31 20:47:39 +01:00
|
|
|
self.audio = AudioSegment.from_file(self.path)
|
2024-12-29 20:10:06 +01:00
|
|
|
|
2024-12-21 19:00:06 +01:00
|
|
|
def remaining(self, position: int):
|
2024-12-21 20:20:06 +01:00
|
|
|
remaining_audio = self.audio[position:]
|
|
|
|
|
|
|
|
wav = remaining_audio.export(format="wav")
|
|
|
|
|
|
|
|
sound = Sound(wav)
|
|
|
|
|
2024-12-24 17:22:30 +01:00
|
|
|
# return the remaining part of the track's audio and the duration of the remaining part
|
2024-12-21 20:20:06 +01:00
|
|
|
return sound, len(remaining_audio)
|
2025-02-28 19:28:07 +01:00
|
|
|
|
|
|
|
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)
|