def __on_folder_changed(self, widget): """ Update the location in the database. Start an import scan or a rebase operation. """ new_path = self.location_chooser.get_file().get_path() # First test if the new location is already in the database if Storage.select().where(Storage.path == new_path).count() > 0: return # If not, add it to the database old_path = Storage.select().where(Storage.id == self.db_id).get().path self.path = new_path is_external = self._filesystem_monitor.is_external(new_path) Storage.update( path=new_path, external=is_external).where(Storage.id == self.db_id).execute() self._settings.invalidate() self.set_external(is_external) # Run a reimport or rebase if old_path == "": self.parent.emit_event("storage-added", self.path) log.info("New audiobook location added. Starting import scan.") self.ui.scan(None, None) else: self.parent.emit_event("storage-changed", self.path) log.info( "Audio book location changed, rebasing the location in cozy.") self._block_list.rebase_path(old_path, new_path) thread = Thread(target=self._library.rebase_path, args=(old_path, new_path), name="RebaseStorageLocationThread") thread.start()
def __update_db_3(db): current_path = Settings.get().path db.create_tables([Storage]) Storage.create(path=current_path, default=True) Settings.update(path="NOT_USED").execute() Settings.update(version=3).execute()
def copy_to_audiobook_folder(path): """ Copies the given path (folder or file) to the audio book folder. """ try: name = os.path.basename(os.path.normpath(path)) shutil.copytree( path, Storage.select().where(Storage.default == True).get().path + "/" + name) except OSError as exc: reporter.exception("importer", exc) if exc.errno == errno.ENOTDIR: try: shutil.copy( path, Storage.select().where(Storage.default == True).get().path) except OSError as e: if e.errno == 95: log.error("Could not import file " + path) log.error(exc) else: log.error(e) elif exc.errno == errno.ENOTSUP: log.error("Could not import file " + path) log.error(exc) else: log.error("Could not import file " + path) log.error(exc)
def __on_folder_changed(self, widget): """ Update the location in the database. Start an import scan or a rebase operation. """ new_path = self.location_chooser.get_file().get_path() # First test if the new location is already in the database if Storage.select().where(Storage.path == new_path).count() > 0: return # If not, add it to the database old_path = Storage.select().where(Storage.id == self.db_id).get().path self.path = new_path Storage.update(path=new_path).where(Storage.id == self.db_id).execute() # Run a reimport or rebase if old_path == "": self.parent.emit_event("storage-added", self.path) log.info("New audiobook location added. Starting import scan.") self.ui.scan(None, False) else: self.parent.emit_event("storage-changed", self.path) log.info( "Audio book location changed, rebasing the location in cozy.") self.ui.switch_to_working(_("Changing audio book location…"), False) thread = Thread(target=importer.rebase_location, args=(self.ui, old_path, new_path), name="RebaseStorageLocationThread") thread.start()
def peewee_database(): from cozy.db.track import Track from cozy.db.book import Book from cozy.db.settings import Settings from cozy.db.storage_blacklist import StorageBlackList from cozy.db.storage import Storage db_path, models, test_db = prepare_db() path_of_test_folder = os.path.dirname(os.path.realpath(__file__)) + '/' with open(path_of_test_folder + 'books.json') as json_file: book_data = json.load(json_file) with open(path_of_test_folder + 'tracks.json') as json_file: track_data = json.load(json_file) Book.insert_many(book_data).execute() for chunk in chunks(track_data, 25): Track.insert_many(chunk).execute() with open(path_of_test_folder + 'storages.json') as json_file: storage_data = json.load(json_file) Storage.insert_many(storage_data).execute() Settings.create(path="", last_played_book=Book.get()) StorageBlackList.create(path="/path/to/replace/test1.mp3") StorageBlackList.create(path="/path/to/not/replace/test2.mp3") print("Provide database...") yield test_db teardown_db(db_path, models, test_db)
def set_default(self, default): """ Set this storage location as the default :param default: Boolean """ self.default = default self.default_image.set_visible(default) Storage.update(default=default).where(Storage.id == self.db_id).execute()
def test_ensure_default_storage_present_does_nothing_if_default_is_present(peewee_database): from cozy.model.settings import Settings from cozy.db.storage import Storage settings = Settings() settings._load_all_storage_locations() settings._ensure_default_storage_present() assert not Storage.get(1).default assert Storage.get(2).default
def __on_remove_storage_clicked(self, widget): """ Remove a storage selector from the ui and database. """ row = self.storage_list_box.get_selected_row() Storage.select().where(Storage.path == row.path).get().delete_instance() self.storage_list_box.remove(row) self.emit_event("storage-removed", row.path) thread = Thread(target=remove_tracks_with_path, args=(self.ui, row.path), name=("RemoveStorageFromDB")) thread.start() self.__on_storage_box_changed(None, None)
def test_ensure_default_storage_present_adds_default_if_not_present(peewee_database): from cozy.model.settings import Settings from cozy.db.storage import Storage Storage.update(default=False).where(Storage.id == 2).execute() settings = Settings() settings._load_all_storage_locations() settings._ensure_default_storage_present() assert Storage.get(1).default assert not Storage.get(2).default
def __on_no_media_folder_changed(self, sender): """ Get's called when the user changes the audiobook location from the no media screen. Now we want to do a first scan instead of a rebase. """ location = self.no_media_file_chooser.get_file().get_path() external = self.external_switch.get_active() Storage.delete().where(Storage.path != "").execute() Storage.create(path=location, default=True, external=external) self.main_stack.props.visible_child_name = "import" self.scan(None, True) self.settings._init_storage() self.fs_monitor.init_offline_mode()
def set_external(self, external): """ Set this entry as external/internal storage. This method also writes the setting to the cozy. """ self.external = external if external: self.type_image.set_from_icon_name("network-server-symbolic", Gtk.IconSize.LARGE_TOOLBAR) self.type_image.set_tooltip_text(_("External drive")) else: self.type_image.set_from_icon_name("drive-harddisk-symbolic", Gtk.IconSize.LARGE_TOOLBAR) self.type_image.set_tooltip_text(_("Internal drive")) Storage.update(external=external).where(Storage.id == self.db_id).execute()
def check_for_tracks(self): """ Check if there are any imported files. If there aren't display a welcome screen. """ if books().count() < 1: path = "" if Storage.select().count() > 0: path = Storage.select().where( Storage.default == True).get().path self.no_media_file_chooser.set_current_folder(path) self.main_stack.props.visible_child_name = "no_media" self.block_ui_buttons(True) self.category_toolbar.set_visible(False)
def get_external_storage_locations(): """ Returns a list of all external storage locations. """ directories = Storage.select().where(Storage.external == True) return directories
def __on_add_storage_clicked(self, widget): """ Add a new storage selector to the ui. """ db_obj = Storage.create(path="") self.storage_list_box.add( StorageListBoxRow(self, db_obj.id, "", False, False))
def is_external(book): """ Tests whether the given book is saved on external storage. """ return any(storage.path in Track.select().join(Book).where( Book.id == book.id).first().file for storage in Storage.select().where(Storage.external == True))
def _load_all_storage_locations(self): for storage_db_obj in StorageModel.select(StorageModel.id): try: self._storages.append(Storage(self._db, storage_db_obj.id)) except InvalidPath: log.error( "Invalid path found in database, skipping: {}".format( storage_db_obj.path))
def do_activate(self): main_window_builder = self.ui.get_builder() self.app_controller = AppController(self, main_window_builder, self.ui) self.ui.activate(self.app_controller.library_view) if Settings.get().first_start: Settings.update(first_start=False).execute() path = os.path.join(Path.home(), _("Audiobooks")) Storage.create(path=path, default=True) os.makedirs(path, exist_ok=True) self.add_window(self.ui.window) mpris = MPRIS(self) mpris._on_current_changed()
def test_external_storage_locations_contain_only_external_storages(peewee_database): from cozy.model.settings import Settings from cozy.db.storage import Storage settings = Settings() storage_locations = Storage.select().where(Storage.external == True) assert len(settings.external_storage_locations) == len(storage_locations) assert all([storage.external for storage in settings.external_storage_locations])
def test_setting_path_updates_in_track_object_and_database( peewee_database_storage): from cozy.db.storage import Storage as StorageModel from cozy.model.storage import Storage new_path = "/tmp/media2" storage = Storage(peewee_database_storage, 1) storage.path = new_path assert storage.path == new_path assert StorageModel.get_by_id(1).path == new_path
def test_setting_external_updates_in_track_object_and_database( peewee_database_storage): from cozy.db.storage import Storage as StorageModel from cozy.model.storage import Storage new_external = True storage = Storage(peewee_database_storage, 1) storage.external = new_external assert storage.external == new_external assert StorageModel.get_by_id(1).external == new_external
def test_setting_default_updates_in_track_object_and_database( peewee_database_storage): from cozy.db.storage import Storage as StorageModel from cozy.model.storage import Storage new_default = True storage = Storage(peewee_database_storage, 1) storage.default = new_default assert storage.default == new_default assert StorageModel.get_by_id(1).default == new_default
def test_setting_location_type_updates_in_track_object_and_database( peewee_database_storage): from cozy.db.storage import Storage as StorageModel from cozy.model.storage import Storage new_location_type = 555 storage = Storage(peewee_database_storage, 1) storage.location_type = new_location_type assert storage.location_type == new_location_type assert StorageModel.get_by_id(1).location_type == new_location_type
def peewee_database_storage(): from cozy.db.storage import Storage from cozy.db.settings import Settings from cozy.db.storage_blacklist import StorageBlackList db_path, models, test_db = prepare_db() path_of_test_folder = os.path.dirname(os.path.realpath(__file__)) + '/' with open(path_of_test_folder + 'storages.json') as json_file: storage_data = json.load(json_file) Storage.insert_many(storage_data).execute() Settings.create(path="", last_played_book=None) StorageBlackList.create(path="/path/to/replace/test1.mp3") StorageBlackList.create(path="/path/to/not/replace/test2.mp3") print("Provide database...") yield test_db teardown_db(db_path, models, test_db)
def check_for_tracks(self): """ Check if there are any imported files. If there aren't display a welcome screen. """ if books().count() < 1: path = "" if Storage.select().count() > 0: path = Storage.select().where( Storage.default == True).get().path if not path: path = os.path.join(os.path.expanduser("~"), _("Audiobooks")) if not os.path.exists(path): os.mkdir(path) Storage.create(path=path, default=True) self.no_media_file_chooser.set_current_folder(path) self.main_stack.props.visible_child_name = "no_media" self.block_ui_buttons(True) self.titlebar.stop() self.category_toolbar.set_visible(False)
def test_storage_locations_contains_every_storage_location_from_db(peewee_database): from cozy.model.settings import Settings from cozy.db.storage import Storage settings = Settings() storage_locations = Storage.select() assert len(settings.storage_locations) == len(storage_locations) assert [storage.path for storage in settings.storage_locations] == [storage.path for storage in storage_locations] assert [storage.location_type for storage in settings.storage_locations] == [storage.location_type for storage in storage_locations] assert [storage.default for storage in settings.storage_locations] == [storage.default for storage in storage_locations] assert [storage.external for storage in settings.storage_locations] == [storage.external for storage in storage_locations]
def _init_storage(self): """ Display settings from the database in the ui. """ found_default = False self.storage_list_box.remove_all_children() for location in Storage.select(): row = StorageListBoxRow(self, location.id, location.path, location.external, location.default) self.storage_list_box.add(row) if location.default: if found_default: row.set_default(False) else: found_default = True self.storage_list_box.select_row(row) self.__on_storage_box_changed(None, None)
def test_all_existing_paths_are_included(mocker): from cozy.media.importer import Importer from cozy.db.storage import Storage mocker.patch("os.path.exists", return_value=True) importer = Importer() paths_to_import = importer._get_configured_storage_paths() internal_storages_in_db = Storage.select() internal_storage_paths = [ storage.path for storage in internal_storages_in_db ] assert len(internal_storage_paths) == len(paths_to_import) assert all( [a == b for a, b in zip(internal_storage_paths, paths_to_import)])
def test_external_paths_are_excluded_when_offline(mocker): from cozy.media.importer import Importer from cozy.db.storage import Storage mocker.patch("os.path.exists", return_value=True) mocker.patch( "cozy.control.filesystem_monitor.FilesystemMonitor.is_storage_online", autospec=True, return_value=False) importer = Importer() paths_to_import = importer._get_configured_storage_paths() internal_storages_in_db = Storage.select().where( Storage.external is not False) internal_storage_paths = [ storage.path for storage in internal_storages_in_db ] assert len(internal_storage_paths) == len(paths_to_import) assert all( [a == b for a, b in zip(internal_storage_paths, paths_to_import)])
def _get_db_object(self): self._db_object: StorageModel = StorageModel.get(self.id)
def update_database(ui, force=False): """ Scans the audio book directory for changes and new files. Also removes entries from the db that are no longer existent. """ paths = [] for location in Storage.select(): if os.path.exists(location.path): paths.append(location.path) # clean artwork cache artwork_cache.delete_artwork_cache() # are UI buttons currently blocked? player_blocked, importer_blocked = ui.get_ui_buttons_blocked() i = 0 percent_counter = 0 file_count = 0 for path in paths: file_count += sum([len(files) for r, d, files in os.walk(path)]) percent_threshold = file_count / 1000 failed = "" tracks_to_import = [] # Tracks which changed and need to be updated if they are cached tracks_cache_update = [] start = time.time() for path in paths: for directory, subdirectories, files in os.walk(path): for file in files: if file.lower().endswith(('.mp3', '.ogg', '.flac', '.m4a', '.m4b', '.wav', '.opus')): path = os.path.join(directory, file) imported = True try: if force: imported, ignore = import_file( file, directory, path, True) tracks_cache_update.append(path) # Is the track already in the database? elif Track.select().where( Track.file == path).count() < 1: imported, track_data = import_file( file, directory, path) if track_data: tracks_to_import.append(track_data) # Has the modified date changed? elif (Track.select().where( Track.file == path).first().modified < os.path.getmtime(path)): imported, ignore = import_file(file, directory, path, update=True) tracks_cache_update.append(path) if not imported: failed += path + "\n" except UnicodeEncodeError as e: log.warning( "Could not import file because of invalid path or filename: " + path) reporter.exception("importer", e) failed += path + "\n" except Exception as e: log.warning("Could not import file: " + path) log.warning(traceback.format_exc()) reporter.exception("importer", e) failed += path + "\n" i = i + 1 if len(tracks_to_import) > 100: write_tracks_to_db(tracks_to_import) tracks_to_import = [] # don't flood gui updates if percent_counter < percent_threshold: percent_counter = percent_counter + 1 else: percent_counter = 1 Gdk.threads_add_idle( GLib.PRIORITY_DEFAULT_IDLE, ui.titlebar.progress_bar.set_fraction, i / file_count) Gdk.threads_add_idle( GLib.PRIORITY_DEFAULT_IDLE, ui.titlebar.update_progress_bar.set_fraction, i / file_count) write_tracks_to_db(tracks_to_import) end = time.time() log.info("Total import time: " + str(end - start)) # remove entries from the db that are no longer existent remove_invalid_entries() artwork_cache.generate_artwork_cache() Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, importer.emit_event, "import-finished") Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, ui.switch_to_playing) Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, ui.check_for_tracks) if len(failed) > 0: Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, ui.display_failed_imports, failed) OfflineCache().update_cache(tracks_cache_update) OfflineCache()._process_queue()