Skip to content

Commit 8126eaa

Browse files
authored
Merge branch 'master' into feature/add-artist-to-item-entry-template
2 parents d476af8 + 24dd40e commit 8126eaa

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

76 files changed

+1958
-2417
lines changed

.git-blame-ignore-revs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,5 @@ c490ac5810b70f3cf5fd8649669838e8fdb19f4d
5151
9147577b2b19f43ca827e9650261a86fb0450cef
5252
# Copy paste query, types from library to dbcore
5353
1a045c91668c771686f4c871c84f1680af2e944b
54+
# Library restructure (split library.py into multiple modules)
55+
0ad4e19d4f870db757373f44d12ff3be2441363a

.github/workflows/ci.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ jobs:
5252
- if: ${{ env.IS_MAIN_PYTHON != 'true' }}
5353
name: Test without coverage
5454
run: |
55-
poetry install --extras=autobpm --extras=lyrics
55+
poetry install --extras=autobpm --extras=lyrics --extras=embedart
5656
poe test
5757
5858
- if: ${{ env.IS_MAIN_PYTHON == 'true' }}

beets/dbcore/query.py

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -411,39 +411,6 @@ def __init__(
411411
super().__init__(field_name, pattern_int, fast)
412412

413413

414-
class BytesQuery(FieldQuery[bytes]):
415-
"""Match a raw bytes field (i.e., a path). This is a necessary hack
416-
to work around the `sqlite3` module's desire to treat `bytes` and
417-
`unicode` equivalently in Python 2. Always use this query instead of
418-
`MatchQuery` when matching on BLOB values.
419-
"""
420-
421-
def __init__(self, field_name: str, pattern: bytes | str | memoryview):
422-
# Use a buffer/memoryview representation of the pattern for SQLite
423-
# matching. This instructs SQLite to treat the blob as binary
424-
# rather than encoded Unicode.
425-
if isinstance(pattern, (str, bytes)):
426-
if isinstance(pattern, str):
427-
bytes_pattern = pattern.encode("utf-8")
428-
else:
429-
bytes_pattern = pattern
430-
self.buf_pattern = memoryview(bytes_pattern)
431-
elif isinstance(pattern, memoryview):
432-
self.buf_pattern = pattern
433-
bytes_pattern = bytes(pattern)
434-
else:
435-
raise ValueError("pattern must be bytes, str, or memoryview")
436-
437-
super().__init__(field_name, bytes_pattern)
438-
439-
def col_clause(self) -> tuple[str, Sequence[SQLiteType]]:
440-
return self.field + " = ?", [self.buf_pattern]
441-
442-
@classmethod
443-
def value_match(cls, pattern: bytes, value: Any) -> bool:
444-
return pattern == value
445-
446-
447414
class NumericQuery(FieldQuery[str]):
448415
"""Matches numeric fields. A syntax using Ruby-style range ellipses
449416
(``..``) lets users specify one- or two-sided ranges. For example,

beets/importer/tasks.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626

2727
import mediafile
2828

29-
from beets import autotag, config, dbcore, library, plugins, util
29+
from beets import autotag, config, library, plugins, util
30+
from beets.dbcore.query import PathQuery
3031

3132
from .state import ImportState
3233

@@ -520,9 +521,7 @@ def record_replaced(self, lib: library.Library):
520521
)
521522
replaced_album_ids = set()
522523
for item in self.imported_items():
523-
dup_items = list(
524-
lib.items(query=dbcore.query.BytesQuery("path", item.path))
525-
)
524+
dup_items = list(lib.items(query=PathQuery("path", item.path)))
526525
self.replaced_items[item] = dup_items
527526
for dup_item in dup_items:
528527
if (

beets/library/__init__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from .exceptions import FileOperationError, ReadError, WriteError
2+
from .library import Library
3+
from .models import Album, Item, LibModel
4+
from .queries import parse_query_parts, parse_query_string
5+
6+
__all__ = [
7+
"Library",
8+
"LibModel",
9+
"Album",
10+
"Item",
11+
"parse_query_parts",
12+
"parse_query_string",
13+
"FileOperationError",
14+
"ReadError",
15+
"WriteError",
16+
]

beets/library/exceptions.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from beets import util
2+
3+
4+
class FileOperationError(Exception):
5+
"""Indicate an error when interacting with a file on disk.
6+
7+
Possibilities include an unsupported media type, a permissions
8+
error, and an unhandled Mutagen exception.
9+
"""
10+
11+
def __init__(self, path, reason):
12+
"""Create an exception describing an operation on the file at
13+
`path` with the underlying (chained) exception `reason`.
14+
"""
15+
super().__init__(path, reason)
16+
self.path = path
17+
self.reason = reason
18+
19+
def __str__(self):
20+
"""Get a string representing the error.
21+
22+
Describe both the underlying reason and the file path in question.
23+
"""
24+
return f"{util.displayable_path(self.path)}: {self.reason}"
25+
26+
27+
class ReadError(FileOperationError):
28+
"""An error while reading a file (i.e. in `Item.read`)."""
29+
30+
def __str__(self):
31+
return "error reading " + str(super())
32+
33+
34+
class WriteError(FileOperationError):
35+
"""An error while writing a file (i.e. in `Item.write`)."""
36+
37+
def __str__(self):
38+
return "error writing " + str(super())

beets/library/library.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
5+
import platformdirs
6+
7+
import beets
8+
from beets import dbcore
9+
from beets.util import normpath
10+
11+
from .models import Album, Item
12+
from .queries import PF_KEY_DEFAULT, parse_query_parts, parse_query_string
13+
14+
if TYPE_CHECKING:
15+
from beets.dbcore import Results
16+
17+
18+
class Library(dbcore.Database):
19+
"""A database of music containing songs and albums."""
20+
21+
_models = (Item, Album)
22+
23+
def __init__(
24+
self,
25+
path="library.blb",
26+
directory: str | None = None,
27+
path_formats=((PF_KEY_DEFAULT, "$artist/$album/$track $title"),),
28+
replacements=None,
29+
):
30+
timeout = beets.config["timeout"].as_number()
31+
super().__init__(path, timeout=timeout)
32+
33+
self.directory = normpath(directory or platformdirs.user_music_path())
34+
35+
self.path_formats = path_formats
36+
self.replacements = replacements
37+
38+
# Used for template substitution performance.
39+
self._memotable: dict[tuple[str, ...], str] = {}
40+
41+
# Adding objects to the database.
42+
43+
def add(self, obj):
44+
"""Add the :class:`Item` or :class:`Album` object to the library
45+
database.
46+
47+
Return the object's new id.
48+
"""
49+
obj.add(self)
50+
self._memotable = {}
51+
return obj.id
52+
53+
def add_album(self, items):
54+
"""Create a new album consisting of a list of items.
55+
56+
The items are added to the database if they don't yet have an
57+
ID. Return a new :class:`Album` object. The list items must not
58+
be empty.
59+
"""
60+
if not items:
61+
raise ValueError("need at least one item")
62+
63+
# Create the album structure using metadata from the first item.
64+
values = {key: items[0][key] for key in Album.item_keys}
65+
album = Album(self, **values)
66+
67+
# Add the album structure and set the items' album_id fields.
68+
# Store or add the items.
69+
with self.transaction():
70+
album.add(self)
71+
for item in items:
72+
item.album_id = album.id
73+
if item.id is None:
74+
item.add(self)
75+
else:
76+
item.store()
77+
78+
return album
79+
80+
# Querying.
81+
82+
def _fetch(self, model_cls, query, sort=None):
83+
"""Parse a query and fetch.
84+
85+
If an order specification is present in the query string
86+
the `sort` argument is ignored.
87+
"""
88+
# Parse the query, if necessary.
89+
try:
90+
parsed_sort = None
91+
if isinstance(query, str):
92+
query, parsed_sort = parse_query_string(query, model_cls)
93+
elif isinstance(query, (list, tuple)):
94+
query, parsed_sort = parse_query_parts(query, model_cls)
95+
except dbcore.query.InvalidQueryArgumentValueError as exc:
96+
raise dbcore.InvalidQueryError(query, exc)
97+
98+
# Any non-null sort specified by the parsed query overrides the
99+
# provided sort.
100+
if parsed_sort and not isinstance(parsed_sort, dbcore.query.NullSort):
101+
sort = parsed_sort
102+
103+
return super()._fetch(model_cls, query, sort)
104+
105+
@staticmethod
106+
def get_default_album_sort():
107+
"""Get a :class:`Sort` object for albums from the config option."""
108+
return dbcore.sort_from_strings(
109+
Album, beets.config["sort_album"].as_str_seq()
110+
)
111+
112+
@staticmethod
113+
def get_default_item_sort():
114+
"""Get a :class:`Sort` object for items from the config option."""
115+
return dbcore.sort_from_strings(
116+
Item, beets.config["sort_item"].as_str_seq()
117+
)
118+
119+
def albums(self, query=None, sort=None) -> Results[Album]:
120+
"""Get :class:`Album` objects matching the query."""
121+
return self._fetch(Album, query, sort or self.get_default_album_sort())
122+
123+
def items(self, query=None, sort=None) -> Results[Item]:
124+
"""Get :class:`Item` objects matching the query."""
125+
return self._fetch(Item, query, sort or self.get_default_item_sort())
126+
127+
# Convenience accessors.
128+
129+
def get_item(self, id):
130+
"""Fetch a :class:`Item` by its ID.
131+
132+
Return `None` if no match is found.
133+
"""
134+
return self._get(Item, id)
135+
136+
def get_album(self, item_or_id):
137+
"""Given an album ID or an item associated with an album, return
138+
a :class:`Album` object for the album.
139+
140+
If no such album exists, return `None`.
141+
"""
142+
if isinstance(item_or_id, int):
143+
album_id = item_or_id
144+
else:
145+
album_id = item_or_id.album_id
146+
if album_id is None:
147+
return None
148+
return self._get(Album, album_id)

0 commit comments

Comments
 (0)