add logging module, use logging in cli.rename, cli uses decorator to parse options...
[audiomangler.git] / audiomangler / scanner.py
blob408056ef0198f2b0549bb8ca6313fcfca365e3c2
1 # -*- coding: utf-8 -*-
2 ###########################################################################
3 # Copyright (C) 2008 by Andrew Mahone
4 # <andrew.mahone@gmail.com>
6 # Copyright: See COPYING file that comes with this distribution
8 ###########################################################################
9 import os
10 import os.path
11 try:
12 import shelve
13 except ImportError:
14 shelve = None
15 from audiomangler import from_config, NormMetaData, Expr
16 from mutagen import File
17 from mutagen import version as mutagen_version
18 import time
20 db_version = (mutagen_version, (0,0))
22 def scan_track(path, from_dir = ''):
23 t = None
24 try:
25 t = File(path)
26 except Exception: pass
27 if t is not None:
28 if from_dir:
29 t.relpath = t.filename.replace(from_dir,'',1).lstrip('/')
30 t.reldir = t.relpath.rsplit('/',1)
31 if len(t.reldir) > 1:
32 t.reldir = t.reldir[0]
33 else:
34 t.reldir = ''
35 return t
37 def scan(items, groupby = None, sortby = None, trackid = None):
38 groupbytxt, sortbytxt, trackidtxt = from_config('groupby', 'sortby', 'trackid')
39 groupby = Expr(groupby or groupbytxt)
40 sortby = Expr(sortby or sortbytxt)
41 trackid = Expr(trackid or trackidtxt)
42 homedir = os.getenv('HOME')
43 if homedir is not None:
44 cachefile = os.path.join(homedir,'.audiomangler')
45 try:
46 os.mkdir(cachefile)
47 except OSError:
48 pass
49 cachefile = os.path.join(cachefile,'cache')
50 else:
51 cachefile = 'audiomangler.cache'
52 dircache = {}
53 if shelve:
54 try:
55 dircache = shelve.open(cachefile,protocol=2)
56 except Exception:
57 pass
58 if dircache.get('//version',None) != db_version:
59 dircache.clear()
60 dircache['//version'] = db_version
61 if isinstance(items,basestring):
62 items = (items,)
63 tracks = []
64 scanned = set()
65 newdircache = {}
66 items = map(os.path.abspath, items)
67 for item in items:
68 if item in scanned:
69 continue
70 t = scan_track(item)
71 if t is not None:
72 tracks.append(t)
73 scanned.add(item)
74 else:
75 dirs = [item]
76 while dirs:
77 path = dirs.pop(0)
78 try:
79 dst = os.stat(path)
80 except OSError:
81 print "unable to stat dir %s" % path
82 continue
83 cached = dircache.get(path,None)
84 if cached and cached['key'] == (dst.st_ino, dst.st_mtime):
85 newcached = cached
86 newtracks = []
87 newfiles = []
88 for track in cached['tracks']:
89 try:
90 fst = os.stat(track['path'])
91 except Exception:
92 continue
93 if track['key'] == (fst.st_ino, fst.st_size, fst.st_mtime):
94 newtracks.append(track)
95 t = track['obj']
96 t._meta_cache = (False,False)
97 t.relpath = t.filename.replace(item,'',1).lstrip('/')
98 t.reldir = t.relpath.rsplit('/',1)
99 if len(t.reldir) > 1:
100 t.reldir = t.reldir[0]
101 else:
102 t.reldir = ''
103 tracks.append(t)
104 else:
105 t = scan_track(track['path'])
106 if t is not None:
107 tracks.append(t)
108 newtracks.append({'path':track['path'],'obj':t,'key':(fst.st_ino,fst.st_size,fst.st_mtime)})
109 for file_ in cached['files']:
110 try:
111 fst = os.stat(file_['path'])
112 except Exception:
113 continue
114 if file_['key'] == (fst.st_ino, fst.st_size, fst.st_mtime):
115 newfiles.append(file_)
116 else:
117 t = scan_track(file_['path'])
118 if t is not None:
119 tracks.append(t)
120 newtracks.append({'path':file_['path'],'obj':t,'key':(fst.st_ino,fst.st_size,fst.st_mtime)})
121 else:
122 newfiles.append({'path':file_['path'],'key':(fst.st_ino,fst.st_size,fst.st_mtime)})
123 newcached['tracks'] = newtracks
124 newcached['files'] = newfiles
125 else:
126 newcached = {'tracks':[],'dirs':[],'files':[]}
127 newcached['key'] = (dst.st_ino, dst.st_mtime)
128 paths = (os.path.join(path,f) for f in sorted(os.listdir(path)))
129 for filename in paths:
130 if filename in scanned:
131 continue
132 else:
133 scanned.add(filename)
134 if os.path.isdir(filename):
135 newcached['dirs'].append(filename)
136 elif os.path.isfile(filename):
137 try:
138 fst = os.stat(filename)
139 except Exception:
140 continue
141 t = scan_track(filename)
142 if t is not None:
143 tracks.append(t)
144 newcached['tracks'].append({'path':filename,'obj':t,'key':(fst.st_ino,fst.st_size,fst.st_mtime)})
145 else:
146 newcached['files'].append({'path':filename,'key':(fst.st_ino,fst.st_size,fst.st_mtime)})
147 else:
148 continue
149 dirs.extend(newcached['dirs'])
150 newdircache[path] = newcached
151 for item in items:
152 for key in dircache.keys():
153 if key.startswith(item):
154 del dircache[key]
155 dircache.update(newdircache)
156 if hasattr(dircache,'close'):
157 dircache.close()
158 albums = {}
159 dirs = {}
160 trackids = {}
161 for t in tracks:
162 t.sortkey = t.meta.evaluate(sortby)
163 albums.setdefault(t.meta.evaluate(groupby),[]).append(t)
164 dirs.setdefault(os.path.split(t.filename)[0],[]).append(t)
165 t.tid = t.meta.evaluate(trackid)
166 if t.tid in trackids:
167 print "trackid collision"
168 print t.filename
169 print trackids[t.tid].filename
170 trackids[t.tid] = t
171 #trying not to evaluate sort expressions for every comparison. don't modify
172 #metadata during sort. ;)
173 for v in albums.itervalues():
174 v.sort(lambda x,y: cmp(x.sortkey,y.sortkey))
175 for v in dirs.itervalues():
176 v.sort(lambda x,y: cmp(x.sortkey,y.sortkey))
177 return albums, dirs, trackids
179 __all__ = ['scan']