1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225
|
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009 Samson Yeung, Phil Sung
#
# This file is part of Zeya.
#
# Zeya is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Zeya is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Zeya. If not, see <http://www.gnu.org/licenses/>.
# Directory backend.
#
# Files in the specified directory are read for artist/title/album tag which is
# then saved in a database (zeya.db) stored in that directory.
import os
import tagpy
import pickle
from backends import LibraryBackend
from backends import extract_metadata
from common import tokenize_filename
from m3u import M3uPlaylist
from pls import PlsPlaylist
KEY = 'key'
DB = 'db'
KEY_FILENAME = 'key_filename'
MTIMES = 'mtimes'
class DirectoryBackend(LibraryBackend):
"""
Object that controls access to music in a given directory.
"""
def __init__(self, media_path, save_db=True):
"""
Initializes a DirectoryBackend that reads from the specified directory.
save_db can be set to False to prevent the db from being written back
to disk. This is probably only useful for debugging purposes.
"""
self._media_path = os.path.expanduser(media_path)
self._save_db = save_db
# Sequence of dicts containing song metadata (key, artist, title, album)
self.db = []
# Playlists
self._playlists = []
# Dict mapping keys to source filenames and vice versa
self.key_filename = {}
self.filename_key = {}
# Dict mapping filenames to mtimes
self.mtimes = {}
self.setup_db()
def get_db_filename(self):
return os.path.join(self._media_path, 'zeya.db')
def setup_db(self):
# Load the previous database from file, and convert it to a
# representation where it can serve as a metadata cache (keyed by
# filename) when we load the file collection.
previous_db = self.load_previous_db()
self.fill_db(previous_db)
if self._save_db:
self.save_db()
def load_previous_db(self):
"""
Read the existing database on disk and return a dict mapping each
filename to the (mtime, metadata) associated with the filename.
"""
filename_to_metadata_map = {}
try:
# Load the old data structures from file.
info = pickle.load(open(self.get_db_filename(), 'r'))
key_to_metadata_map = {}
prev_mtimes = info[MTIMES]
# Construct a map from keys to metadata.
for db_entry in info[DB]:
key_to_metadata_map[db_entry[KEY]] = db_entry
# Construct a map from filename to (mtime, metadata) associated
# with that file.
for (key, filename) in info[KEY_FILENAME].iteritems():
filename_to_metadata_map[filename] = \
(prev_mtimes[filename], key_to_metadata_map[key])
except IOError:
# Couldn't read the file. Just return an empty data structure.
pass
return filename_to_metadata_map
def save_db(self):
self.info = {DB: self.db,
MTIMES: self.mtimes,
KEY_FILENAME: self.key_filename}
try:
pickle.dump(self.info, open(self.get_db_filename(), 'wb+'))
except IOError, e:
print "Warning: the metadata cache could not be written to disk:"
print " " + str(e)
print "(Zeya will continue, but the directory will need to be",
print "re-scanned the next time Zeya is run.)"
def write_metadata(self, filename, previous_db):
"""
Obtains and writes the metadata for the specified FILENAME to the
database. The metadata may be found by looking in the cache
(PREVIOUS_DB), and failing that, by pulling the metadata from the file
itself.
Returns the key associated with the filename.
"""
if filename in self.filename_key:
# First, if the filename is already in our database, we don't have
# to do anything. We can encounter the same filename twice if a
# playlist contains a reference to a file we've already scanned.
key = self.filename_key[filename]
else:
# The filename is not in the database. We have to obtain a metadata
# entry, either by reading it out of our cache, or by calling out
# to tagpy.
#
# previous_db acts as a cache of mtime and metadata, keyed by
# filename.
rec_mtime, old_metadata = previous_db.get(filename, (None, None))
file_mtime = os.stat(filename).st_mtime
if rec_mtime is not None and rec_mtime >= file_mtime:
# Use cached data. However, we potentially renumber the keys
# every time the program runs, so the old KEY is no good. We'll
# fix up the KEY field below.
metadata = old_metadata
else:
# In this branch, we actually need to read the file and
# extract its metadata.
metadata = extract_metadata(filename)
# Assign a key for this song. These are just integers assigned
# sequentially.
key = len(self.key_filename)
metadata[KEY] = key
self.db.append(metadata)
self.key_filename[key] = filename
self.filename_key[filename] = key
self.mtimes[filename] = file_mtime
return key
def fill_db(self, previous_db):
"""
Populate the database, given the output of load_previous_db.
"""
# By default, os.walk will happily accept a non-existent directory and
# return an empty sequence. Detect the case of a non-existent path and
# bail out early.
if not os.path.exists(self._media_path):
raise IOError("Error: directory %r doesn't exist." % (self._media_path,))
print "Scanning for music in %r..." % (os.path.abspath(self._media_path),)
# Iterate over all the files.
try:
all_files_recursively = os.walk(self._media_path, followlinks=True)
except TypeError:
# os.walk in Python 2.5 and earlier don't support the followlinks
# argument. Fall back to not including it (in this case, Zeya will
# not index music underneath symlinked directories).
all_files_recursively = os.walk(self._media_path)
for path, dirs, files in all_files_recursively:
# Sort dirs so that subdirectories will subsequently be visited
# alphabetically (see os.walk).
dirs.sort(key=tokenize_filename)
for filename in sorted(files, key=tokenize_filename):
filename = os.path.abspath(os.path.join(path, filename))
# Skip broken symlinks
if not os.path.exists(filename):
continue
if filename.lower().endswith('.m3u') or filename.lower().endswith('.pls'):
# Encountered a playlist file.
try:
fileobj = open(filename)
except IOError:
print "Error opening playlist file: %r" % (filename,)
continue
if filename.lower().endswith('.m3u'):
playlist = M3uPlaylist(filename, fileobj)
elif filename.lower().endswith('.pls'):
playlist = PlsPlaylist(filename, fileobj)
items = []
for song_filename in playlist.get_filenames():
try:
song_key = self.write_metadata(
song_filename, previous_db)
except (OSError, ValueError):
continue
items.append(song_key)
self._playlists.append(
{'name' : playlist.get_title(), 'items': items})
else:
# Encountered what is possibly a regular music file.
try:
self.write_metadata(filename, previous_db)
except (OSError, ValueError):
continue
def get_library_contents(self):
return self.db
def get_playlists(self):
return self._playlists
def get_filename_from_key(self, key):
return self.key_filename[int(key)]
|