diff --git a/app/ds.py b/app/ds.py index a9c6c3e..d0fbe89 100644 --- a/app/ds.py +++ b/app/ds.py @@ -437,14 +437,14 @@ def update_track( track = session.get(Tracks, track_id) if not track: raise ApplicationError(f"Can't retrieve Track ({track_id=})") - track.path = (str(metadata["path"]),) - track.title = (str(metadata["title"]),) - track.artist = (str(metadata["artist"]),) - track.duration = (int(metadata["duration"]),) - track.start_gap = (int(metadata["start_gap"]),) - track.fade_at = (int(metadata["fade_at"]),) - track.silence_at = (int(metadata["silence_at"]),) - track.bitrate = (int(metadata["bitrate"]),) + track.path = str(metadata["path"]) + track.title = str(metadata["title"]), + track.artist = str(metadata["artist"]), + track.duration = int(metadata["duration"]), + track.start_gap = int(metadata["start_gap"]), + track.fade_at = int(metadata["fade_at"]), + track.silence_at = int(metadata["silence_at"]), + track.bitrate = int(metadata["bitrate"]), session.commit() @@ -470,6 +470,15 @@ def set_track_intro(track_id: int, intro: int) -> None: session.commit() +def delete_track(track_id: int) -> None: + """Delete track""" + + with db.Session() as session: + track = session.get(Tracks, track_id) + session.delete(track) + session.commit() + + # Playlist functions @log_call def _playlists_where( diff --git a/app/helpers.py b/app/helpers.py index 66a6232..d1c8492 100644 --- a/app/helpers.py +++ b/app/helpers.py @@ -168,7 +168,7 @@ def get_name(prompt: str, default: str = "") -> str | None: def get_relative_date( - past_date: Optional[dt.datetime], now: Optional[dt.datetime] = None + past_date: Optional[dt.datetime], reference_date: Optional[dt.datetime] = None ) -> str: """ Return how long before reference_date past_date is as string. @@ -182,33 +182,37 @@ def get_relative_date( if not past_date or past_date == Config.EPOCH: return "Never" - if not now: - now = dt.datetime.now() + if not reference_date: + reference_date = dt.datetime.now() # Check parameters - if past_date > now: + if past_date > reference_date: raise ApplicationError("get_relative_date() past_date is after relative_date") - delta = now - past_date + delta = reference_date - past_date days = delta.days if days == 0: - return "(Today)" + return Config.LAST_PLAYED_TODAY_STRING + " " + past_date.strftime("%H:%M") + elif days == 1: return "(Yesterday)" - years, days_remain = divmod(days, 365) - months, days_final = divmod(days_remain, 30) + years, days_remain_years = divmod(days, 365) + months, days_remain_months = divmod(days_remain_years, 30) + weeks, days_final = divmod(days_remain_months, 7) parts = [] if years: parts.append(f"{years}y") if months: parts.append(f"{months}m") + if weeks: + parts.append(f"{weeks}w") if days_final: parts.append(f"{days_final}d") - formatted = " ".join(parts) - return f"({formatted} ago)" + formatted = ", ".join(parts) + return formatted def get_tags(path: str) -> Tags: diff --git a/app/musicmuster.py b/app/musicmuster.py index c072e87..9be3086 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -66,6 +66,7 @@ from classes import ( ApplicationError, Filter, MusicMusterSignals, + PlaylistDTO, PlayTrack, QueryDTO, TrackInfo, @@ -1412,7 +1413,7 @@ class Window(QMainWindow): return ds.create_playlist(name, template_id) @log_call - def _open_playlist(self, playlist: Playlists, is_template: bool = False) -> int: + def _open_playlist(self, playlist: PlaylistDTO, is_template: bool = False) -> int: """ With passed playlist: - create models diff --git a/app/playlistmodel.py b/app/playlistmodel.py index e40d054..87bbd04 100644 --- a/app/playlistmodel.py +++ b/app/playlistmodel.py @@ -713,18 +713,15 @@ class PlaylistModel(QAbstractTableModel): super().beginInsertRows(QModelIndex(), new_row_number, new_row_number) - new_row = ds.insert_row( + _ = ds.insert_row( playlist_id=self.playlist_id, row_number=new_row_number, track_id=track_id, note=note, ) - - # Move rows down to make room - for destination_row in range(len(self.playlist_rows), new_row_number, -1): - self.playlist_rows[destination_row] = self.playlist_rows[destination_row - 1] - # Insert into self.playlist_rows - self.playlist_rows[new_row_number] = PlaylistRow(new_row) + # Need to refresh self.playlist_rows because row numbers will have + # changed + self.refresh_data() super().endInsertRows() diff --git a/tests/X_test_file_importer.py b/tests/X_test_file_importer.py new file mode 100644 index 0000000..3bb2433 --- /dev/null +++ b/tests/X_test_file_importer.py @@ -0,0 +1,480 @@ +""" +Tests are named 'test_nnn_xxxx' where 'nn n' is a number. This is used to ensure that +the tests run in order as we rely (in some cases) upon the results of an earlier test. +Yes, we shouldn't do that. +""" + +# Standard library imports +import os +import shutil +import tempfile +import unittest +from unittest.mock import MagicMock, patch + +# PyQt imports +from PyQt6.QtWidgets import QDialog, QFileDialog + +# Third party imports +from mutagen.mp3 import MP3 # type: ignore +import pytest +from pytestqt.plugin import QtBot # type: ignore + +# App imports +from app import ds, musicmuster +from app.models import ( + db, + Tracks, +) +from config import Config +from file_importer import FileImporter + + +# Custom fixture to adapt qtbot for use with unittest.TestCase +@pytest.fixture(scope="class") +def qtbot_adapter(qapp, request): + """Adapt qtbot fixture for usefixtures and unittest.TestCase""" + request.cls.qtbot = QtBot(request) + + +# Fixture for tmp_path to be available in the class +@pytest.fixture(scope="class") +def class_tmp_path(request, tmp_path_factory): + """Provide a class-wide tmp_path""" + request.cls.tmp_path = tmp_path_factory.mktemp("pytest_tmp") + + +@pytest.mark.usefixtures("qtbot_adapter", "class_tmp_path") +class MyTestCase(unittest.TestCase): + @classmethod + def setUpClass(cls): + """Runs once before any test in this class""" + + db.create_all() + + cls.widget = musicmuster.Window() + + # Create a playlist for all tests + playlist_name = "file importer playlist" + playlist = ds.create_playlist(name=playlist_name, template_id=0) + cls.widget._open_playlist(playlist) + + # Create our musicstore + cls.import_source = tempfile.mkdtemp(suffix="_MMsource_pytest", dir="/tmp") + Config.REPLACE_FILES_DEFAULT_SOURCE = cls.import_source + cls.musicstore = tempfile.mkdtemp(suffix="_MMstore_pytest", dir="/tmp") + Config.IMPORT_DESTINATION = cls.musicstore + + @classmethod + def tearDownClass(cls): + """Runs once after all tests""" + + db.drop_all() + shutil.rmtree(cls.musicstore) + shutil.rmtree(cls.import_source) + + def setUp(self): + """Runs before each test""" + + with self.qtbot.waitExposed(self.widget): + self.widget.show() + + def tearDown(self): + """Runs after each test""" + self.widget.close() # Close UI to prevent side effects + + def wait_for_workers(self, timeout: int = 10000): + """ + Let import threads workers run to completion + """ + + def workers_empty(): + assert FileImporter.workers == {} + + self.qtbot.waitUntil(workers_empty, timeout=timeout) + + def test_001_import_no_files(self): + """Try importing with no files to import""" + + with patch("file_importer.show_OK") as mock_show_ok: + self.widget.import_files_wrapper() + mock_show_ok.assert_called_once_with( + "File import", + f"No files in {Config.REPLACE_FILES_DEFAULT_SOURCE} to import", + None, + ) + + def test_002_import_file_and_cancel(self): + """Cancel file import""" + + test_track_path = "testdata/isa.mp3" + shutil.copy(test_track_path, self.import_source) + + with ( + patch("file_importer.PickMatch") as MockPickMatch, + patch("file_importer.show_OK") as mock_show_ok, + ): + # Create a mock instance of PickMatch + mock_dialog_instance = MagicMock() + MockPickMatch.return_value = mock_dialog_instance + + # Simulate the user clicking OK in the dialog + mock_dialog_instance.exec.return_value = QDialog.DialogCode.Rejected + mock_dialog_instance.selected_track_id = -1 # Simulated return value + + self.widget.import_files_wrapper() + + # Ensure PickMatch was instantiated correctly + MockPickMatch.assert_called_once_with( + new_track_description="I'm So Afraid (Fleetwood Mac)", + choices=[("Do not import", -1, ""), ("Import as new track", 0, "")], + default=1, + ) + + # Verify exec() was called + mock_dialog_instance.exec.assert_called_once() + + # Ensure selected_track_id was accessed after dialog.exec() + assert mock_dialog_instance.selected_track_id < 0 + + mock_show_ok.assert_called_once_with( + "File not imported", + "isa.mp3 will not be imported because you asked not to import this file", + ) + + def test_003_import_first_file(self): + """Import file into empty directory""" + + test_track_path = "testdata/isa.mp3" + shutil.copy(test_track_path, self.import_source) + + with patch("file_importer.PickMatch") as MockPickMatch: + # Create a mock instance of PickMatch + mock_dialog_instance = MagicMock() + MockPickMatch.return_value = mock_dialog_instance + + # Simulate the user clicking OK in the dialog + mock_dialog_instance.exec.return_value = QDialog.DialogCode.Accepted + mock_dialog_instance.selected_track_id = 0 # Simulated return value + + self.widget.import_files_wrapper() + + # Ensure PickMatch was instantiated correctly + MockPickMatch.assert_called_once_with( + new_track_description="I'm So Afraid (Fleetwood Mac)", + choices=[("Do not import", -1, ""), ("Import as new track", 0, "")], + default=1, + ) + + # Verify exec() was called + mock_dialog_instance.exec.assert_called_once() + + # Ensure selected_track_id was accessed after dialog.exec() + assert mock_dialog_instance.selected_track_id == 0 + + self.wait_for_workers() + + # Check track was imported + tracks = ds.get_all_tracks() + assert len(tracks) == 1 + track = tracks[0] + assert track.title == "I'm So Afraid" + assert track.artist == "Fleetwood Mac" + track_file = os.path.join( + self.musicstore, os.path.basename(test_track_path) + ) + assert track.path == track_file + assert os.path.exists(track_file) + assert os.listdir(self.import_source) == [] + + def test_004_import_second_file(self): + """Import a second file""" + + test_track_path = "testdata/lovecats.mp3" + shutil.copy(test_track_path, self.import_source) + + with patch("file_importer.PickMatch") as MockPickMatch: + # Create a mock instance of PickMatch + mock_dialog_instance = MagicMock() + MockPickMatch.return_value = mock_dialog_instance + + # Simulate the user clicking OK in the dialog + mock_dialog_instance.exec.return_value = QDialog.DialogCode.Accepted + mock_dialog_instance.selected_track_id = 0 # Simulated return value + + self.widget.import_files_wrapper() + + # Ensure PickMatch was instantiated correctly + MockPickMatch.assert_called_once_with( + new_track_description="The Lovecats (The Cure)", + choices=[("Do not import", -1, ""), ("Import as new track", 0, "")], + default=1, + ) + + # Verify exec() was called + mock_dialog_instance.exec.assert_called_once() + + # Ensure selected_track_id was accessed after dialog.exec() + assert mock_dialog_instance.selected_track_id == 0 + + self.wait_for_workers() + + # Check track was imported + tracks = ds.get_all_tracks() + assert len(tracks) == 2 + track = tracks[1] + assert track.title == "The Lovecats" + assert track.artist == "The Cure" + track_file = os.path.join( + self.musicstore, os.path.basename(test_track_path) + ) + assert track.path == track_file + assert os.path.exists(track_file) + assert os.listdir(self.import_source) == [] + + def test_005_replace_file(self): + """Import the same file again and update existing track""" + + test_track_path = "testdata/lovecats.mp3" + shutil.copy(test_track_path, self.import_source) + + with patch("file_importer.PickMatch") as MockPickMatch: + # Create a mock instance of PickMatch + mock_dialog_instance = MagicMock() + MockPickMatch.return_value = mock_dialog_instance + + # Simulate the user clicking OK in the dialog + mock_dialog_instance.exec.return_value = QDialog.DialogCode.Accepted + mock_dialog_instance.selected_track_id = 2 # Simulated return value + + self.widget.import_files_wrapper() + + # Ensure PickMatch was instantiated correctly + MockPickMatch.assert_called_once_with( + new_track_description="The Lovecats (The Cure)", + choices=[ + ("Do not import", -1, ""), + ("Import as new track", 0, ""), + ( + "The Lovecats (The Cure) (100%)", + 2, + os.path.join( + self.musicstore, os.path.basename(test_track_path) + ), + ), + ], + default=2, + ) + + # Verify exec() was called + mock_dialog_instance.exec.assert_called_once() + + self.wait_for_workers() + + # Check track was imported + tracks = ds.get_all_tracks() + assert len(tracks) == 2 + track = tracks[1] + assert track.title == "The Lovecats" + assert track.artist == "The Cure" + assert track.track_id == 2 + track_file = os.path.join( + self.musicstore, os.path.basename(test_track_path) + ) + assert track.path == track_file + assert os.path.exists(track_file) + assert os.listdir(self.import_source) == [] + + def test_006_import_file_no_tags(self) -> None: + """Try to import untagged file""" + + test_track_path = "testdata/lovecats.mp3" + test_filename = os.path.basename(test_track_path) + + shutil.copy(test_track_path, self.import_source) + import_file = os.path.join(self.import_source, test_filename) + assert os.path.exists(import_file) + + # Remove tags + src = MP3(import_file) + src.delete() + src.save() + + with patch("file_importer.show_OK") as mock_show_ok: + self.widget.import_files_wrapper() + mock_show_ok.assert_called_once_with( + "File not imported", + f"{test_filename} will not be imported because of tag errors " + f"(Missing tags in {import_file})", + ) + + def test_007_import_unreadable_file(self) -> None: + """Import unreadable file""" + + test_track_path = "testdata/lovecats.mp3" + test_filename = os.path.basename(test_track_path) + + shutil.copy(test_track_path, self.import_source) + import_file = os.path.join(self.import_source, test_filename) + assert os.path.exists(import_file) + + # Make undreadable + os.chmod(import_file, 0) + + with patch("file_importer.show_OK") as mock_show_ok: + self.widget.import_files_wrapper() + mock_show_ok.assert_called_once_with( + "File not imported", + f"{test_filename} will not be imported because {import_file} is unreadable", + ) + + # clean up + os.chmod(import_file, 0o777) + os.unlink(import_file) + + def test_008_import_new_file_existing_destination(self) -> None: + """Import duplicate file""" + + test_track_path = "testdata/lovecats.mp3" + test_filename = os.path.basename(test_track_path) + new_destination = os.path.join(self.musicstore, "lc2.mp3") + + shutil.copy(test_track_path, self.import_source) + import_file = os.path.join(self.import_source, test_filename) + assert os.path.exists(import_file) + + with ( + patch("file_importer.PickMatch") as MockPickMatch, + patch.object( + QFileDialog, "getSaveFileName", return_value=(new_destination, "") + ) as mock_file_dialog, + patch("file_importer.show_OK") as mock_show_ok, + ): + mock_file_dialog.return_value = ( + new_destination, + "", + ) # Ensure mock correctly returns expected value + + # Create a mock instance of PickMatch + mock_dialog_instance = MagicMock() + MockPickMatch.return_value = mock_dialog_instance + + # Simulate the user clicking OK in the dialog + mock_dialog_instance.exec.return_value = QDialog.DialogCode.Accepted + mock_dialog_instance.selected_track_id = 0 # Simulated return value + + self.widget.import_files_wrapper() + + # Ensure PickMatch was instantiated correctly + MockPickMatch.assert_called_once_with( + new_track_description="The Lovecats (The Cure)", + choices=[ + ("Do not import", -1, ""), + ("Import as new track", 0, ""), + ( + "The Lovecats (The Cure) (100%)", + 2, + os.path.join( + self.musicstore, os.path.basename(test_track_path) + ), + ), + ], + default=2, + ) + + # Verify exec() was called + mock_dialog_instance.exec.assert_called_once() + + destination = os.path.join(self.musicstore, test_filename) + mock_show_ok.assert_called_once_with( + title="Desintation path exists", + msg=f"New import requested but default destination path ({destination}) " + "already exists. Click OK and choose where to save this track", + parent=None, + ) + + self.wait_for_workers() + + # Ensure QFileDialog was called and returned expected value + assert mock_file_dialog.called # Ensure the mock was used + result = mock_file_dialog() + assert result[0] == new_destination # Validate return value + + # Check track was imported + tracks = ds.get_all_tracks() + track = tracks[2] + assert track.title == "The Lovecats" + assert track.artist == "The Cure" + assert track.track_id == 3 + assert track.path == new_destination + assert os.path.exists(new_destination) + assert os.listdir(self.import_source) == [] + + # Remove file so as not to interfere with later tests + ds.delete(track) + tracks = ds.get_all_tracks() + assert len(tracks) == 2 + + os.unlink(new_destination) + assert not os.path.exists(new_destination) + + def test_009_import_similar_file(self) -> None: + """Import file with similar, but different, title""" + + test_track_path = "testdata/lovecats.mp3" + test_filename = os.path.basename(test_track_path) + + shutil.copy(test_track_path, self.import_source) + import_file = os.path.join(self.import_source, test_filename) + assert os.path.exists(import_file) + + # Change title tag + src = MP3(import_file) + src["TIT2"].text[0] += " xyz" + src.save() + + with patch("file_importer.PickMatch") as MockPickMatch: + # Create a mock instance of PickMatch + mock_dialog_instance = MagicMock() + MockPickMatch.return_value = mock_dialog_instance + + # Simulate the user clicking OK in the dialog + mock_dialog_instance.exec.return_value = QDialog.DialogCode.Accepted + mock_dialog_instance.selected_track_id = 2 # Simulated return value + + self.widget.import_files_wrapper() + + # Ensure PickMatch was instantiated correctly + MockPickMatch.assert_called_once_with( + new_track_description="The Lovecats xyz (The Cure)", + choices=[ + ("Do not import", -1, ""), + ("Import as new track", 0, ""), + ( + "The Lovecats (The Cure) (93%)", + 2, + os.path.join( + self.musicstore, os.path.basename(test_track_path) + ), + ), + ], + default=2, + ) + + # Verify exec() was called + mock_dialog_instance.exec.assert_called_once() + + self.wait_for_workers() + + # Check track was imported + tracks = ds.get_all_tracks() + assert len(tracks) == 2 + track = tracks[1] + assert track.title == "The Lovecats xyz" + assert track.artist == "The Cure" + assert track.track_id == 2 + track_file = os.path.join( + self.musicstore, os.path.basename(test_track_path) + ) + assert track.path == track_file + assert os.path.exists(track_file) + assert os.listdir(self.import_source) == [] diff --git a/tests/test_repository.py b/tests/test_ds.py similarity index 73% rename from tests/test_repository.py rename to tests/test_ds.py index 4219d85..3a416e9 100644 --- a/tests/test_repository.py +++ b/tests/test_ds.py @@ -7,7 +7,7 @@ import unittest # App imports from app import playlistmodel -from app import repository +from app import ds from app.models import db from classes import PlaylistDTO from helpers import get_all_track_metadata @@ -41,7 +41,7 @@ class MyTestCase(unittest.TestCase): self, playlist_name: str ) -> (PlaylistDTO, PlaylistModel): # Create a playlist and model - playlist = repository.create_playlist(name=playlist_name, template_id=0) + playlist = ds.create_playlist(name=playlist_name, template_id=0) assert playlist model = playlistmodel.PlaylistModel(playlist.playlist_id, is_template=False) assert model @@ -52,25 +52,25 @@ class MyTestCase(unittest.TestCase): (playlist, model) = self.create_playlist_and_model(playlist_name) # Create tracks metadata1 = get_all_track_metadata(self.isa_path) - self.track1 = repository.create_track(self.isa_path, metadata1) + self.track1 = ds.create_track(self.isa_path, metadata1) metadata2 = get_all_track_metadata(self.mom_path) - self.track2 = repository.create_track(self.mom_path, metadata2) + self.track2 = ds.create_track(self.mom_path, metadata2) # Add tracks and header to playlist - self.row0 = repository.insert_row( + self.row0 = ds.insert_row( playlist.playlist_id, row_number=0, track_id=self.track1.track_id, note="track 1", ) - self.row1 = repository.insert_row( + self.row1 = ds.insert_row( playlist.playlist_id, row_number=1, track_id=0, note="Header row", ) - self.row2 = repository.insert_row( + self.row2 = ds.insert_row( playlist.playlist_id, row_number=2, track_id=self.track2.track_id, @@ -82,7 +82,7 @@ class MyTestCase(unittest.TestCase): ) -> (PlaylistDTO, PlaylistModel): (playlist, model) = self.create_playlist_and_model(playlist_name) for row_number in range(number_of_rows): - repository.insert_row( + ds.insert_row( playlist.playlist_id, row_number, None, str(row_number) ) @@ -97,60 +97,60 @@ class MyTestCase(unittest.TestCase): """Add a track to a header row""" self.create_playlist_model_tracks("my playlist") - repository.add_track_to_header(self.row1.playlistrow_id, self.track2.track_id) - result = repository.get_playlist_row(self.row1.playlistrow_id) + ds.add_track_to_header(self.row1.playlistrow_id, self.track2.track_id) + result = ds.get_playlist_row(self.row1.playlistrow_id) assert result.track.track_id == self.track2.track_id def test_create_track(self): metadata = get_all_track_metadata(self.isa_path) - repository.create_track(self.isa_path, metadata) - results = repository.get_all_tracks() + ds.create_track(self.isa_path, metadata) + results = ds.get_all_tracks() assert len(results) == 1 assert results[0].path == self.isa_path def test_get_track_by_id(self): metadata = get_all_track_metadata(self.isa_path) - dto = repository.create_track(self.isa_path, metadata) - result = repository.track_by_id(dto.track_id) + dto = ds.create_track(self.isa_path, metadata) + result = ds.track_by_id(dto.track_id) assert result.path == self.isa_path def test_get_track_by_artist(self): metadata = get_all_track_metadata(self.isa_path) - _ = repository.create_track(self.isa_path, metadata) + _ = ds.create_track(self.isa_path, metadata) metadata = get_all_track_metadata(self.mom_path) - _ = repository.create_track(self.mom_path, metadata) - result_isa = repository.tracks_by_artist(self.isa_artist) + _ = ds.create_track(self.mom_path, metadata) + result_isa = ds.tracks_by_artist(self.isa_artist) assert len(result_isa) == 1 assert result_isa[0].artist == self.isa_artist - result_mom = repository.tracks_by_artist(self.mom_artist) + result_mom = ds.tracks_by_artist(self.mom_artist) assert len(result_mom) == 1 assert result_mom[0].artist == self.mom_artist def test_get_track_by_title(self): metadata_isa = get_all_track_metadata(self.isa_path) - _ = repository.create_track(self.isa_path, metadata_isa) + _ = ds.create_track(self.isa_path, metadata_isa) metadata_mom = get_all_track_metadata(self.mom_path) - _ = repository.create_track(self.mom_path, metadata_mom) - result_isa = repository.tracks_by_title(self.isa_title) + _ = ds.create_track(self.mom_path, metadata_mom) + result_isa = ds.tracks_by_title(self.isa_title) assert len(result_isa) == 1 assert result_isa[0].title == self.isa_title - result_mom = repository.tracks_by_title(self.mom_title) + result_mom = ds.tracks_by_title(self.mom_title) assert len(result_mom) == 1 assert result_mom[0].title == self.mom_title def test_tracks_get_all_tracks(self): self.create_playlist_model_tracks(playlist_name="test_track_get_all_tracks") - all_tracks = repository.get_all_tracks() + all_tracks = ds.get_all_tracks() assert len(all_tracks) == 2 def test_tracks_by_path(self): metadata_isa = get_all_track_metadata(self.isa_path) - _ = repository.create_track(self.isa_path, metadata_isa) + _ = ds.create_track(self.isa_path, metadata_isa) metadata_mom = get_all_track_metadata(self.mom_path) - _ = repository.create_track(self.mom_path, metadata_mom) - result_isa = repository.track_by_path(self.isa_path) + _ = ds.create_track(self.mom_path, metadata_mom) + result_isa = ds.track_by_path(self.isa_path) assert result_isa.title == self.isa_title - result_mom = repository.track_by_path(self.mom_path) + result_mom = ds.track_by_path(self.mom_path) assert result_mom.title == self.mom_title def test_move_rows_test1(self): @@ -159,11 +159,11 @@ class MyTestCase(unittest.TestCase): number_of_rows = 10 (playlist, model) = self.create_rows("test_move_rows_test1", number_of_rows) - repository.move_rows([3], playlist.playlist_id, 5) + ds.move_rows([3], playlist.playlist_id, 5) # Check we have all rows and plr_rownums are correct new_order = [] - for row in repository.get_playlist_rows(playlist.playlist_id): + for row in ds.get_playlist_rows(playlist.playlist_id): new_order.append(int(row.note)) assert new_order == [0, 1, 2, 4, 5, 3, 6, 7, 8, 9] @@ -173,11 +173,11 @@ class MyTestCase(unittest.TestCase): number_of_rows = 10 (playlist, model) = self.create_rows("test_move_rows_test2", number_of_rows) - repository.move_rows([4], playlist.playlist_id, 3) + ds.move_rows([4], playlist.playlist_id, 3) # Check we have all rows and plr_rownums are correct new_order = [] - for row in repository.get_playlist_rows(playlist.playlist_id): + for row in ds.get_playlist_rows(playlist.playlist_id): new_order.append(int(row.note)) assert new_order == [0, 1, 2, 4, 3, 5, 6, 7, 8, 9] @@ -187,11 +187,11 @@ class MyTestCase(unittest.TestCase): number_of_rows = 10 (playlist, model) = self.create_rows("test_move_rows_test3", number_of_rows) - repository.move_rows([4], playlist.playlist_id, 2) + ds.move_rows([4], playlist.playlist_id, 2) # Check we have all rows and plr_rownums are correct new_order = [] - for row in repository.get_playlist_rows(playlist.playlist_id): + for row in ds.get_playlist_rows(playlist.playlist_id): new_order.append(int(row.note)) assert new_order == [0, 1, 4, 2, 3, 5, 6, 7, 8, 9] @@ -201,11 +201,11 @@ class MyTestCase(unittest.TestCase): number_of_rows = 11 (playlist, model) = self.create_rows("test_move_rows_test4", number_of_rows) - repository.move_rows([1, 4, 5, 10], playlist.playlist_id, 8) + ds.move_rows([1, 4, 5, 10], playlist.playlist_id, 8) # Check we have all rows and plr_rownums are correct new_order = [] - for row in repository.get_playlist_rows(playlist.playlist_id): + for row in ds.get_playlist_rows(playlist.playlist_id): new_order.append(int(row.note)) assert new_order == [0, 2, 3, 6, 7, 8, 1, 4, 5, 10, 9] @@ -215,11 +215,11 @@ class MyTestCase(unittest.TestCase): number_of_rows = 11 (playlist, model) = self.create_rows("test_move_rows_test5", number_of_rows) - repository.move_rows([3, 6], playlist.playlist_id, 5) + ds.move_rows([3, 6], playlist.playlist_id, 5) # Check we have all rows and plr_rownums are correct new_order = [] - for row in repository.get_playlist_rows(playlist.playlist_id): + for row in ds.get_playlist_rows(playlist.playlist_id): new_order.append(int(row.note)) assert new_order == [0, 1, 2, 4, 5, 3, 6, 7, 8, 9, 10] @@ -229,11 +229,11 @@ class MyTestCase(unittest.TestCase): number_of_rows = 11 (playlist, model) = self.create_rows("test_move_rows_test6", number_of_rows) - repository.move_rows([3, 5, 6], playlist.playlist_id, 8) + ds.move_rows([3, 5, 6], playlist.playlist_id, 8) # Check we have all rows and plr_rownums are correct new_order = [] - for row in repository.get_playlist_rows(playlist.playlist_id): + for row in ds.get_playlist_rows(playlist.playlist_id): new_order.append(int(row.note)) assert new_order == [0, 1, 2, 4, 7, 8, 9, 10, 3, 5, 6] @@ -243,11 +243,11 @@ class MyTestCase(unittest.TestCase): number_of_rows = 11 (playlist, model) = self.create_rows("test_move_rows_test7", number_of_rows) - repository.move_rows([7, 8, 10], playlist.playlist_id, 5) + ds.move_rows([7, 8, 10], playlist.playlist_id, 5) # Check we have all rows and plr_rownums are correct new_order = [] - for row in repository.get_playlist_rows(playlist.playlist_id): + for row in ds.get_playlist_rows(playlist.playlist_id): new_order.append(int(row.note)) assert new_order == [0, 1, 2, 3, 4, 7, 8, 10, 5, 6, 9] @@ -258,11 +258,11 @@ class MyTestCase(unittest.TestCase): number_of_rows = 11 (playlist, model) = self.create_rows("test_move_rows_test8", number_of_rows) - repository.move_rows([0, 1, 2, 3], playlist.playlist_id, 0) + ds.move_rows([0, 1, 2, 3], playlist.playlist_id, 0) # Check we have all rows and plr_rownums are correct new_order = [] - for row in repository.get_playlist_rows(playlist.playlist_id): + for row in ds.get_playlist_rows(playlist.playlist_id): new_order.append(int(row.note)) assert new_order == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] @@ -274,17 +274,17 @@ class MyTestCase(unittest.TestCase): (playlist_src, model_src) = self.create_rows("src playlist", number_of_rows) (playlist_dst, model_dst) = self.create_rows("dst playlist", number_of_rows) - repository.move_rows( + ds.move_rows( rows_to_move, playlist_src.playlist_id, to_row, playlist_dst.playlist_id ) # Check we have all rows and plr_rownums are correct new_order_src = [] - for row in repository.get_playlist_rows(playlist_src.playlist_id): + for row in ds.get_playlist_rows(playlist_src.playlist_id): new_order_src.append(int(row.note)) assert new_order_src == [0, 1, 3, 5, 7, 8, 9, 10] new_order_dst = [] - for row in repository.get_playlist_rows(playlist_dst.playlist_id): + for row in ds.get_playlist_rows(playlist_dst.playlist_id): new_order_dst.append(int(row.note)) assert new_order_dst == [0, 1, 2, 3, 4, 2, 4, 6, 5, 6, 7, 8, 9, 10] diff --git a/tests/test_file_importer.py b/tests/test_file_importer.py index f49e557..7544cd4 100644 --- a/tests/test_file_importer.py +++ b/tests/test_file_importer.py @@ -20,12 +20,8 @@ import pytest from pytestqt.plugin import QtBot # type: ignore # App imports -from app import musicmuster -from app.models import ( - db, - Playlists, - Tracks, -) +from app import ds, musicmuster +from app.models import db from config import Config from file_importer import FileImporter @@ -56,9 +52,8 @@ class MyTestCase(unittest.TestCase): # Create a playlist for all tests playlist_name = "file importer playlist" - with db.Session() as session: - playlist = Playlists(session=session, name=playlist_name, template_id=0) - cls.widget._open_playlist(playlist) + playlist = ds.create_playlist(name=playlist_name, template_id=0) + cls.widget._open_playlist(playlist) # Create our musicstore cls.import_source = tempfile.mkdtemp(suffix="_MMsource_pytest", dir="/tmp") @@ -176,18 +171,17 @@ class MyTestCase(unittest.TestCase): self.wait_for_workers() # Check track was imported - with db.Session() as session: - tracks = Tracks.get_all(session) - assert len(tracks) == 1 - track = tracks[0] - assert track.title == "I'm So Afraid" - assert track.artist == "Fleetwood Mac" - track_file = os.path.join( - self.musicstore, os.path.basename(test_track_path) - ) - assert track.path == track_file - assert os.path.exists(track_file) - assert os.listdir(self.import_source) == [] + tracks = ds.get_all_tracks() + assert len(tracks) == 1 + track = tracks[0] + assert track.title == "I'm So Afraid" + assert track.artist == "Fleetwood Mac" + track_file = os.path.join( + self.musicstore, os.path.basename(test_track_path) + ) + assert track.path == track_file + assert os.path.exists(track_file) + assert os.listdir(self.import_source) == [] def test_004_import_second_file(self): """Import a second file""" @@ -222,18 +216,17 @@ class MyTestCase(unittest.TestCase): self.wait_for_workers() # Check track was imported - with db.Session() as session: - tracks = Tracks.get_all(session) - assert len(tracks) == 2 - track = tracks[1] - assert track.title == "The Lovecats" - assert track.artist == "The Cure" - track_file = os.path.join( - self.musicstore, os.path.basename(test_track_path) - ) - assert track.path == track_file - assert os.path.exists(track_file) - assert os.listdir(self.import_source) == [] + tracks = ds.get_all_tracks() + assert len(tracks) == 2 + track = tracks[1] + assert track.title == "The Lovecats" + assert track.artist == "The Cure" + track_file = os.path.join( + self.musicstore, os.path.basename(test_track_path) + ) + assert track.path == track_file + assert os.path.exists(track_file) + assert os.listdir(self.import_source) == [] def test_005_replace_file(self): """Import the same file again and update existing track""" @@ -275,19 +268,18 @@ class MyTestCase(unittest.TestCase): self.wait_for_workers() # Check track was imported - with db.Session() as session: - tracks = Tracks.get_all(session) - assert len(tracks) == 2 - track = tracks[1] - assert track.title == "The Lovecats" - assert track.artist == "The Cure" - assert track.id == 2 - track_file = os.path.join( - self.musicstore, os.path.basename(test_track_path) - ) - assert track.path == track_file - assert os.path.exists(track_file) - assert os.listdir(self.import_source) == [] + tracks = ds.get_all_tracks() + assert len(tracks) == 2 + track = tracks[1] + assert track.title == "The Lovecats" + assert track.artist == "The Cure" + assert track.track_id == 2 + track_file = os.path.join( + self.musicstore, os.path.basename(test_track_path) + ) + assert track.path == track_file + assert os.path.exists(track_file) + assert os.listdir(self.import_source) == [] def test_006_import_file_no_tags(self) -> None: """Try to import untagged file""" @@ -405,25 +397,22 @@ class MyTestCase(unittest.TestCase): assert result[0] == new_destination # Validate return value # Check track was imported - with db.Session() as session: - tracks = Tracks.get_all(session) - assert len(tracks) == 3 - track = tracks[2] - assert track.title == "The Lovecats" - assert track.artist == "The Cure" - assert track.id == 3 - assert track.path == new_destination - assert os.path.exists(new_destination) - assert os.listdir(self.import_source) == [] + tracks = ds.get_all_tracks() + track = tracks[2] + assert track.title == "The Lovecats" + assert track.artist == "The Cure" + assert track.track_id == 3 + assert track.path == new_destination + assert os.path.exists(new_destination) + assert os.listdir(self.import_source) == [] - # Remove file so as not to interfere with later tests - session.delete(track) - tracks = Tracks.get_all(session) - assert len(tracks) == 2 - session.commit() + # Remove file so as not to interfere with later tests + ds.delete(track) + tracks = ds.get_all_tracks() + assert len(tracks) == 2 - os.unlink(new_destination) - assert not os.path.exists(new_destination) + os.unlink(new_destination) + assert not os.path.exists(new_destination) def test_009_import_similar_file(self) -> None: """Import file with similar, but different, title""" @@ -474,16 +463,15 @@ class MyTestCase(unittest.TestCase): self.wait_for_workers() # Check track was imported - with db.Session() as session: - tracks = Tracks.get_all(session) - assert len(tracks) == 2 - track = tracks[1] - assert track.title == "The Lovecats xyz" - assert track.artist == "The Cure" - assert track.id == 2 - track_file = os.path.join( - self.musicstore, os.path.basename(test_track_path) - ) - assert track.path == track_file - assert os.path.exists(track_file) - assert os.listdir(self.import_source) == [] + tracks = ds.get_all_tracks() + assert len(tracks) == 2 + track = tracks[1] + assert track.title == "The Lovecats xyz" + assert track.artist == "The Cure" + assert track.track_id == 2 + track_file = os.path.join( + self.musicstore, os.path.basename(test_track_path) + ) + assert track.path == track_file + assert os.path.exists(track_file) + assert os.listdir(self.import_source) == [] diff --git a/tests/test_helpers.py b/tests/test_helpers.py index e7cd365..0a2418a 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -64,9 +64,9 @@ class TestMMHelpers(unittest.TestCase): today_at_11 = dt.datetime.now().replace(hour=11, minute=0) assert get_relative_date(today_at_10, today_at_11) == "Today 10:00" eight_days_ago = today_at_10 - dt.timedelta(days=8) - assert get_relative_date(eight_days_ago, today_at_11) == "1 week, 1 day" + assert get_relative_date(eight_days_ago, today_at_11) == "1w, 1d" sixteen_days_ago = today_at_10 - dt.timedelta(days=16) - assert get_relative_date(sixteen_days_ago, today_at_11) == "2 weeks, 2 days" + assert get_relative_date(sixteen_days_ago, today_at_11) == "2w, 2d" def test_leading_silence(self): test_track_path = "testdata/isa.mp3" diff --git a/tests/test_playlistmodel.py b/tests/test_playlistmodel.py index 1d8d3d3..ec2344a 100644 --- a/tests/test_playlistmodel.py +++ b/tests/test_playlistmodel.py @@ -14,6 +14,9 @@ from app.models import ( Playlists, Tracks, ) +from classes import ( + InsertTrack, +) class TestMMMiscTracks(unittest.TestCase): @@ -35,14 +38,14 @@ class TestMMMiscTracks(unittest.TestCase): # Create a playlist and model with db.Session() as session: self.playlist = Playlists(session, PLAYLIST_NAME, template_id=0) - self.model = playlistmodel.PlaylistModel(self.playlist.id, is_template=False) + self.model = playlistmodel.PlaylistModel( + self.playlist.id, is_template=False + ) for row in range(len(self.test_tracks)): track_path = self.test_tracks[row % len(self.test_tracks)] track = Tracks(session, **get_all_track_metadata(track_path)) - self.model.insert_row( - proposed_row_number=row, track_id=track.id, note=f"{row=}" - ) + self.model.insert_row(track_id=track.id, note=f"{row=}") session.commit() @@ -62,8 +65,11 @@ class TestMMMiscTracks(unittest.TestCase): START_ROW = 0 END_ROW = 2 - self.model.insert_row(proposed_row_number=START_ROW, note="start+") - self.model.insert_row(proposed_row_number=END_ROW, note="-") + # Fake selected row in model + self.model.selected_rows = [self.model.playlist_rows[START_ROW]] + self.model.insert_row(note="start+") + self.model.selected_rows = [self.model.playlist_rows[END_ROW]] + self.model.insert_row(note="-") prd = self.model.playlist_rows[START_ROW] qv_value = self.model._display_role( @@ -102,7 +108,7 @@ class TestMMMiscNoPlaylist(unittest.TestCase): track_path = self.test_tracks[0] metadata = get_all_track_metadata(track_path) track = Tracks(session, **metadata) - model.insert_row(proposed_row_number=0, track_id=track.id) + model.insert_row(track_id=track.id) prd = model.playlist_rows[model.rowCount() - 1] # test repr @@ -125,9 +131,11 @@ class TestMMMiscRowMove(unittest.TestCase): with db.Session() as session: self.playlist = Playlists(session, self.PLAYLIST_NAME, template_id=0) - self.model = playlistmodel.PlaylistModel(self.playlist.id, is_template=False) + self.model = playlistmodel.PlaylistModel( + self.playlist.id, is_template=False + ) for row in range(self.ROWS_TO_CREATE): - self.model.insert_row(proposed_row_number=row, note=str(row)) + self.model.insert_row(note=str(row)) session.commit() @@ -140,7 +148,7 @@ class TestMMMiscRowMove(unittest.TestCase): note_text = "test text" assert self.model.rowCount() == self.ROWS_TO_CREATE - self.model.insert_row(proposed_row_number=None, note=note_text) + self.model.insert_row(note=note_text) assert self.model.rowCount() == self.ROWS_TO_CREATE + 1 prd = self.model.playlist_rows[self.model.rowCount() - 1] # Test against edit_role because display_role for headers is @@ -158,7 +166,7 @@ class TestMMMiscRowMove(unittest.TestCase): note_text = "test text" insert_row = 6 - self.model.insert_row(proposed_row_number=insert_row, note=note_text) + self.model.insert_row(note=note_text) assert self.model.rowCount() == self.ROWS_TO_CREATE + 1 prd = self.model.playlist_rows[insert_row] # Test against edit_role because display_role for headers is @@ -174,11 +182,21 @@ class TestMMMiscRowMove(unittest.TestCase): note_text = "test text" insert_row = 6 - self.model.insert_row(proposed_row_number=insert_row, note=note_text) + self.model.insert_row(note=note_text) assert self.model.rowCount() == self.ROWS_TO_CREATE + 1 + # Fake selected row in model + self.model.selected_rows = [self.model.playlist_rows[insert_row]] + prd = self.model.playlist_rows[1] - self.model.add_track_to_header(insert_row, prd.track_id) + import pdb; pdb.set_trace() + self.model.add_track_to_header( + InsertTrack( + playlist_id=self.model.playlist_id, + track_id=prd.track_id, + note=note_text, + ) + ) def test_reverse_row_groups_one_row(self): rows_to_move = [3] @@ -205,7 +223,7 @@ class TestMMMiscRowMove(unittest.TestCase): playlist_dst = Playlists(session, destination_playlist, template_id=0) model_dst = playlistmodel.PlaylistModel(playlist_dst.id, is_template=False) for row in range(self.ROWS_TO_CREATE): - model_dst.insert_row(proposed_row_number=row, note=str(row)) + model_dst.insert_row(note=str(row)) model_src.move_rows_between_playlists(from_rows, to_row, model_dst.playlist_id) model_dst.refresh_data(session) @@ -226,7 +244,7 @@ class TestMMMiscRowMove(unittest.TestCase): playlist_dst = Playlists(session, destination_playlist, template_id=0) model_dst = playlistmodel.PlaylistModel(playlist_dst.id, is_template=False) for row in range(self.ROWS_TO_CREATE): - model_dst.insert_row(proposed_row_number=row, note=str(row)) + model_dst.insert_row(note=str(row)) model_src.move_rows_between_playlists(from_rows, to_row, model_dst.playlist_id) model_dst.refresh_data(session) @@ -253,7 +271,7 @@ class TestMMMiscRowMove(unittest.TestCase): playlist_dst = Playlists(session, destination_playlist, template_id=0) model_dst = playlistmodel.PlaylistModel(playlist_dst.id, is_template=False) for row in range(self.ROWS_TO_CREATE): - model_dst.insert_row(proposed_row_number=row, note=str(row)) + model_dst.insert_row(note=str(row)) model_src.move_rows_between_playlists(from_rows, to_row, model_dst.playlist_id) model_dst.refresh_data(session)