This commit is contained in:
Vedant Madane 2026-03-23 05:36:10 +00:00 committed by GitHub
commit a88605c346
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 80 additions and 4 deletions

View file

@ -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"

View file

@ -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: {}",

View file

@ -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``,

View file

@ -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

View file

@ -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())