adding cleanup of stale track entries
This commit is contained in:
parent
228d44ce98
commit
1a2506742f
|
@ -106,6 +106,7 @@ def scan(
|
||||||
initialize()
|
initialize()
|
||||||
with database_manager() as manager:
|
with database_manager() as manager:
|
||||||
scanner = media_scanner(root=root, db=manager.session)
|
scanner = media_scanner(root=root, db=manager.session)
|
||||||
|
scanner.cleanup()
|
||||||
count = scanner.scan()
|
count = scanner.scan()
|
||||||
logging.info(f"Imported {count} new tracks.")
|
logging.info(f"Imported {count} new tracks.")
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import music_tag
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable, Union, Iterable
|
from typing import Callable, Union, Iterable
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func, delete
|
||||||
|
|
||||||
import groove.db
|
import groove.db
|
||||||
import groove.path
|
import groove.path
|
||||||
|
@ -40,10 +40,31 @@ class MediaScanner:
|
||||||
async def _do_import():
|
async def _do_import():
|
||||||
logging.debug("Scanning filesystem (this may take a minute)...")
|
logging.debug("Scanning filesystem (this may take a minute)...")
|
||||||
for path in sources:
|
for path in sources:
|
||||||
|
if path.exists() and not path.is_dir():
|
||||||
asyncio.create_task(self._import_one_track(path))
|
asyncio.create_task(self._import_one_track(path))
|
||||||
asyncio.run(_do_import())
|
asyncio.run(_do_import())
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
|
|
||||||
|
def cleanup(self) -> int:
|
||||||
|
"""
|
||||||
|
Check for the existence of every track in the databse.
|
||||||
|
"""
|
||||||
|
async def _del(track):
|
||||||
|
path = self.root / Path(track.relpath)
|
||||||
|
if path.exists():
|
||||||
|
return
|
||||||
|
logging.info(f"Deleting missing track {track.relpath}")
|
||||||
|
self.db.execute(
|
||||||
|
delete(groove.db.track).where(groove.db.track.c.id == track.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _do_cleanup():
|
||||||
|
logging.debug("Locating stale track definitions in the database...")
|
||||||
|
for track in self.db.query(groove.db.track).all():
|
||||||
|
asyncio.create_task(_del(track))
|
||||||
|
asyncio.run(_do_cleanup())
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
def _get_tags(self, path): # pragma: no cover
|
def _get_tags(self, path): # pragma: no cover
|
||||||
tags = music_tag.load_file(path)
|
tags = music_tag.load_file(path)
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -66,7 +66,7 @@ class Playlist:
|
||||||
@property
|
@property
|
||||||
def info(self):
|
def info(self):
|
||||||
count = len(self.entries)
|
count = len(self.entries)
|
||||||
return f"{self.name}: {self.url} [{count} tracks]\n{self.description}\n"
|
return f"{self.name}: {self.url} [{count} tracks]\n{self.description}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self) -> str:
|
def url(self) -> str:
|
||||||
|
@ -133,7 +133,7 @@ class Playlist:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def as_string(self) -> str:
|
def as_string(self) -> str:
|
||||||
text = self.info
|
text = self.info + self.description
|
||||||
for (tracknum, entry) in enumerate(self.entries):
|
for (tracknum, entry) in enumerate(self.entries):
|
||||||
text += f" - {tracknum+1} {entry.artist} - {entry.title}\n"
|
text += f" - {tracknum+1} {entry.artist} - {entry.title}\n"
|
||||||
return text
|
return text
|
||||||
|
|
|
@ -14,6 +14,11 @@ class BasePrompt(Completer):
|
||||||
self._values = []
|
self._values = []
|
||||||
self._parent = parent
|
self._parent = parent
|
||||||
self._manager = manager
|
self._manager = manager
|
||||||
|
self._commands = {'help': self.help}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def commands(self):
|
||||||
|
return self._commands
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def usage(self):
|
def usage(self):
|
||||||
|
@ -40,7 +45,7 @@ class BasePrompt(Completer):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def values(self):
|
def values(self):
|
||||||
return self._values
|
return [k for k in self.commands.keys() if not k.startswith('_')]
|
||||||
|
|
||||||
def get_completions(self, document, complete_event):
|
def get_completions(self, document, complete_event):
|
||||||
word = document.get_word_before_cursor()
|
word = document.get_word_before_cursor()
|
||||||
|
@ -59,7 +64,7 @@ class BasePrompt(Completer):
|
||||||
def start(self, cmd=''):
|
def start(self, cmd=''):
|
||||||
while True:
|
while True:
|
||||||
if not cmd:
|
if not cmd:
|
||||||
cmd = prompt(f'{self.prompt} ', completer=self)
|
cmd = prompt(f'{self.prompt} ', completer=self, complete_while_typing=True)
|
||||||
if not cmd:
|
if not cmd:
|
||||||
return
|
return
|
||||||
cmd, *parts = cmd.split()
|
cmd, *parts = cmd.split()
|
||||||
|
@ -67,6 +72,13 @@ class BasePrompt(Completer):
|
||||||
return
|
return
|
||||||
cmd = ''
|
cmd = ''
|
||||||
|
|
||||||
|
def help(self, parts):
|
||||||
|
if not parts:
|
||||||
|
print(self.__doc__)
|
||||||
|
else:
|
||||||
|
print(getattr(self, parts[0]).__doc__)
|
||||||
|
return True
|
||||||
|
|
||||||
def default_completer(self, document, complete_event):
|
def default_completer(self, document, complete_event):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
|
@ -54,8 +54,8 @@ class CommandPrompt(BasePrompt):
|
||||||
session=self.manager.session,
|
session=self.manager.session,
|
||||||
create_ok=True
|
create_ok=True
|
||||||
)
|
)
|
||||||
res = self.commands['_playlist'].start()
|
self.commands['_playlist'].start()
|
||||||
return True and res
|
return True
|
||||||
|
|
||||||
|
|
||||||
def start(): # pragma: no cover
|
def start(): # pragma: no cover
|
||||||
|
|
|
@ -8,21 +8,14 @@ from groove import db
|
||||||
|
|
||||||
|
|
||||||
class _playlist(BasePrompt):
|
class _playlist(BasePrompt):
|
||||||
|
"""
|
||||||
def __init__(self, parent, manager=None):
|
PLAYLIST
|
||||||
super().__init__(manager=manager, parent=parent)
|
"""
|
||||||
self._parent = parent
|
|
||||||
self._prompt = ''
|
|
||||||
self._commands = None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def prompt(self):
|
def prompt(self):
|
||||||
return f"{self.parent.playlist}\n{self.parent.playlist.slug}> "
|
return f"{self.parent.playlist}\n{self.parent.playlist.slug}> "
|
||||||
|
|
||||||
@property
|
|
||||||
def values(self):
|
|
||||||
return self.commands.keys()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def commands(self):
|
def commands(self):
|
||||||
if not self._commands:
|
if not self._commands:
|
||||||
|
@ -31,9 +24,20 @@ class _playlist(BasePrompt):
|
||||||
'delete': self.delete,
|
'delete': self.delete,
|
||||||
'add': self.add,
|
'add': self.add,
|
||||||
'edit': self.edit,
|
'edit': self.edit,
|
||||||
|
'help': self.help
|
||||||
}
|
}
|
||||||
return self._commands
|
return self._commands
|
||||||
|
|
||||||
|
def _add_track(self, text):
|
||||||
|
sess = self.parent.manager.session
|
||||||
|
try:
|
||||||
|
track = sess.query(db.track).filter(db.track.c.relpath == text).one()
|
||||||
|
self.parent.playlist.create_entries([track])
|
||||||
|
except NoResultFound:
|
||||||
|
print("No match for '{text}'")
|
||||||
|
return
|
||||||
|
return text
|
||||||
|
|
||||||
def process(self, cmd, *parts):
|
def process(self, cmd, *parts):
|
||||||
res = True
|
res = True
|
||||||
if cmd in self.commands:
|
if cmd in self.commands:
|
||||||
|
@ -66,16 +70,6 @@ class _playlist(BasePrompt):
|
||||||
return True
|
return True
|
||||||
self._add_track(text)
|
self._add_track(text)
|
||||||
|
|
||||||
def _add_track(self, text):
|
|
||||||
sess = self.parent.manager.session
|
|
||||||
try:
|
|
||||||
track = sess.query(db.track).filter(db.track.c.relpath == text).one()
|
|
||||||
self.parent.playlist.create_entries([track])
|
|
||||||
except NoResultFound:
|
|
||||||
print("No match for '{text}'")
|
|
||||||
return
|
|
||||||
return text
|
|
||||||
|
|
||||||
def delete(self, parts):
|
def delete(self, parts):
|
||||||
res = prompt(
|
res = prompt(
|
||||||
'Type DELETE to permanently delete the playlist '
|
'Type DELETE to permanently delete the playlist '
|
||||||
|
|
0
test/fixtures/themes/alt_theme/static
vendored
Normal file
0
test/fixtures/themes/alt_theme/static
vendored
Normal file
|
@ -38,6 +38,10 @@ def test_scanner(monkeypatch, in_memory_db, media):
|
||||||
# replace the filesystem glob with the test fixture generator
|
# replace the filesystem glob with the test fixture generator
|
||||||
monkeypatch.setattr(scanner.MediaScanner, 'find_sources', MagicMock(return_value=media()))
|
monkeypatch.setattr(scanner.MediaScanner, 'find_sources', MagicMock(return_value=media()))
|
||||||
|
|
||||||
|
# pretend things exist
|
||||||
|
monkeypatch.setattr(scanner.Path, 'exists', MagicMock(return_value=True))
|
||||||
|
monkeypatch.setattr(scanner.Path, 'is_dir', MagicMock(return_value=False))
|
||||||
|
|
||||||
def mock_loader(path):
|
def mock_loader(path):
|
||||||
return {
|
return {
|
||||||
'artist': 'foo',
|
'artist': 'foo',
|
||||||
|
|
|
@ -16,6 +16,10 @@ def response_factory(responses):
|
||||||
return MagicMock(side_effect=responses + ([''] * 10))
|
return MagicMock(side_effect=responses + ([''] * 10))
|
||||||
|
|
||||||
|
|
||||||
|
def test_commands(cmd_prompt):
|
||||||
|
assert cmd_prompt.commands.keys() == cmd_prompt.commands.keys()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('inputs, expected', [
|
@pytest.mark.parametrize('inputs, expected', [
|
||||||
(['stats'], 'Database contains 4 playlists'), # match the db fixture
|
(['stats'], 'Database contains 4 playlists'), # match the db fixture
|
||||||
])
|
])
|
||||||
|
|
Loading…
Reference in New Issue
Block a user