2 # Copyright 2009 James Bunton <jamesbunton@fastmail.fm>
3 # Licensed for distribution under the GPL version 2, check COPYING for details
10 from Foundation
import *
13 def read_plist(filename
):
15 data
= buffer(open(filename
).read())
18 plist
, fmt
, err
= NSPropertyListSerialization
.propertyListFromData_mutabilityOption_format_errorDescription_(data
, NSPropertyListMutableContainers
, None, None)
20 errStr
= err
.encode("utf-8")
21 err
.release() # Doesn't follow Cocoa conventions for some reason
22 raise TypeError(errStr
)
25 class Playlist(object):
26 def __init__(self
, name
, tracks
, parent
=None):
30 if parent
is not None:
31 parent
.children
.append(self
)
33 class ITunesLibrary(NSObject
):
35 return self
.initWithFilename_("~/Music/iTunes/iTunes Music Library.xml")
37 def initWithFilename_(self
, filename
):
38 filename
= os
.path
.expanduser(filename
)
39 plist
= read_plist(os
.path
.expanduser(filename
))
40 self
.folder
= self
.loc2name(plist
["Music Folder"])
41 pl_tracks
= plist
["Tracks"]
43 for pl_playlist
in plist
["Playlists"]:
44 self
.playlists
.append(self
.make_playlist(pl_playlist
, pl_tracks
))
47 def loc2name(self
, location
):
48 return urllib
.splithost(urllib
.splittype(urllib
.unquote(location
))[1])[1]
50 def make_playlist(self
, pl_playlist
, pl_tracks
):
51 name
= pl_playlist
["Name"]
53 for item
in pl_playlist
.get("Playlist Items", []):
54 trackID
= item
["Track ID"]
55 filename
= str(pl_tracks
[str(trackID
)]["Location"])
56 filename
= self
.loc2name(filename
)
57 filename
= filename
[len(self
.folder
):]
58 filename
= eval(repr(filename
).lstrip("u")).decode("utf-8")
59 tracks
.append(filename
)
60 return Playlist(name
, tracks
)
62 def has_playlist(self
, playlist
):
63 for p
in self
.playlists
:
64 if p
.name
== playlist
:
68 def get_playlist(self
, name
):
69 playlist
= [p
for p
in self
.playlists
if p
.name
== name
][0]
70 return playlist
.tracks
72 def list_playlists(self
):
73 return [p
.name
for p
in self
.playlists
]
75 def outlineView_numberOfChildrenOfItem_(self
, view
, item
):
77 return len(self
.playlists
)
81 def outlineView_isItemExpandable_(self
, view
, item
):
84 def outlineView_child_ofItem_(self
, view
, index
, item
):
86 return self
.playlists
[index
]
90 def outlineView_objectValueForTableColumn_byItem_(self
, view
, column
, item
):
94 def export_m3u(dry_run
, dest
, drive_letter
, music_dir
, playlist_name
, files
):
97 f
= open(os
.path
.join(dest
, playlist_name
) + ".m3u", "w")
98 for filename
in files
:
99 filename
= filename
.replace("/", "\\").encode("utf-8")
100 f
.write("%s:\\%s\\%s\n" % (drive_letter
, music_dir
, filename
))
103 def strip_prefix(s
, prefix
):
104 assert s
.startswith(prefix
)
106 if s
.startswith("/"):
111 if os
.path
.isdir(path
):
115 path
= os
.path
.split(path
)[0]
117 for path
in reversed(paths
):
123 def sync(dry_run
, source
, dest
, files
):
126 logging
.info("Calculating files to sync and deleting old files")
128 for dirpath
, dirnames
, filenames
in os
.walk(dest
):
129 full_dirpath
= dirpath
130 dirpath
= strip_prefix(dirpath
, dest
)
132 for filename
in filenames
:
133 filename
= join(dirpath
, filename
).decode("utf-8")
135 # Whenever 'file' is deleted OSX will helpfully remove '._file'
136 if not os
.path
.exists(join(dest
, filename
)):
139 if filename
in files
:
140 sourcestat
= os
.stat(join(source
, filename
))
141 deststat
= os
.stat(join(dest
, filename
))
142 same_time
= abs(sourcestat
.st_mtime
- deststat
.st_mtime
) < 5
143 same_size
= sourcestat
.st_size
== deststat
.st_size
144 if same_time
and same_size
:
145 files
.remove(filename
)
146 logging
.debug("keep: " + filename
)
148 logging
.debug("update: " + filename
)
150 elif not filename
.startswith("Playlists/"):
151 logging
.debug("delete: " + filename
)
153 os
.unlink(join(dest
, filename
))
155 if len(os
.listdir(full_dirpath
)) == 0:
156 logging
.debug("rmdir: " + dirpath
)
158 os
.rmdir(full_dirpath
)
161 logging
.info("Copying new files")
164 for filename
in files
:
165 logging
.debug("copy: " + filename
)
167 mkdirhier(os
.path
.dirname(join(dest
, filename
)))
168 shutil
.copy2(join(source
, filename
), join(dest
, filename
))