mirror of
https://github.com/beetbox/beets.git
synced 2026-03-26 23:33:43 +01:00
Merge 1f37d6c875 into 72b4c77161
This commit is contained in:
commit
a88605c346
5 changed files with 80 additions and 4 deletions
|
|
@ -1115,7 +1115,7 @@ class Database:
|
|||
data is written in a transaction.
|
||||
"""
|
||||
|
||||
def __init__(self, path, timeout: float = 5.0):
|
||||
def __init__(self, path, timeout: float = 30.0):
|
||||
if sqlite3.threadsafety == 0:
|
||||
raise RuntimeError(
|
||||
"sqlite3 must be compiled with multi-threading support"
|
||||
|
|
|
|||
|
|
@ -891,9 +891,25 @@ def _open_library(config: confuse.LazyConfig) -> library.Library:
|
|||
lib.get_item(0) # Test database connection.
|
||||
except (sqlite3.OperationalError, sqlite3.DatabaseError) as db_error:
|
||||
log.debug("{}", traceback.format_exc())
|
||||
# Check for permission-related errors and provide a helpful message
|
||||
error_str = str(db_error).lower()
|
||||
dbpath_display = util.displayable_path(dbpath)
|
||||
if "unable to open" in error_str:
|
||||
# Normalize path and get directory
|
||||
normalized_path = os.path.abspath(dbpath)
|
||||
db_dir = os.path.dirname(normalized_path)
|
||||
# Handle edge case where path has no directory component
|
||||
if not db_dir:
|
||||
db_dir = b"."
|
||||
raise UserError(
|
||||
f"database file {dbpath_display} could not be opened. "
|
||||
f"This may be due to a permissions issue. If the database "
|
||||
f"does not exist yet, please check that the file or directory "
|
||||
f"{util.displayable_path(db_dir)} is writable "
|
||||
f"(original error: {db_error})."
|
||||
)
|
||||
raise UserError(
|
||||
f"database file {util.displayable_path(dbpath)} cannot not be"
|
||||
f" opened: {db_error}"
|
||||
f"database file {dbpath_display} could not be opened: {db_error}"
|
||||
)
|
||||
log.debug(
|
||||
"library database: {}\nlibrary directory: {}",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,12 @@ Bug fixes
|
|||
- :ref:`import-cmd` Autotagging by explicit release or recording IDs now keeps
|
||||
candidates from all enabled metadata sources instead of dropping matches when
|
||||
different providers share the same ID. :bug:`6178` :bug:`6181`
|
||||
- Improved error message when database cannot be opened. When SQLite fails to
|
||||
open the database with 'unable to open' error, beets now provides a helpful
|
||||
message suggesting it may be a permissions issue and recommends checking that
|
||||
the file or directory is writable. The original SQLite error is included for
|
||||
debugging. Also fixed typo in error message ('cannot not' to 'could not').
|
||||
:bug:`1676`
|
||||
- :doc:`plugins/mbsync` and :doc:`plugins/missing` now use each item's stored
|
||||
``data_source`` for ID lookups, with a fallback to ``MusicBrainz``.
|
||||
- :doc:`plugins/musicbrainz`: Use ``va_name`` config for ``albumartist_sort``,
|
||||
|
|
|
|||
|
|
@ -329,6 +329,7 @@ ignore = [
|
|||
"beets/**" = ["PT"]
|
||||
"test/plugins/test_ftintitle.py" = ["E501"]
|
||||
"test/test_util.py" = ["E501"]
|
||||
"test/ui/test_ui_init.py" = ["PT"]
|
||||
"test/util/test_diff.py" = ["E501"]
|
||||
"test/util/test_id_extractors.py" = ["E501"]
|
||||
"test/**" = ["RUF001"] # we use Unicode characters in tests
|
||||
|
|
|
|||
|
|
@ -16,11 +16,13 @@
|
|||
|
||||
import os
|
||||
import shutil
|
||||
import sqlite3
|
||||
import unittest
|
||||
from copy import deepcopy
|
||||
from random import random
|
||||
from unittest import mock
|
||||
|
||||
from beets import config, ui
|
||||
from beets import config, library, ui
|
||||
from beets.test import _common
|
||||
from beets.test.helper import BeetsTestCase, IOMixin
|
||||
|
||||
|
|
@ -119,3 +121,54 @@ class ParentalDirCreation(IOMixin, BeetsTestCase):
|
|||
if lib:
|
||||
lib._close()
|
||||
raise OSError("Parent directories should not be created.")
|
||||
|
||||
|
||||
class DatabaseErrorTest(BeetsTestCase):
|
||||
"""Test database error handling with improved error messages."""
|
||||
|
||||
def test_database_error_with_unable_to_open(self):
|
||||
"""Test error message when database fails with 'unable to open' error."""
|
||||
test_config = deepcopy(config)
|
||||
test_config["library"] = _common.os.fsdecode(
|
||||
os.path.join(self.temp_dir, b"test.db")
|
||||
)
|
||||
|
||||
# Mock Library to raise OperationalError with "unable to open"
|
||||
with mock.patch.object(
|
||||
library,
|
||||
"Library",
|
||||
side_effect=sqlite3.OperationalError(
|
||||
"unable to open database file"
|
||||
),
|
||||
):
|
||||
with self.assertRaises(ui.UserError) as cm:
|
||||
ui._open_library(test_config)
|
||||
|
||||
error_message = str(cm.exception)
|
||||
# Should mention permissions and directory
|
||||
self.assertIn("directory", error_message.lower())
|
||||
self.assertIn("writable", error_message.lower())
|
||||
self.assertIn("permissions", error_message.lower())
|
||||
|
||||
def test_database_error_fallback(self):
|
||||
"""Test fallback error message for other database errors."""
|
||||
test_config = deepcopy(config)
|
||||
test_config["library"] = _common.os.fsdecode(
|
||||
os.path.join(self.temp_dir, b"test.db")
|
||||
)
|
||||
|
||||
# Mock Library to raise a different OperationalError
|
||||
with mock.patch.object(
|
||||
library,
|
||||
"Library",
|
||||
side_effect=sqlite3.OperationalError("disk I/O error"),
|
||||
):
|
||||
with self.assertRaises(ui.UserError) as cm:
|
||||
ui._open_library(test_config)
|
||||
|
||||
error_message = str(cm.exception)
|
||||
# Should contain the error but not the permissions message
|
||||
self.assertIn("could not be opened", error_message)
|
||||
self.assertIn("disk I/O error", error_message)
|
||||
# Should NOT have the permissions-related message
|
||||
self.assertNotIn("permissions", error_message.lower())
|
||||
|
|
|
|||
Loading…
Reference in a new issue