working on making id3 tags easier to work with
This commit is contained in:
parent
d3313db6ab
commit
554b06a667
@ -63,6 +63,7 @@ from configparser import ConfigParser
|
|||||||
|
|
||||||
|
|
||||||
class MusicTable(QTableView):
|
class MusicTable(QTableView):
|
||||||
|
playlistStatsSignal = pyqtSignal(str)
|
||||||
playPauseSignal = pyqtSignal()
|
playPauseSignal = pyqtSignal()
|
||||||
playSignal = pyqtSignal(str)
|
playSignal = pyqtSignal(str)
|
||||||
enterKey = pyqtSignal()
|
enterKey = pyqtSignal()
|
||||||
@ -162,7 +163,7 @@ class MusicTable(QTableView):
|
|||||||
self.model2.layoutChanged.connect(self.restore_scroll_position)
|
self.model2.layoutChanged.connect(self.restore_scroll_position)
|
||||||
self.horizontal_header.sectionResized.connect(self.on_header_resized)
|
self.horizontal_header.sectionResized.connect(self.on_header_resized)
|
||||||
# Final actions
|
# Final actions
|
||||||
self.load_music_table()
|
# self.load_music_table()
|
||||||
self.setup_keyboard_shortcuts()
|
self.setup_keyboard_shortcuts()
|
||||||
self.load_header_widths()
|
self.load_header_widths()
|
||||||
|
|
||||||
@ -455,7 +456,7 @@ class MusicTable(QTableView):
|
|||||||
except IndexError:
|
except IndexError:
|
||||||
pass
|
pass
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
debug(f'on_add_files_to_database_finished() | Something went wrong: {e}')
|
debug(f"on_add_files_to_database_finished() | Something went wrong: {e}")
|
||||||
|
|
||||||
# ____________________
|
# ____________________
|
||||||
# | |
|
# | |
|
||||||
@ -690,9 +691,6 @@ class MusicTable(QTableView):
|
|||||||
# Fetch playlist data
|
# Fetch playlist data
|
||||||
selected_playlist_id = playlist_id[0]
|
selected_playlist_id = playlist_id[0]
|
||||||
try:
|
try:
|
||||||
debug(
|
|
||||||
f"load_music_table() | selected_playlist_id: {selected_playlist_id}"
|
|
||||||
)
|
|
||||||
with DBA.DBAccess() as db:
|
with DBA.DBAccess() as db:
|
||||||
data = db.query(
|
data = db.query(
|
||||||
"SELECT s.id, s.title, s.artist, s.album, s.track_number, s.genre, s.codec, s.album_date, s.filepath FROM song s JOIN song_playlist sp ON s.id = sp.song_id WHERE sp.playlist_id = ?",
|
"SELECT s.id, s.title, s.artist, s.album, s.track_number, s.genre, s.codec, s.album_date, s.filepath FROM song s JOIN song_playlist sp ON s.id = sp.song_id WHERE sp.playlist_id = ?",
|
||||||
@ -712,7 +710,10 @@ class MusicTable(QTableView):
|
|||||||
error(f"load_music_table() | Unhandled exception: {e}")
|
error(f"load_music_table() | Unhandled exception: {e}")
|
||||||
return
|
return
|
||||||
# Populate the model
|
# Populate the model
|
||||||
|
row_count: int = 0
|
||||||
|
total_time: int = 0 # total time of all songs in seconds
|
||||||
for row_data in data:
|
for row_data in data:
|
||||||
|
row_count += 1
|
||||||
id, *rest_of_data = row_data
|
id, *rest_of_data = row_data
|
||||||
# handle different datatypes
|
# handle different datatypes
|
||||||
items = []
|
items = []
|
||||||
@ -731,6 +732,7 @@ class MusicTable(QTableView):
|
|||||||
for item in items:
|
for item in items:
|
||||||
item.setData(id, Qt.ItemDataRole.UserRole)
|
item.setData(id, Qt.ItemDataRole.UserRole)
|
||||||
self.model2.layoutChanged.emit() # emits a signal that the view should be updated
|
self.model2.layoutChanged.emit() # emits a signal that the view should be updated
|
||||||
|
self.playlistStatsSignal.emit(f"Songs: {row_count} | Total time: {total_time}")
|
||||||
self.connect_data_changed()
|
self.connect_data_changed()
|
||||||
self.connect_layout_changed()
|
self.connect_layout_changed()
|
||||||
|
|
||||||
@ -739,7 +741,7 @@ class MusicTable(QTableView):
|
|||||||
Loads the header widths from the last application close.
|
Loads the header widths from the last application close.
|
||||||
"""
|
"""
|
||||||
table_view_column_widths = str(self.config["table"]["column_widths"]).split(",")
|
table_view_column_widths = str(self.config["table"]["column_widths"]).split(",")
|
||||||
debug(f'loaded header widths: {table_view_column_widths}')
|
debug(f"loaded header widths: {table_view_column_widths}")
|
||||||
if not isinstance(table_view_column_widths, list):
|
if not isinstance(table_view_column_widths, list):
|
||||||
for i in range(self.model2.columnCount() - 1):
|
for i in range(self.model2.columnCount() - 1):
|
||||||
self.setColumnWidth(i, int(table_view_column_widths[i]))
|
self.setColumnWidth(i, int(table_view_column_widths[i]))
|
||||||
@ -830,10 +832,6 @@ class MusicTable(QTableView):
|
|||||||
"""Returns the selected songs filepath"""
|
"""Returns the selected songs filepath"""
|
||||||
return self.selected_song_filepath
|
return self.selected_song_filepath
|
||||||
|
|
||||||
def get_selected_song_metadata(self) -> ID3 | dict:
|
|
||||||
"""Returns the selected song's ID3 tags"""
|
|
||||||
return get_id3_tags(self.selected_song_filepath)[0]
|
|
||||||
|
|
||||||
def get_selected_songs_db_ids(self) -> list:
|
def get_selected_songs_db_ids(self) -> list:
|
||||||
"""Returns a list of id's for the selected songs"""
|
"""Returns a list of id's for the selected songs"""
|
||||||
indexes = self.selectedIndexes()
|
indexes = self.selectedIndexes()
|
||||||
@ -854,6 +852,10 @@ class MusicTable(QTableView):
|
|||||||
"""Returns the currently playing song's ID3 tags"""
|
"""Returns the currently playing song's ID3 tags"""
|
||||||
return get_id3_tags(self.current_song_filepath)[0]
|
return get_id3_tags(self.current_song_filepath)[0]
|
||||||
|
|
||||||
|
def get_selected_song_metadata(self) -> ID3 | dict:
|
||||||
|
"""Returns the selected song's ID3 tags"""
|
||||||
|
return get_id3_tags(self.selected_song_filepath)[0]
|
||||||
|
|
||||||
def get_current_song_album_art(self) -> bytes:
|
def get_current_song_album_art(self) -> bytes:
|
||||||
"""Returns the APIC data (album art lol) for the currently playing song"""
|
"""Returns the APIC data (album art lol) for the currently playing song"""
|
||||||
return get_album_art(self.current_song_filepath)
|
return get_album_art(self.current_song_filepath)
|
||||||
|
|||||||
21
main.py
21
main.py
@ -37,7 +37,7 @@ from PyQt5.QtCore import (
|
|||||||
QThreadPool,
|
QThreadPool,
|
||||||
QRunnable,
|
QRunnable,
|
||||||
)
|
)
|
||||||
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QAudioProbe
|
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QAudioProbe, QMediaPlaylist
|
||||||
from PyQt5.QtGui import QClipboard, QCloseEvent, QFont, QPixmap, QResizeEvent
|
from PyQt5.QtGui import QClipboard, QCloseEvent, QFont, QPixmap, QResizeEvent
|
||||||
from utils import (
|
from utils import (
|
||||||
delete_album_art,
|
delete_album_art,
|
||||||
@ -162,7 +162,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
|||||||
|
|
||||||
# self.vLayoutAlbumArt.SetFixedSize()
|
# self.vLayoutAlbumArt.SetFixedSize()
|
||||||
self.status_bar = QStatusBar()
|
self.status_bar = QStatusBar()
|
||||||
self.permanent_status_label = QLabel("Status...")
|
self.permanent_status_label = QLabel("")
|
||||||
self.status_bar.addPermanentWidget(self.permanent_status_label)
|
self.status_bar.addPermanentWidget(self.permanent_status_label)
|
||||||
self.setStatusBar(self.status_bar)
|
self.setStatusBar(self.status_bar)
|
||||||
|
|
||||||
@ -174,6 +174,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
|||||||
# widget bits
|
# widget bits
|
||||||
self.album_art_scene: QGraphicsScene = QGraphicsScene()
|
self.album_art_scene: QGraphicsScene = QGraphicsScene()
|
||||||
self.player: QMediaPlayer = QMediaPlayer() # Audio player object
|
self.player: QMediaPlayer = QMediaPlayer() # Audio player object
|
||||||
|
self.playlist: QMediaPlaylist = QMediaPlaylist()
|
||||||
self.probe: QAudioProbe = QAudioProbe() # Gets audio buffer data
|
self.probe: QAudioProbe = QAudioProbe() # Gets audio buffer data
|
||||||
self.audio_visualizer: AudioVisualizer = AudioVisualizer(
|
self.audio_visualizer: AudioVisualizer = AudioVisualizer(
|
||||||
self.player, self.probe, self.PlotWidget
|
self.player, self.probe, self.PlotWidget
|
||||||
@ -248,13 +249,15 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
|||||||
|
|
||||||
## CONNECTIONS
|
## CONNECTIONS
|
||||||
# tableView
|
# tableView
|
||||||
# self.tableView.doubleClicked.connect(self.play_audio_file)
|
|
||||||
# self.tableView.enterKey.connect(self.play_audio_file)
|
|
||||||
self.tableView.playSignal.connect(self.play_audio_file)
|
self.tableView.playSignal.connect(self.play_audio_file)
|
||||||
self.tableView.playPauseSignal.connect(
|
self.tableView.playPauseSignal.connect(
|
||||||
self.on_play_clicked
|
self.on_play_clicked
|
||||||
) # Spacebar toggle play/pause signal
|
) # Spacebar toggle play/pause signal
|
||||||
self.tableView.handleProgressSignal.connect(self.handle_progress)
|
self.tableView.handleProgressSignal.connect(self.handle_progress)
|
||||||
|
self.tableView.playlistStatsSignal.connect(
|
||||||
|
self.set_permanent_status_bar_message
|
||||||
|
)
|
||||||
|
self.tableView.load_music_table()
|
||||||
|
|
||||||
# playlistTreeView
|
# playlistTreeView
|
||||||
self.playlistTreeView.playlistChoiceSignal.connect(
|
self.playlistTreeView.playlistChoiceSignal.connect(
|
||||||
@ -289,7 +292,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
|||||||
for i in range(self.tableView.model2.columnCount()):
|
for i in range(self.tableView.model2.columnCount()):
|
||||||
list_of_column_widths.append(str(self.tableView.columnWidth(i)))
|
list_of_column_widths.append(str(self.tableView.columnWidth(i)))
|
||||||
column_widths_as_string = ",".join(list_of_column_widths)
|
column_widths_as_string = ",".join(list_of_column_widths)
|
||||||
debug(f'saving column widths: {column_widths_as_string}')
|
debug(f"saving column widths: {column_widths_as_string}")
|
||||||
self.config["table"]["column_widths"] = column_widths_as_string
|
self.config["table"]["column_widths"] = column_widths_as_string
|
||||||
self.config["settings"]["volume"] = str(self.current_volume)
|
self.config["settings"]["volume"] = str(self.current_volume)
|
||||||
self.config["settings"]["window_size"] = (
|
self.config["settings"]["window_size"] = (
|
||||||
@ -301,7 +304,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
|||||||
with open(self.cfg_file, "w") as configfile:
|
with open(self.cfg_file, "w") as configfile:
|
||||||
self.config.write(configfile)
|
self.config.write(configfile)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
debug(f'wtf man {e}')
|
debug(f"wtf man {e}")
|
||||||
if a0 is not None:
|
if a0 is not None:
|
||||||
super().closeEvent(a0)
|
super().closeEvent(a0)
|
||||||
|
|
||||||
@ -327,7 +330,10 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
|||||||
self.speedLabel.setText("{:.2f}".format(rate / 50))
|
self.speedLabel.setText("{:.2f}".format(rate / 50))
|
||||||
|
|
||||||
def on_play_clicked(self) -> None:
|
def on_play_clicked(self) -> None:
|
||||||
"""Updates the Play & Pause buttons when clicked"""
|
"""
|
||||||
|
Plays & pauses the song
|
||||||
|
Updates the button icons
|
||||||
|
"""
|
||||||
pixmapi = QStyle.StandardPixmap.SP_MediaPlay
|
pixmapi = QStyle.StandardPixmap.SP_MediaPlay
|
||||||
play_icon = self.style().standardIcon(pixmapi) # type: ignore
|
play_icon = self.style().standardIcon(pixmapi) # type: ignore
|
||||||
pixmapi = QStyle.StandardPixmap.SP_MediaPause
|
pixmapi = QStyle.StandardPixmap.SP_MediaPause
|
||||||
@ -405,7 +411,6 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
|||||||
"""
|
"""
|
||||||
Sets the permanent message label in the status bar
|
Sets the permanent message label in the status bar
|
||||||
"""
|
"""
|
||||||
# what does this do?
|
|
||||||
self.permanent_status_label.setText(message)
|
self.permanent_status_label.setText(message)
|
||||||
|
|
||||||
def show_status_bar_message(self, message: str, timeout: int | None = None) -> None:
|
def show_status_bar_message(self, message: str, timeout: int | None = None) -> None:
|
||||||
|
|||||||
@ -3,7 +3,7 @@ from .convert_id3_timestamp_to_datetime import convert_id3_timestamp_to_datetime
|
|||||||
from .initialize_db import initialize_db
|
from .initialize_db import initialize_db
|
||||||
from .safe_get import safe_get
|
from .safe_get import safe_get
|
||||||
from .get_album_art import get_album_art
|
from .get_album_art import get_album_art
|
||||||
from .get_id3_tags import get_id3_tags
|
from .get_id3_tags import get_id3_tags, id3_remap
|
||||||
from .get_reorganize_vars import get_reorganize_vars
|
from .get_reorganize_vars import get_reorganize_vars
|
||||||
from .set_id3_tag import set_id3_tag
|
from .set_id3_tag import set_id3_tag
|
||||||
from .delete_song_id_from_database import delete_song_id_from_database
|
from .delete_song_id_from_database import delete_song_id_from_database
|
||||||
|
|||||||
@ -2,7 +2,7 @@ from PyQt5.QtWidgets import QMessageBox
|
|||||||
from mutagen.id3 import ID3
|
from mutagen.id3 import ID3
|
||||||
import DBA
|
import DBA
|
||||||
from logging import debug
|
from logging import debug
|
||||||
from utils import get_id3_tags, convert_id3_timestamp_to_datetime
|
from utils import get_id3_tags, convert_id3_timestamp_to_datetime, id3_remap
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from appdirs import user_config_dir
|
from appdirs import user_config_dir
|
||||||
@ -14,8 +14,12 @@ def add_files_to_database(files, progress_callback=None):
|
|||||||
Args:
|
Args:
|
||||||
files: list() of fully qualified paths to audio file(s)
|
files: list() of fully qualified paths to audio file(s)
|
||||||
progress_callback: emit data for user feedback
|
progress_callback: emit data for user feedback
|
||||||
Returns:
|
|
||||||
True on success, else False
|
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"})
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
config = ConfigParser()
|
config = ConfigParser()
|
||||||
cfg_file = (
|
cfg_file = (
|
||||||
@ -23,7 +27,7 @@ def add_files_to_database(files, progress_callback=None):
|
|||||||
)
|
)
|
||||||
config.read(cfg_file)
|
config.read(cfg_file)
|
||||||
if not files:
|
if not files:
|
||||||
return False, {'Failure': 'All operations failed in add_files_to_database()'}
|
return False, {"Failure": "All operations failed in add_files_to_database()"}
|
||||||
extensions = config.get("settings", "extensions").split(",")
|
extensions = config.get("settings", "extensions").split(",")
|
||||||
failed_dict = {}
|
failed_dict = {}
|
||||||
insert_data = [] # To store data for batch insert
|
insert_data = [] # To store data for batch insert
|
||||||
@ -33,55 +37,24 @@ def add_files_to_database(files, progress_callback=None):
|
|||||||
progress_callback.emit(filepath)
|
progress_callback.emit(filepath)
|
||||||
filename = filepath.split("/")[-1]
|
filename = filepath.split("/")[-1]
|
||||||
|
|
||||||
audio, details = get_id3_tags(filepath)
|
tags, details = get_id3_tags(filepath)
|
||||||
# print('got id3 tags')
|
if details:
|
||||||
# print(type(audio))
|
|
||||||
# print(audio)
|
|
||||||
if not isinstance(audio, ID3):
|
|
||||||
failed_dict[filepath] = details
|
failed_dict[filepath] = details
|
||||||
continue
|
continue
|
||||||
|
audio = id3_remap(tags)
|
||||||
try:
|
|
||||||
title = audio["TIT2"].text[0]
|
|
||||||
except KeyError:
|
|
||||||
title = filename
|
|
||||||
try:
|
|
||||||
artist = audio["TPE1"].text[0]
|
|
||||||
except KeyError:
|
|
||||||
artist = ""
|
|
||||||
try:
|
|
||||||
album = audio["TALB"].text[0]
|
|
||||||
except KeyError:
|
|
||||||
album = ""
|
|
||||||
try:
|
|
||||||
track_number = audio["TRCK"].text[0]
|
|
||||||
except KeyError:
|
|
||||||
track_number = None
|
|
||||||
try:
|
|
||||||
genre = audio["TCON"].text[0]
|
|
||||||
except KeyError:
|
|
||||||
genre = ""
|
|
||||||
try:
|
|
||||||
date = convert_id3_timestamp_to_datetime(audio["TDRC"].text[0])
|
|
||||||
except KeyError:
|
|
||||||
date = ""
|
|
||||||
try:
|
|
||||||
bitrate = audio["TBIT"].text[0]
|
|
||||||
except KeyError:
|
|
||||||
bitrate = ""
|
|
||||||
|
|
||||||
# Append data tuple to insert_data list
|
# Append data tuple to insert_data list
|
||||||
insert_data.append(
|
insert_data.append(
|
||||||
(
|
(
|
||||||
filepath,
|
filepath,
|
||||||
title,
|
audio["title"],
|
||||||
album,
|
audio["album"],
|
||||||
artist,
|
audio["artist"],
|
||||||
track_number,
|
audio["track_number"],
|
||||||
genre,
|
audio["genre"],
|
||||||
filename.split(".")[-1],
|
filename.split(".")[-1],
|
||||||
date,
|
audio["date"],
|
||||||
bitrate,
|
audio["bitrate"],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
# Check if batch size is reached
|
# Check if batch size is reached
|
||||||
|
|||||||
@ -1,31 +1,29 @@
|
|||||||
import os
|
import os
|
||||||
from logging import debug, error
|
from logging import debug, error
|
||||||
from mutagen.id3 import ID3
|
from mutagen.id3 import ID3
|
||||||
|
from mutagen.mp3 import MP3
|
||||||
|
from mutagen.flac import FLAC
|
||||||
from mutagen.id3._frames import TIT2
|
from mutagen.id3._frames import TIT2
|
||||||
from mutagen.id3._util import ID3NoHeaderError
|
from mutagen.id3._util import ID3NoHeaderError
|
||||||
|
|
||||||
def get_mp3_tags(filename: str) -> tuple[ID3 | dict, str]:
|
from utils import convert_id3_timestamp_to_datetime
|
||||||
|
|
||||||
|
|
||||||
|
def get_mp3_tags(filename: str) -> tuple[MP3 | ID3 | FLAC, str]:
|
||||||
"""Get ID3 tags for mp3 file"""
|
"""Get ID3 tags for mp3 file"""
|
||||||
try:
|
try:
|
||||||
# Open the MP3 file and read its content
|
# Open the MP3 file and read its content
|
||||||
audio = ID3(filename)
|
audio = MP3(filename)
|
||||||
except ID3NoHeaderError:
|
except ID3NoHeaderError:
|
||||||
audio = ID3()
|
audio = MP3()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if os.path.exists(filename):
|
if os.path.exists(filename):
|
||||||
audio.save(os.path.abspath(filename))
|
audio.save(os.path.abspath(filename))
|
||||||
|
|
||||||
# NOTE: If 'TIT2' tag is not set, we add it with a default value
|
|
||||||
# title = filename without extension
|
|
||||||
|
|
||||||
title = os.path.splitext(os.path.basename(filename))[0]
|
title = os.path.splitext(os.path.basename(filename))[0]
|
||||||
if "TIT2" in list(audio.keys()):
|
if "TIT2" not in list(audio.keys()):
|
||||||
audio.save()
|
# if title tag doesnt exist, create it - filename
|
||||||
return audio, ""
|
|
||||||
else:
|
|
||||||
# if title tag doesnt exist,
|
|
||||||
# create it and return
|
|
||||||
tit2_tag = TIT2(encoding=3, text=[title])
|
tit2_tag = TIT2(encoding=3, text=[title])
|
||||||
audio["TIT2"] = tit2_tag
|
audio["TIT2"] = tit2_tag
|
||||||
# Save the updated tags
|
# Save the updated tags
|
||||||
@ -33,9 +31,26 @@ def get_mp3_tags(filename: str) -> tuple[ID3 | dict, str]:
|
|||||||
return audio, ""
|
return audio, ""
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {}, f"Could not assign ID3 tag to file: {e}"
|
return MP3(), f"Could not assign ID3 tag to file: {e}"
|
||||||
|
|
||||||
def get_id3_tags(filename: str) -> tuple[ID3 | dict, str]:
|
|
||||||
|
def id3_remap(audio: MP3 | ID3 | FLAC) -> dict:
|
||||||
|
"""
|
||||||
|
Turns an ID3 dict into a normal dict that I the human can use.
|
||||||
|
with words...
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"title": audio["TIT2"].text[0],
|
||||||
|
"artist": audio["TPE1"].text[0],
|
||||||
|
"album": audio["TALB"].text[0],
|
||||||
|
"track_number": audio["TRCK"].text[0],
|
||||||
|
"genre": audio["TCON"].text[0],
|
||||||
|
"date": convert_id3_timestamp_to_datetime(audio["TDRC"].text[0]),
|
||||||
|
"bitrate": audio["TBIT"].text[0],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_id3_tags(filename: str) -> tuple[MP3 | ID3 | FLAC, str]:
|
||||||
"""
|
"""
|
||||||
Get the ID3 tags for an audio file
|
Get the ID3 tags for an audio file
|
||||||
Returns a tuple of:
|
Returns a tuple of:
|
||||||
@ -52,7 +67,5 @@ def get_id3_tags(filename: str) -> tuple[ID3 | dict, str]:
|
|||||||
if filename.endswith(".mp3"):
|
if filename.endswith(".mp3"):
|
||||||
tags, details = get_mp3_tags(filename)
|
tags, details = get_mp3_tags(filename)
|
||||||
else:
|
else:
|
||||||
tags, details = {}, "non mp3 file"
|
tags, details = ID3(), "non mp3 file"
|
||||||
return tags, details
|
return tags, details
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,7 @@ CREATE TABLE song(
|
|||||||
album varchar(255),
|
album varchar(255),
|
||||||
artist varchar(255),
|
artist varchar(255),
|
||||||
track_number integer,
|
track_number integer,
|
||||||
|
length_seconds integer,
|
||||||
genre varchar(255),
|
genre varchar(255),
|
||||||
codec varchar(15),
|
codec varchar(15),
|
||||||
album_date date,
|
album_date date,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user