Greatly improve database update

This commit is contained in:
Keith Edmunds 2021-07-04 19:27:01 +01:00
parent 28396d136f
commit a027cbe776
2 changed files with 143 additions and 15 deletions

View File

@ -14,6 +14,7 @@ from sqlalchemy import (
String,
func
)
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
from sqlalchemy.orm import relationship, sessionmaker
@ -106,6 +107,14 @@ class Playdates(Base):
track.update_lastplayed()
session.commit()
@staticmethod
def last_played(session, track):
"Return datetime track last played or None"
return session.query(Playdates).filter(
(Playdates.track_id == track.id)
).order_by(Playdates.lastplayed.desc()).first()
class Playlists(Base):
"""
@ -279,6 +288,20 @@ class PlaylistTracks(Base):
session.add(plt)
session.commit()
@staticmethod
def get_playlists_containing_track_id(session, track_id):
playlists = []
playlist_ids = session.query(PlaylistTracks.playlist_id).filter(
PlaylistTracks.track_id == track_id).all()
for p in playlist_ids:
playlist = session.query(Playlists).filter(
Playlists.id == p[0]).first()
if playlist:
playlists.append(playlist)
return playlists
@staticmethod
def move_track(session, from_playlist_id, row, to_playlist_id):
DEBUG(
@ -458,6 +481,44 @@ class Tracks(Base):
ERROR(f"get_track({id}): not found")
return None
@staticmethod
def get_track_from_filename(session, filename):
"""
Return track if one and only one track in database has passed
filename (ie, basename of path). Return None if zero or more
than one track matches.
"""
DEBUG(f"Tracks.get_track_from_filename({filename=})")
try:
track = session.query(Tracks).filter(Tracks.path.ilike(
# TODO: filename separator is hardcoded here
f'%/{filename}')).one()
return track
except (NoResultFound, MultipleResultsFound):
return None
@staticmethod
def get_track_from_path(session, path):
"""
Return track with passee path, or None.
"""
DEBUG(f"Tracks.get_track_from_path({path=})")
return session.query(Tracks).filter(Tracks.path == path).first()
@staticmethod
def remove_path(session, path):
"Remove track with passed path from database"
DEBUG(f"Tracks.remove_path({path=})")
try:
session.query(Tracks).filter(Tracks.path == path).delete()
except IntegrityError as exception:
ERROR(f"Can't remove track with {path=} ({exception=})")
@staticmethod
def search_titles(session, text):
return (
@ -473,3 +534,6 @@ class Tracks(Base):
def update_lastplayed(self):
self.lastplayed = datetime.now()
def update_path(self, newpath):
self.path = newpath

View File

@ -7,7 +7,7 @@ import tempfile
from config import Config
from log import DEBUG, INFO
from model import Tracks, Session
from model import Tracks, Playdates, PlaylistTracks, Session
from mutagen.flac import FLAC
from pydub import AudioSegment, effects
from tinytag import TinyTag
@ -22,7 +22,10 @@ def main():
p = argparse.ArgumentParser()
p.add_argument('-u', '--update',
action="store_true", dest="update",
default=True, help="Update database")
default=False, help="Update database")
p.add_argument('-f', '--full-update',
action="store_true", dest="full_update",
default=False, help="Update database")
args = p.parse_args()
# Run as required
@ -30,25 +33,32 @@ def main():
INFO("Updating database")
with Session() as session:
update_db(session)
elif args.full_update:
INFO("Full update of database")
with Session() as session:
full_update_db(session)
else:
INFO("No action specified")
INFO("Finished")
def create_track_from_file(session, path):
"""
Create track in database from passed path.
Create track in database from passed path, or update database entry
if path already in database.
Return track.
"""
track = Tracks.get_or_create(session, path)
tag = TinyTag.get(path)
audio = get_audio_segment(path)
t = get_music_info(path)
track.title = t['title']
track.artist = t['artist']
track.duration = int(round(
t['duration'], Config.MILLISECOND_SIGFIGS) * 1000)
track.title = tag.title
track.artist = tag.artist
track.duration = int(round(tag.duration,
Config.MILLISECOND_SIGFIGS) * 1000)
audio = get_audio_segment(path)
track.start_gap = leading_silence(audio)
track.fade_at = round(fade_point(audio) / 1000,
Config.MILLISECOND_SIGFIGS) * 1000
@ -92,6 +102,12 @@ def create_track_from_file(session, path):
return track
def full_update_db():
"Rescan all entries in database"
pass
def get_audio_segment(path):
try:
if path.endswith('.mp3'):
@ -102,6 +118,21 @@ def get_audio_segment(path):
return None
def get_music_info(path):
"""
Return a dictionary of title, artist, duration-in-milliseconds and path.
"""
tag = TinyTag.get(path)
return dict(
title=tag.title,
artist=tag.artist,
duration=tag.duration,
path=path
)
def leading_silence(audio_segment, silence_threshold=Config.DBFS_SILENCE,
chunk_size=Config.AUDIO_SEGMENT_CHUNK_SIZE):
"""
@ -187,14 +218,47 @@ def update_db(session):
os_paths_list.append(path)
os_paths = set(os_paths_list)
for path in list(db_paths - os_paths):
# TODO
INFO(f"To remove from database: {path}")
# If a track is moved, only the path will have changed.
# For any files we have found whose paths are not in the database,
# check to see whether the filename (basename) is present in the
# database:
for path in list(os_paths - db_paths):
# TODO
INFO(f"Adding to dataabase: {path}")
create_track_from_file(session, path)
DEBUG(f"songdb.update_db: {path=} not in database")
# is filename in database?
track = Tracks.get_track_from_filename(session, os.path.basename(path))
if not track:
DEBUG(f"songdb.update_db: Adding to database: {path}")
create_track_from_file(session, path)
else:
# Check track info matches found track
t = get_music_info(path)
if t['artist'] == track.artist and t['title'] == track.title:
track.update_path(path)
else:
create_track_from_file(session, path)
# Refresh database paths
db_paths = set(Tracks.get_all_paths(session))
# Remote any tracks from database whose paths don't exist
for path in list(db_paths - os_paths):
track = Tracks.get_track_from_path(session, path)
DEBUG(f"songdb.update_db(): remove from database: {path=} {track=}")
played = Playdates.last_played(session, track)
playlists = PlaylistTracks.get_playlists_containing_track_id(
session, track.id)
if played:
INFO(
f"songdb.update_db: Can't remove {track.id=} ({track.path=}) "
f"as it's in playdates.id={played.id}"
)
elif playlists:
INFO(
f"songdb.update_db: Can't remove {track.id=} ({track.path=} "
f"as it's in playlists {[p.name for p in playlists]}"
)
else:
Tracks.remove_path(session, path)
if __name__ == '__main__' and '__file__' in globals():