implement wave, flac, m4a, aiff files
This commit is contained in:
parent
df9399403a
commit
890a6171c9
@ -648,9 +648,9 @@ class MusicTable(QTableView):
|
||||
"""
|
||||
# debug(f'add_files_to_library() files={files}')
|
||||
worker = Worker(add_files_to_database, files, None)
|
||||
_ = worker.signals.signal_progress.connect(self.qapp.handle_progress) # type: ignore
|
||||
_ = worker.signals.signal_result.connect(self.on_add_files_to_database_finished)
|
||||
_ = worker.signals.signal_finished.connect(self.load_music_table)
|
||||
worker.signals.signal_progress.connect(self.qapp.handle_progress) # type: ignore
|
||||
worker.signals.signal_result.connect(self.on_add_files_to_database_finished)
|
||||
worker.signals.signal_finished.connect(self.load_music_table)
|
||||
if self.qapp:
|
||||
threadpool = self.qapp.threadpool # type: ignore
|
||||
threadpool.start(worker)
|
||||
|
||||
2
main.py
2
main.py
@ -739,7 +739,7 @@ if __name__ == "__main__":
|
||||
sys.exit(1)
|
||||
|
||||
# Allow for dynamic imports of my custom classes and utilities
|
||||
# ?
|
||||
# such as `from utils import whatever`
|
||||
project_root = os.path.abspath(os.path.dirname(__file__))
|
||||
sys.path.append(project_root)
|
||||
# Start the app
|
||||
|
||||
@ -13,11 +13,12 @@ def add_files_to_database(files: list[str], playlist_id: int | None = None, prog
|
||||
files: list() of fully qualified paths to audio file(s)
|
||||
progress_callback: emit data for user feedback
|
||||
|
||||
Returns a tuple where the first value is the success state
|
||||
and the second value is a list of failed to add items
|
||||
```
|
||||
(True, {"filename.mp3":"failed because i said so"})
|
||||
```
|
||||
Returns:
|
||||
tuple
|
||||
- 1st value is the success state
|
||||
- 2nd value is a list of failed to add items
|
||||
|
||||
(e.g., (True, {"filename.mp3":"failed because i said so"}))
|
||||
"""
|
||||
debug('add_files_to_database()')
|
||||
# yea
|
||||
@ -52,14 +53,17 @@ def add_files_to_database(files: list[str], playlist_id: int | None = None, prog
|
||||
# FIXME: can this be improved?
|
||||
# filename.basename or something like that
|
||||
filename = filepath.split("/")[-1]
|
||||
print('getting tags')
|
||||
tags, fail_reason = get_tags(filepath)
|
||||
if fail_reason:
|
||||
# if we fail to get audio tags, skip to next song
|
||||
failed_dict[filepath] = fail_reason
|
||||
continue
|
||||
print('remapping tags')
|
||||
# remap tags from ID3 to database tags
|
||||
audio: dict[str, str | int | None] = id3_remap(tags)
|
||||
# Append data tuple to insert_data list
|
||||
print('appending extra info')
|
||||
insert_data.append(
|
||||
(
|
||||
filepath,
|
||||
@ -79,7 +83,10 @@ def add_files_to_database(files: list[str], playlist_id: int | None = None, prog
|
||||
debug(f"inserting a LOT of songs: {len(insert_data)}")
|
||||
with DBA.DBAccess() as db:
|
||||
db.executemany(
|
||||
"INSERT OR IGNORE INTO song (filepath, title, album, artist, track_number, genre, codec, album_date, bitrate, length_ms) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
|
||||
"""
|
||||
INSERT OR IGNORE INTO song
|
||||
(filepath, title, album, artist, track_number, genre, codec, album_date, bitrate, length_ms)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);""",
|
||||
insert_data,
|
||||
)
|
||||
insert_data = [] # Reset the insert_data list
|
||||
@ -88,7 +95,9 @@ def add_files_to_database(files: list[str], playlist_id: int | None = None, prog
|
||||
# debug(f'inserting the rest of the songs | insert_data={insert_data}')
|
||||
with DBA.DBAccess() as db:
|
||||
db.executemany(
|
||||
"INSERT OR IGNORE INTO song (filepath, title, album, artist, track_number, genre, codec, album_date, bitrate, length_ms) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
|
||||
"""INSERT OR IGNORE INTO song
|
||||
(filepath, title, album, artist, track_number, genre, codec, album_date, bitrate, length_ms)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);""",
|
||||
insert_data,
|
||||
)
|
||||
return True, failed_dict
|
||||
|
||||
@ -1,26 +1,100 @@
|
||||
from mutagen.id3 import ID3
|
||||
from mutagen.id3 import ID3, APIC
|
||||
from mutagen.flac import FLAC, Picture
|
||||
from mutagen.mp4 import MP4
|
||||
from mutagen.wave import WAVE
|
||||
from mutagen.aiff import AIFF
|
||||
from logging import debug, error
|
||||
|
||||
|
||||
# def get_album_art(file: str | None) -> bytes:
|
||||
# """
|
||||
# Get the album art for an audio file
|
||||
# # Parameters
|
||||
# `file` | str | Fully qualified path to file
|
||||
# # Returns
|
||||
# bytes for album art or placeholder artwork
|
||||
# """
|
||||
# default_image_path = "./assets/default_album_art.jpg"
|
||||
# if file:
|
||||
# try:
|
||||
# audio = ID3(file)
|
||||
# for tag in audio.getall("APIC"):
|
||||
# if tag.type == 3: # 3 is the type for front cover
|
||||
# return tag.data
|
||||
# if audio.getall("APIC"):
|
||||
# return audio.getall("APIC")[0].data
|
||||
# except Exception as e:
|
||||
# error(f"Error retrieving album art: {e}")
|
||||
# return bytes()
|
||||
# with open(default_image_path, "rb") as f:
|
||||
# # debug("loading placeholder album art")
|
||||
# return f.read()
|
||||
|
||||
|
||||
|
||||
|
||||
def get_album_art(file: str | None) -> bytes:
|
||||
"""Get the album art for an audio file
|
||||
# Parameters
|
||||
`file` | str | Fully qualified path to file
|
||||
# Returns
|
||||
bytes for album art or placeholder artwork
|
||||
"""
|
||||
Get album art for MP3, FLAC, WAV, AIFF, M4A files.
|
||||
Parameters:
|
||||
- `file` | str | Fully qualified path to file
|
||||
|
||||
Returns:
|
||||
- bytes for album art or placeholder artwork
|
||||
"""
|
||||
|
||||
default_image_path = "./assets/default_album_art.jpg"
|
||||
if file:
|
||||
def fallback():
|
||||
try:
|
||||
with open(default_image_path, "rb") as f:
|
||||
return f.read()
|
||||
except Exception:
|
||||
return b""
|
||||
if not file:
|
||||
return fallback()
|
||||
|
||||
try:
|
||||
ext = file.lower().split(".")[-1]
|
||||
# ---------------- MP3 ----------------
|
||||
if ext == "mp3":
|
||||
audio = ID3(file)
|
||||
for tag in audio.getall("APIC"):
|
||||
if tag.type == 3: # 3 is the type for front cover
|
||||
return tag.data
|
||||
if audio.getall("APIC"):
|
||||
return audio.getall("APIC")[0].data
|
||||
except Exception as e:
|
||||
error(f"Error retrieving album art: {e}")
|
||||
return bytes()
|
||||
with open(default_image_path, "rb") as f:
|
||||
# debug("loading placeholder album art")
|
||||
return f.read()
|
||||
apics = audio.getall("APIC")
|
||||
if apics:
|
||||
# prefer front cover (type 3)
|
||||
for tag in apics:
|
||||
if tag.type == 3:
|
||||
return tag.data
|
||||
return apics[0].data
|
||||
# ---------------- FLAC ----------------
|
||||
elif ext == "flac":
|
||||
audio = FLAC(file)
|
||||
if audio.pictures:
|
||||
# prefer front cover
|
||||
for pic in audio.pictures:
|
||||
if pic.type == 3:
|
||||
return pic.data
|
||||
return audio.pictures[0].data
|
||||
# ---------------- M4A / MP4 ----------------
|
||||
elif ext in ("m4a", "mp4"):
|
||||
audio = MP4(file)
|
||||
cov = audio.tags.get("covr")
|
||||
if cov:
|
||||
# covr is a list of MP4Cover objects
|
||||
return bytes(cov[0])
|
||||
# ---------------- WAV ----------------
|
||||
elif ext == "wav":
|
||||
return fallback()
|
||||
# audio = WAVE(file)
|
||||
# if hasattr(audio, "pictures") and audio.pictures:
|
||||
# return audio.pictures[0].data
|
||||
# ---------------- AIFF ----------------
|
||||
elif ext in ("aiff", "aif"):
|
||||
return fallback()
|
||||
# audio = AIFF(file)
|
||||
# if hasattr(audio, "pictures") and audio.pictures:
|
||||
# return audio.pictures[0].data
|
||||
|
||||
except Exception as e:
|
||||
error(f"Error retrieving album art: {e}")
|
||||
|
||||
return fallback()
|
||||
|
||||
@ -3,11 +3,93 @@ from logging import debug, error
|
||||
from mutagen.id3 import ID3
|
||||
from mutagen.mp3 import MP3
|
||||
from mutagen.flac import FLAC
|
||||
from mutagen.wave import WAVE
|
||||
from mutagen.aiff import AIFF
|
||||
from mutagen.mp4 import MP4
|
||||
from mutagen.id3._frames import TIT2
|
||||
from mutagen.id3._util import ID3NoHeaderError
|
||||
|
||||
from utils import handle_date_tag
|
||||
|
||||
def get_flac_tags(filename: str) -> tuple[FLAC, str]:
|
||||
try:
|
||||
audio = FLAC(filename)
|
||||
except Exception:
|
||||
audio = FLAC()
|
||||
try:
|
||||
if os.path.exists(filename):
|
||||
audio.save(os.path.abspath(filename))
|
||||
title = os.path.splitext(os.path.basename(filename))[0]
|
||||
if audio.tags is None:
|
||||
audio.add_tags()
|
||||
if "title" not in audio:
|
||||
audio["title"] = [title]
|
||||
audio.save()
|
||||
return audio, ""
|
||||
|
||||
except Exception as e:
|
||||
return FLAC(), f"Could not assign FLAC tag: {e}"
|
||||
|
||||
def get_m4a_tags(filename: str) -> tuple[MP4, str]:
|
||||
try:
|
||||
audio = MP4(filename)
|
||||
except Exception:
|
||||
audio = MP4()
|
||||
try:
|
||||
if os.path.exists(filename):
|
||||
audio.save(os.path.abspath(filename))
|
||||
title = os.path.splitext(os.path.basename(filename))[0]
|
||||
if audio.tags is None:
|
||||
audio.add_tags()
|
||||
tags = audio.tags
|
||||
if "©nam" not in tags:
|
||||
tags["©nam"] = [title]
|
||||
audio.save()
|
||||
return audio, ""
|
||||
except Exception as e:
|
||||
return MP4(), f"Could not assign M4A tag: {e}"
|
||||
|
||||
def get_aiff_tags(filename: str) -> tuple[AIFF, str]:
|
||||
try:
|
||||
audio = AIFF(filename)
|
||||
except Exception:
|
||||
audio = AIFF()
|
||||
try:
|
||||
if os.path.exists(filename):
|
||||
audio.save(os.path.abspath(filename))
|
||||
title = os.path.splitext(os.path.basename(filename))[0]
|
||||
if audio.tags is None:
|
||||
audio.add_tags()
|
||||
if "TIT2" not in audio:
|
||||
audio["TIT2"] = TIT2(encoding=3, text=[title])
|
||||
audio.save()
|
||||
return audio, ""
|
||||
except Exception as e:
|
||||
return AIFF(), f"Could not assign AIFF tag: {e}"
|
||||
|
||||
|
||||
|
||||
def get_wav_tags(filename: str) -> tuple[WAVE, str]:
|
||||
"""Get or create basic WAV tags (RIFF INFO)"""
|
||||
try:
|
||||
audio = WAVE(filename)
|
||||
except Exception:
|
||||
audio = WAVE()
|
||||
try:
|
||||
if os.path.exists(filename):
|
||||
audio = WAVE(filename)
|
||||
title = os.path.splitext(os.path.basename(filename))[0]
|
||||
# IMPORTANT: ensure tag container exists
|
||||
if audio.tags is None:
|
||||
audio.add_tags()
|
||||
# RIFF INFO lives in audio.tags.info in many Mutagen versions
|
||||
if not hasattr(audio.tags, "info") or audio.tags.info is None:
|
||||
audio.add_tags()
|
||||
audio.tags["INAM"] = title
|
||||
audio.save(filename)
|
||||
return audio, ""
|
||||
except Exception as e:
|
||||
return WAVE(), f"Could not assign WAV tag to file: {e}"
|
||||
|
||||
def get_mp3_tags(filename: str) -> tuple[MP3 | ID3 | FLAC, str]:
|
||||
"""Get ID3 tags for mp3 file"""
|
||||
@ -16,11 +98,9 @@ def get_mp3_tags(filename: str) -> tuple[MP3 | ID3 | FLAC, str]:
|
||||
audio = MP3(filename)
|
||||
except ID3NoHeaderError:
|
||||
audio = MP3()
|
||||
|
||||
try:
|
||||
if os.path.exists(filename):
|
||||
audio.save(os.path.abspath(filename))
|
||||
|
||||
title = os.path.splitext(os.path.basename(filename))[0]
|
||||
if "TIT2" not in list(audio.keys()):
|
||||
# if title tag doesnt exist, create it - filename
|
||||
@ -29,23 +109,21 @@ def get_mp3_tags(filename: str) -> tuple[MP3 | ID3 | FLAC, str]:
|
||||
# Save the updated tags
|
||||
audio.save()
|
||||
return audio, ""
|
||||
|
||||
except Exception as e:
|
||||
return MP3(), f"Could not assign ID3 tag to file: {e}"
|
||||
|
||||
|
||||
def id3_remap(audio: MP3 | ID3 | FLAC) -> dict[str, str | int | None]:
|
||||
def id3_remap(audio: MP3 | ID3 | FLAC | WAVE | AIFF | MP4) -> dict[str, str | int | None]:
|
||||
"""
|
||||
Turns the ID3 dict of an audio file into a normal dict that I, the human, can use.
|
||||
Add extra fields too :D yahooo
|
||||
"""
|
||||
# FIXME: implement other filetypes here too
|
||||
# TODO: implement other filetypes here too
|
||||
remap = {}
|
||||
# .mp3
|
||||
if isinstance(audio, MP3):
|
||||
# so ugly
|
||||
uslt_tags = [tag for tag in audio.keys() if tag.startswith("USLT::")]
|
||||
lyrics = next((audio[tag].text for tag in uslt_tags), "")
|
||||
# so ugly
|
||||
remap = {
|
||||
"title": audio.get("TIT2"),
|
||||
"artist": audio.get("TPE1"),
|
||||
@ -64,10 +142,96 @@ def id3_remap(audio: MP3 | ID3 | FLAC) -> dict[str, str | int | None]:
|
||||
continue
|
||||
if not isinstance(v, str) and not isinstance(v, int):
|
||||
remap[k] = v.text[0]
|
||||
# .wav
|
||||
if isinstance(audio, WAVE):
|
||||
print('is wave file')
|
||||
remap = {
|
||||
"title": audio.get("INAM"),
|
||||
"artist": audio.get("IART"),
|
||||
"album": audio.get("IPRD"),
|
||||
"track_number": audio.get("ITRK"),
|
||||
"genre": audio.get("IGNR"),
|
||||
"date": audio.get("ICRD"),
|
||||
"comment": audio.get("ICMT"),
|
||||
"bitrate": getattr(audio.info, "bitrate", None),
|
||||
"length": int(round(audio.info.length, 0)),
|
||||
"lyrics": audio.get("USLT") or "",
|
||||
}
|
||||
for k, v in remap.items():
|
||||
print(f'k: {k}, v: {v}')
|
||||
if v is None:
|
||||
continue
|
||||
# WAV tags are often mutagen.id3.TextFrame-like or lists/bytes
|
||||
if not isinstance(v, str) and not isinstance(v, int):
|
||||
try:
|
||||
# RIFF INFO tags are often simple strings but sometimes wrapped
|
||||
remap[k] = v.text[0] if hasattr(v, "text") else str(v)
|
||||
except Exception:
|
||||
remap[k] = str(v)
|
||||
if isinstance(audio, AIFF):
|
||||
tags = audio.tags or {}
|
||||
remap = {
|
||||
"title": audio.get("TIT2"),
|
||||
"artist": audio.get("TPE1"),
|
||||
"album": audio.get("TALB"),
|
||||
"track_number": audio.get("TRCK"),
|
||||
"genre": audio.get("TCON"),
|
||||
"date": audio.get("TDRC"),
|
||||
"lyrics": audio.get("USLT::eng") if "USLT::eng" in tags else "",
|
||||
"bitrate": getattr(audio.info, "bitrate", None),
|
||||
"length": int(round(audio.info.length, 0)),
|
||||
}
|
||||
for k, v in remap.items():
|
||||
if v is None:
|
||||
continue
|
||||
if not isinstance(v, (str, int)):
|
||||
remap[k] = v.text[0] if hasattr(v, "text") else str(v)
|
||||
|
||||
if isinstance(audio, MP4):
|
||||
tags = audio.tags or {}
|
||||
|
||||
remap = {
|
||||
"title": audio.get("©nam"),
|
||||
"artist": audio.get("©ART"),
|
||||
"album": audio.get("©alb"),
|
||||
"track_number": audio.get("trkn"),
|
||||
"genre": audio.get("©gen"),
|
||||
"date": audio.get("©day"),
|
||||
"lyrics": audio.get("©lyr"),
|
||||
"length": int(round(audio.info.length, 0)) if audio.info else 0,
|
||||
"bitrate": getattr(audio.info, "bitrate", None),
|
||||
}
|
||||
|
||||
for k, v in remap.items():
|
||||
if v is None:
|
||||
continue
|
||||
if not isinstance(v, (str, int)):
|
||||
remap[k] = str(v)
|
||||
|
||||
if isinstance(audio, FLAC):
|
||||
tags = audio.tags or {}
|
||||
|
||||
remap = {
|
||||
"title": audio.get("title"),
|
||||
"artist": audio.get("artist"),
|
||||
"album": audio.get("album"),
|
||||
"track_number": audio.get("tracknumber"),
|
||||
"genre": audio.get("genre"),
|
||||
"date": audio.get("date"),
|
||||
"lyrics": audio.get("lyrics"),
|
||||
"bitrate": getattr(audio.info, "bitrate", None),
|
||||
"length": int(round(audio.info.length, 0)),
|
||||
}
|
||||
|
||||
for k, v in remap.items():
|
||||
if v is None:
|
||||
continue
|
||||
if not isinstance(v, (str, int)):
|
||||
remap[k] = str(v)
|
||||
return remap
|
||||
|
||||
|
||||
def get_tags(filename: str) -> tuple[MP3 | ID3 | FLAC, str]:
|
||||
def get_tags(filename: str) -> tuple[MP3 | ID3 | WAVE | FLAC | AIFF | MP4, str]:
|
||||
"""
|
||||
Get the "ID3" tags for an audio file
|
||||
Returns a tuple of:
|
||||
@ -77,7 +241,7 @@ def get_tags(filename: str) -> tuple[MP3 | ID3 | FLAC, str]:
|
||||
Args
|
||||
- filename
|
||||
Returns
|
||||
- tuple(ID3/dict, fail_reason)
|
||||
- tuple(MP3/WAVE/FLAC/ID3/dict, fail_reason)
|
||||
|
||||
ID3 dict looks like this:
|
||||
{
|
||||
@ -86,12 +250,33 @@ def get_tags(filename: str) -> tuple[MP3 | ID3 | FLAC, str]:
|
||||
...
|
||||
}
|
||||
"""
|
||||
# FIXME: this is where i implement other filetypes
|
||||
if filename.lower().endswith(".mp3"):
|
||||
try:
|
||||
tags, details = get_mp3_tags(filename)
|
||||
except Exception as e:
|
||||
tags, details = ID3(), str(e)
|
||||
elif filename.lower().endswith(".wav"):
|
||||
try:
|
||||
tags, details = get_wav_tags(filename)
|
||||
except Exception as e:
|
||||
tags, details = WAVE(), str(e)
|
||||
elif filename.lower().endswith(".flac"):
|
||||
try:
|
||||
tags, details = get_flac_tags(filename)
|
||||
except Exception as e:
|
||||
tags, details = FLAC(), str(e)
|
||||
elif filename.lower().endswith(".aiff"):
|
||||
try:
|
||||
tags, details = get_aiff_tags(filename)
|
||||
except Exception as e:
|
||||
tags, details = AIFF(), str(e)
|
||||
elif filename.lower().endswith(".m4a"):
|
||||
try:
|
||||
tags, details = get_m4a_tags(filename)
|
||||
except Exception as e:
|
||||
tags, details = MP4(), str(e)
|
||||
else:
|
||||
tags, details = ID3(), "non mp3 file"
|
||||
tags, details = ID3(), "Unsupported filetype"
|
||||
return tags, details
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user