16
16
17
17
from __future__ import annotations
18
18
19
+ import re
20
+ import time
19
21
import typing
20
22
from abc import ABC
21
23
from typing import TYPE_CHECKING , Any , Generic , TypeVar , cast
22
24
23
- from beets .util import str2bool
25
+ import beets
26
+ from beets import util
24
27
25
- from .query import (
26
- BooleanQuery ,
27
- FieldQueryType ,
28
- NumericQuery ,
29
- SQLiteType ,
30
- SubstringQuery ,
31
- )
28
+ from . import query
29
+
30
+ SQLiteType = query .SQLiteType
31
+ # needs to be defined in query due to circular import.
32
+ # TODO: remove the need for it in query.py and define it here instead.
33
+ BLOB_TYPE = query .BLOB_TYPE
32
34
33
35
34
36
class ModelType (typing .Protocol ):
@@ -61,7 +63,7 @@ class Type(ABC, Generic[T, N]):
61
63
"""The SQLite column type for the value.
62
64
"""
63
65
64
- query : FieldQueryType = SubstringQuery
66
+ query : query . FieldQueryType = query . SubstringQuery
65
67
"""The `Query` subclass to be used when querying the field.
66
68
"""
67
69
@@ -160,7 +162,7 @@ class BaseInteger(Type[int, N]):
160
162
"""A basic integer type."""
161
163
162
164
sql = "INTEGER"
163
- query = NumericQuery
165
+ query = query . NumericQuery
164
166
model_type = int
165
167
166
168
def normalize (self , value : Any ) -> int | N :
@@ -241,7 +243,7 @@ class BaseFloat(Type[float, N]):
241
243
"""
242
244
243
245
sql = "REAL"
244
- query : FieldQueryType = NumericQuery
246
+ query : query . FieldQueryType = query . NumericQuery
245
247
model_type = float
246
248
247
249
def __init__ (self , digits : int = 1 ):
@@ -271,7 +273,7 @@ class BaseString(Type[T, N]):
271
273
"""A Unicode string type."""
272
274
273
275
sql = "TEXT"
274
- query = SubstringQuery
276
+ query = query . SubstringQuery
275
277
276
278
def normalize (self , value : Any ) -> T | N :
277
279
if value is None :
@@ -312,14 +314,142 @@ class Boolean(Type):
312
314
"""A boolean type."""
313
315
314
316
sql = "INTEGER"
315
- query = BooleanQuery
317
+ query = query . BooleanQuery
316
318
model_type = bool
317
319
318
320
def format (self , value : bool ) -> str :
319
321
return str (bool (value ))
320
322
321
323
def parse (self , string : str ) -> bool :
322
- return str2bool (string )
324
+ return util .str2bool (string )
325
+
326
+
327
+ class BasePathType (Type [bytes , N ]):
328
+ """A dbcore type for filesystem paths.
329
+
330
+ These are represented as `bytes` objects, in keeping with
331
+ the Unix filesystem abstraction.
332
+ """
333
+
334
+ sql = "BLOB"
335
+ query = query .PathQuery
336
+ model_type = bytes
337
+
338
+ def parse (self , string : str ) -> bytes :
339
+ return util .normpath (string )
340
+
341
+ def normalize (self , value : Any ) -> bytes | N :
342
+ if isinstance (value , str ):
343
+ # Paths stored internally as encoded bytes.
344
+ return util .bytestring_path (value )
345
+
346
+ elif isinstance (value , BLOB_TYPE ):
347
+ # We unwrap buffers to bytes.
348
+ return bytes (value )
349
+
350
+ else :
351
+ return value
352
+
353
+ def to_sql (self , value : bytes ) -> BLOB_TYPE :
354
+ if isinstance (value , bytes ):
355
+ value = BLOB_TYPE (value )
356
+ return value
357
+
358
+
359
+ class NullPathType (BasePathType [None ]):
360
+ @property
361
+ def null (self ) -> None :
362
+ return None
363
+
364
+ def format (self , value : bytes | None ) -> str :
365
+ return util .displayable_path (value or b"" )
366
+
367
+
368
+ class PathType (BasePathType [bytes ]):
369
+ @property
370
+ def null (self ) -> bytes :
371
+ return b""
372
+
373
+ def format (self , value : bytes ) -> str :
374
+ return util .displayable_path (value or b"" )
375
+
376
+
377
+ class DateType (Float ):
378
+ # TODO representation should be `datetime` object
379
+ # TODO distinguish between date and time types
380
+ query = query .DateQuery
381
+
382
+ def format (self , value ):
383
+ return time .strftime (
384
+ beets .config ["time_format" ].as_str (), time .localtime (value or 0 )
385
+ )
386
+
387
+ def parse (self , string ):
388
+ try :
389
+ # Try a formatted date string.
390
+ return time .mktime (
391
+ time .strptime (string , beets .config ["time_format" ].as_str ())
392
+ )
393
+ except ValueError :
394
+ # Fall back to a plain timestamp number.
395
+ try :
396
+ return float (string )
397
+ except ValueError :
398
+ return self .null
399
+
400
+
401
+ class MusicalKey (String ):
402
+ """String representing the musical key of a song.
403
+
404
+ The standard format is C, Cm, C#, C#m, etc.
405
+ """
406
+
407
+ ENHARMONIC = {
408
+ r"db" : "c#" ,
409
+ r"eb" : "d#" ,
410
+ r"gb" : "f#" ,
411
+ r"ab" : "g#" ,
412
+ r"bb" : "a#" ,
413
+ }
414
+
415
+ null = None
416
+
417
+ def parse (self , key ):
418
+ key = key .lower ()
419
+ for flat , sharp in self .ENHARMONIC .items ():
420
+ key = re .sub (flat , sharp , key )
421
+ key = re .sub (r"[\W\s]+minor" , "m" , key )
422
+ key = re .sub (r"[\W\s]+major" , "" , key )
423
+ return key .capitalize ()
424
+
425
+ def normalize (self , key ):
426
+ if key is None :
427
+ return None
428
+ else :
429
+ return self .parse (key )
430
+
431
+
432
+ class DurationType (Float ):
433
+ """Human-friendly (M:SS) representation of a time interval."""
434
+
435
+ query = query .DurationQuery
436
+
437
+ def format (self , value ):
438
+ if not beets .config ["format_raw_length" ].get (bool ):
439
+ return util .human_seconds_short (value or 0.0 )
440
+ else :
441
+ return value
442
+
443
+ def parse (self , string ):
444
+ try :
445
+ # Try to format back hh:ss to seconds.
446
+ return util .raw_seconds_short (string )
447
+ except ValueError :
448
+ # Fall back to a plain float.
449
+ try :
450
+ return float (string )
451
+ except ValueError :
452
+ return self .null
323
453
324
454
325
455
# Shared instances of common types.
@@ -331,6 +461,7 @@ def parse(self, string: str) -> bool:
331
461
NULL_FLOAT = NullFloat ()
332
462
STRING = String ()
333
463
BOOLEAN = Boolean ()
464
+ DATE = DateType ()
334
465
SEMICOLON_SPACE_DSV = DelimitedString (delimiter = "; " )
335
466
336
467
# Will set the proper null char in mediafile
0 commit comments