Greatly improve database update
This commit is contained in:
parent
28396d136f
commit
a027cbe776
64
app/model.py
64
app/model.py
@ -14,6 +14,7 @@ from sqlalchemy import (
|
|||||||
String,
|
String,
|
||||||
func
|
func
|
||||||
)
|
)
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
|
from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
|
||||||
from sqlalchemy.orm import relationship, sessionmaker
|
from sqlalchemy.orm import relationship, sessionmaker
|
||||||
|
|
||||||
@ -106,6 +107,14 @@ class Playdates(Base):
|
|||||||
track.update_lastplayed()
|
track.update_lastplayed()
|
||||||
session.commit()
|
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):
|
class Playlists(Base):
|
||||||
"""
|
"""
|
||||||
@ -279,6 +288,20 @@ class PlaylistTracks(Base):
|
|||||||
session.add(plt)
|
session.add(plt)
|
||||||
session.commit()
|
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
|
@staticmethod
|
||||||
def move_track(session, from_playlist_id, row, to_playlist_id):
|
def move_track(session, from_playlist_id, row, to_playlist_id):
|
||||||
DEBUG(
|
DEBUG(
|
||||||
@ -458,6 +481,44 @@ class Tracks(Base):
|
|||||||
ERROR(f"get_track({id}): not found")
|
ERROR(f"get_track({id}): not found")
|
||||||
return None
|
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
|
@staticmethod
|
||||||
def search_titles(session, text):
|
def search_titles(session, text):
|
||||||
return (
|
return (
|
||||||
@ -473,3 +534,6 @@ class Tracks(Base):
|
|||||||
|
|
||||||
def update_lastplayed(self):
|
def update_lastplayed(self):
|
||||||
self.lastplayed = datetime.now()
|
self.lastplayed = datetime.now()
|
||||||
|
|
||||||
|
def update_path(self, newpath):
|
||||||
|
self.path = newpath
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import tempfile
|
|||||||
|
|
||||||
from config import Config
|
from config import Config
|
||||||
from log import DEBUG, INFO
|
from log import DEBUG, INFO
|
||||||
from model import Tracks, Session
|
from model import Tracks, Playdates, PlaylistTracks, Session
|
||||||
from mutagen.flac import FLAC
|
from mutagen.flac import FLAC
|
||||||
from pydub import AudioSegment, effects
|
from pydub import AudioSegment, effects
|
||||||
from tinytag import TinyTag
|
from tinytag import TinyTag
|
||||||
@ -22,7 +22,10 @@ def main():
|
|||||||
p = argparse.ArgumentParser()
|
p = argparse.ArgumentParser()
|
||||||
p.add_argument('-u', '--update',
|
p.add_argument('-u', '--update',
|
||||||
action="store_true", dest="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()
|
args = p.parse_args()
|
||||||
|
|
||||||
# Run as required
|
# Run as required
|
||||||
@ -30,25 +33,32 @@ def main():
|
|||||||
INFO("Updating database")
|
INFO("Updating database")
|
||||||
with Session() as session:
|
with Session() as session:
|
||||||
update_db(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")
|
INFO("Finished")
|
||||||
|
|
||||||
|
|
||||||
def create_track_from_file(session, path):
|
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.
|
Return track.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
track = Tracks.get_or_create(session, path)
|
track = Tracks.get_or_create(session, path)
|
||||||
tag = TinyTag.get(path)
|
t = get_music_info(path)
|
||||||
audio = get_audio_segment(path)
|
track.title = t['title']
|
||||||
|
track.artist = t['artist']
|
||||||
|
track.duration = int(round(
|
||||||
|
t['duration'], Config.MILLISECOND_SIGFIGS) * 1000)
|
||||||
|
|
||||||
track.title = tag.title
|
audio = get_audio_segment(path)
|
||||||
track.artist = tag.artist
|
|
||||||
track.duration = int(round(tag.duration,
|
|
||||||
Config.MILLISECOND_SIGFIGS) * 1000)
|
|
||||||
track.start_gap = leading_silence(audio)
|
track.start_gap = leading_silence(audio)
|
||||||
track.fade_at = round(fade_point(audio) / 1000,
|
track.fade_at = round(fade_point(audio) / 1000,
|
||||||
Config.MILLISECOND_SIGFIGS) * 1000
|
Config.MILLISECOND_SIGFIGS) * 1000
|
||||||
@ -92,6 +102,12 @@ def create_track_from_file(session, path):
|
|||||||
return track
|
return track
|
||||||
|
|
||||||
|
|
||||||
|
def full_update_db():
|
||||||
|
"Rescan all entries in database"
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def get_audio_segment(path):
|
def get_audio_segment(path):
|
||||||
try:
|
try:
|
||||||
if path.endswith('.mp3'):
|
if path.endswith('.mp3'):
|
||||||
@ -102,6 +118,21 @@ def get_audio_segment(path):
|
|||||||
return None
|
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,
|
def leading_silence(audio_segment, silence_threshold=Config.DBFS_SILENCE,
|
||||||
chunk_size=Config.AUDIO_SEGMENT_CHUNK_SIZE):
|
chunk_size=Config.AUDIO_SEGMENT_CHUNK_SIZE):
|
||||||
"""
|
"""
|
||||||
@ -187,14 +218,47 @@ def update_db(session):
|
|||||||
os_paths_list.append(path)
|
os_paths_list.append(path)
|
||||||
os_paths = set(os_paths_list)
|
os_paths = set(os_paths_list)
|
||||||
|
|
||||||
for path in list(db_paths - os_paths):
|
# If a track is moved, only the path will have changed.
|
||||||
# TODO
|
# For any files we have found whose paths are not in the database,
|
||||||
INFO(f"To remove from database: {path}")
|
# check to see whether the filename (basename) is present in the
|
||||||
|
# database:
|
||||||
|
|
||||||
for path in list(os_paths - db_paths):
|
for path in list(os_paths - db_paths):
|
||||||
# TODO
|
DEBUG(f"songdb.update_db: {path=} not in database")
|
||||||
INFO(f"Adding to dataabase: {path}")
|
# is filename in database?
|
||||||
create_track_from_file(session, path)
|
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():
|
if __name__ == '__main__' and '__file__' in globals():
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user