Compare commits
No commits in common. "ce213221175776f84f47577da4289749d2dc9afe" and "42092d3d392c34350260729d9241162eaaf92b8b" have entirely different histories.
ce21322117
...
42092d3d39
@ -49,7 +49,6 @@ class Config(object):
|
|||||||
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
|
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
|
||||||
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS') is not None
|
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS') is not None
|
||||||
MAX_INFO_TABS = 3
|
MAX_INFO_TABS = 3
|
||||||
MAX_MISSING_FILES_TO_REPORT = 10
|
|
||||||
MILLISECOND_SIGFIGS = 0
|
MILLISECOND_SIGFIGS = 0
|
||||||
MYSQL_CONNECT = os.environ.get('MYSQL_CONNECT') or "mysql+mysqldb://musicmuster:musicmuster@localhost/musicmuster_v2" # noqa E501
|
MYSQL_CONNECT = os.environ.get('MYSQL_CONNECT') or "mysql+mysqldb://musicmuster:musicmuster@localhost/musicmuster_v2" # noqa E501
|
||||||
NORMALISE_ON_IMPORT = True
|
NORMALISE_ON_IMPORT = True
|
||||||
|
|||||||
@ -46,7 +46,7 @@ elif MM_ENV == 'DEVELOPMENT':
|
|||||||
else:
|
else:
|
||||||
raise ValueError(f"Unknown MusicMuster environment: {MM_ENV=}")
|
raise ValueError(f"Unknown MusicMuster environment: {MM_ENV=}")
|
||||||
|
|
||||||
DEBUG(f"Using {dbname} database")
|
DEBUG(f"Using {dbname} database", True)
|
||||||
MYSQL_CONNECT = f"mysql+mysqldb://{dbuser}:{dbpw}@{dbhost}/{dbname}"
|
MYSQL_CONNECT = f"mysql+mysqldb://{dbuser}:{dbpw}@{dbhost}/{dbname}"
|
||||||
|
|
||||||
engine = sqlalchemy.create_engine(
|
engine = sqlalchemy.create_engine(
|
||||||
@ -64,8 +64,9 @@ def Session():
|
|||||||
function = frame.function
|
function = frame.function
|
||||||
lineno = frame.lineno
|
lineno = frame.lineno
|
||||||
Session = scoped_session(sessionmaker(bind=engine))
|
Session = scoped_session(sessionmaker(bind=engine))
|
||||||
DEBUG(f"Session acquired, {file=}, {function=}, {lineno=}, {Session=}")
|
DEBUG(f"Session acquired, {file=}, {function=}, {lineno=}, {Session=}",
|
||||||
|
True)
|
||||||
yield Session
|
yield Session
|
||||||
DEBUG(" Session released")
|
DEBUG(" Session released", True)
|
||||||
Session.commit()
|
Session.commit()
|
||||||
Session.close()
|
Session.close()
|
||||||
|
|||||||
@ -59,7 +59,7 @@ def log_uncaught_exceptions(ex_cls, ex, tb):
|
|||||||
sys.excepthook = log_uncaught_exceptions
|
sys.excepthook = log_uncaught_exceptions
|
||||||
|
|
||||||
|
|
||||||
def DEBUG(msg: str, force_stderr: bool = False) -> None:
|
def DEBUG(msg: str, force_stderr: bool = True) -> None:
|
||||||
"""
|
"""
|
||||||
Outupt a log message at level DEBUG. If force_stderr is True,
|
Outupt a log message at level DEBUG. If force_stderr is True,
|
||||||
output this message to stderr regardless of default stderr level
|
output this message to stderr regardless of default stderr level
|
||||||
|
|||||||
@ -153,30 +153,6 @@ class Notes(Base):
|
|||||||
session.query(Notes).filter_by(id=self.id).delete()
|
session.query(Notes).filter_by(id=self.id).delete()
|
||||||
session.flush()
|
session.flush()
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def max_used_row(session: Session, playlist_id: int) -> Optional[int]:
|
|
||||||
"""
|
|
||||||
Return maximum notes row for passed playlist ID or None if not notes
|
|
||||||
"""
|
|
||||||
|
|
||||||
last_row = session.query(func.max(Notes.row)).filter_by(
|
|
||||||
playlist_id=playlist_id).first()
|
|
||||||
# if there are no rows, the above returns (None, ) which is True
|
|
||||||
if last_row and last_row[0] is not None:
|
|
||||||
return last_row[0]
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def move_row(self, session: Session, row: int, to_playlist_id: int) \
|
|
||||||
-> None:
|
|
||||||
"""
|
|
||||||
Move note to another playlist
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.row = row
|
|
||||||
self.playlist_id = to_playlist_id
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_by_id(cls, session: Session, note_id: int) -> Optional["Notes"]:
|
def get_by_id(cls, session: Session, note_id: int) -> Optional["Notes"]:
|
||||||
"""Return note or None"""
|
"""Return note or None"""
|
||||||
@ -289,8 +265,8 @@ class Playlists(Base):
|
|||||||
If row=None, add to end of playlist
|
If row=None, add to end of playlist
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if row is None:
|
if not row:
|
||||||
row = self.next_free_row(session, self.id)
|
row = PlaylistTracks.next_free_row(session, self.id)
|
||||||
|
|
||||||
PlaylistTracks(session, self.id, track_id, row)
|
PlaylistTracks(session, self.id, track_id, row)
|
||||||
|
|
||||||
@ -342,23 +318,17 @@ class Playlists(Base):
|
|||||||
self.last_used = datetime.now()
|
self.last_used = datetime.now()
|
||||||
session.flush()
|
session.flush()
|
||||||
|
|
||||||
@staticmethod
|
def move_track(
|
||||||
def next_free_row(session: Session, playlist_id: int) -> int:
|
self, session: Session, rows: List[int],
|
||||||
"""Return next free row for this playlist"""
|
to_playlist: "Playlists") -> None:
|
||||||
|
"""Move tracks to another playlist"""
|
||||||
|
|
||||||
max_notes_row = Notes.max_used_row(session, playlist_id)
|
for row in rows:
|
||||||
max_tracks_row = PlaylistTracks.max_used_row(session, playlist_id)
|
track = self.tracks[row]
|
||||||
|
to_playlist.add_track(session, track.id)
|
||||||
|
del self.tracks[row]
|
||||||
|
|
||||||
if max_notes_row is not None and max_tracks_row is not None:
|
session.flush()
|
||||||
return max(max_notes_row, max_tracks_row) + 1
|
|
||||||
|
|
||||||
if max_notes_row is None and max_tracks_row is None:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
if max_notes_row is None:
|
|
||||||
return max_tracks_row + 1
|
|
||||||
else:
|
|
||||||
return max_notes_row + 1
|
|
||||||
|
|
||||||
def remove_all_tracks(self, session: Session) -> None:
|
def remove_all_tracks(self, session: Session) -> None:
|
||||||
"""
|
"""
|
||||||
@ -417,30 +387,48 @@ class PlaylistTracks(Base):
|
|||||||
session.flush()
|
session.flush()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def max_used_row(session: Session, playlist_id: int) -> Optional[int]:
|
def next_free_row(session: Session, playlist_id: int) -> int:
|
||||||
"""
|
"""Return next free row number"""
|
||||||
Return highest track row number used or None if there are no
|
|
||||||
tracks
|
row: int
|
||||||
"""
|
|
||||||
|
|
||||||
last_row = session.query(
|
last_row = session.query(
|
||||||
func.max(PlaylistTracks.row)
|
func.max(PlaylistTracks.row)
|
||||||
).filter_by(playlist_id=playlist_id).first()
|
).filter_by(playlist_id=playlist_id).first()
|
||||||
# if there are no rows, the above returns (None, ) which is True
|
# if there are no rows, the above returns (None, ) which is True
|
||||||
if last_row and last_row[0] is not None:
|
if last_row and last_row[0] is not None:
|
||||||
return last_row[0]
|
row = last_row[0] + 1
|
||||||
else:
|
else:
|
||||||
return None
|
row = 0
|
||||||
|
|
||||||
|
return row
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def move_row(session: Session, from_row: int, from_playlist_id: int,
|
def move_rows(
|
||||||
to_row: int, to_playlist_id: int) -> None:
|
session: Session, rows: List[int], from_playlist_id: int,
|
||||||
"""Move row to another playlist"""
|
to_playlist_id: int) -> None:
|
||||||
|
"""Move rows between playlists"""
|
||||||
|
|
||||||
|
# A constraint deliberately blocks duplicate (playlist_id, row)
|
||||||
|
# entries in database; however, unallocated rows in the database
|
||||||
|
# are fine (ie, we can have rows 1, 4, 6 and no 2, 3, 5).
|
||||||
|
# Unallocated rows will be automatically removed when the
|
||||||
|
# playlist is saved.
|
||||||
|
|
||||||
|
lowest_source_row: int = min(rows)
|
||||||
|
first_destination_free_row = PlaylistTracks.next_free_row(
|
||||||
|
session, to_playlist_id)
|
||||||
|
# Calculate offset that will put the lowest row number being
|
||||||
|
# moved at the first free row in destination playlist
|
||||||
|
offset = first_destination_free_row - lowest_source_row
|
||||||
|
|
||||||
session.query(PlaylistTracks).filter(
|
session.query(PlaylistTracks).filter(
|
||||||
PlaylistTracks.playlist_id == from_playlist_id,
|
PlaylistTracks.playlist_id == from_playlist_id,
|
||||||
PlaylistTracks.row == from_row).update(
|
PlaylistTracks.row.in_(rows)
|
||||||
{'playlist_id': to_playlist_id, 'row': to_row}, False)
|
).update({'playlist_id': to_playlist_id,
|
||||||
|
'row': PlaylistTracks.row + offset},
|
||||||
|
False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Settings(Base):
|
class Settings(Base):
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import os.path
|
||||||
import psutil
|
import psutil
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
@ -33,7 +34,8 @@ import helpers
|
|||||||
import music
|
import music
|
||||||
|
|
||||||
from config import Config
|
from config import Config
|
||||||
from models import (Base, Playdates, Playlists, Settings, Tracks)
|
from models import (Base, Playdates, Playlists, PlaylistTracks,
|
||||||
|
Settings, Tracks)
|
||||||
from playlists import PlaylistTab
|
from playlists import PlaylistTab
|
||||||
from sqlalchemy.orm.exc import DetachedInstanceError
|
from sqlalchemy.orm.exc import DetachedInstanceError
|
||||||
from ui.dlg_search_database_ui import Ui_Dialog
|
from ui.dlg_search_database_ui import Ui_Dialog
|
||||||
@ -502,8 +504,10 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
return
|
return
|
||||||
destination_playlist = dlg.playlist
|
destination_playlist = dlg.playlist
|
||||||
|
|
||||||
self.visible_playlist_tab().move_selected_to_playlist(
|
# Update database for both source and destination playlists
|
||||||
session, destination_playlist.id)
|
rows = visible_tab.get_selected_rows()
|
||||||
|
PlaylistTracks.move_rows(session, rows, source_playlist.id,
|
||||||
|
destination_playlist.id)
|
||||||
|
|
||||||
# Update destination playlist_tab if visible (if not visible, it
|
# Update destination playlist_tab if visible (if not visible, it
|
||||||
# will be re-populated when it is opened)
|
# will be re-populated when it is opened)
|
||||||
@ -523,6 +527,9 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
destination_visible_playlist_tab.populate(
|
destination_visible_playlist_tab.populate(
|
||||||
session, dlg.playlist.id)
|
session, dlg.playlist.id)
|
||||||
|
|
||||||
|
# Update source playlist
|
||||||
|
self.visible_playlist_tab().remove_rows(rows)
|
||||||
|
|
||||||
def open_info_tabs(self) -> None:
|
def open_info_tabs(self) -> None:
|
||||||
"""
|
"""
|
||||||
Ensure we have info tabs for next and current track titles
|
Ensure we have info tabs for next and current track titles
|
||||||
@ -1070,7 +1077,6 @@ class SelectPlaylistDialog(QDialog):
|
|||||||
self.ui.buttonBox.accepted.connect(self.open)
|
self.ui.buttonBox.accepted.connect(self.open)
|
||||||
self.ui.buttonBox.rejected.connect(self.close)
|
self.ui.buttonBox.rejected.connect(self.close)
|
||||||
self.session = session
|
self.session = session
|
||||||
self.playlist = None
|
|
||||||
self.plid = None
|
self.plid = None
|
||||||
|
|
||||||
record = Settings.get_int_settings(
|
record = Settings.get_int_settings(
|
||||||
|
|||||||
@ -19,8 +19,6 @@ from PyQt5.QtWidgets import (
|
|||||||
import helpers
|
import helpers
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
|
||||||
import threading
|
|
||||||
|
|
||||||
from config import Config
|
from config import Config
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
@ -30,7 +28,6 @@ from models import (
|
|||||||
Notes,
|
Notes,
|
||||||
Playdates,
|
Playdates,
|
||||||
Playlists,
|
Playlists,
|
||||||
PlaylistTracks,
|
|
||||||
Settings,
|
Settings,
|
||||||
Tracks,
|
Tracks,
|
||||||
NoteColours
|
NoteColours
|
||||||
@ -151,7 +148,7 @@ class PlaylistTab(QTableWidget):
|
|||||||
self.populate(session, self.playlist_id)
|
self.populate(session, self.playlist_id)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"<PlaylistTab(id={self.playlist_id}"
|
return (f"<PlaylistTab(id={self.playlist_id}")
|
||||||
|
|
||||||
# ########## Events ##########
|
# ########## Events ##########
|
||||||
|
|
||||||
@ -227,11 +224,6 @@ class PlaylistTab(QTableWidget):
|
|||||||
act_info.triggered.connect(lambda: self._info_row(row))
|
act_info.triggered.connect(lambda: self._info_row(row))
|
||||||
self.menu.addSeparator()
|
self.menu.addSeparator()
|
||||||
if row not in self._get_notes_rows():
|
if row not in self._get_notes_rows():
|
||||||
act_mplayer = self.menu.addAction(
|
|
||||||
"Play track with mplayer")
|
|
||||||
act_mplayer.triggered.connect(
|
|
||||||
lambda: self._mplayer(row))
|
|
||||||
self.menu.addSeparator()
|
|
||||||
if not current and not next_row:
|
if not current and not next_row:
|
||||||
act_setnext = self.menu.addAction("Set next")
|
act_setnext = self.menu.addAction("Set next")
|
||||||
with Session() as session:
|
with Session() as session:
|
||||||
@ -248,8 +240,6 @@ class PlaylistTab(QTableWidget):
|
|||||||
act_audacity.triggered.connect(
|
act_audacity.triggered.connect(
|
||||||
lambda: self._audacity(row))
|
lambda: self._audacity(row))
|
||||||
if not current and not next_row:
|
if not current and not next_row:
|
||||||
act_move = self.menu.addAction('Move to playlist...')
|
|
||||||
act_move.triggered.connect(self.musicmuster.move_selected)
|
|
||||||
self.menu.addSeparator()
|
self.menu.addSeparator()
|
||||||
act_delete = self.menu.addAction('Delete')
|
act_delete = self.menu.addAction('Delete')
|
||||||
act_delete.triggered.connect(self._delete_rows)
|
act_delete.triggered.connect(self._delete_rows)
|
||||||
@ -315,10 +305,10 @@ class PlaylistTab(QTableWidget):
|
|||||||
return self.selectionModel().selectedRows()[0].row()
|
return self.selectionModel().selectedRows()[0].row()
|
||||||
|
|
||||||
def get_selected_rows(self) -> List[int]:
|
def get_selected_rows(self) -> List[int]:
|
||||||
"""Return a sorted list of selected row numbers"""
|
"""Return a list of selected row numbers"""
|
||||||
|
|
||||||
rows = self.selectionModel().selectedRows()
|
rows = self.selectionModel().selectedRows()
|
||||||
return sorted([row.row() for row in rows])
|
return [row.row() for row in rows]
|
||||||
|
|
||||||
def get_selected_title(self) -> Optional[str]:
|
def get_selected_title(self) -> Optional[str]:
|
||||||
"""Return title of selected row or None"""
|
"""Return title of selected row or None"""
|
||||||
@ -399,48 +389,21 @@ class PlaylistTab(QTableWidget):
|
|||||||
self.save_playlist(session)
|
self.save_playlist(session)
|
||||||
self.update_display(session, clear_selection=False)
|
self.update_display(session, clear_selection=False)
|
||||||
|
|
||||||
def move_selected_to_playlist(self, session: Session, playlist_id: int) \
|
def remove_rows(self, rows) -> None:
|
||||||
-> None:
|
"""Remove rows passed in rows list"""
|
||||||
"""
|
|
||||||
Move selected rows and any immediately preceding notes to
|
|
||||||
other playlist
|
|
||||||
"""
|
|
||||||
|
|
||||||
notes_rows = self._get_notes_rows()
|
# Row number will change as we delete rows so remove them in
|
||||||
destination_row = Playlists.next_free_row(session, playlist_id)
|
# reverse order.
|
||||||
rows_to_remove = []
|
|
||||||
|
|
||||||
for row in self.get_selected_rows():
|
|
||||||
if row in notes_rows:
|
|
||||||
note_obj = self._get_row_notes_object(row, session)
|
|
||||||
note_obj.move_row(session, destination_row, playlist_id)
|
|
||||||
else:
|
|
||||||
# For tracks, check for a preceding notes row and move
|
|
||||||
# that as well if it exists
|
|
||||||
if row - 1 in notes_rows:
|
|
||||||
note_obj = self._get_row_notes_object(row - 1, session)
|
|
||||||
note_obj.move_row(session, destination_row, playlist_id)
|
|
||||||
destination_row += 1
|
|
||||||
rows_to_remove.append(row - 1)
|
|
||||||
# Move track
|
|
||||||
PlaylistTracks.move_row(
|
|
||||||
session, row, self.playlist_id,
|
|
||||||
destination_row, playlist_id
|
|
||||||
)
|
|
||||||
destination_row += 1
|
|
||||||
rows_to_remove.append(row)
|
|
||||||
|
|
||||||
# Remove rows. Row number will change as we delete rows so
|
|
||||||
# remove them in reverse order.
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.selecting_in_progress = True
|
self.selecting_in_progress = True
|
||||||
for row in sorted(rows_to_remove, reverse=True):
|
for row in sorted(rows, reverse=True):
|
||||||
self.removeRow(row)
|
self.removeRow(row)
|
||||||
finally:
|
finally:
|
||||||
self.selecting_in_progress = False
|
self.selecting_in_progress = False
|
||||||
self._select_event()
|
self._select_event()
|
||||||
|
|
||||||
|
with Session() as session:
|
||||||
self.save_playlist(session)
|
self.save_playlist(session)
|
||||||
self.update_display(session)
|
self.update_display(session)
|
||||||
|
|
||||||
@ -610,7 +573,6 @@ class PlaylistTab(QTableWidget):
|
|||||||
track_id: int = self.item(
|
track_id: int = self.item(
|
||||||
row, self.COL_USERDATA).data(self.CONTENT_OBJECT)
|
row, self.COL_USERDATA).data(self.CONTENT_OBJECT)
|
||||||
playlist.add_track(session, track_id, row)
|
playlist.add_track(session, track_id, row)
|
||||||
session.commit()
|
|
||||||
|
|
||||||
def select_next_row(self) -> None:
|
def select_next_row(self) -> None:
|
||||||
"""
|
"""
|
||||||
@ -885,7 +847,8 @@ class PlaylistTab(QTableWidget):
|
|||||||
# This is a track row other than next or current
|
# This is a track row other than next or current
|
||||||
if row in played:
|
if row in played:
|
||||||
# Played today, so update last played column
|
# Played today, so update last played column
|
||||||
last_playedtime = track.lastplayed
|
last_playedtime = Playdates.last_played(
|
||||||
|
session, track.id)
|
||||||
last_played_str = get_relative_date(last_playedtime)
|
last_played_str = get_relative_date(last_playedtime)
|
||||||
self.item(row, self.COL_LAST_PLAYED).setText(
|
self.item(row, self.COL_LAST_PLAYED).setText(
|
||||||
last_played_str)
|
last_played_str)
|
||||||
@ -1142,11 +1105,6 @@ class PlaylistTab(QTableWidget):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _get_notes_rows(self) -> List[int]:
|
|
||||||
"""Return rows marked as notes, or None"""
|
|
||||||
|
|
||||||
return self._meta_search(RowMeta.NOTE, one=False)
|
|
||||||
|
|
||||||
def _find_next_track_row(self, starting_row: int = None) -> Optional[int]:
|
def _find_next_track_row(self, starting_row: int = None) -> Optional[int]:
|
||||||
"""
|
"""
|
||||||
Find next track to play. If a starting row is given, start there;
|
Find next track to play. If a starting row is given, start there;
|
||||||
@ -1206,6 +1164,11 @@ class PlaylistTab(QTableWidget):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _get_notes_rows(self) -> List[int]:
|
||||||
|
"""Return rows marked as notes, or None"""
|
||||||
|
|
||||||
|
return self._meta_search(RowMeta.NOTE, one=False)
|
||||||
|
|
||||||
def _get_row_duration(self, row: int) -> int:
|
def _get_row_duration(self, row: int) -> int:
|
||||||
"""Return duration associated with this row"""
|
"""Return duration associated with this row"""
|
||||||
|
|
||||||
@ -1455,21 +1418,6 @@ class PlaylistTab(QTableWidget):
|
|||||||
self.item(row, self.COL_USERDATA).setData(
|
self.item(row, self.COL_USERDATA).setData(
|
||||||
self.ROW_METADATA, new_metadata)
|
self.ROW_METADATA, new_metadata)
|
||||||
|
|
||||||
def _mplayer(self, row: int) -> None:
|
|
||||||
"""Play track with mplayer"""
|
|
||||||
|
|
||||||
DEBUG(f"_mplayer({row})")
|
|
||||||
|
|
||||||
if row in self._get_notes_rows():
|
|
||||||
return None
|
|
||||||
|
|
||||||
with Session() as session:
|
|
||||||
track: Tracks = self._get_row_track_object(row, session)
|
|
||||||
cmd_list = ['gmplayer', '-vc', 'null', '-vo', 'null', track.path]
|
|
||||||
thread = threading.Thread(
|
|
||||||
target=self._run_subprocess, args=(cmd_list,))
|
|
||||||
thread.start()
|
|
||||||
|
|
||||||
def _rescan(self, row: int) -> None:
|
def _rescan(self, row: int) -> None:
|
||||||
"""
|
"""
|
||||||
If passed row is track row, rescan it.
|
If passed row is track row, rescan it.
|
||||||
@ -1485,11 +1433,6 @@ class PlaylistTab(QTableWidget):
|
|||||||
track.rescan(session)
|
track.rescan(session)
|
||||||
self._update_row(session, row, track)
|
self._update_row(session, row, track)
|
||||||
|
|
||||||
def _run_subprocess(self, args):
|
|
||||||
"""Run args in subprocess"""
|
|
||||||
|
|
||||||
subprocess.call(args)
|
|
||||||
|
|
||||||
def _set_current_track_row(self, row: int) -> None:
|
def _set_current_track_row(self, row: int) -> None:
|
||||||
"""Mark this row as current track"""
|
"""Mark this row as current track"""
|
||||||
|
|
||||||
|
|||||||
113
app/utilities.py
113
app/utilities.py
@ -204,9 +204,7 @@ def update_db(session):
|
|||||||
Repopulate database
|
Repopulate database
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Search for tracks that are in the music directory but not the datebase
|
# Search for tracks in only one of directory and database
|
||||||
# Check all paths in database exist
|
|
||||||
# If issues found, write to stdout but do not try to resolve them
|
|
||||||
|
|
||||||
db_paths = set(Tracks.get_all_paths(session))
|
db_paths = set(Tracks.get_all_paths(session))
|
||||||
|
|
||||||
@ -219,74 +217,61 @@ 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)
|
||||||
|
|
||||||
# Find any files in music directory that are not in database
|
# If a track is moved, only the path will have changed.
|
||||||
files_not_in_db = list(os_paths - db_paths)
|
# 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:
|
||||||
|
|
||||||
# Find paths in database missing in music directory
|
for path in list(os_paths - db_paths):
|
||||||
paths_not_found = []
|
DEBUG(f"utilities.update_db: {path=} not in database")
|
||||||
missing_file_count = 0
|
# is filename in database with a different path?
|
||||||
more_files_to_report = False
|
track = Tracks.get_by_filename(session, os.path.basename(path))
|
||||||
for path in list(db_paths - os_paths):
|
|
||||||
if missing_file_count >= Config.MAX_MISSING_FILES_TO_REPORT:
|
|
||||||
more_files_to_report = True
|
|
||||||
break
|
|
||||||
|
|
||||||
missing_file_count += 1
|
|
||||||
|
|
||||||
track = Tracks.get_by_path(session, path)
|
|
||||||
if not track:
|
if not track:
|
||||||
ERROR(f"update_db: {path} not found in db")
|
messages.append(f"{path} missing from database: {path}")
|
||||||
continue
|
else:
|
||||||
|
# Check track info matches found track
|
||||||
|
t = helpers.get_tags(path)
|
||||||
|
if t['artist'] == track.artist and t['title'] == track.title:
|
||||||
|
print(f">>> Update {path=} for {track.title=}")
|
||||||
|
track.update_path(session, path)
|
||||||
|
else:
|
||||||
|
create_track_from_file(session, path)
|
||||||
|
|
||||||
paths_not_found.append(track)
|
# Refresh database paths
|
||||||
|
db_paths = set(Tracks.get_all_paths(session))
|
||||||
|
# Remove any tracks from database whose paths don't exist
|
||||||
|
for path in list(db_paths - os_paths):
|
||||||
|
# Manage tracks listed in database but where path is invalid
|
||||||
|
DEBUG(f"Invalid {path=} in database", True)
|
||||||
|
track = Tracks.get_by_path(session, path)
|
||||||
|
messages.append(f"Remove from database: {path=} {track=}")
|
||||||
|
|
||||||
|
# Remove references from Playdates
|
||||||
|
Playdates.remove_track(session, track.id)
|
||||||
|
|
||||||
|
# Replace playlist entries with a note
|
||||||
|
note_txt = (
|
||||||
|
f"File removed: {track.title=}, {track.artist=}, "
|
||||||
|
f"{track.path=}"
|
||||||
|
)
|
||||||
|
for playlist_track in track.playlists:
|
||||||
|
row = playlist_track.row
|
||||||
|
# Remove playlist entry
|
||||||
|
DEBUG(f"Remove {row=} from {playlist_track.playlist_id}", True)
|
||||||
|
playlist_track.playlist.remove_track(session, row)
|
||||||
|
# Create note
|
||||||
|
DEBUG(f"Add note at {row=} to {playlist_track.playlist_id=}", True)
|
||||||
|
Notes(session, playlist_track.playlist_id, row, note_txt)
|
||||||
|
|
||||||
|
# Remove Track entry pointing to invalid path
|
||||||
|
Tracks.remove_by_path(session, path)
|
||||||
|
|
||||||
# Output messages (so if running via cron, these will get sent to
|
# Output messages (so if running via cron, these will get sent to
|
||||||
# user)
|
# user)
|
||||||
if files_not_in_db:
|
if messages:
|
||||||
print("Files in music directory but not in database")
|
print("Messages")
|
||||||
print("--------------------------------------------")
|
print("\n".join(messages))
|
||||||
print("\n".join(files_not_in_db))
|
|
||||||
print("\n")
|
|
||||||
if paths_not_found:
|
|
||||||
print("Invalid paths in database")
|
|
||||||
print("-------------------------")
|
|
||||||
for t in paths_not_found:
|
|
||||||
print(f"""
|
|
||||||
Track ID: {t.id}
|
|
||||||
Path: {t.path}
|
|
||||||
Title: {t.title}
|
|
||||||
Artist: {t.artist}
|
|
||||||
""")
|
|
||||||
if more_files_to_report:
|
|
||||||
print("There were more paths than listed that were not found")
|
|
||||||
|
|
||||||
|
|
||||||
# Spike
|
|
||||||
#
|
|
||||||
# # Manage tracks listed in database but where path is invalid
|
|
||||||
# DEBUG(f"Invalid {path=} in database", True)
|
|
||||||
# track = Tracks.get_by_path(session, path)
|
|
||||||
# messages.append(f"Remove from database: {path=} {track=}")
|
|
||||||
#
|
|
||||||
# # Remove references from Playdates
|
|
||||||
# Playdates.remove_track(session, track.id)
|
|
||||||
#
|
|
||||||
# # Replace playlist entries with a note
|
|
||||||
# note_txt = (
|
|
||||||
# f"File removed: {track.title=}, {track.artist=}, "
|
|
||||||
# f"{track.path=}"
|
|
||||||
# )
|
|
||||||
# for playlist_track in track.playlists:
|
|
||||||
# row = playlist_track.row
|
|
||||||
# # Remove playlist entry
|
|
||||||
# DEBUG(f"Remove {row=} from {playlist_track.playlist_id}", True)
|
|
||||||
# playlist_track.playlist.remove_track(session, row)
|
|
||||||
# # Create note
|
|
||||||
# DEBUG(f"Add note at {row=} to {playlist_track.playlist_id=}", True)
|
|
||||||
# Notes(session, playlist_track.playlist_id, row, note_txt)
|
|
||||||
#
|
|
||||||
# # Remove Track entry pointing to invalid path
|
|
||||||
# Tracks.remove_by_path(session, path)
|
|
||||||
|
|
||||||
if __name__ == '__main__' and '__file__' in globals():
|
if __name__ == '__main__' and '__file__' in globals():
|
||||||
main()
|
main()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user