diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/audiocloudweb.cfg | 7 | ||||
-rw-r--r-- | src/get_music_url.py | 23 | ||||
-rw-r--r-- | src/index_files.py | 58 | ||||
-rw-r--r-- | src/index_search.py | 10 | ||||
-rw-r--r-- | src/index_service.py | 27 | ||||
-rw-r--r-- | src/indexer.py | 113 | ||||
-rw-r--r-- | src/load_filesystem.py | 11 | ||||
-rw-r--r-- | src/load_itunes.py | 326 | ||||
-rw-r--r-- | src/parse_mp4.py | 67 | ||||
-rw-r--r-- | src/parse_smart_playlist.py | 636 | ||||
-rw-r--r-- | src/test.html | 16 | ||||
-rw-r--r-- | src/test.py | 21 | ||||
-rwxr-xr-x | src/track_service.py | 132 | ||||
-rw-r--r-- | src/util.py | 29 |
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] | ||
2 | access_key = | ||
3 | secret_key = | ||
4 | |||
5 | [audiocloud] | ||
6 | access_key = | ||
7 | secret_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 @@ | |||
1 | import sqlite3 | ||
2 | import urllib | ||
3 | from util import SimpleConfigParser | ||
4 | from 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 | |||
9 | db = sqlite3.connect('iTunesLibrary.db') | ||
10 | curs = db.cursor() | ||
11 | cfg = SimpleConfigParser('audiocloudweb.cfg', 'audiocloud') | ||
12 | s3 = S3Connection(cfg.get('access_key'), cfg.get('secret_key')) | ||
13 | bucket = s3.get_bucket('mecmusic') | ||
14 | |||
15 | |||
16 | |||
17 | #curs.execute('select location from track where artist = 629 limit 1') | ||
18 | curs.execute('select location from track where track_id = 29510') | ||
19 | track = curs.fetchone()[0][6:]#.encode('utf-8') | ||
20 | |||
21 | key = bucket.get_key(track, validate=True) | ||
22 | key.metadata = {} | ||
23 | print 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 @@ | |||
1 | import os | ||
2 | import re | ||
3 | import json | ||
4 | import mutagen | ||
5 | from pprint import pprint | ||
6 | |||
7 | |||
8 | music = re.compile('.*\.(mp3|m4a)$') | ||
9 | |||
10 | |||
11 | tags = set() | ||
12 | |||
13 | |||
14 | def 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 | |||
28 | for 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 @@ | |||
1 | from whoosh import index | ||
2 | from whoosh.qparser import MultifieldParser | ||
3 | |||
4 | idx = index.open_dir("indexdir") | ||
5 | parser = MultifieldParser(["track_name", "artist", "album"], schema=idx.schema) | ||
6 | |||
7 | with 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 @@ | |||
1 | from whoosh import index | ||
2 | from whoosh.qparser import MultifieldParser | ||
3 | |||
4 | import bottle | ||
5 | from bottle import route, run, request | ||
6 | |||
7 | |||
8 | @route('/search') | ||
9 | def 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 | |||
23 | bottle.debug(True) | ||
24 | app = bottle.default_app() | ||
25 | |||
26 | #if __name__ == "__main__": | ||
27 | run(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 @@ | |||
1 | import sqlite3 | ||
2 | |||
3 | from whoosh import fields, index | ||
4 | |||
5 | TRACK_QUERY = """ | ||
6 | SELECT | ||
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 | ||
13 | FROM | ||
14 | track t | ||
15 | INNER JOIN | ||
16 | genre g | ||
17 | ON t.genre = g.id | ||
18 | INNER JOIN | ||
19 | artist a | ||
20 | ON t.artist = a.id | ||
21 | INNER JOIN | ||
22 | album ab | ||
23 | ON t.album = ab.id | ||
24 | INNER JOIN | ||
25 | track_physical p | ||
26 | ON t.track_id = p.track_id | ||
27 | INNER JOIN | ||
28 | kind k | ||
29 | ON p.kind = k.id | ||
30 | """ | ||
31 | |||
32 | def safe_unicode(value): | ||
33 | return value | ||
34 | return value.decode("utf-8") if value else None | ||
35 | |||
36 | def safe_int(value): | ||
37 | return int(value) if value is not None else None | ||
38 | |||
39 | def to_boolean(value): | ||
40 | return 1 if value else 0 | ||
41 | |||
42 | def first_value(*items): | ||
43 | for item in items: | ||
44 | if item is not None: | ||
45 | return item | ||
46 | |||
47 | return None | ||
48 | |||
49 | |||
50 | db = sqlite3.connect('iTunesLibrary.db') | ||
51 | db.row_factory = sqlite3.Row | ||
52 | |||
53 | curs = db.cursor() | ||
54 | curs.execute(TRACK_QUERY) | ||
55 | |||
56 | schema = 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 | |||
81 | idx = index.create_in("indexdir", schema) | ||
82 | writer = idx.writer() | ||
83 | |||
84 | for 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 | |||
112 | writer.commit() | ||
113 | db.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 @@ | |||
1 | import sqlite3 | ||
2 | import codecs | ||
3 | |||
4 | db = sqlite3.connect('iTunes/iTunesLibrary.db') | ||
5 | curs = db.cursor() | ||
6 | |||
7 | with 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 | |||
11 | db.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 @@ | |||
1 | import sqlite3 | ||
2 | import plistlib | ||
3 | |||
4 | library = plistlib.readPlist('iTunes-Music-Library.xml') | ||
5 | |||
6 | |||
7 | class 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 | |||
47 | class 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 | |||
127 | class 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 | |||
287 | assert library['Major Version'] == 1, "Invalid major version" | ||
288 | assert library['Minor Version'] == 1, "Invalid minor version" | ||
289 | |||
290 | def 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 | |||
307 | def 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 | |||
323 | db = sqlite3.connect('iTunesLibrary.db') | ||
324 | #insert_playlists(db, library) | ||
325 | insert_tracks(db, library) | ||
326 | db.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 @@ | |||
1 | from mutagen import mp4 | ||
2 | from mutagen.easyid3 import EasyID3 | ||
3 | |||
4 | |||
5 | class 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 | |||
66 | class 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 @@ | |||
1 | import sqlite3 | ||
2 | |||
3 | def unfuck_unicode(text): | ||
4 | return ''.join([chr(n) for n in [ord(i) for i in text]]).decode('utf-8') | ||
5 | |||
6 | conn = sqlite3.connect('iTunesLibrary.db') | ||
7 | curs = conn.cursor() | ||
8 | upcurs = conn.cursor() | ||
9 | |||
10 | curs.execute('select track_id, location from track where location is not null') | ||
11 | |||
12 | |||
13 | for 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 | |||
21 | conn.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 | |||
3 | import sqlite3 | ||
4 | import bottle | ||
5 | import urllib | ||
6 | |||
7 | db = sqlite3.connect('iTunesLibrary.db') | ||
8 | db.row_factory = sqlite3.Row | ||
9 | |||
10 | ARTIST_COUNT_QUERY = """ | ||
11 | select a.*, count(t.track_id) as total_tracks | ||
12 | from track t | ||
13 | inner join artist a on t.artist = a.id | ||
14 | {where} | ||
15 | group by t.artist | ||
16 | """ | ||
17 | |||
18 | GENRE_COUNT_QUERY = """ | ||
19 | select g.*, count(t.track_id) as total_tracks | ||
20 | from track t | ||
21 | inner join genre g on t.genre = g.id | ||
22 | {where} | ||
23 | group by t.genre | ||
24 | """ | ||
25 | |||
26 | ALBUM_COUNT_QUERY = """ | ||
27 | select a.id, a.title, count(t.track_id) as total_tracks | ||
28 | from track t | ||
29 | inner join album a on t.album = a.id | ||
30 | {where} | ||
31 | group by t.album | ||
32 | """ | ||
33 | |||
34 | TRACK_QUERY = """ | ||
35 | select | ||
36 | t.track_id, t.name, t.track_number, t.disc_number, | ||
37 | ar.artist, | ||
38 | ab.title, ab.track_count, ab.disc_count, | ||
39 | g.name | ||
40 | from track t | ||
41 | inner join artist ar on t.artist = ar.id | ||
42 | inner join album ab on t.album = ab.id | ||
43 | inner join genre g on t.genre = g.id | ||
44 | {where} | ||
45 | """ | ||
46 | |||
47 | def _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 | |||
60 | def _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 | |||
69 | def _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') | ||
94 | def 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') | ||
101 | def 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') | ||
124 | def 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 | |||
131 | bottle.debug(True) | ||
132 | bottle.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 @@ | |||
1 | from ConfigParser import SafeConfigParser | ||
2 | |||
3 | class 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) | ||