Compare commits
No commits in common. "a0c074adada397576348406d5859d207b999ec1e" and "1bae79265da0f4ae1bc1b3512095d3040c5aedc1" have entirely different histories.
a0c074adad
...
1bae79265d
@ -6,12 +6,7 @@ from typing import List, Optional
|
|||||||
class Config(object):
|
class Config(object):
|
||||||
AUDACITY_COMMAND = "/usr/bin/audacity"
|
AUDACITY_COMMAND = "/usr/bin/audacity"
|
||||||
AUDIO_SEGMENT_CHUNK_SIZE = 10
|
AUDIO_SEGMENT_CHUNK_SIZE = 10
|
||||||
BITRATE_LOW_THRESHOLD = 192
|
|
||||||
BITRATE_OK_THRESHOLD = 300
|
|
||||||
CHECK_AUDACITY_AT_STARTUP = True
|
CHECK_AUDACITY_AT_STARTUP = True
|
||||||
COLOUR_BITRATE_LOW = "#ffcdd2"
|
|
||||||
COLOUR_BITRATE_MEDIUM = "#ffeb6f"
|
|
||||||
COLOUR_BITRATE_OK = "#dcedc8"
|
|
||||||
COLOUR_CURRENT_HEADER = "#d4edda"
|
COLOUR_CURRENT_HEADER = "#d4edda"
|
||||||
COLOUR_CURRENT_PLAYLIST = "#7eca8f"
|
COLOUR_CURRENT_PLAYLIST = "#7eca8f"
|
||||||
COLOUR_CURRENT_TAB = "#248f24"
|
COLOUR_CURRENT_TAB = "#248f24"
|
||||||
@ -29,7 +24,6 @@ class Config(object):
|
|||||||
COLOUR_WARNING_TIMER = "#ffc107"
|
COLOUR_WARNING_TIMER = "#ffc107"
|
||||||
COLUMN_NAME_ARTIST = "Artist"
|
COLUMN_NAME_ARTIST = "Artist"
|
||||||
COLUMN_NAME_AUTOPLAY = "A"
|
COLUMN_NAME_AUTOPLAY = "A"
|
||||||
COLUMN_NAME_BITRATE = "bps"
|
|
||||||
COLUMN_NAME_END_TIME = "End"
|
COLUMN_NAME_END_TIME = "End"
|
||||||
COLUMN_NAME_LAST_PLAYED = "Last played"
|
COLUMN_NAME_LAST_PLAYED = "Last played"
|
||||||
COLUMN_NAME_LEADING_SILENCE = "Gap"
|
COLUMN_NAME_LEADING_SILENCE = "Gap"
|
||||||
|
|||||||
@ -86,7 +86,6 @@ def get_tags(path: str) -> Dict[str, Union[str, int]]:
|
|||||||
return dict(
|
return dict(
|
||||||
title=tag.title,
|
title=tag.title,
|
||||||
artist=tag.artist,
|
artist=tag.artist,
|
||||||
bitrate=round(tag.bitrate),
|
|
||||||
duration=int(round(tag.duration, Config.MILLISECOND_SIGFIGS) * 1000),
|
duration=int(round(tag.duration, Config.MILLISECOND_SIGFIGS) * 1000),
|
||||||
path=path
|
path=path
|
||||||
)
|
)
|
||||||
|
|||||||
@ -508,9 +508,8 @@ class Tracks(Base):
|
|||||||
start_gap = Column(Integer, index=False)
|
start_gap = Column(Integer, index=False)
|
||||||
fade_at = Column(Integer, index=False)
|
fade_at = Column(Integer, index=False)
|
||||||
silence_at = Column(Integer, index=False)
|
silence_at = Column(Integer, index=False)
|
||||||
path = Column(String(2048), index=False, nullable=False, unique=True)
|
path = Column(String(2048), index=False, nullable=False)
|
||||||
mtime = Column(Float, index=True)
|
mtime = Column(Float, index=True)
|
||||||
bitrate = Column(Integer, nullable=True, default=None)
|
|
||||||
playlistrows = relationship("PlaylistRows", back_populates="track")
|
playlistrows = relationship("PlaylistRows", back_populates="track")
|
||||||
playlists = association_proxy("playlistrows", "playlist")
|
playlists = association_proxy("playlistrows", "playlist")
|
||||||
playdates = relationship("Playdates", back_populates="track")
|
playdates = relationship("Playdates", back_populates="track")
|
||||||
@ -547,11 +546,11 @@ class Tracks(Base):
|
|||||||
session.add(self)
|
session.add(self)
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
@classmethod
|
@staticmethod
|
||||||
def get_all(cls, session) -> List["Tracks"]:
|
def get_all_paths(session) -> List[str]:
|
||||||
"""Return a list of all tracks"""
|
"""Return a list of paths of all tracks"""
|
||||||
|
|
||||||
return session.execute(select(cls)).scalars().all()
|
return session.execute(select(Tracks.path)).scalars().all()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_or_create(cls, session: Session, path: str) -> "Tracks":
|
def get_or_create(cls, session: Session, path: str) -> "Tracks":
|
||||||
@ -560,8 +559,9 @@ class Tracks(Base):
|
|||||||
else created new track and return it
|
else created new track and return it
|
||||||
"""
|
"""
|
||||||
|
|
||||||
track = cls.get_by_path(session, path)
|
try:
|
||||||
if not track:
|
track = session.query(cls).filter(cls.path == path).one()
|
||||||
|
except NoResultFound:
|
||||||
track = Tracks(session, path)
|
track = Tracks(session, path)
|
||||||
|
|
||||||
return track
|
return track
|
||||||
@ -572,15 +572,12 @@ class Tracks(Base):
|
|||||||
Return track with passed path, or None.
|
Return track with passed path, or None.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
return (
|
||||||
return (
|
session.execute(
|
||||||
session.execute(
|
select(Tracks)
|
||||||
select(Tracks)
|
.where(Tracks.path == path)
|
||||||
.where(Tracks.path == path)
|
).scalar_one()
|
||||||
).scalar_one()
|
)
|
||||||
)
|
|
||||||
except NoResultFound:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def rescan(self, session: Session) -> None:
|
def rescan(self, session: Session) -> None:
|
||||||
"""
|
"""
|
||||||
@ -596,7 +593,7 @@ class Tracks(Base):
|
|||||||
Config.MILLISECOND_SIGFIGS) * 1000
|
Config.MILLISECOND_SIGFIGS) * 1000
|
||||||
self.start_gap = leading_silence(audio)
|
self.start_gap = leading_silence(audio)
|
||||||
session.add(self)
|
session.add(self)
|
||||||
session.commit()
|
session.flush()
|
||||||
|
|
||||||
# @staticmethod
|
# @staticmethod
|
||||||
# def remove_by_path(session: Session, path: str) -> None:
|
# def remove_by_path(session: Session, path: str) -> None:
|
||||||
|
|||||||
@ -41,7 +41,7 @@ from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist # type: ignore
|
|||||||
from ui.downloadcsv_ui import Ui_DateSelect # type: ignore
|
from ui.downloadcsv_ui import Ui_DateSelect # type: ignore
|
||||||
from config import Config
|
from config import Config
|
||||||
from ui.main_window_ui import Ui_MainWindow # type: ignore
|
from ui.main_window_ui import Ui_MainWindow # type: ignore
|
||||||
from utilities import create_track_from_file, check_db, update_bitrates
|
from utilities import create_track_from_file, check_db
|
||||||
|
|
||||||
|
|
||||||
class TrackData:
|
class TrackData:
|
||||||
@ -1134,9 +1134,6 @@ if __name__ == "__main__":
|
|||||||
p = argparse.ArgumentParser()
|
p = argparse.ArgumentParser()
|
||||||
# Only allow at most one option to be specified
|
# Only allow at most one option to be specified
|
||||||
group = p.add_mutually_exclusive_group()
|
group = p.add_mutually_exclusive_group()
|
||||||
group.add_argument('-b', '--bitrates',
|
|
||||||
action="store_true", dest="update_bitrates",
|
|
||||||
default=False, help="Update bitrates in database")
|
|
||||||
group.add_argument('-c', '--check-database',
|
group.add_argument('-c', '--check-database',
|
||||||
action="store_true", dest="check_db",
|
action="store_true", dest="check_db",
|
||||||
default=False, help="Check and report on database")
|
default=False, help="Check and report on database")
|
||||||
@ -1147,10 +1144,6 @@ if __name__ == "__main__":
|
|||||||
log.debug("Updating database")
|
log.debug("Updating database")
|
||||||
with Session() as session:
|
with Session() as session:
|
||||||
check_db(session)
|
check_db(session)
|
||||||
elif args.update_bitrates:
|
|
||||||
log.debug("Update bitrates")
|
|
||||||
with Session() as session:
|
|
||||||
update_bitrates(session)
|
|
||||||
else:
|
else:
|
||||||
# Normal run
|
# Normal run
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -77,8 +77,7 @@ columns["duration"] = Column(idx=4, heading=Config.COLUMN_NAME_LENGTH)
|
|||||||
columns["start_time"] = Column(idx=5, heading=Config.COLUMN_NAME_START_TIME)
|
columns["start_time"] = Column(idx=5, heading=Config.COLUMN_NAME_START_TIME)
|
||||||
columns["end_time"] = Column(idx=6, heading=Config.COLUMN_NAME_END_TIME)
|
columns["end_time"] = Column(idx=6, heading=Config.COLUMN_NAME_END_TIME)
|
||||||
columns["lastplayed"] = Column(idx=7, heading=Config.COLUMN_NAME_LAST_PLAYED)
|
columns["lastplayed"] = Column(idx=7, heading=Config.COLUMN_NAME_LAST_PLAYED)
|
||||||
columns["bitrate"] = Column(idx=8, heading=Config.COLUMN_NAME_BITRATE)
|
columns["row_notes"] = Column(idx=8, heading=Config.COLUMN_NAME_NOTES)
|
||||||
columns["row_notes"] = Column(idx=9, heading=Config.COLUMN_NAME_NOTES)
|
|
||||||
|
|
||||||
|
|
||||||
class NoSelectDelegate(QStyledItemDelegate):
|
class NoSelectDelegate(QStyledItemDelegate):
|
||||||
@ -96,7 +95,6 @@ class NoSelectDelegate(QStyledItemDelegate):
|
|||||||
return QPlainTextEdit(parent)
|
return QPlainTextEdit(parent)
|
||||||
return super().createEditor(parent, option, index)
|
return super().createEditor(parent, option, index)
|
||||||
|
|
||||||
|
|
||||||
class PlaylistTab(QTableWidget):
|
class PlaylistTab(QTableWidget):
|
||||||
# Qt.UserRoles
|
# Qt.UserRoles
|
||||||
ROW_FLAGS = Qt.UserRole
|
ROW_FLAGS = Qt.UserRole
|
||||||
@ -584,13 +582,6 @@ class PlaylistTab(QTableWidget):
|
|||||||
end_item = QTableWidgetItem()
|
end_item = QTableWidgetItem()
|
||||||
self.setItem(row, columns['end_time'].idx, end_item)
|
self.setItem(row, columns['end_time'].idx, end_item)
|
||||||
|
|
||||||
if row_data.track.bitrate:
|
|
||||||
bitrate = str(row_data.track.bitrate)
|
|
||||||
else:
|
|
||||||
bitrate = ""
|
|
||||||
bitrate_item = QTableWidgetItem(bitrate)
|
|
||||||
self.setItem(row, columns['bitrate'].idx, bitrate_item)
|
|
||||||
|
|
||||||
# As we have track info, any notes should be contained in
|
# As we have track info, any notes should be contained in
|
||||||
# the notes column
|
# the notes column
|
||||||
notes_item = QTableWidgetItem(row_data.note)
|
notes_item = QTableWidgetItem(row_data.note)
|
||||||
@ -983,9 +974,6 @@ class PlaylistTab(QTableWidget):
|
|||||||
track = session.get(Tracks, track_id)
|
track = session.get(Tracks, track_id)
|
||||||
|
|
||||||
if track:
|
if track:
|
||||||
# Reset colour in case it was current/next/unplayable
|
|
||||||
self._set_row_colour(row, None)
|
|
||||||
|
|
||||||
# Render unplayable tracks in correct colour
|
# Render unplayable tracks in correct colour
|
||||||
if not file_is_readable(track.path):
|
if not file_is_readable(track.path):
|
||||||
self._set_row_colour(row, QColor(Config.COLOUR_UNREADABLE))
|
self._set_row_colour(row, QColor(Config.COLOUR_UNREADABLE))
|
||||||
@ -1004,17 +992,6 @@ class PlaylistTab(QTableWidget):
|
|||||||
# Ensure content is visible by wrapping cells
|
# Ensure content is visible by wrapping cells
|
||||||
self.resizeRowToContents(row)
|
self.resizeRowToContents(row)
|
||||||
|
|
||||||
# Highlight low bitrates
|
|
||||||
if track.bitrate:
|
|
||||||
if track.bitrate < Config.BITRATE_LOW_THRESHOLD:
|
|
||||||
cell_colour = Config.COLOUR_BITRATE_LOW
|
|
||||||
elif track.bitrate < Config.BITRATE_OK_THRESHOLD:
|
|
||||||
cell_colour = Config.COLOUR_BITRATE_MEDIUM
|
|
||||||
else:
|
|
||||||
cell_colour = Config.COLOUR_BITRATE_OK
|
|
||||||
brush = QBrush(QColor(cell_colour))
|
|
||||||
self.item(row, columns['bitrate'].idx).setBackground(brush)
|
|
||||||
|
|
||||||
# Render playing track
|
# Render playing track
|
||||||
if row == current_row:
|
if row == current_row:
|
||||||
# Set start time
|
# Set start time
|
||||||
@ -1060,6 +1037,10 @@ class PlaylistTab(QTableWidget):
|
|||||||
self._set_row_bold(row)
|
self._set_row_bold(row)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# This is a track row other than next or current
|
||||||
|
# Reset colour in case it was current/next
|
||||||
|
self._set_row_colour(row, None)
|
||||||
|
|
||||||
if row in played:
|
if row in played:
|
||||||
# Played today, so update last played column
|
# Played today, so update last played column
|
||||||
self.item(row, columns['lastplayed'].idx).setText(
|
self.item(row, columns['lastplayed'].idx).setText(
|
||||||
@ -1441,7 +1422,6 @@ class PlaylistTab(QTableWidget):
|
|||||||
f"Artist: {track.artist}\n"
|
f"Artist: {track.artist}\n"
|
||||||
f"Track ID: {track.id}\n"
|
f"Track ID: {track.id}\n"
|
||||||
f"Track duration: {ms_to_mmss(track.duration)}\n"
|
f"Track duration: {ms_to_mmss(track.duration)}\n"
|
||||||
f"Track bitrate: {track.bitrate}\n"
|
|
||||||
f"Track fade at: {ms_to_mmss(track.fade_at)}\n"
|
f"Track fade at: {ms_to_mmss(track.fade_at)}\n"
|
||||||
f"Track silence at: {ms_to_mmss(track.silence_at)}"
|
f"Track silence at: {ms_to_mmss(track.silence_at)}"
|
||||||
"\n\n"
|
"\n\n"
|
||||||
|
|||||||
@ -1,54 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Script to manage renaming existing files in given directory and
|
|
||||||
# propagating that change to database. Typical usage: renaming files
|
|
||||||
# from 'title.mp3' to title - artist.mp3'
|
|
||||||
#
|
|
||||||
# Actions:
|
|
||||||
#
|
|
||||||
# - record all filenames and inode numbers
|
|
||||||
# - external: rename the files
|
|
||||||
# - update records with new filenames for each inode number
|
|
||||||
# - update external database with new paths
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sqlite3
|
|
||||||
|
|
||||||
PHASE = 2
|
|
||||||
|
|
||||||
# Check file of same name exists in parent directory
|
|
||||||
source_dir = '/home/kae/tmp/Singles' # os.getcwd()
|
|
||||||
db = "/home/kae/tmp/singles.sqlite"
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
with sqlite3.connect(db) as connection:
|
|
||||||
cursor = connection.cursor()
|
|
||||||
if PHASE == 1:
|
|
||||||
cursor.execute(
|
|
||||||
"CREATE TABLE IF NOT EXISTS mp3s "
|
|
||||||
"(inode INTEGER, oldname TEXT, newname TEXT)"
|
|
||||||
)
|
|
||||||
|
|
||||||
for fname in os.listdir(source_dir):
|
|
||||||
fullpath = os.path.join(source_dir, fname)
|
|
||||||
inode = os.stat(fullpath).st_ino
|
|
||||||
sql = f'INSERT INTO mp3s VALUES ({inode}, "{fname}", "")'
|
|
||||||
cursor.execute(sql)
|
|
||||||
|
|
||||||
if PHASE == 2:
|
|
||||||
for fname in os.listdir(source_dir):
|
|
||||||
fullpath = os.path.join(source_dir, fname)
|
|
||||||
inode = os.stat(fullpath).st_ino
|
|
||||||
sql = (
|
|
||||||
f'UPDATE mp3s SET newname = "{fname}" WHERE inode={inode}'
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
cursor.execute(sql)
|
|
||||||
except sqlite3.OperationalError:
|
|
||||||
print(f"Error with {inode} -> {fname}")
|
|
||||||
|
|
||||||
cursor.close()
|
|
||||||
|
|
||||||
|
|
||||||
main()
|
|
||||||
@ -1,247 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Script to replace existing files in parent directory. Typical usage:
|
|
||||||
# the current directory contains a "better" version of the file than the
|
|
||||||
# parent (eg, bettet bitrate).
|
|
||||||
#
|
|
||||||
# Actions:
|
|
||||||
#
|
|
||||||
# - check that the same filename is present in the parent directory
|
|
||||||
# - check that the artist and title tags are the same
|
|
||||||
# - append ".bak" to the version in the parent directory
|
|
||||||
# - move file to parent directory
|
|
||||||
# - normalise file
|
|
||||||
# - update duration, start_gap, fade_at, silence_at, mtime in database
|
|
||||||
|
|
||||||
import glob
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from helpers import (
|
|
||||||
fade_point,
|
|
||||||
get_audio_segment,
|
|
||||||
get_tags,
|
|
||||||
leading_silence,
|
|
||||||
trailing_silence,
|
|
||||||
)
|
|
||||||
|
|
||||||
from models import Tracks
|
|
||||||
from dbconfig import Session
|
|
||||||
from thefuzz import process # type: ignore
|
|
||||||
from sqlalchemy.exc import IntegrityError
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
# ###################### SETTINGS #########################
|
|
||||||
process_multiple_matches = False
|
|
||||||
do_processing = False
|
|
||||||
process_no_matches = False
|
|
||||||
# #########################################################
|
|
||||||
|
|
||||||
|
|
||||||
def insensitive_glob(pattern):
|
|
||||||
def either(c):
|
|
||||||
return '[%s%s]' % (c.lower(), c.upper()) if c.isalpha() else c
|
|
||||||
return glob.glob(''.join(map(either, pattern)))
|
|
||||||
|
|
||||||
|
|
||||||
# Check file of same name exists in parent directory
|
|
||||||
source_dir = '/home/kae/music/Singles/tmp' # os.getcwd()
|
|
||||||
parent_dir = os.path.dirname(source_dir)
|
|
||||||
assert source_dir != parent_dir
|
|
||||||
|
|
||||||
name_and_tags: List[str] = []
|
|
||||||
name_not_tags: List[str] = []
|
|
||||||
tags_not_name: List[str] = []
|
|
||||||
multiple_similar: List[str] = []
|
|
||||||
no_match: List[str] = []
|
|
||||||
possibles: List[str] = []
|
|
||||||
|
|
||||||
print(f"{source_dir=}, {parent_dir=}")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
if 'musicmuster_prod' not in os.environ.get('MM_DB'):
|
|
||||||
response = input("Not on production database - c to continue: ")
|
|
||||||
if response != "c":
|
|
||||||
sys.exit(0)
|
|
||||||
tracks = os.listdir(parent_dir)
|
|
||||||
for fname in os.listdir(source_dir):
|
|
||||||
parent_file = os.path.join(parent_dir, fname)
|
|
||||||
new_file = os.path.join(source_dir, fname)
|
|
||||||
us = get_tags(new_file)
|
|
||||||
us_t = us['title']
|
|
||||||
us_a = us['artist']
|
|
||||||
|
|
||||||
if os.path.exists(parent_file):
|
|
||||||
# File exists, check tags
|
|
||||||
p = get_tags(parent_file)
|
|
||||||
p_t = p['title']
|
|
||||||
p_a = p['artist']
|
|
||||||
if (
|
|
||||||
(str(p_t).lower() != str(us_t).lower()) or
|
|
||||||
(str(p_a).lower() != str(us_a).lower())
|
|
||||||
):
|
|
||||||
name_not_tags.append(
|
|
||||||
f" {fname=}, {p_t} → {us_t}, {p_a} → {us_a}")
|
|
||||||
process_track(new_file, parent_file, us_t, us_a)
|
|
||||||
continue
|
|
||||||
name_and_tags.append(new_file)
|
|
||||||
process_track(new_file, parent_file, us_t, us_a)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Try to find a near match
|
|
||||||
stem = fname.split(".")[0]
|
|
||||||
matches = insensitive_glob(os.path.join(parent_dir, stem) + '*')
|
|
||||||
match_count = len(matches)
|
|
||||||
if match_count == 0:
|
|
||||||
if process_no_matches:
|
|
||||||
print(f"\n file={fname}\n title={us_t}\n artist={us_a}\n")
|
|
||||||
# Try fuzzy search
|
|
||||||
d = {}
|
|
||||||
while True:
|
|
||||||
for i, match in enumerate(
|
|
||||||
[a[0] for a in process.extract(fname, tracks, limit=5)]
|
|
||||||
):
|
|
||||||
d[i] = match
|
|
||||||
for k, v in d.items():
|
|
||||||
print(f"{k}: {v}")
|
|
||||||
data = input("pick one, return to quit: ")
|
|
||||||
if data == "":
|
|
||||||
no_match.append(f"{fname}, {us_t=}, {us_a=}")
|
|
||||||
break
|
|
||||||
try:
|
|
||||||
key = int(data)
|
|
||||||
except ValueError:
|
|
||||||
continue
|
|
||||||
if key in d:
|
|
||||||
old_file = os.path.join(parent_dir, d[key])
|
|
||||||
oldtags = get_tags(old_file)
|
|
||||||
old_title = oldtags['title']
|
|
||||||
old_artist = oldtags['artist']
|
|
||||||
print()
|
|
||||||
print(f" Title tag will change {old_title} → {us_t}")
|
|
||||||
print(f" Artist tag will change {old_artist} → {us_a}")
|
|
||||||
print()
|
|
||||||
data = input("Go ahead (y to accept)? ")
|
|
||||||
if data == "y":
|
|
||||||
process_track(new_file, old_file, us_t, us_a)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
no_match.append(f"{fname}, {us_t=}, {us_a=}")
|
|
||||||
continue
|
|
||||||
no_match.append(f"{fname}, {us_t=}, {us_a=}")
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
no_match.append(f"{fname}, {us_t=}, {us_a=}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
if match_count > 1:
|
|
||||||
multiple_similar.append(fname + "\n " + "\n ".join(matches))
|
|
||||||
if match_count <= 26 and process_multiple_matches:
|
|
||||||
print(f"\n file={fname}\n title={us_t}\n artist={us_a}\n")
|
|
||||||
d = {}
|
|
||||||
while True:
|
|
||||||
for i, match in enumerate(matches):
|
|
||||||
d[i] = match
|
|
||||||
for k, v in d.items():
|
|
||||||
print(f"{k}: {v}")
|
|
||||||
data = input("pick one, return to quit: ")
|
|
||||||
if data == "":
|
|
||||||
break
|
|
||||||
try:
|
|
||||||
key = int(data)
|
|
||||||
except ValueError:
|
|
||||||
continue
|
|
||||||
if key in d:
|
|
||||||
dst = d[key]
|
|
||||||
process_track(new_file, dst, us_t, us_a)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
continue
|
|
||||||
continue # from break after testing for "" in data
|
|
||||||
# One match, check tags
|
|
||||||
sim_name = matches[0]
|
|
||||||
p = get_tags(sim_name)
|
|
||||||
p_t = p['title']
|
|
||||||
p_a = p['artist']
|
|
||||||
if (
|
|
||||||
(str(p_t).lower() != str(us_t).lower()) or
|
|
||||||
(str(p_a).lower() != str(us_a).lower())
|
|
||||||
):
|
|
||||||
possibles.append(
|
|
||||||
f"File: {os.path.basename(sim_name)} → {fname}"
|
|
||||||
f"\n {p_t} → {us_t}\n {p_a} → {us_a}"
|
|
||||||
)
|
|
||||||
process_track(new_file, sim_name, us_t, us_a)
|
|
||||||
continue
|
|
||||||
tags_not_name.append(f"Rename {os.path.basename(sim_name)} → {fname}")
|
|
||||||
process_track(new_file, sim_name, us_t, us_a)
|
|
||||||
|
|
||||||
print(f"Name and tags match ({len(name_and_tags)}):")
|
|
||||||
# print(" \n".join(name_and_tags))
|
|
||||||
# print()
|
|
||||||
|
|
||||||
print(f"Name but not tags match ({len(name_not_tags)}):")
|
|
||||||
print(" \n".join(name_not_tags))
|
|
||||||
print()
|
|
||||||
|
|
||||||
print(f"Tags but not name match ({len(tags_not_name)}):")
|
|
||||||
# print(" \n".join(tags_not_name))
|
|
||||||
# print()
|
|
||||||
|
|
||||||
print(f"Multiple similar names ({len(multiple_similar)}):")
|
|
||||||
print(" \n".join(multiple_similar))
|
|
||||||
print()
|
|
||||||
|
|
||||||
print(f"Possibles: ({len(possibles)}):")
|
|
||||||
print(" \n".join(possibles))
|
|
||||||
print()
|
|
||||||
|
|
||||||
print(f"No match ({len(no_match)}):")
|
|
||||||
print(" \n".join(no_match))
|
|
||||||
print()
|
|
||||||
|
|
||||||
|
|
||||||
def process_track(src, dst, title, artist):
|
|
||||||
|
|
||||||
new_path = os.path.join(os.path.dirname(dst), os.path.basename(src))
|
|
||||||
print(
|
|
||||||
f"process_track:\n {src=}\n {new_path=}\n "
|
|
||||||
f"{dst=}\n {title=}, {artist=}\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not do_processing:
|
|
||||||
return
|
|
||||||
|
|
||||||
with Session() as session:
|
|
||||||
track = Tracks.get_by_path(session, dst)
|
|
||||||
if track:
|
|
||||||
track.title = title
|
|
||||||
track.artist = artist
|
|
||||||
track.path = new_path
|
|
||||||
try:
|
|
||||||
session.commit()
|
|
||||||
except IntegrityError:
|
|
||||||
# https://jira.mariadb.org/browse/MDEV-29345 workaround
|
|
||||||
session.rollback()
|
|
||||||
track.title = title
|
|
||||||
track.artist = artist
|
|
||||||
track.path = "DUMMY"
|
|
||||||
session.commit()
|
|
||||||
track.path = new_path
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
print(f"os.unlink({dst}")
|
|
||||||
print(f"shutil.move({src}, {new_path}")
|
|
||||||
|
|
||||||
os.unlink(dst)
|
|
||||||
shutil.move(src, new_path)
|
|
||||||
track = Tracks.get_by_path(session, new_path)
|
|
||||||
if track:
|
|
||||||
track.rescan(session)
|
|
||||||
else:
|
|
||||||
print(f"Can't find copied track {src=}, {dst=}")
|
|
||||||
|
|
||||||
|
|
||||||
main()
|
|
||||||
@ -43,7 +43,6 @@ def create_track_from_file(session, path, normalise=None, tags=None):
|
|||||||
track.silence_at = round(trailing_silence(audio) / 1000,
|
track.silence_at = round(trailing_silence(audio) / 1000,
|
||||||
Config.MILLISECOND_SIGFIGS) * 1000
|
Config.MILLISECOND_SIGFIGS) * 1000
|
||||||
track.mtime = os.path.getmtime(path)
|
track.mtime = os.path.getmtime(path)
|
||||||
track.bitrate = t['bitrate']
|
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
if normalise or normalise is None and Config.NORMALISE_ON_IMPORT:
|
if normalise or normalise is None and Config.NORMALISE_ON_IMPORT:
|
||||||
@ -108,7 +107,7 @@ def check_db(session):
|
|||||||
Check all paths in database exist
|
Check all paths in database exist
|
||||||
"""
|
"""
|
||||||
|
|
||||||
db_paths = set([a.path for a in Tracks.get_all(session)])
|
db_paths = set(Tracks.get_all_paths(session))
|
||||||
|
|
||||||
os_paths_list = []
|
os_paths_list = []
|
||||||
for root, dirs, files in os.walk(Config.ROOT):
|
for root, dirs, files in os.walk(Config.ROOT):
|
||||||
@ -163,19 +162,6 @@ def check_db(session):
|
|||||||
print("There were more paths than listed that were not found")
|
print("There were more paths than listed that were not found")
|
||||||
|
|
||||||
|
|
||||||
def update_bitrates(session):
|
|
||||||
"""
|
|
||||||
Update bitrates on all tracks in database
|
|
||||||
"""
|
|
||||||
|
|
||||||
for track in Tracks.get_all(session):
|
|
||||||
try:
|
|
||||||
t = get_tags(track.path)
|
|
||||||
track.bitrate = t["bitrate"]
|
|
||||||
except FileNotFoundError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
|
|
||||||
# # Spike
|
# # Spike
|
||||||
# #
|
# #
|
||||||
# # # Manage tracks listed in database but where path is invalid
|
# # # Manage tracks listed in database but where path is invalid
|
||||||
|
|||||||
@ -1,28 +0,0 @@
|
|||||||
"""Add column for bitrate in Tracks
|
|
||||||
|
|
||||||
Revision ID: ed3100326c38
|
|
||||||
Revises: fe2e127b3332
|
|
||||||
Create Date: 2022-08-22 16:16:42.181848
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = 'ed3100326c38'
|
|
||||||
down_revision = 'fe2e127b3332'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.add_column('tracks', sa.Column('bitrate', sa.Integer(), nullable=True))
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.drop_column('tracks', 'bitrate')
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
"""Don't allow duplicate track paths
|
|
||||||
|
|
||||||
Revision ID: fe2e127b3332
|
|
||||||
Revises: 0c604bf490f8
|
|
||||||
Create Date: 2022-08-21 19:46:35.768659
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = 'fe2e127b3332'
|
|
||||||
down_revision = '0c604bf490f8'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.create_unique_constraint(None, 'tracks', ['path'])
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.drop_constraint(None, 'tracks', type_='unique')
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
32
poetry.lock
generated
32
poetry.lock
generated
@ -366,14 +366,6 @@ category = "dev"
|
|||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8,<4.0"
|
python-versions = ">=3.8,<4.0"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pyfzf"
|
|
||||||
version = "0.3.1"
|
|
||||||
description = "Python wrapper for junegunn's fuzzyfinder (fzf)"
|
|
||||||
category = "main"
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pygments"
|
name = "pygments"
|
||||||
version = "2.11.2"
|
version = "2.11.2"
|
||||||
@ -489,14 +481,6 @@ pytest = ">=3.0.0"
|
|||||||
dev = ["pre-commit", "tox"]
|
dev = ["pre-commit", "tox"]
|
||||||
doc = ["sphinx", "sphinx-rtd-theme"]
|
doc = ["sphinx", "sphinx-rtd-theme"]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "python-levenshtein"
|
|
||||||
version = "0.12.2"
|
|
||||||
description = "Python extension for computing string edit distances and similarities."
|
|
||||||
category = "main"
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-slugify"
|
name = "python-slugify"
|
||||||
version = "6.1.2"
|
version = "6.1.2"
|
||||||
@ -595,17 +579,6 @@ category = "main"
|
|||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "thefuzz"
|
|
||||||
version = "0.19.0"
|
|
||||||
description = "Fuzzy string matching in python"
|
|
||||||
category = "main"
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
speedup = ["python-levenshtein (>=0.12)"]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinytag"
|
name = "tinytag"
|
||||||
version = "1.8.1"
|
version = "1.8.1"
|
||||||
@ -671,7 +644,7 @@ python-versions = "*"
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.9"
|
python-versions = "^3.9"
|
||||||
content-hash = "b181eb743e8b6c9cb7e03c4db0bcef425fe410d2ec3c4c801ce20e448a26f166"
|
content-hash = "9b4cf9915bf250afd948596a6ba82794f82abf6a6d4891bc51845409632c15fb"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
alembic = [
|
alembic = [
|
||||||
@ -944,7 +917,6 @@ pydub = [
|
|||||||
{file = "pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f"},
|
{file = "pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f"},
|
||||||
]
|
]
|
||||||
pydub-stubs = []
|
pydub-stubs = []
|
||||||
pyfzf = []
|
|
||||||
pygments = [
|
pygments = [
|
||||||
{file = "Pygments-2.11.2-py3-none-any.whl", hash = "sha256:44238f1b60a76d78fc8ca0528ee429702aae011c265fe6a8dd8b63049ae41c65"},
|
{file = "Pygments-2.11.2-py3-none-any.whl", hash = "sha256:44238f1b60a76d78fc8ca0528ee429702aae011c265fe6a8dd8b63049ae41c65"},
|
||||||
{file = "Pygments-2.11.2.tar.gz", hash = "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a"},
|
{file = "Pygments-2.11.2.tar.gz", hash = "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a"},
|
||||||
@ -1014,7 +986,6 @@ pytest-qt = [
|
|||||||
{file = "pytest-qt-4.0.2.tar.gz", hash = "sha256:dfc5240dec7eb43b76bcb5f9a87eecae6ef83592af49f3af5f1d5d093acaa93e"},
|
{file = "pytest-qt-4.0.2.tar.gz", hash = "sha256:dfc5240dec7eb43b76bcb5f9a87eecae6ef83592af49f3af5f1d5d093acaa93e"},
|
||||||
{file = "pytest_qt-4.0.2-py2.py3-none-any.whl", hash = "sha256:e03847ac02a890ccaac0fde1748855b9dce425aceba62005c6cfced6cf7d5456"},
|
{file = "pytest_qt-4.0.2-py2.py3-none-any.whl", hash = "sha256:e03847ac02a890ccaac0fde1748855b9dce425aceba62005c6cfced6cf7d5456"},
|
||||||
]
|
]
|
||||||
python-levenshtein = []
|
|
||||||
python-slugify = []
|
python-slugify = []
|
||||||
python-vlc = [
|
python-vlc = [
|
||||||
{file = "python-vlc-3.0.16120.tar.gz", hash = "sha256:92f98fee088f72bd6d063b3b3312d0bd29b37e7ad65ddeb3a7303320300c2807"},
|
{file = "python-vlc-3.0.16120.tar.gz", hash = "sha256:92f98fee088f72bd6d063b3b3312d0bd29b37e7ad65ddeb3a7303320300c2807"},
|
||||||
@ -1034,7 +1005,6 @@ stack-data = [
|
|||||||
{file = "stack_data-0.2.0.tar.gz", hash = "sha256:45692d41bd633a9503a5195552df22b583caf16f0b27c4e58c98d88c8b648e12"},
|
{file = "stack_data-0.2.0.tar.gz", hash = "sha256:45692d41bd633a9503a5195552df22b583caf16f0b27c4e58c98d88c8b648e12"},
|
||||||
]
|
]
|
||||||
text-unidecode = []
|
text-unidecode = []
|
||||||
thefuzz = []
|
|
||||||
tinytag = [
|
tinytag = [
|
||||||
{file = "tinytag-1.8.1.tar.gz", hash = "sha256:363ab3107831a5598b68aaa061aba915fb1c7b4254d770232e65d5db8487636d"},
|
{file = "tinytag-1.8.1.tar.gz", hash = "sha256:363ab3107831a5598b68aaa061aba915fb1c7b4254d770232e65d5db8487636d"},
|
||||||
]
|
]
|
||||||
|
|||||||
@ -19,9 +19,6 @@ pydub = "^0.25.1"
|
|||||||
PyQt5-sip = "^12.9.1"
|
PyQt5-sip = "^12.9.1"
|
||||||
types-psutil = "^5.8.22"
|
types-psutil = "^5.8.22"
|
||||||
python-slugify = "^6.1.2"
|
python-slugify = "^6.1.2"
|
||||||
thefuzz = "^0.19.0"
|
|
||||||
python-Levenshtein = "^0.12.2"
|
|
||||||
pyfzf = "^0.3.1"
|
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
ipdb = "^0.13.9"
|
ipdb = "^0.13.9"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user