--HG--
extra : convert_revision : svn%3A41726ec3-264d-0410-9c23-a9f1637257cc/trunk%40133
This commit is contained in:
adrian.sampson 2009-02-10 02:54:11 +00:00
parent e52f0aaaf1
commit 1cdf13ea8d
4 changed files with 107 additions and 78 deletions

View file

@ -1,7 +1,11 @@
import sqlite3, os, sys, operator, re, shutil
from beets.mediafile import MediaFile, FileTypeError
import sqlite3
import os
import operator
import re
import shutil
from string import Template
import logging
from beets.mediafile import MediaFile, FileTypeError
# Fields in the "items" table; all the metadata available for items in the
# library. These are used directly in SQL; they are vulnerable to injection if
@ -46,7 +50,6 @@ library_options = {
}
# Logger.
log = logging.getLogger('beets')
log.setLevel(logging.DEBUG)
log.addHandler(logging.StreamHandler())
@ -64,12 +67,14 @@ class InvalidFieldError(Exception):
def _normpath(path):
"""Provide the canonical form of the path suitable for storing in the
database."""
database.
"""
return os.path.normpath(os.path.abspath(os.path.expanduser(path)))
def _ancestry(path):
"""Return a list consisting of path's parent directory, its grandparent,
and so on. For instance, _ancestry('/a/b/c') == ['/', '/a', '/a/b']."""
and so on. For instance, _ancestry('/a/b/c') == ['/', '/a', '/a/b'].
"""
out = []
while path and path != '/':
path = os.path.dirname(path)
@ -80,7 +85,8 @@ def _ancestry(path):
def _walk_files(path):
"""Like os.walk, but only yields the files in the directory tree. The full
pathnames to the files (under path) are given. Also, if path is a file,
_walk_files just yields that."""
_walk_files just yields that.
"""
if os.path.isfile(path):
yield path
else:
@ -124,8 +130,8 @@ class Item(object):
def __getattr__(self, key):
"""If key is an item attribute (i.e., a column in the database),
returns the record entry for that key. Otherwise, performs an ordinary
getattr."""
getattr.
"""
if key in item_keys:
return self.record[key]
else:
@ -137,8 +143,8 @@ class Item(object):
attribute in the database or in the file's tags, one must call store()
or write().
Otherwise, performs an ordinary setattr."""
Otherwise, performs an ordinary setattr.
"""
if key in item_keys:
if (not (key in self.record)) or (self.record[key] != value):
# don't dirty if value unchanged
@ -152,8 +158,8 @@ class Item(object):
def load(self, load_id=None):
"""Refresh the item's metadata from the library database. If fetch_id
is not specified, use the current item's id."""
is not specified, use the current item's id.
"""
if not self.library:
raise LibraryError('no library to load from')
@ -169,8 +175,8 @@ class Item(object):
def store(self, store_id=None, store_all=False):
"""Save the item's metadata into the library database. If store_id is
specified, use it instead of the item's current id. If store_all is
true, save the entire record instead of just the dirty fields."""
true, save the entire record instead of just the dirty fields.
"""
if not self.library:
raise LibraryError('no library to store to')
@ -201,8 +207,8 @@ class Item(object):
def add(self, library=None):
"""Add the item as a new object to the library database. The id field
will be updated; the new id is returned. If library is specified, set
the item's library before adding."""
the item's library before adding.
"""
if library:
self.library = library
if not self.library:
@ -228,8 +234,8 @@ class Item(object):
return new_id
def remove(self):
"""Removes the item from the database (leaving the file on disk)."""
"""Removes the item from the database (leaving the file on disk).
"""
self.library.conn.execute('delete from items where id=?',
(self.id,) )
@ -238,8 +244,8 @@ class Item(object):
def read(self, read_path=None):
"""Read the metadata from the associated file. If read_path is
specified, read metadata from that file instead."""
specified, read metadata from that file instead.
"""
if read_path is None:
read_path = self.path
f = MediaFile(read_path)
@ -249,7 +255,8 @@ class Item(object):
self.path = read_path
def write(self):
"""Writes the item's metadata to the associated file."""
"""Writes the item's metadata to the associated file.
"""
f = MediaFile(self.path)
for key in metadata_rw_keys:
setattr(f, key, getattr(self, key))
@ -260,8 +267,8 @@ class Item(object):
def destination(self):
"""Returns the path within the library directory designated for this
item (i.e., where the file ought to be)."""
item (i.e., where the file ought to be).
"""
libpath = self.library.options['directory']
subpath_tmpl = Template(self.library.options['path_format'])
@ -302,8 +309,8 @@ class Item(object):
moving/copying fails.
Note that one should almost certainly call store() and library.save()
after this method in order to keep on-disk data consistent."""
after this method in order to keep on-disk data consistent.
"""
dest = self.destination()
# Create necessary ancestry for the move. Like os.renames but only
@ -326,8 +333,8 @@ class Item(object):
Also calls remove(), deleting the appropriate row from the database.
As with move(), library.save() should almost certainly be called after
invoking this (although store() should not)."""
invoking this (although store() should not).
"""
os.unlink(self.path)
self.remove()
@ -335,7 +342,8 @@ class Item(object):
def from_path(cls, path, library=None):
"""Creates a new item from the media file at the specified path. Sets
the item's library (but does not add the item) if library is
specified."""
specified.
"""
i = cls({})
i.read(path)
i.library = library
@ -353,19 +361,22 @@ class Query(object):
def clause(self):
"""Returns (clause, subvals) where clause is a valid sqlite WHERE
clause implementing the query and subvals is a list of items to be
substituted for ?s in the clause."""
substituted for ?s in the clause.
"""
raise NotImplementedError
def statement(self, columns='*'):
"""Returns (query, subvals) where clause is a sqlite SELECT statement
to enact this query and subvals is a list of values to substitute in
for ?s in the query."""
for ?s in the query.
"""
clause, subvals = self.clause()
return ('select ' + columns + ' from items where ' + clause, subvals)
def execute(self, library):
"""Runs the query in the specified library, returning a
ResultIterator."""
ResultIterator.
"""
c = library.conn.cursor()
c.execute(*self.statement())
return ResultIterator(c, library)
@ -399,7 +410,8 @@ class SubstringQuery(FieldQuery):
class CollectionQuery(Query):
"""An abstract query class that aggregates other queries. Can be indexed
like a list to access the sub-queries."""
like a list to access the sub-queries.
"""
def __init__(self, subqueries = ()):
self.subqueries = subqueries
@ -412,7 +424,8 @@ class CollectionQuery(Query):
def clause_with_joiner(self, joiner):
"""Returns a clause created by joining together the clauses of all
subqueries with the string joiner (padded by spaces)."""
subqueries with the string joiner (padded by spaces).
"""
clause_parts = []
subvals = []
for subq in self.subqueries:
@ -425,7 +438,8 @@ class CollectionQuery(Query):
@classmethod
def from_dict(cls, matches):
"""Construct a query from a dictionary, matches, whose keys are item
field names and whose values are substring patterns."""
field names and whose values are substring patterns.
"""
subqueries = []
for key, pattern in matches.iteritems():
subqueries.append(SubstringQuery(key, pattern))
@ -489,7 +503,8 @@ class AnySubstringQuery(CollectionQuery):
class MutableCollectionQuery(CollectionQuery):
"""A collection query whose subqueries may be modified after the query is
initialized."""
initialized.
"""
def __setitem__(self, key, value): self.subqueries[key] = value
def __delitem__(self, key): del self.subqueries[key]
@ -514,7 +529,8 @@ class ResultIterator(object):
def count(self):
"""Returns the number of matched rows and invalidates the
iterator."""
iterator.
"""
# Apparently, there is no good way to get the number of rows
# returned by an sqlite SELECT.
num = 0
@ -547,7 +563,7 @@ class Library(object):
self._setup()
def _setup(self):
"Set up the schema of the library file."
"""Set up the schema of the library file."""
# options (library data) table
setup_sql = """
@ -558,7 +574,7 @@ class Library(object):
# items (things in the library) table
setup_sql += 'create table if not exists items ('
setup_sql += ', '.join(map(' '.join, item_fields))
setup_sql += ', '.join([' '.join(f) for f in item_fields])
setup_sql += ');'
self.conn.executescript(setup_sql)
@ -603,7 +619,8 @@ class Library(object):
def add(self, path, copy=False):
"""Add a file to the library or recursively search a directory and add
all its contents. If copy is True, copy files to their destination in
the library directory while adding."""
the library directory while adding.
"""
for f in _walk_files(path):
try:
@ -616,7 +633,8 @@ class Library(object):
def get(self, query=None):
"""Returns a ResultIterator to the items matching query, which may be
None (match the entire library), a Query object, or a query string."""
None (match the entire library), a Query object, or a query string.
"""
if query is None:
query = TrueQuery()
elif isinstance(query, str) or isinstance(query, unicode):
@ -626,5 +644,7 @@ class Library(object):
return query.execute(self)
def save(self):
"""Writes the library to disk (completing a sqlite transaction)."""
"""Writes the library to disk (completing a sqlite transaction).
"""
self.conn.commit()

View file

@ -3,15 +3,17 @@ automatically detect file types and provide a unified interface for a useful
subset of music files' tags.
Usage:
>>> f = MediaFile('Lucy.mp3')
>>> f.title
u'Lucy in the Sky with Diamonds'
>>> f.artist = 'The Beatles'
>>> f.save()
>>> f = MediaFile('Lucy.mp3')
>>> f.title
u'Lucy in the Sky with Diamonds'
>>> f.artist = 'The Beatles'
>>> f.save()
A field will always return a reasonable value of the correct type, even if no
tag is present. If no value is available, the value will be false (e.g., zero
or the empty string)."""
or the empty string).
"""
import mutagen
import datetime
@ -42,7 +44,8 @@ packing = Enumeration('SLASHED', # pair delimited by /
class StorageStyle(object):
"""Parameterizes the storage behavior of a single field for a certain tag
format."""
format.
"""
def __init__(self,
# The Mutagen key used to access the data for this field.
key,
@ -73,14 +76,16 @@ class StorageStyle(object):
class Packed(object):
"""Makes a packed list of values subscriptable. To access the packed output
after making changes, use packed_thing.items."""
after making changes, use packed_thing.items.
"""
def __init__(self, items, packstyle, none_val=0, out_type=int):
"""Create a Packed object for subscripting the packed values in items.
The items are packed using packstyle, which is a value from the
packing enum. none_val is returned from a request when no suitable
value is found in the items. Vales are converted to out_type before
they are returned."""
they are returned.
"""
self.items = items
self.packstyle = packstyle
self.none_val = none_val
@ -160,7 +165,8 @@ class MediaField(object):
"""A descriptor providing access to a particular (abstract) metadata
field. out_type is the type that users of MediaFile should see and can
be unicode, int, or bool. id3, mp4, and flac are StorageStyle instances
parameterizing the field's storage for each type."""
parameterizing the field's storage for each type.
"""
def __init__(self,
# The field's semantic (exterior) type.
@ -180,7 +186,8 @@ class MediaField(object):
def _fetchdata(self, obj):
"""Get the value associated with this descriptor's key (and id3_desc if
present) from the mutagen tag dict. Unwraps from a list if
necessary."""
necessary.
"""
style = self._style(obj)
try:
@ -210,7 +217,8 @@ class MediaField(object):
def _storedata(self, obj, val):
"""Store val for this descriptor's key in the tag dictionary. Store it
as a single-item list if necessary. Uses id3_desc if present."""
as a single-item list if necessary. Uses id3_desc if present.
"""
style = self._style(obj)
# wrap as a list if necessary
@ -244,7 +252,8 @@ class MediaField(object):
def _style(self, obj): return self.styles[obj.type]
def __get__(self, obj, owner):
"""Retrieve the value of this metadata field."""
"""Retrieve the value of this metadata field.
"""
style = self._style(obj)
out = self._fetchdata(obj)
@ -274,7 +283,8 @@ class MediaField(object):
return out
def __set__(self, obj, val):
"""Set the value of this metadata field."""
"""Set the value of this metadata field.
"""
style = self._style(obj)
if style.packing:
@ -317,27 +327,25 @@ class MediaField(object):
self._storedata(obj, out)
class CompositeDateField(object):
"""
A MediaFile field for conveniently accessing the year, month, and day fields
as a datetime.date object. Allows both getting and setting of the component
fields.
"""A MediaFile field for conveniently accessing the year, month, and day
fields as a datetime.date object. Allows both getting and setting of the
component fields.
"""
def __init__(self, year_field, month_field, day_field):
"""
Create a new date field from the indicated MediaFields for the component
values.
"""Create a new date field from the indicated MediaFields for the
component values.
"""
self.year_field = year_field
self.month_field = month_field
self.day_field = day_field
def __get__(self, obj, owner):
"""
Return a datetime.date object whose components indicating the smallest
valid date whose components are at least as large as the three component
fields (that is, if year == 1999, month == 0, and day == 0, then
date == datetime.date(1999, 1, 1)). If the components indicate an
invalid date (e.g., if month == 47), datetime.date.min is returned.
"""Return a datetime.date object whose components indicating the
smallest valid date whose components are at least as large as the
three component fields (that is, if year == 1999, month == 0, and
day == 0, then date == datetime.date(1999, 1, 1)). If the components
indicate an invalid date (e.g., if month == 47), datetime.date.min is
returned.
"""
try:
return datetime.date(max(self.year_field.__get__(obj, owner),
@ -349,8 +357,7 @@ class CompositeDateField(object):
return datetime.date.min
def __set__(self, obj, val):
"""
Set the year, month, and day fields to match the components of the
"""Set the year, month, and day fields to match the components of the
provided datetime.date object.
"""
self.year_field.__set__(obj, val.year)
@ -361,7 +368,8 @@ class CompositeDateField(object):
class MediaFile(object):
"""Represents a multimedia file on disk and provides access to its
metadata."""
metadata.
"""
def __init__(self, path):
try:

View file

@ -627,7 +627,7 @@ class BGServer(Server):
"""
def __init__(self, library, host='127.0.0.1', port=DEFAULT_PORT):
import gstplayer
import beets.player.gstplayer
super(BGServer, self).__init__(host, port)
self.lib = library
self.player = gstplayer.GstPlayer(self.play_finished)

View file

@ -87,9 +87,9 @@ def MakeWritingTest(path, correct_dict, field, testsuffix='_test'):
if readfield=='date' and field in ('year', 'month', 'day'):
try:
correct = datetime.date(
self.value if field=='year' else correct.year,
self.value if field=='month' else correct.month,
self.value if field=='day' else correct.day
self.value if field=='year' else correct.year,
self.value if field=='month' else correct.month,
self.value if field=='day' else correct.day
)
except ValueError:
correct = datetime.date.min
@ -98,9 +98,10 @@ def MakeWritingTest(path, correct_dict, field, testsuffix='_test'):
correct = getattr(self.value, readfield)
self.assertEqual(got, correct,
readfield + ' changed when it should not have (expected'
' ' + repr(correct) + ', got ' + repr(got) + ') when '
'modifying ' + field + ' in ' + os.path.basename(path))
readfield + ' changed when it should not have'
' (expected ' + repr(correct) + ', got ' + \
repr(got) + ') when modifying ' + field + ' in ' + \
os.path.basename(path))
def tearDown(self):
os.remove(self.tpath)