Skip to content

Commit 45fce27

Browse files
committed
DB: define field types outside of the models
1 parent decfd3a commit 45fce27

File tree

8 files changed

+500
-393
lines changed

8 files changed

+500
-393
lines changed

beets/dbcore/db.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
import time
2525
from abc import ABC
2626
from collections import defaultdict
27-
from collections.abc import Mapping
27+
from typing import Mapping
2828
from itertools import chain
2929
from sqlite3 import Connection
3030
from types import TracebackType

beets/dbcore/fields.py

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
from typing import Dict
2+
3+
from . import types
4+
5+
TYPE_BY_FIELD: Dict[str, types.Type] = {
6+
"acoustid_fingerprint": types.STRING,
7+
"acoustid_id": types.STRING,
8+
"added": types.DateType(),
9+
"albumartist_credit": types.STRING,
10+
"albumartists_credit": types.MULTI_VALUE_DSV,
11+
"albumartist_sort": types.STRING,
12+
"albumartists_sort": types.MULTI_VALUE_DSV,
13+
"albumartists": types.MULTI_VALUE_DSV,
14+
"albumartist": types.STRING,
15+
"albumdisambig": types.STRING,
16+
"album_id": types.FOREIGN_ID,
17+
"albumstatus": types.STRING,
18+
"album": types.STRING,
19+
"albumtypes": types.SEMICOLON_SPACE_DSV,
20+
"albumtype": types.STRING,
21+
"arranger": types.STRING,
22+
"artist_credit": types.STRING,
23+
"artists_credit": types.MULTI_VALUE_DSV,
24+
"artists_ids": types.MULTI_VALUE_DSV,
25+
"artist_sort": types.STRING,
26+
"artists_sort": types.MULTI_VALUE_DSV,
27+
"artists": types.MULTI_VALUE_DSV,
28+
"artist": types.STRING,
29+
"artpath": types.PathType(True),
30+
"asin": types.STRING,
31+
"barcode": types.STRING,
32+
"bitdepth": types.INTEGER,
33+
"bitrate_mode": types.STRING,
34+
"bitrate": types.ScaledInt(1000, "kbps"),
35+
"bpm": types.INTEGER,
36+
"catalognum": types.STRING,
37+
"channels": types.INTEGER,
38+
"comments": types.STRING,
39+
"composer_sort": types.STRING,
40+
"composer": types.STRING,
41+
"comp": types.BOOLEAN,
42+
"country": types.STRING,
43+
"data_source": types.STRING,
44+
"day": types.PaddedInt(2),
45+
"discogs_albumid": types.INTEGER,
46+
"discogs_artistid": types.INTEGER,
47+
"discogs_labelid": types.INTEGER,
48+
"disctitle": types.STRING,
49+
"disctotal": types.PaddedInt(2),
50+
"disc": types.PaddedInt(2),
51+
"encoder_info": types.STRING,
52+
"encoder_settings": types.STRING,
53+
"encoder": types.STRING,
54+
"format": types.STRING,
55+
"genre": types.STRING,
56+
"grouping": types.STRING,
57+
"id": types.PRIMARY_ID,
58+
"initial_key": types.MusicalKey(),
59+
"isrc": types.STRING,
60+
"label": types.STRING,
61+
"language": types.STRING,
62+
"length": types.DurationType(),
63+
"lyricist": types.STRING,
64+
"lyrics": types.STRING,
65+
"mb_albumartistids": types.MULTI_VALUE_DSV,
66+
"mb_albumartistid": types.STRING,
67+
"mb_albumid": types.STRING,
68+
"mb_artistids": types.MULTI_VALUE_DSV,
69+
"mb_artistid": types.STRING,
70+
"mb_releasegroupid": types.STRING,
71+
"mb_releasetrackid": types.STRING,
72+
"mb_trackid": types.STRING,
73+
"mb_workid": types.STRING,
74+
"media": types.STRING,
75+
"month": types.PaddedInt(2),
76+
"mtime": types.DateType(),
77+
"original_day": types.PaddedInt(2),
78+
"original_month": types.PaddedInt(2),
79+
"original_year": types.PaddedInt(4),
80+
"path": types.PathType(),
81+
"r128_album_gain": types.NULL_FLOAT,
82+
"r128_track_gain": types.NULL_FLOAT,
83+
"releasegroupdisambig": types.STRING,
84+
"release_group_title": types.STRING,
85+
"remixer": types.STRING,
86+
"rg_album_gain": types.NULL_FLOAT,
87+
"rg_album_peak": types.NULL_FLOAT,
88+
"rg_track_gain": types.NULL_FLOAT,
89+
"rg_track_peak": types.NULL_FLOAT,
90+
"samplerate": types.ScaledInt(1000, "kHz"),
91+
"script": types.STRING,
92+
"style": types.STRING,
93+
"title": types.STRING,
94+
"trackdisambig": types.STRING,
95+
"tracktotal": types.PaddedInt(2),
96+
"track": types.PaddedInt(2),
97+
"work_disambig": types.STRING,
98+
"work": types.STRING,
99+
"year": types.PaddedInt(4),
100+
}

beets/dbcore/query.py

+99
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from __future__ import annotations
1818

19+
import os
1920
import re
2021
import unicodedata
2122
from abc import ABC, abstractmethod
@@ -42,6 +43,11 @@
4243

