summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/audiocloudweb.cfg7
-rw-r--r--src/get_music_url.py23
-rw-r--r--src/index_files.py58
-rw-r--r--src/index_search.py10
-rw-r--r--src/index_service.py27
-rw-r--r--src/indexer.py113
-rw-r--r--src/load_filesystem.py11
-rw-r--r--src/load_itunes.py326
-rw-r--r--src/parse_mp4.py67
-rw-r--r--src/parse_smart_playlist.py636
-rw-r--r--src/test.html16
-rw-r--r--src/test.py21
-rwxr-xr-xsrc/track_service.py132
-rw-r--r--src/util.py29
14 files changed, 1476 insertions, 0 deletions
diff --git a/src/audiocloudweb.cfg b/src/audiocloudweb.cfg
new file mode 100644
index 0000000..8df17d0
--- /dev/null
+++ b/src/audiocloudweb.cfg
@@ -0,0 +1,7 @@
1[audiocloudweb]
2access_key =
3secret_key =
4
5[audiocloud]
6access_key =
7secret_key =
diff --git a/src/get_music_url.py b/src/get_music_url.py
new file mode 100644
index 0000000..d4284d4
--- /dev/null
+++ b/src/get_music_url.py
@@ -0,0 +1,23 @@
1import sqlite3
2import urllib
3from util import SimpleConfigParser
4from boto.s3.connection import S3Connection
5
6# remove key metadata
7#key = key.copy(key.bucket.name, key.name, metadata={'Content-Type': 'audio/mp4'}, preserve_acl=True)
8
9db = sqlite3.connect('iTunesLibrary.db')
10curs = db.cursor()
11cfg = SimpleConfigParser('audiocloudweb.cfg', 'audiocloud')
12s3 = S3Connection(cfg.get('access_key'), cfg.get('secret_key'))
13bucket = s3.get_bucket('mecmusic')
14
15
16
17#curs.execute('select location from track where artist = 629 limit 1')
18curs.execute('select location from track where track_id = 29510')
19track = curs.fetchone()[0][6:]#.encode('utf-8')
20
21key = bucket.get_key(track, validate=True)
22key.metadata = {}
23print key.generate_url(60 * 10)
diff --git a/src/index_files.py b/src/index_files.py
new file mode 100644
index 0000000..ece51aa
--- /dev/null
+++ b/src/index_files.py
@@ -0,0 +1,58 @@
1import os
2import re
3import json
4import mutagen
5from pprint import pprint
6
7
8music = re.compile('.*\.(mp3|m4a)$')
9
10
11tags = set()
12
13
14def parse_frame(txt):
15 frame_type, remainder = txt.split(':', 1)
16 content = None
17
18 if remainder.startswith('http'):
19 remainder = remainder.split(':', 2)
20 content = remainder[-1]
21 remainder = ':'.join(remainder[:2])
22 elif ':' in remainder:
23 remainder, content = remainder.split(':', 1)
24
25 return frame_type, remainder, content
26
27
28for path, dirs, files in os.walk(os.path.expanduser('~/Desktop/Music')):
29 for file in files:
30 fullname = os.path.join(path, file)
31
32 if not music.match(fullname):
33 continue
34
35 file = mutagen.File(fullname)
36
37 if not file or not file.tags:
38 print "ERROR: ", fullname
39 continue
40
41 for tag, value in file.tags.items():
42 #if hasattr(value, 'text'):
43 # value = value.text
44
45 #if isinstance(value, list) and len(value) == 1:
46 # value = value[0]
47
48 #if tag == 'covr':
49 # continue
50
51 #if hasattr(value, 'mime') and value.mime.startswith('image'):
52 # continue
53
54 #if tag.startswith(('PRIV', 'APIC', 'COMM', 'USLT', 'WCOM')):
55 # continue
56
57 #if not isinstance(value, unicode):
58 print repr(tag), type(value), repr(value)
diff --git a/src/index_search.py b/src/index_search.py
new file mode 100644
index 0000000..805a9fb
--- /dev/null
+++ b/src/index_search.py
@@ -0,0 +1,10 @@
1from whoosh import index
2from whoosh.qparser import MultifieldParser
3
4idx = index.open_dir("indexdir")
5parser = MultifieldParser(["track_name", "artist", "album"], schema=idx.schema)
6
7with idx.searcher() as searcher:
8 results = searcher.search(parser.parse("nine inch nails"), groupedby="genre")
9 import pdb; pdb.set_trace()
10 True
diff --git a/src/index_service.py b/src/index_service.py
new file mode 100644
index 0000000..5038fce
--- /dev/null
+++ b/src/index_service.py
@@ -0,0 +1,27 @@
1from whoosh import index
2from whoosh.qparser import MultifieldParser
3
4import bottle
5from bottle import route, run, request
6
7
8@route('/search')
9def search():
10 term = request.query.get("q")
11
12 if not term:
13 return { 'error': 'No search term specified' }
14
15 idx = index.open_dir("indexdir")
16 parser = MultifieldParser(
17 ["track_name", "artist", "album"], schema=idx.schema)
18
19 with idx.searcher() as searcher:
20 results = searcher.search(parser.parse(term))
21 return { 'results': [dict(result) for result in results] }
22
23bottle.debug(True)
24app = bottle.default_app()
25
26#if __name__ == "__main__":
27run(host='localhost', port=8080)
diff --git a/src/indexer.py b/src/indexer.py
new file mode 100644
index 0000000..283cdbc
--- /dev/null
+++ b/src/indexer.py
@@ -0,0 +1,113 @@
1import sqlite3
2
3from whoosh import fields, index
4
5TRACK_QUERY = """
6SELECT
7 t.track_id, t.composer, t.explicit, t.disc_number, t.name as track_name,
8 t.track_number, t.year as track_year, g.name as genre, a.artist,
9 ab.title as album, ab.artist as album_artist, ab.compilation,
10 ab.disc_count, ab.gapless, ab.release_date, ab.track_count,
11 ab.year as album_year, p.bpm, p.bit_rate, p.sample_rate, p.total_time,
12 k.kind
13FROM
14 track t
15INNER JOIN
16 genre g
17ON t.genre = g.id
18INNER JOIN
19 artist a
20ON t.artist = a.id
21INNER JOIN
22 album ab
23ON t.album = ab.id
24INNER JOIN
25 track_physical p
26ON t.track_id = p.track_id
27INNER JOIN
28 kind k
29ON p.kind = k.id
30"""
31
32def safe_unicode(value):
33 return value
34 return value.decode("utf-8") if value else None
35
36def safe_int(value):
37 return int(value) if value is not None else None
38
39def to_boolean(value):
40 return 1 if value else 0
41
42def first_value(*items):
43 for item in items:
44 if item is not None:
45 return item
46
47 return None
48
49
50db = sqlite3.connect('iTunesLibrary.db')
51db.row_factory = sqlite3.Row
52
53curs = db.cursor()
54curs.execute(TRACK_QUERY)
55
56schema = fields.Schema(
57 track_id=fields.ID(stored=True),
58 composer=fields.TEXT(stored=True),
59 explicit=fields.BOOLEAN(stored=True),
60 disc_number=fields.NUMERIC(stored=True),
61
62 track_name=fields.NGRAM(stored=True),
63 genre=fields.NGRAM(stored=True),
64 artist=fields.NGRAM(stored=True),
65 album=fields.NGRAM(stored=True),
66
67 track_number=fields.NUMERIC(stored=True),
68 year=fields.NUMERIC(stored=True),
69 compilation=fields.BOOLEAN(stored=True),
70 disc_count=fields.NUMERIC(stored=True),
71 gapless=fields.BOOLEAN(stored=True),
72 release_date=fields.DATETIME(stored=True),
73 track_count=fields.NUMERIC(stored=True),
74 bpm=fields.NUMERIC(stored=True),
75 bit_rate=fields.NUMERIC(stored=True),
76 sample_rate=fields.NUMERIC(stored=True),
77 total_time=fields.NUMERIC(stored=True),
78 kind=fields.TEXT(stored=True)
79)
80
81idx = index.create_in("indexdir", schema)
82writer = idx.writer()
83
84for record in curs.fetchall():
85 writer.add_document(
86 track_id=str(record['track_id']).decode("ascii"),
87
88 composer=safe_unicode(record['composer']),
89 genre=safe_unicode(record['genre']),
90 album=safe_unicode(record['album']),
91 artist=safe_unicode(first_value(record['artist'], record['album_artist'])),
92 track_name=safe_unicode(record['track_name']),
93 kind=safe_unicode(record['kind']),
94
95 #release_date=record['release_date'],
96
97 explicit=to_boolean(record['explicit']),
98 compilation=to_boolean(record['compilation']),
99 gapless=to_boolean(record['gapless']),
100
101 disc_number=safe_int(record['disc_number']),
102 track_number=safe_int(record['track_number']),
103 year=safe_int(first_value(record['track_year'], record['album_year'])),
104 disc_count=safe_int(record['disc_count']),
105 track_count=safe_int(record['track_count']),
106 bpm=safe_int(record['bpm']),
107 bit_rate=safe_int(record['bit_rate']),
108 sample_rate=safe_int(record['sample_rate']),
109 total_time=safe_int(record['total_time']),
110 )
111
112writer.commit()
113db.close()
diff --git a/src/load_filesystem.py b/src/load_filesystem.py
new file mode 100644
index 0000000..6181017
--- /dev/null
+++ b/src/load_filesystem.py
@@ -0,0 +1,11 @@
1import sqlite3
2import codecs
3
4db = sqlite3.connect('iTunes/iTunesLibrary.db')
5curs = db.cursor()
6
7with codecs.open('tracks.txt', 'r', 'utf-8') as fp:
8 for track in fp:
9 curs.execute('insert into filesystem(location) values(?)', (track.strip(),))
10
11db.commit()
diff --git a/src/load_itunes.py b/src/load_itunes.py
new file mode 100644
index 0000000..f4131b1
--- /dev/null
+++ b/src/load_itunes.py
@@ -0,0 +1,326 @@
1import sqlite3
2import plistlib
3
4library = plistlib.readPlist('iTunes-Music-Library.xml')
5
6
7class BasicLibraryItem(object):
8
9 def get_insert_query(self):
10 return 'INSERT INTO {} VALUES ({})'.format(
11 self.TABLE_NAME, self.get_bind_string(self.COLUMN_COUNT))
12
13 @staticmethod
14 def get_bind_string(count):
15 return ('?,' * count)[:-1]
16
17 @staticmethod
18 def as_data_string(value):
19 if not value:
20 return None
21 else:
22 return value.asBase64()
23
24 @staticmethod
25 def as_boolean(value):
26 return 1 if value else 0
27
28 @staticmethod
29 def format_name(name):
30 return name.lower().replace(' ', '_')
31
32 @classmethod
33 def from_plist_entry(cls, entry):
34 self = cls()
35
36 for key, value in entry.items():
37 name = cls.format_name(key)
38
39 if not hasattr(self, name):
40 raise Exception("No attribute named %r" % name)
41
42 setattr(self, name, value)
43
44 return self
45
46
47class Playlist(BasicLibraryItem):
48
49 COLUMN_COUNT = 21
50 TABLE_NAME = 'playlist'
51
52 def __init__(self):
53 self.playlist_id = None
54 self.playlist_persistent_id = None
55 self.parent_persistent_id = None
56 self.distinguished_kind = None
57 self.genius_track_id = None
58 self.name = None
59
60 self.master = False
61 self.visible = False
62 self.all_items = False
63 self.folder = False
64
65 self.music = False
66 self.movies = False
67 self.tv_shows = False
68 self.podcasts = False
69 self.itunesu = False
70 self.audiobooks = False
71 self.books = False
72 self.purchased_music = False
73
74 self.smart_info = None
75 self.smart_criteria = None
76
77 self.items = []
78
79 def as_dbrow_tuple(self):
80 return (
81 self.playlist_id, #int
82 1, # user_id
83 self.playlist_persistent_id,
84 self.parent_persistent_id,
85 self.distinguished_kind, #int
86 self.genius_track_id, #int
87 self.name,
88
89 self.as_boolean(self.master),
90 self.as_boolean(self.visible),
91 self.as_boolean(self.all_items),
92 self.as_boolean(self.folder),
93 self.as_boolean(self.music),
94 self.as_boolean(self.movies),
95 self.as_boolean(self.tv_shows),
96 self.as_boolean(self.podcasts),
97 self.as_boolean(self.itunesu),
98 self.as_boolean(self.audiobooks),
99 self.as_boolean(self.books),
100 self.as_boolean(self.purchased_music),
101
102 self.as_data_string(self.smart_info),
103 self.as_data_string(self.smart_criteria)
104 )
105
106 @classmethod
107 def from_plist_entry(cls, entry):
108 self = cls()
109
110 for key, value in entry.items():
111 if key == 'Playlist Items':
112 for item in value:
113 self.items.append(item['Track ID'])
114
115 continue
116
117 name = cls.format_name(key)
118
119 if not hasattr(self, name):
120 raise Exception("No attribute named %r" % name)
121
122 setattr(self, name, value)
123
124 return self
125
126
127class Track(BasicLibraryItem):
128
129 COLUMN_COUNT = 53
130 TABLE_NAME = 'track'
131
132 def __init__(self):
133 self.album_rating = None
134 self.album_rating_computed = False
135 self.comments = None
136 self.volume_adjustment = None
137 self.unplayed = False
138 self.date_added = None
139 self.date_modified = None
140 self.disabled = False
141 self.play_count = None
142 self.play_date = None
143 self.play_date_utc = None
144 self.rating = None
145 self.skip_count = None
146 self.skip_date = None
147 self.track_id = None
148 self.persistent_id = None
149 self.library_folder_count = None
150 self.file_folder_count = None
151 self.location = None
152 self.track_type = None
153 self.file_type = None
154 self.artwork_count = None
155 self.hd = False
156 self.has_video = False
157 self.itunesu = False
158 self.tv_show = False
159 self.podcast = False
160 self.protected = False
161 self.purchased = False
162 self.movie = False
163 self.music_video = False
164 self.bpm = None
165 self.bit_rate = None
166 self.sample_rate = None
167 self.size = None
168 self.total_time = None
169 self.kind = None
170 self.video_height = None
171 self.video_width = None
172 self.sort_album = None
173 self.sort_album_artist = None
174 self.sort_artist = None
175 self.sort_composer = None
176 self.sort_name = None
177 self.sort_series = None
178 self.album = None
179 self.album_artist = None
180 self.artist = None
181 self.clean = False
182 self.compilation = False
183 self.composer = None
184 self.content_rating = None
185 self.disc_count = None
186 self.disc_number = None
187 self.episode = None
188 self.episode_order = None
189 self.explicit = False
190 self.genre = None
191 self.grouping = None
192 self.name = None
193 self.part_of_gapless_album = False
194 self.release_date = None
195 self.season = None
196 self.series = None
197 self.track_count = None
198 self.track_number = None
199 self.year = None
200
201 def as_dbrow_tuple(self):
202 return (
203 self.track_id,
204 self.persistent_id,
205 self.library_folder_count,
206 self.file_folder_count,
207 self.location,
208 self.track_type,
209 self.file_type,
210 self.artwork_count,
211 self.as_boolean(self.hd),
212 self.as_boolean(self.has_video),
213 self.as_boolean(self.itunesu),
214 self.as_boolean(self.tv_show),
215 self.as_boolean(self.podcast),
216 self.as_boolean(self.protected),
217 self.as_boolean(self.purchased),
218 self.as_boolean(self.movie),
219 self.as_boolean(self.music_video),
220 self.bpm,
221 self.bit_rate,
222 self.sample_rate,
223 self.size,
224 self.total_time,
225 self.kind,
226 self.video_height,
227 self.video_width,
228 self.sort_album,
229 self.sort_album_artist,
230 self.sort_artist,
231 self.sort_composer,
232 self.sort_name,
233 self.sort_series,
234 self.album,
235 self.album_artist,
236 self.artist,
237 self.as_boolean(self.clean),
238 self.as_boolean(self.compilation),
239 self.composer,
240 self.content_rating,
241 self.disc_count,
242 self.disc_number,
243 self.episode,
244 self.episode_order,
245 self.as_boolean(self.explicit),
246 self.genre,
247 self.grouping,
248 self.name,
249 self.as_boolean(self.part_of_gapless_album),
250 self.release_date,
251 self.season,
252 self.series,
253 self.track_count,
254 self.track_number,
255 self.year
256 )
257
258 def usermeta_as_dbrow_tuple(self):
259 return (
260 1,
261 self.track_id,
262 self.album_rating,
263 self.as_boolean(self.album_rating_computed),
264 self.comments,
265 self.volume_adjustment,
266 self.as_boolean(self.unplayed),
267 self.date_added,
268 self.date_modified,
269 self.as_boolean(self.disabled),
270 self.play_count,
271 self.play_date,
272 self.play_date_utc,
273 self.rating,
274 self.skip_count,
275 self.skip_date
276 )
277
278 USERMETA_COLUMN_COUNT = 16
279 USERMETA_TABLE_NAME = 'user_track_metadata'
280
281 def get_usermeta_insert_query(self):
282 return 'INSERT INTO {} VALUES ({})'.format(
283 self.USERMETA_TABLE_NAME,
284 self.get_bind_string(self.USERMETA_COLUMN_COUNT))
285
286
287assert library['Major Version'] == 1, "Invalid major version"
288assert library['Minor Version'] == 1, "Invalid minor version"
289
290def insert_playlists(db, library):
291 playlists = [Playlist.from_plist_entry(entry)
292 for entry in library['Playlists']]
293
294 curs = db.cursor()
295 track_insert_query = playlists[0].get_insert_query()
296
297 for playlist in playlists:
298 curs.execute(track_insert_query, playlist.as_dbrow_tuple())
299
300 for item in playlist.items:
301 curs.execute('INSERT INTO playlist_track VALUES (?, ?)',
302 (playlist.playlist_id, item))
303
304 db.commit()
305
306
307def insert_tracks(db, library):
308 music_folder = library['Music Folder']
309 tracks = [Track.from_plist_entry(entry)
310 for entry in library['Tracks'].values()]
311
312 curs = db.cursor()
313 track_insert_query = tracks[0].get_insert_query()
314 usermeta_insert_query = tracks[0].get_usermeta_insert_query()
315
316 for track in tracks:
317 curs.execute(track_insert_query, track.as_dbrow_tuple())
318 curs.execute(usermeta_insert_query, track.usermeta_as_dbrow_tuple())
319
320 db.commit()
321
322
323db = sqlite3.connect('iTunesLibrary.db')
324#insert_playlists(db, library)
325insert_tracks(db, library)
326db.close()
diff --git a/src/parse_mp4.py b/src/parse_mp4.py
new file mode 100644
index 0000000..3e92330
--- /dev/null
+++ b/src/parse_mp4.py
@@ -0,0 +1,67 @@
1from mutagen import mp4
2from mutagen.easyid3 import EasyID3
3
4
5class MP4Tags(mp4.MP4Tags):
6
7 fields = {
8 'cover': 'covr',
9 'tempo': 'tmpo',
10 'track_num': 'trkn',
11 'disc_num': 'disk',
12 'is_part_of_compilation': 'cpil',
13 'is_part_of_gapless_album': 'pgap',
14 'is_podcast': 'pcst',
15 'track_title': '\xa9nam',
16 'album': '\xa9alb',
17 'artist': '\xa9ART',
18 'album_artist': 'aART',
19 'composer': '\xa9wrt',
20 'year': '\xa9day',
21 'comment': '\xa9cmt',
22 'description': 'desc',
23 'purchase_date': 'purd',
24 'grouping': '\xa9grp',
25 'genre': '\xa9gen',
26 'lyrics': '\xa9lyr',
27 'podcast_url': 'purl',
28 'podcast_episode_id': 'egid',
29 'podcast_category': 'catg',
30 'podcast_keyword': 'keyw',
31 'encoded_by': '\xa9too',
32 'copyright': 'cprt',
33 'sort_album': 'soal',
34 'sort_album_artist': 'soaa',
35 'sort_artist': 'soar',
36 'sort_title': 'sonm',
37 'sort_composer': 'soco',
38 'sort_show': 'sosn',
39 'tv_show_name': 'tvsh',
40 }
41
42 @property
43 def track_number(self):
44 return self.track_num[0]
45
46 @property
47 def total_tracks(self):
48 return self.track_num[1]
49
50 @property
51 def disc_number(self):
52 return self.disc_num[0]
53
54 @property
55 def total_discs(self):
56 return self.disc_num[1]
57
58 def __getattr__(self, attr):
59 value = self[self.fields[attr]]
60 if len(value) == 1:
61 return value[0]
62 else:
63 return value
64
65
66class MP4(mp4.MP4):
67 MP4Tags = MP4Tags
diff --git a/src/parse_smart_playlist.py b/src/parse_smart_playlist.py
new file mode 100644
index 0000000..0bfde9d
--- /dev/null
+++ b/src/parse_smart_playlist.py
@@ -0,0 +1,636 @@
1# // http://banshee-itunes-import-plugin.googlecode.com/svn/trunk/
2# using System;
3# using System.Collections.Generic;
4# using System.Text;
5# using Banshee.SmartPlaylist;
6#
7# namespace Banshee.Plugins.iTunesImporter
8# {
9# internal struct SmartPlaylist
10# {
11# public string Query, Ignore, OrderBy, Name, Output;
12# public uint LimitNumber;
13# public byte LimitMethod;
14# }
15#
16# internal static partial class SmartPlaylistParser
17# {
18# private delegate bool KindEvalDel(Kind kind, string query);
19#
20# // INFO OFFSETS
21# //
22# // Offsets for bytes which...
23# const int MATCHBOOLOFFSET = 1; // determin whether logical matching is to be performed - Absolute offset
24# const int LIMITBOOLOFFSET = 2; // determin whether results are limited - Absolute offset
25# const int LIMITMETHODOFFSET = 3; // determin by what criteria the results are limited - Absolute offset
26# const int SELECTIONMETHODOFFSET = 7; // determin by what criteria limited playlists are populated - Absolute offset
27# const int LIMITINTOFFSET = 11; // determin the limited - Absolute offset
28# const int SELECTIONMETHODSIGNOFFSET = 13;// determin whether certain selection methods are "most" or "least" - Absolute offset
29#
30# // CRITERIA OFFSETS
31# //
32# // Offsets for bytes which...
33# const int LOGICTYPEOFFSET = 15; // determin whether all or any criteria must match - Absolute offset
34# const int FIELDOFFSET = 139; // determin what is being matched (Artist, Album, &c) - Absolute offset
35# const int LOGICSIGNOFFSET = 1; // determin whether the matching rule is positive or negative (e.g., is vs. is not) - Relative offset from FIELDOFFSET
36# const int LOGICRULEOFFSET = 4; // determin the kind of logic used (is, contains, begins, &c) - Relative offset from FIELDOFFSET
37# const int STRINGOFFSET = 54; // begin string data - Relative offset from FIELDOFFSET
38# const int INTAOFFSET = 60; // begin the first int - Relative offset from FIELDOFFSET
39# const int INTBOFFSET = 24; // begin the second int - Relative offset from INTAOFFSET
40# const int TIMEMULTIPLEOFFSET = 76;// begin the int with the multiple of time - Relative offset from FIELDOFFSET
41# const int TIMEVALUEOFFSET = 68; // begin the inverse int with the value of time - Relative offset from FIELDOFFSET
42#
43# const int INTLENGTH = 64; // The length on a int criteria starting at the first int
44# static DateTime STARTOFTIME = new DateTime(1904, 1, 1); // Dates are recorded as seconds since Jan 1, 1904
45#
46# static bool or, again;
47# static string conjunctionOutput, conjunctionQuery, output, query, ignore;
48# static int offset, logicSignOffset,logicRulesOffset, stringOffset, intAOffset, intBOffset,
49# timeMultipleOffset, timeValueOffset;
50# static byte[] info, criteria;
51#
52# static KindEvalDel KindEval;
53#
54# public static SmartPlaylist Parse(byte[] i, byte[] c)
55# {
56# info = i;
57# criteria = c;
58# SmartPlaylist result = new SmartPlaylist();
59# offset = FIELDOFFSET;
60# output = "";
61# query = "";
62# ignore = "";
63#
64# if(info[MATCHBOOLOFFSET] == 1) {
65# or = (criteria[LOGICTYPEOFFSET] == 1) ? true : false;
66# if(or) {
67# conjunctionQuery = " OR ";
68# conjunctionOutput = " or\n";
69# } else {
70# conjunctionQuery = " AND ";
71# conjunctionOutput = " and\n";
72# }
73# do {
74# again = false;
75# logicSignOffset = offset + LOGICSIGNOFFSET;
76# logicRulesOffset = offset + LOGICRULEOFFSET;
77# stringOffset = offset + STRINGOFFSET;
78# intAOffset = offset + INTAOFFSET;
79# intBOffset = intAOffset + INTBOFFSET;
80# timeMultipleOffset = offset + TIMEMULTIPLEOFFSET;
81# timeValueOffset = offset + TIMEVALUEOFFSET;
82#
83# if(Enum.IsDefined(typeof(StringFields), (int)criteria[offset])) {
84# ProcessStringField();
85# } else if(Enum.IsDefined(typeof(IntFields), (int)criteria[offset])) {
86# ProcessIntField();
87# } else if(Enum.IsDefined(typeof(DateFields), (int)criteria[offset])) {
88# ProcessDateField();
89# } else {
90# ignore += "Not processed";
91# }
92# }
93# while(again);
94# }
95# result.Output = output;
96# result.Query = query;
97# result.Ignore = ignore;
98# if(info[LIMITBOOLOFFSET] == 1) {
99# uint limit = BytesToUInt(info, LIMITINTOFFSET);
100# result.LimitNumber = (info[LIMITMETHODOFFSET] == (byte)LimitMethods.GB) ? limit * 1024 : limit;
101# if(output.Length > 0) {
102# output += "\n";
103# }
104# output += "Limited to " + limit.ToString() + " " +
105# Enum.GetName(typeof(LimitMethods), (int)info[LIMITMETHODOFFSET]) + " selected by ";
106# switch(info[LIMITMETHODOFFSET]) {
107# case (byte)LimitMethods.Items:
108# result.LimitMethod = 0;
109# break;
110# case (byte)LimitMethods.Minutes:
111# result.LimitMethod = 1;
112# break;
113# case (byte)LimitMethods.Hours:
114# result.LimitMethod = 2;
115# break;
116# case (byte)LimitMethods.MB:
117# result.LimitMethod = 3;
118# break;
119# case (byte)LimitMethods.GB:
120# goto case (byte)LimitMethods.MB;
121# }
122# switch(info[SELECTIONMETHODOFFSET]) {
123# case (byte)SelectionMethods.Random:
124# output += "random";
125# result.OrderBy = "RANDOM()";
126# break;
127# case (byte)SelectionMethods.HighestRating:
128# output += "highest rated";
129# result.OrderBy = "Rating DESC";
130# break;
131# case (byte)SelectionMethods.LowestRating:
132# output += "lowest rated";
133# result.OrderBy = "Rating ASC";
134# break;
135# case (byte)SelectionMethods.RecentlyPlayed:
136# output += (info[SELECTIONMETHODSIGNOFFSET] == 0)
137# ? "most recently played" : "least recently played";
138# result.OrderBy = (info[SELECTIONMETHODSIGNOFFSET] == 0)
139# ? "LastPlayedStamp DESC" : "LastPlayedStamp ASC";
140# break;
141# case (byte)SelectionMethods.OftenPlayed:
142# output += (info[SELECTIONMETHODSIGNOFFSET] == 0)
143# ? "most often played" : "least often played";
144# result.OrderBy = (info[SELECTIONMETHODSIGNOFFSET] == 0)
145# ? "NumberOfPlays DESC" : "NumberOfPlays ASC";
146# break;
147# case (byte)SelectionMethods.RecentlyAdded:
148# output += (info[SELECTIONMETHODSIGNOFFSET] == 0)
149# ? "most recently added" : "least recently added";
150# result.OrderBy = (info[SELECTIONMETHODSIGNOFFSET] == 0)
151# ? "DateAddedStamp DESC" : "DateAddedStamp ASC";
152# break;
153# default:
154# result.OrderBy = Enum.GetName(typeof(SelectionMethods), (int)info[SELECTIONMETHODOFFSET]);
155# break;
156# }
157# }
158# if(ignore.Length > 0) {
159# output += "\n\nIGNORING:\n" + ignore;
160# }
161#
162# if(query.Length > 0) {
163# output += "\n\nQUERY:\n" + query;
164# }
165# return result;
166# }
167#
168# private static void ProcessStringField()
169# {
170# bool end = false;
171# string workingOutput = Enum.GetName(typeof(StringFields), criteria[offset]);
172# string workingQuery = "(lower(" + Enum.GetName(typeof(StringFields), criteria[offset]) + ")";
173# switch(criteria[logicRulesOffset]) {
174# case (byte)LogicRule.Contains:
175# if((criteria[logicSignOffset] == (byte)LogicSign.StringPositive)) {
176# workingOutput += " contains ";
177# workingQuery += " LIKE '%";
178# } else {
179# workingOutput += " does not contain ";
180# workingQuery += " NOT LIKE '%";
181# }
182# if(criteria[offset] == (byte)StringFields.Kind) {
183# KindEval = delegate(Kind kind, string query) {
184# return (kind.Name.IndexOf(query) != -1);
185# };
186# }
187# end = true;
188# break;
189# case (byte)LogicRule.Is:
190# if((criteria[logicSignOffset] == (byte)LogicSign.StringPositive)) {
191# workingOutput += " is ";
192# workingQuery += " = '";
193# } else {
194# workingOutput += " is not ";
195# workingQuery += " != '";
196# }
197# if(criteria[offset] == (byte)StringFields.Kind) {
198# KindEval = delegate(Kind kind, string query) {
199# return (kind.Name == query);
200# };
201# }
202# break;
203# case (byte)LogicRule.Starts:
204# workingOutput += " starts with ";
205# workingQuery += " LIKE '";
206# if(criteria[offset] == (byte)StringFields.Kind) {
207# KindEval = delegate (Kind kind, string query) {
208# return (kind.Name.IndexOf(query) == 0);
209# };
210# }
211# end = true;
212# break;
213# case (byte)LogicRule.Ends:
214# workingOutput += " ends with ";
215# workingQuery += " LIKE '%";
216# if(criteria[offset] == (byte)StringFields.Kind) {
217# KindEval = delegate (Kind kind, string query) {
218# return (kind.Name.IndexOf(query) == (kind.Name.Length - query.Length));
219# };
220# }
221# break;
222# }
223# workingOutput += "\"";
224# byte[] character = new byte[1];
225# string content = "";
226# bool onByte = true;
227# for(int i = (stringOffset); i < criteria.Length; i++) {
228# // Off bytes are 0
229# if(onByte) {
230# // If the byte is 0 and it's not the last byte,
231# // we have another condition
232# if(criteria[i] == 0 && i != (criteria.Length - 1)) {
233# again = true;
234# FinishStringField(content, workingOutput, workingQuery, end);
235# offset = i + 2;
236# return;
237# }
238# character[0] = criteria[i];
239# content += Encoding.UTF8.GetString(character);
240# }
241# onByte = !onByte;
242# }
243# FinishStringField(content, workingOutput, workingQuery, end);
244# }
245#
246# private static void FinishStringField(string content, string workingOutput, string workingQuery, bool end)
247# {
248# workingOutput += content;
249# workingOutput += "\" ";
250# bool failed = false;
251# if(criteria[offset] == (byte)StringFields.Kind) {
252# workingQuery = "";
253# foreach(Kind kind in Kinds) {
254# if(KindEval(kind, content)) {
255# if(workingQuery.Length > 0) {
256# if((query.Length == 0 && !again) || or) {
257# workingQuery += " OR ";
258# } else {
259# failed = true;
260# break;
261# }
262# }
263# workingQuery += "(lower(Uri)";
264# workingQuery += ((criteria[logicSignOffset] == (byte)LogicSign.StringPositive))
265# ? " LIKE '%" + kind.Extension + "')" : " NOT LIKE '%" + kind.Extension + "%')";
266# }
267# }
268# } else {
269# workingQuery += content.ToLower();
270# workingQuery += (end) ? "%')" : "')";
271# }
272# if(Enum.IsDefined(typeof(IgnoreStringFields),
273# (int)criteria[offset]) || failed) {
274# if(ignore.Length > 0) {
275# ignore += conjunctionOutput;
276# }
277# ignore += workingOutput;
278# } else {
279# if(output.Length > 0) {
280# output += conjunctionOutput;
281# }
282# if(query.Length > 0) {
283# query += conjunctionQuery;
284# }
285# output += workingOutput;
286# query += workingQuery;
287# }
288# }
289#
290# private static void ProcessIntField()
291# {
292# string workingOutput = Enum.GetName(typeof(IntFields), criteria[offset]);
293# string workingQuery = "(" + Enum.GetName(typeof(IntFields), criteria[offset]);
294#
295# switch(criteria[logicRulesOffset]) {
296# case (byte)LogicRule.Is:
297# if(criteria[logicSignOffset] == (byte)LogicSign.IntPositive) {
298# workingOutput += " is ";
299# workingQuery += " = ";
300# } else {
301# workingOutput += " is not ";
302# workingQuery += " != ";
303# }
304# goto case 255;
305# case (byte)LogicRule.Greater:
306# workingOutput += " is greater than ";
307# workingQuery += " > ";
308# goto case 255;
309# case (byte)LogicRule.Less:
310# workingOutput += " is less than ";
311# workingQuery += " > ";
312# goto case 255;
313# case 255:
314# uint number = (criteria[offset] == (byte)IntFields.Rating)
315# ? (BytesToUInt(criteria, intAOffset) / 20) : BytesToUInt(criteria, intAOffset);
316# workingOutput += number.ToString();
317# workingQuery += number.ToString();
318# break;
319# case (byte)LogicRule.Other:
320# if(criteria[logicSignOffset + 2] == 1) {
321# workingOutput += " is in the range of ";
322# workingQuery += " BETWEEN ";
323# uint num = (criteria[offset] == (byte)IntFields.Rating)
324# ? (BytesToUInt(criteria, intAOffset) / 20) : BytesToUInt(criteria, intAOffset);
325# workingOutput += num.ToString();
326# workingQuery += num.ToString();
327# workingOutput += " to ";
328# workingQuery += " AND ";
329# num = (criteria[offset] == (byte)IntFields.Rating)
330# ? ((BytesToUInt(criteria, intBOffset) - 19) / 20) : BytesToUInt(criteria, intBOffset);
331# workingOutput += num.ToString();
332# workingQuery += num.ToString();
333# }
334# break;
335# }
336# workingQuery += ")";
337# if(Enum.IsDefined(typeof(IgnoreIntFields),
338# (int)criteria[offset])) {
339# if(ignore.Length > 0) {
340# ignore += conjunctionOutput;
341# }
342# ignore += workingOutput;
343# } else {
344# if(output.Length > 0) {
345# output += conjunctionOutput;
346# }
347# if(query.Length > 0) {
348# query += conjunctionQuery;
349# }
350# output += workingOutput;
351# query += workingQuery;
352# }
353# offset = intAOffset + INTLENGTH;
354# if(criteria.Length > offset) {
355# again = true;
356# }
357# }
358#
359# private static void ProcessDateField()
360# {
361# bool isIgnore = false;
362# string workingOutput = Enum.GetName(typeof(DateFields), criteria[offset]);
363# string workingQuery = "((strftime(\"%s\", current_timestamp) - DateAddedStamp + 3600)";
364# switch(criteria[logicRulesOffset]) {
365# case (byte)LogicRule.Greater:
366# workingOutput += " is after ";
367# workingQuery += " > ";
368# goto case 255;
369# case (byte)LogicRule.Less:
370# workingOutput += " is before ";
371# workingQuery += " > ";
372# goto case 255;
373# case 255:
374# isIgnore = true;
375# DateTime time = BytesToDateTime(criteria, intAOffset);
376# workingOutput += time.ToString();
377# workingQuery += ((int)DateTime.Now.Subtract(time).TotalSeconds).ToString();
378# break;
379# case (byte)LogicRule.Other:
380# if(criteria[logicSignOffset + 2] == 1) {
381# isIgnore = true;
382# DateTime t2 = BytesToDateTime(criteria, intAOffset);
383# DateTime t1 = BytesToDateTime(criteria, intBOffset);
384# if(criteria[logicSignOffset] == (byte)LogicSign.IntPositive) {
385# workingOutput += " is in the range of ";
386# workingQuery += " BETWEEN " +
387# ((int)DateTime.Now.Subtract(t1).TotalSeconds).ToString() +
388# " AND " +
389# ((int)DateTime.Now.Subtract(t2).TotalSeconds).ToString();
390# } else {
391# workingOutput += " is not in the range of ";
392# }
393# workingOutput += t1.ToString();
394# workingOutput += " to ";
395# workingOutput += t2.ToString();
396# } else if(criteria[logicSignOffset + 2] == 2) {
397# if(criteria[logicSignOffset] == (byte)LogicSign.IntPositive) {
398# workingOutput += " is in the last ";
399# workingQuery += " < ";
400# } else {
401# workingOutput += " is not in the last ";
402# workingQuery += " > ";
403# }
404# uint t = InverseBytesToUInt(criteria, timeValueOffset);
405# uint multiple = BytesToUInt(criteria, timeMultipleOffset);
406# workingQuery += (t * multiple).ToString();
407# workingOutput += t.ToString() + " ";
408# switch(multiple) {
409# case 86400:
410# workingOutput += "days";
411# break;
412# case 604800:
413# workingOutput += "weeks";
414# break;
415# case 2628000:
416# workingOutput += "months";
417# break;
418# }
419# }
420# break;
421# }
422# workingQuery += ")";
423# if(isIgnore || Enum.IsDefined(typeof(IgnoreDateFields), (int)criteria[offset])) {
424# if(ignore.Length > 0) {
425# ignore += conjunctionOutput;
426# }
427# ignore += workingOutput;
428# } else {
429# if(output.Length > 0) {
430# output += conjunctionOutput;
431# }
432# output += workingOutput;
433# if(query.Length > 0) {
434# query += conjunctionQuery;
435# }
436# query += workingQuery;
437# }
438# offset = intAOffset + INTLENGTH;
439# if(criteria.Length > offset) {
440# again = true;
441# }
442# }
443#
444# /// <summary>
445# /// Converts 4 bytes to a uint
446# /// </summary>
447# /// <param name="byteArray">A byte array</param>
448# /// <param name="offset">Should be the byte of the uint with the 0th-power position</param>
449# /// <returns></returns>
450# private static uint BytesToUInt(byte[] byteArray, int offset)
451# {
452# uint output = 0;
453# for (byte i = 0; i <= 4; i++) {
454# output += (uint)(byteArray[offset - i] * Math.Pow(2, (8 * i)));
455# }
456# return output;
457# }
458#
459# private static uint InverseBytesToUInt(byte[] byteArray, int offset)
460# {
461# uint output = 0;
462# for (byte i = 0; i <= 4; i++) {
463# output += (uint)((255 - (uint)(byteArray[offset - i])) * Math.Pow(2, (8 * i)));
464# }
465# return ++output;
466# }
467#
468# private static DateTime BytesToDateTime (byte[] byteArray, int offset)
469# {
470# uint number = BytesToUInt(byteArray, offset);
471# return STARTOFTIME.AddSeconds(number);
472# }
473# }
474# }
475# namespace Banshee.Plugins.iTunesImporter
476# {
477# internal struct Kind
478# {
479# public string Name, Extension;
480# public Kind(string name, string extension)
481# {
482# Name = name;
483# Extension = extension;
484# }
485# }
486#
487# internal partial class SmartPlaylistParser
488# {
489# private static Kind[] Kinds = {
490# new Kind("Protected AAC audio file", ".m4p"),
491# new Kind("MPEG audio file", ".mp3"),
492# new Kind("AIFF audio file", ".aiff"),
493# new Kind("WAV audio file", ".wav"),
494# new Kind("QuickTime movie file", ".mov"),
495# new Kind("MPEG-4 video file", ".mp4"),
496# new Kind("AAC audio file", ".m4a")
497# };
498#
499# /// <summary>
500# /// The methods by which the number of songs in a playlist are limited
501# /// </summary>
502# private enum LimitMethods
503# {
504# Minutes = 0x01,
505# MB = 0x02,
506# Items = 0x03,
507# Hours = 0x04,
508# GB = 0x05,
509# }
510# /// <summary>
511# /// The methods by which songs are selected for inclusion in a limited playlist
512# /// </summary>
513# private enum SelectionMethods
514# {
515# Random = 0x02,
516# Title = 0x05,
517# AlbumTitle = 0x06,
518# Artist = 0x07,
519# Genre = 0x09,
520# HighestRating = 0x1c,
521# LowestRating = 0x01,
522# RecentlyPlayed = 0x1a,
523# OftenPlayed = 0x19,
524# RecentlyAdded = 0x15
525# }
526# /// <summary>
527# /// The matching criteria which take string data
528# /// </summary>
529# private enum StringFields
530# {
531# AlbumTitle = 0x03,
532# AlbumArtist = 0x47,
533# Artist = 0x04,
534# Category = 0x37,
535# Comments = 0x0e,
536# Composer = 0x12,
537# Description = 0x36,
538# Genre = 0x08,
539# Grouping = 0x27,
540# Kind = 0x09,
541# Title = 0x02,
542# Show = 0x3e
543# }
544# /// <summary>
545# /// The matching criteria which take integer data
546# /// </summary>
547# private enum IntFields
548# {
549# BPM = 0x23,
550# BitRate = 0x05,
551# Compilation = 0x1f,
552# DiskNumber = 0x18,
553# NumberOfPlays = 0x16,
554# Rating = 0x19,
555# Playlist = 0x28, // FIXME Move this?
556# Podcast = 0x39,
557# SampleRate = 0x06,
558# Season = 0x3f,
559# Size = 0x0c,
560# SkipCount = 0x44,
561# Duration = 0x0d,
562# TrackNumber = 0x0b,
563# VideoKind = 0x3c,
564# Year = 0x07
565# }
566# /// <summary>
567# /// The matching criteria which take date data
568# /// </summary>
569# private enum DateFields
570# {
571# DateAdded = 0x10,
572# DateModified = 0x0a,
573# LastPlayed = 0x17,
574# LastSkipped = 0x45
575# }
576# /// <summary>
577# /// The matching criteria which we do no handle
578# /// </summary>
579# private enum IgnoreStringFields
580# {
581# AlbumArtist = 0x47,
582# Category = 0x37,
583# Comments = 0x0e,
584# Composer = 0x12,
585# Description = 0x36,
586# Grouping = 0x27,
587# Show = 0x3e
588# }
589# /// <summary>
590# /// The matching criteria which we do no handle
591# /// </summary>
592# private enum IgnoreIntFields
593# {
594# BPM = 0x23,
595# BitRate = 0x05,
596# Compilation = 0x1f,
597# DiskNumber = 0x18,
598# Playlist = 0x28,
599# Podcast = 0x39,
600# SampleRate = 0x06,
601# Season = 0x3f,
602# Size = 0x0c,
603# SkipCount = 0x44,
604# TrackNumber = 0x0b,
605# VideoKind = 0x3c
606# }
607# private enum IgnoreDateFields
608# {
609# DateModified = 0x0a,
610# LastSkipped = 0x45
611# }
612# /// <summary>
613# /// The signs which apply to different kinds of logic (is vs. is not, contains vs. doesn't contain, etc.)
614# /// </summary>
615# private enum LogicSign
616# {
617# IntPositive = 0x00,
618# StringPositive = 0x01,
619# IntNegative = 0x02,
620# StringNegative = 0x03
621# }
622# /// <summary>
623# /// The logical rules
624# /// </summary>
625# private enum LogicRule
626# {
627# Other = 0x00,
628# Is = 0x01,
629# Contains = 0x02,
630# Starts = 0x04,
631# Ends = 0x08,
632# Greater = 0x10,
633# Less = 0x40
634# }
635# }
636# }
diff --git a/src/test.html b/src/test.html
new file mode 100644
index 0000000..b1636dd
--- /dev/null
+++ b/src/test.html
@@ -0,0 +1,16 @@
1<!doctype html>
2<html class="no-js" lang="">
3 <head>
4 <meta charset="utf-8">
5 <meta http-equiv="X-UA-Compatible" content="IE=edge">
6 <title></title>
7 <meta name="description" content="">
8 <meta name="viewport" content="width=device-width, initial-scale=1">
9 </head>
10 <body>
11 <audio controls>
12 <source src="https://mecmusic.s3.amazonaws.com/Katy%20Perry/E.T.%20%28feat.%20Kanye%20West%29%20-%20Single/01%20E.T.%20%28feat.%20Kayne%20West%29.m4a?Signature=H59mUAn6GVYQKs%2B3OpNCWkehGME%3D&Expires=1410061622&AWSAccessKeyId=AKIAJBVOGJDEU5V22WJA&x-amz-meta-s3cmd-attrs=uid%3A501/gname%3Astaff/uname%3Amcrute/gid%3A20/mode%3A33216/mtime%3A1385081486/atime%3A1409625877/ctime%3A1408591516">
13 Your browser does not support the audio element.
14 </audio>
15 </body>
16</html>
diff --git a/src/test.py b/src/test.py
new file mode 100644
index 0000000..3f5ccd8
--- /dev/null
+++ b/src/test.py
@@ -0,0 +1,21 @@
1import sqlite3
2
3def unfuck_unicode(text):
4 return ''.join([chr(n) for n in [ord(i) for i in text]]).decode('utf-8')
5
6conn = sqlite3.connect('iTunesLibrary.db')
7curs = conn.cursor()
8upcurs = conn.cursor()
9
10curs.execute('select track_id, location from track where location is not null')
11
12
13for id, datum in curs.fetchall():
14 try:
15 datum.decode('utf-8')
16 except UnicodeEncodeError:
17 print id, type(datum), datum.encode('utf-8')
18 #upcurs.execute('update track set location = ? where track_id = ?',
19 # (datum.encode('utf-8'), id))
20
21conn.commit()
diff --git a/src/track_service.py b/src/track_service.py
new file mode 100755
index 0000000..4a5a149
--- /dev/null
+++ b/src/track_service.py
@@ -0,0 +1,132 @@
1#!/usr/bin/env python
2
3import sqlite3
4import bottle
5import urllib
6
7db = sqlite3.connect('iTunesLibrary.db')
8db.row_factory = sqlite3.Row
9
10ARTIST_COUNT_QUERY = """
11select a.*, count(t.track_id) as total_tracks
12from track t
13inner join artist a on t.artist = a.id
14{where}
15group by t.artist
16"""
17
18GENRE_COUNT_QUERY = """
19select g.*, count(t.track_id) as total_tracks
20from track t
21inner join genre g on t.genre = g.id
22{where}
23group by t.genre
24"""
25
26ALBUM_COUNT_QUERY = """
27select a.id, a.title, count(t.track_id) as total_tracks
28from track t
29inner join album a on t.album = a.id
30{where}
31group by t.album
32"""
33
34TRACK_QUERY = """
35select
36t.track_id, t.name, t.track_number, t.disc_number,
37ar.artist,
38ab.title, ab.track_count, ab.disc_count,
39g.name
40from track t
41inner join artist ar on t.artist = ar.id
42inner join album ab on t.album = ab.id
43inner join genre g on t.genre = g.id
44{where}
45"""
46
47def _format_json(curs):
48 data = []
49
50 for item in curs.fetchall():
51 data.append({
52 'id': item[0],
53 'name': item[1].encode('utf-8'),
54 'tracks': item[2],
55 })
56
57 return data
58
59
60def _run_query(curs, query, where="", params=None):
61 query = query.format(where=where)
62
63 if params:
64 curs.execute(query, params)
65 else:
66 curs.execute(query)
67
68
69def _build_where(query):
70 where = []
71 params = []
72
73 if 'genre' in query:
74 where.append('t.genre = ?')
75 params.append(query['genre'])
76
77 if 'artist' in query:
78 where.append('t.artist = ?')
79 params.append(query['artist'])
80
81 if 'album' in query:
82 where.append('t.album = ?')
83 params.append(query['album'])
84
85 if where:
86 where = 'WHERE ' + ' AND '.join(where)
87 else:
88 where = ""
89
90 return where, params
91
92
93@bottle.route('/genre')
94def genre_controller():
95 curs = db.cursor()
96 curs.execute(GENRE_COUNT_QUERY.format(where=""))
97 return { 'data': _format_json(curs) }
98
99
100@bottle.route('/browser')
101def browser_controller():
102 curs = db.cursor()
103 where, params = _build_where(bottle.request.query)
104
105 _run_query(curs, ARTIST_COUNT_QUERY, where, params)
106 artists = _format_json(curs)
107
108 _run_query(curs, GENRE_COUNT_QUERY, where, params)
109 genres = _format_json(curs)
110
111 _run_query(curs, ALBUM_COUNT_QUERY, where, params)
112 albums = _format_json(curs)
113
114 return {
115 'data': {
116 'artists': artists,
117 'genres': genres,
118 'albums': albums,
119 }
120 }
121
122
123@bottle.route('/tracks')
124def tracks_controller():
125 curs = db.cursor()
126 where, params = _build_where(bottle.request.query)
127 _run_query(curs, TRACK_QUERY, where, params)
128 return { 'data': [dict(row) for row in curs.fetchall()] }
129
130
131bottle.debug(True)
132bottle.run(host='localhost', port=8100)
diff --git a/src/util.py b/src/util.py
new file mode 100644
index 0000000..ff1df56
--- /dev/null
+++ b/src/util.py
@@ -0,0 +1,29 @@
1from ConfigParser import SafeConfigParser
2
3class SimpleConfigParser(SafeConfigParser, object):
4 """SafeConfigParser that auto-loads files
5
6 This class also supports a more terse form of get that uses the default
7 section which can be passed to the constructor.
8 """
9
10 def __init__(self, filename, default_section=None):
11 super(SimpleConfigParser, self).__init__()
12 self.default_section = default_section
13
14 with open(filename) as fp:
15 self.readfp(fp)
16
17 def get(self, section, option=None):
18 """Get that allows skipping section
19
20 This can be called with one or two arguments. The two argument form is
21 the same as ConfigParser. The one argument form allows skipping the
22 section if the class has a default_section set.
23 """
24 default_get = super(SimpleConfigParser, self).get
25
26 if not option and self.default_section:
27 return default_get(self.default_section, section)
28 else:
29 return default_get(section, option)