4344
from beets import util
4445

46+
# To use the SQLite "blob" type, it doesn't suffice to provide a byte
47+
# string; SQLite treats that as encoded text. Wrapping it in a
48+
# `memoryview` tells it that we actually mean non-text data.
49+
BLOB_TYPE = memoryview
50+
4551
if TYPE_CHECKING:
4652
from beets.dbcore import Model
4753

@@ -824,6 +830,8 @@ class DateQuery(FieldQuery):
824830
825831
The value of a date field can be matched against a date interval by
826832
using an ellipsis interval syntax similar to that of NumericQuery.
833+
834+
TODO: Make this inherit NumericQuery and reuse its logic.
827835
"""
828836

829837
def __init__(self, field, pattern, fast: bool = True):
@@ -891,6 +899,97 @@ def _convert(self, s: str) -> Optional[float]:
891899
)
892900

893901

902+
class PathQuery(FieldQuery):
903+
"""A query that matches all items under a given path.
904+
905+
Matching can either be case-insensitive or case-sensitive. By
906+
default, the behavior depends on the OS: case-insensitive on Windows
907+
and case-sensitive otherwise.
908+
"""
909+
910+
# For tests
911+
force_implicit_query_detection = False
912+
913+
def __init__(self, field, pattern, fast=True, case_sensitive=None):
914+
"""Create a path query.
915+
916+
`pattern` must be a path, either to a file or a directory.
917+
918+
`case_sensitive` can be a bool or `None`, indicating that the
919+
behavior should depend on the filesystem.
920+
"""
921+
super().__init__(field, pattern, fast)
922+
923+
path = util.normpath(pattern)
924+
925+
# By default, the case sensitivity depends on the filesystem
926+
# that the query path is located on.
927+
if case_sensitive is None:
928+
case_sensitive = util.case_sensitive(path)
929+
self.case_sensitive = case_sensitive
930+
931+
# Use a normalized-case pattern for case-insensitive matches.
932+
if not case_sensitive:
933+
# We need to lowercase the entire path, not just the pattern.
934+
# In particular, on Windows, the drive letter is otherwise not
935+
# lowercased.
936+
# This also ensures that the `match()` method below and the SQL
937+
# from `col_clause()` do the same thing.
938+
path = path.lower()
939+
940+
# Match the path as a single file.
941+
self.file_path = path
942+
# As a directory (prefix).
943+
self.dir_path = os.path.join(path, b"")
944+
945+
@classmethod
946+
def is_path_query(cls, query_part):
947+
"""Try to guess whether a unicode query part is a path query.
948+
949+
Condition: separator precedes colon and the file exists.
950+
"""
951+
colon = query_part.find(":")
952+
if colon != -1:
953+
query_part = query_part[:colon]
954+
955+
# Test both `sep` and `altsep` (i.e., both slash and backslash on
956+
# Windows).
957+
if not (
958+
os.sep in query_part or (os.altsep and os.altsep in query_part)
959+
):
960+
return False
961+
962+
if cls.force_implicit_query_detection:
963+
return True
964+
return os.path.exists(util.syspath(util.normpath(query_part)))
965+
966+
def match(self, item):
967+
path = item.path if self.case_sensitive else item.path.lower()
968+
return (path == self.file_path) or path.startswith(self.dir_path)
969+
970+
def col_clause(self):
971+
file_blob = BLOB_TYPE(self.file_path)
972+
dir_blob = BLOB_TYPE(self.dir_path)
973+
974+
if self.case_sensitive:
975+
query_part = "({0} = ?) || (substr({0}, 1, ?) = ?)"
976+
else:
977+
query_part = "(BYTELOWER({0}) = BYTELOWER(?)) || \
978+
(substr(BYTELOWER({0}), 1, ?) = BYTELOWER(?))"
979+
980+
return query_part.format(self.field), (
981+
file_blob,
982+
len(dir_blob),
983+
dir_blob,
984+
)
985+
986+
def __repr__(self) -> str:
987+
return (
988+
f"{self.__class__.__name__}({self.field!r}, {self.pattern!r}, "
989+
f"fast={self.fast}, case_sensitive={self.case_sensitive})"
990+
)
991+
992+
894993
# Sorting.
895994

896995

beets/dbcore/queryparse.py

+4-6
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@
1414

1515
"""Parsing of strings into DBCore queries."""
1616

17-
import itertools
1817
import re
1918
from typing import Collection, Dict, List, Optional, Sequence, Tuple, Type
2019

2120
from .. import library
2221
from . import Model, query
22+
from .fields import TYPE_BY_FIELD
2323
from .query import Query, Sort
2424

2525
PARSE_QUERY_PART_REGEX = re.compile(
@@ -128,11 +128,9 @@ def construct_query_part(
128128

129129
# Use `model_cls` to build up a map from field (or query) names to
130130
# `Query` classes.
131-
query_classes: Dict[str, Type[Query]] = {}
132-
for k, t in itertools.chain(
133-
model_cls._fields.items(), model_cls._types.items()
134-
):
135-
query_classes[k] = t.query
131+
query_classes: Dict[str, Type[Query]] = {
132+
k: t.query for k, t in TYPE_BY_FIELD.items()
133+
}
136134
query_classes.update(model_cls._queries) # Non-field queries.
137135

138136
# Parse the string.

0 commit comments

Comments
 (0)