From: James Bunton Date: Sun, 2 Jan 2011 03:48:49 +0000 (+1100) Subject: Initial checkin X-Git-Tag: notipod-1.0~13 X-Git-Url: https://code.delx.au/notipod/commitdiff_plain/ac7a8ed96909bdd558afec0845cb18c596ce70c5 Initial checkin --- ac7a8ed96909bdd558afec0845cb18c596ce70c5 diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000..12c0927 --- /dev/null +++ b/.hgignore @@ -0,0 +1,7 @@ +syntax: glob +build +.*.swp +*.pyc +*.mode1v3 +*.pbxuser +*.pch diff --git a/English.lproj/InfoPlist.strings b/English.lproj/InfoPlist.strings new file mode 100644 index 0000000..534be77 Binary files /dev/null and b/English.lproj/InfoPlist.strings differ diff --git a/English.lproj/NotiPod.xib b/English.lproj/NotiPod.xib new file mode 100644 index 0000000..817f256 --- /dev/null +++ b/English.lproj/NotiPod.xib @@ -0,0 +1,1334 @@ + + + + 1050 + 9L30 + 677 + 949.54 + 353.00 + + YES + + + + + YES + com.apple.InterfaceBuilderKit + com.apple.InterfaceBuilder.CocoaPlugin + + + YES + + YES + + + YES + + + + YES + + NSApplication + + + FirstResponder + + + NSApplication + + + AMainMenu + + YES + + + NotiPod + + 1048576 + 2147483647 + + NSImage + NSMenuCheckmark + + + NSImage + NSMenuMixedState + + submenuAction: + + NotiPod + + YES + + + About NotiPod + + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Services + + 1048576 + 2147483647 + + + submenuAction: + + Services + + YES + + _NSServicesMenu + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Hide NotiPod + h + 1048576 + 2147483647 + + + + + + Hide Others + h + 1572864 + 2147483647 + + + + + + Show All + + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Quit NotiPod + q + 1048576 + 2147483647 + + + + + _NSAppleMenu + + + + + Help + + 1048576 + 2147483647 + + + submenuAction: + + Help + + YES + + + NotiPod Help + ? + 1048576 + 2147483647 + + + + + + + + _NSMainMenu + + + 7 + 2 + {{335, 417}, {288, 333}} + 1946157056 + NotiPod + NSWindow + + {3.40282e+38, 3.40282e+38} + + + 256 + + YES + + + 268 + {{17, 296}, {161, 17}} + + YES + + 67239488 + 272630784 + Step 1 - Select a playlist: + + LucidaGrande + 1.300000e+01 + 1044 + + + + 6 + System + controlColor + + 3 + MC42NjY2NjY2OQA + + + + 6 + System + controlTextColor + + 3 + MAA + + + + + + + 268 + {{17, 105}, {254, 17}} + + YES + + 67239488 + 272630784 + Step 2 - Output folder or device: + + + + + + + + + 268 + {{118, 12}, {120, 32}} + + YES + + 67239424 + 134217728 + Synchronise! + + + -2038284033 + 129 + + + 200 + 25 + + + + + 268 + {{29, 73}, {206, 26}} + + YES + + -2076049856 + 2048 + + + 109199615 + 1 + + + 400 + 75 + + + Choose folder... + + 1048576 + 2147483647 + 1 + + + _popUpItemAction: + + + YES + + OtherViews + + YES + + + + YES + YES + + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + 1 + YES + YES + 2 + + + + + 268 + {{17, 22}, {105, 17}} + + YES + + 67239488 + 272630784 + Step 3: + + + + + + + + + 268 + + YES + + + 2304 + + YES + + + 256 + {219, 102} + + YES + + + 256 + {219, 17} + + + + + + 256 + {{220, 0}, {16, 17}} + + + + YES + + 1.010000e+02 + 1.600000e+01 + 1.000000e+03 + + 75628032 + 0 + + + LucidaGrande + 1.100000e+01 + 3100 + + + 3 + MC4zMzMzMzI5OQA + + + 6 + System + headerTextColor + + + + + 337772096 + 2048 + Text Cell + + + + 6 + System + controlBackgroundColor + + + + + 3 + YES + YES + + + + 1.120000e+02 + 4.000000e+01 + 1.000000e+03 + + 75628032 + 0 + + + + + + + 337772096 + 2048 + Text Cell + + + + + + 3 + YES + YES + + + + 3.000000e+00 + 2.000000e+00 + + 3 + MQA + + + 6 + System + gridColor + + 3 + MC41AA + + + 1.700000e+01 + -767557632 + 4 + 15 + 0 + YES + + + {{1, 17}, {219, 102}} + + + + + 4 + + + + 256 + {{220, 17}, {15, 102}} + + + _doScroller: + 3.700000e+01 + 1.390978e-01 + + + + 256 + {{1, 119}, {219, 15}} + + 1 + + _doScroller: + 5.714286e-01 + + + + 2304 + + YES + + + {{1, 0}, {219, 17}} + + + + + 4 + + + + {{32, 153}, {236, 135}} + + + 50 + + + + + + QSAAAEEgAABBmAAAQZgAAA + + + {288, 333} + + + {{0, 0}, {1024, 746}} + {3.40282e+38, 3.40282e+38} + + + NotiPodController + + + NotiPodAppDelegate + + + 7 + 2 + {{196, 436}, {204, 74}} + 603979776 + Window + NSWindow + + {3.40282e+38, 3.40282e+38} + + + 256 + + YES + + + 1292 + + {{18, 16}, {168, 20}} + + 16394 + 2.000000e+01 + 1.000000e+02 + + + + 268 + {{17, 44}, {111, 17}} + + YES + + 67239488 + 272630784 + Loading library... + + + + + + + + {204, 74} + + {{0, 0}, {1024, 746}} + {3.40282e+38, 3.40282e+38} + + + + + YES + + + orderFrontStandardAboutPanel: + + + + 142 + + + + showHelp: + + + + 360 + + + + hide: + + + + 367 + + + + hideOtherApplications: + + + + 368 + + + + terminate: + + + + 369 + + + + unhideAllApplications: + + + + 370 + + + + doSync: + + + + 407 + + + + window + + + + 420 + + + + doSelectFolder: + + + + 421 + + + + loadingSheet + + + + 434 + + + + delegate + + + + 435 + + + + playlistView + + + + 450 + + + + folderPopup + + + + 451 + + + + + YES + + 0 + + YES + + + + + + -2 + + + RmlsZSdzIE93bmVyA + + + -1 + + + First Responder + + + -3 + + + Application + + + 29 + + + YES + + + + + MainMenu + + + 56 + + + YES + + + + + + 103 + + + YES + + + + 1 + + + 106 + + + YES + + + + 2 + + + 111 + + + + + 57 + + + YES + + + + + + + + + + + + + + 58 + + + + + 134 + + + + + 150 + + + + + 136 + + + 1111 + + + 144 + + + + + 236 + + + + + 131 + + + YES + + + + + + 149 + + + + + 145 + + + + + 130 + + + + + 371 + + + YES + + + + NotiPod + + + 372 + + + YES + + + + + + + + + + + 391 + + + YES + + + + + + 392 + + + + + 393 + + + YES + + + + + + 394 + + + + + 395 + + + YES + + + + + + 396 + + + + + 404 + + + NotiPodController + + + 411 + + + YES + + + + + + 412 + + + YES + + + + + + 413 + + + YES + + + + + + + 414 + + + + + 425 + + + NotiPodAppDelegate + + + 428 + + + YES + + + + NotiPodLoading + + + 429 + + + YES + + + + + + + 430 + + + + + 431 + + + YES + + + + + + 432 + + + + + 436 + + + + + 437 + + + YES + + + + + + 438 + + + + + 440 + + + YES + + + + + + + + + 441 + + + + + 442 + + + + + 443 + + + YES + + + + + + + 444 + + + + + 445 + + + YES + + + + + + 446 + + + YES + + + + + + 447 + + + + + 448 + + + + + + + YES + + YES + -1.IBPluginDependency + -2.IBPluginDependency + -3.IBPluginDependency + 103.IBPluginDependency + 103.ImportedFromIB2 + 106.IBPluginDependency + 106.ImportedFromIB2 + 106.editorWindowContentRectSynchronizationRect + 111.IBPluginDependency + 111.ImportedFromIB2 + 130.IBPluginDependency + 130.ImportedFromIB2 + 130.editorWindowContentRectSynchronizationRect + 131.IBPluginDependency + 131.ImportedFromIB2 + 134.IBPluginDependency + 134.ImportedFromIB2 + 136.IBPluginDependency + 136.ImportedFromIB2 + 144.IBPluginDependency + 144.ImportedFromIB2 + 145.IBPluginDependency + 145.ImportedFromIB2 + 149.IBPluginDependency + 149.ImportedFromIB2 + 150.IBPluginDependency + 150.ImportedFromIB2 + 236.IBPluginDependency + 236.ImportedFromIB2 + 29.IBEditorWindowLastContentRect + 29.IBPluginDependency + 29.ImportedFromIB2 + 29.WindowOrigin + 29.editorWindowContentRectSynchronizationRect + 371.IBEditorWindowLastContentRect + 371.IBWindowTemplateEditedContentRect + 371.NSWindowTemplate.visibleAtLaunch + 371.editorWindowContentRectSynchronizationRect + 372.IBPluginDependency + 391.IBPluginDependency + 392.IBPluginDependency + 393.IBPluginDependency + 394.IBPluginDependency + 395.IBPluginDependency + 396.IBPluginDependency + 404.IBPluginDependency + 411.IBPluginDependency + 412.IBPluginDependency + 413.IBPluginDependency + 413.editorWindowContentRectSynchronizationRect + 414.IBPluginDependency + 425.IBPluginDependency + 428.IBEditorWindowLastContentRect + 428.IBWindowTemplateEditedContentRect + 428.NSWindowTemplate.visibleAtLaunch + 428.editorWindowContentRectSynchronizationRect + 429.IBPluginDependency + 430.IBPluginDependency + 431.IBPluginDependency + 432.IBPluginDependency + 436.IBPluginDependency + 437.IBPluginDependency + 438.IBPluginDependency + 440.IBPluginDependency + 441.IBPluginDependency + 442.IBPluginDependency + 443.IBPluginDependency + 444.IBPluginDependency + 445.IBPluginDependency + 446.IBPluginDependency + 447.IBPluginDependency + 448.IBPluginDependency + 56.IBPluginDependency + 56.ImportedFromIB2 + 57.IBPluginDependency + 57.ImportedFromIB2 + 57.editorWindowContentRectSynchronizationRect + 58.IBPluginDependency + 58.ImportedFromIB2 + + + YES + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilderKit + com.apple.InterfaceBuilderKit + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + {{98, 681}, {163, 23}} + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + {{436, 809}, {64, 6}} + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + {{0, 714}, {148, 20}} + com.apple.InterfaceBuilder.CocoaPlugin + + {74, 862} + {{529, 476}, {148, 20}} + {{343, 40}, {288, 333}} + {{343, 40}, {288, 333}} + + {{505, 213}, {252, 190}} + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + {{89, 581}, {211, 33}} + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + {{21, 626}, {204, 74}} + {{21, 626}, {204, 74}} + + {{196, 436}, {204, 74}} + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + {{23, 551}, {192, 153}} + com.apple.InterfaceBuilder.CocoaPlugin + + + + + YES + + YES + + + YES + + + + + YES + + YES + + + YES + + + + 451 + + + + YES + + NotiPodAppDelegate + NSObject + + IBProjectSource + NotiPodAppDelegate.py + + + + NotiPodAppDelegate + NSObject + + IBUserSource + + + + + NotiPodController + NSObject + + YES + + YES + doSelectFolder: + doSync: + + + YES + id + id + + + + YES + + YES + folderPopup + loadingSheet + playlistView + window + + + YES + id + id + id + id + + + + IBProjectSource + NotiPodController.py + + + + NotiPodController + NSObject + + IBUserSource + + + + + + 0 + ../notipod.xcodeproj + 3 + + diff --git a/Info.plist b/Info.plist new file mode 100644 index 0000000..8387725 --- /dev/null +++ b/Info.plist @@ -0,0 +1,30 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIconFile + + CFBundleIdentifier + com.yourcompany.notipod + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + NSMainNibFile + NotiPod + NSPrincipalClass + NSApplication + + diff --git a/NotiPodAppDelegate.py b/NotiPodAppDelegate.py new file mode 100644 index 0000000..9cf4d28 --- /dev/null +++ b/NotiPodAppDelegate.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python +# Copyright 2009 James Bunton +# Licensed for distribution under the GPL version 2, check COPYING for details + +from Foundation import * +from AppKit import * +import objc + +import itunes + +class NotiPodAppDelegate(NSObject): + + # Delegate methods + + def applicationWillFinishLaunching_(self, _): + prefs = NSUserDefaults.standardUserDefaults() + self._playlists = prefs.stringArrayForKey_("playlists") + self._folders = prefs.stringArrayForKey_("folders") + + def applicationDidFinishLaunching_(self, _): + pass + + def applicationWillTerminate_(self, _): + prefs = NSUserDefaults.standardUserDefaults() + prefs.synchronize() + + def applicationShouldTerminateAfterLastWindowClosed_(self, _): + return True + + + # Public accessors + + def playlists(self): + return self._playlists + + def folders(self): + return self._folders or [] + + def addFolder_(self, folder): + self._folders.insert(0, folder) + + def hasPlaylist_(self, playlist): + return playlist in self._playlists + + def removePlaylist_(self, playlist): + self._playlists.remove(playlist) + + def addPlaylist_(self, playlist): + self._playlists.append(playlist) + + diff --git a/NotiPodController.py b/NotiPodController.py new file mode 100644 index 0000000..e415cef --- /dev/null +++ b/NotiPodController.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python +# Copyright 2009 James Bunton +# Licensed for distribution under the GPL version 2, check COPYING for details + +from Foundation import * +from AppKit import * +import objc + +import itunes + +class NotiPodController(NSObject): + playlistView = objc.IBOutlet() + folderPopup = objc.IBOutlet() + window = objc.IBOutlet() + loadingSheet = objc.IBOutlet() + + + def awakeFromNib(self): +### self.loadLibrary() + self.performSelectorInBackground_withObject_(self.loadLibrary, None) + print "awakeFromNib" + + def finishLoading(self): + self.playlistView.setDataSource_(self.library) + self.folderPopup.addItemsWithTitles_(NSApp.delegate().folders()) + + def loadLibrary(self): + pool = NSAutoreleasePool.alloc().init() + NSApp.beginSheet_modalForWindow_modalDelegate_didEndSelector_contextInfo_(self.loadingSheet, self.window, None, None, None) + self.library = itunes.Library.alloc().init() + self.loadingSheet.close() + self.performSelectorOnMainThread_withObject_waitUntilDone_(self.finishLoading, None, False) + pool.drain() + + @objc.signature("v@:@ii") + def selectFolderEnd_returnCode_contextInfo_(self, panel, ret, _): + if ret == NSOKButton: + assert len(panel.filenames()) == 1 + folder = panel.filenames()[0] +### NSApp.delegate().addFolder_(folder) + self.folderPopup.insertItemWithTitle_atIndex_(folder, 2) + self.folderPopup.selectItemAtIndex_(2) + + @objc.IBAction + def doSelectFolder_(self, sender): + print "select folder" + try: + folders = NSApp.delegate().folders() + if len(folders) > 0: + folder = folders[0] + else: + folder = None + panel = NSOpenPanel.openPanel() + panel.setCanChooseFiles_(False) + panel.setCanChooseDirectories_(True) + panel.setAllowsMultipleSelection_(False) + panel.beginSheetForDirectory_file_types_modalForWindow_modalDelegate_didEndSelector_contextInfo_(folder, None, None, self.window, self, self.selectFolderEnd_returnCode_contextInfo_, None) + except: + import traceback + traceback.print_exc() + + @objc.IBAction + def doSync_(self, sender): + print "hello me" + diff --git a/itunes.py b/itunes.py new file mode 100644 index 0000000..8d2848b --- /dev/null +++ b/itunes.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python +# Copyright 2009 James Bunton +# Licensed for distribution under the GPL version 2, check COPYING for details + +import logging +import os +import shutil +import urllib + +from Foundation import * + + +def read_plist(filename): + try: + data = buffer(open(filename).read()) + except IOError: + return None + plist, fmt, err = NSPropertyListSerialization.propertyListFromData_mutabilityOption_format_errorDescription_(data, NSPropertyListMutableContainers, None, None) + if err is not None: + errStr = err.encode("utf-8") + err.release() # Doesn't follow Cocoa conventions for some reason + raise TypeError(errStr) + return plist + +class Playlist(object): + def __init__(self, name, tracks, parent=None): + self.name = name + self.children = [] + self.tracks = tracks + if parent is not None: + parent.children.append(self) + +class Library(NSObject): + def init(self): + self.initWithFilename_("~/Music/iTunes/iTunes Music Library.xml") + + def initWithFilename_(self, filename): + filename = os.path.expanduser(filename) + plist = read_plist(os.path.expanduser(filename)) + self.folder = self.loc2name(plist["Music Folder"]) + pl_tracks = plist["Tracks"] + self.playlists = [] + for pl_playlist in plist["Playlists"]: + self.playlists.append(self.make_playlist(pl_playlist, pl_tracks)) + + def loc2name(self, location): + return urllib.splithost(urllib.splittype(urllib.unquote(location))[1])[1] + + def make_playlist(self, pl_playlist, pl_tracks): + name = pl_playlist["Name"] + tracks = [] + for item in pl_playlist.get("Playlist Items", []): + trackID = item["Track ID"] + filename = str(pl_tracks[str(trackID)]["Location"]) + filename = self.loc2name(filename) + filename = filename[len(self.folder):] + filename = eval(repr(filename).lstrip("u")).decode("utf-8") + tracks.append(filename) + return Playlist(name, tracks) + + def has_playlist(self, playlist): + for p in self.playlists: + if p.name == playlist: + return True + return False + + def get_playlist(self, name): + playlist = [p for p in self.playlists if p.name == name] + return playlist.tracks + + def list_playlists(self): + return [p.name for p in self.playlists] + + def outlineView_numberOfChildrenOfItem_(self, view, item): + if item == None: + return len(self.playlists) + else: + return 0 + + def outlineView_isItemExpandable_(self, view, item): + return False + + def outlineView_child_ofItem_(self, view, index, item): + if item == None: + return self.playlists[index] + else: + return None + + def outlineView_objectValueForTableColumn_byItem_(self, view, column, item): + return item.name + + +def export_m3u(dry_run, dest, drive_letter, music_dir, playlist_name, files): + if dry_run: + return + f = open(os.path.join(dest, playlist_name) + ".m3u", "w") + for filename in files: + filename = filename.replace("/", "\\").encode("utf-8") + f.write("%s:\\%s\\%s\n" % (drive_letter, music_dir, filename)) + f.close() + +def strip_prefix(s, prefix): + assert s.startswith(prefix) + s = s[len(prefix):] + if s.startswith("/"): + s = s[1:] + return s + +def mkdirhier(path): + if os.path.isdir(path): + return + paths = [path] + while path != "/": + path = os.path.split(path)[0] + paths.append(path) + for path in reversed(paths): + try: + os.mkdir(path) + except OSError: + pass + +def sync(dry_run, source, dest, files): + join = os.path.join + + logging.info("Calculating files to sync and deleting old files") + files = set(files) + for dirpath, dirnames, filenames in os.walk(dest): + full_dirpath = dirpath + dirpath = strip_prefix(dirpath, dest) + + for filename in filenames: + filename = join(dirpath, filename).decode("utf-8") + + # Whenever 'file' is deleted OSX will helpfully remove '._file' + if not os.path.exists(join(dest, filename)): + continue + + if filename in files: + sourcestat = os.stat(join(source, filename)) + deststat = os.stat(join(dest, filename)) + same_time = abs(sourcestat.st_mtime - deststat.st_mtime) < 5 + same_size = sourcestat.st_size == deststat.st_size + if same_time and same_size: + files.remove(filename) + logging.debug("keep: " + filename) + else: + logging.debug("update: " + filename) + + elif not filename.startswith("Playlists/"): + logging.debug("delete: " + filename) + if not dry_run: + os.unlink(join(dest, filename)) + + if len(os.listdir(full_dirpath)) == 0: + logging.debug("rmdir: " + dirpath) + if not dry_run: + os.rmdir(full_dirpath) + + + logging.info("Copying new files") + files = list(files) + files.sort() + for filename in files: + logging.debug("copy: " + filename) + if not dry_run: + mkdirhier(os.path.dirname(join(dest, filename))) + shutil.copy2(join(source, filename), join(dest, filename)) + + diff --git a/main.m b/main.m new file mode 100644 index 0000000..505c846 --- /dev/null +++ b/main.m @@ -0,0 +1,47 @@ +/* Copyright 2009 James Bunton + * Licensed for distribution under the GPL version 2. + */ + +#import +#import + +int +main(int argc, char** argv) +{ + NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; + + NSBundle* mainBundle = [NSBundle mainBundle]; + NSString* resourcePath = [mainBundle resourcePath]; + NSArray* pythonPathArray = [NSArray arrayWithObjects: resourcePath, [resourcePath stringByAppendingPathComponent:@"PyObjC"], nil]; + + setenv("PYTHONPATH", [[pythonPathArray componentsJoinedByString:@":"] UTF8String], 1); + + NSArray* possibleMainExtensions = [NSArray arrayWithObjects: @"py", @"pyc", @"pyo", nil]; + NSString* mainFilePath = nil; + + for(NSString* possibleMainExtension in possibleMainExtensions) { + mainFilePath = [mainBundle pathForResource: @"main" ofType: possibleMainExtension]; + if(mainFilePath != nil) break; + } + + if(!mainFilePath) { + [NSException raise: NSInternalInconsistencyException format: @"%s:%d main() Failed to find the main.{py,pyc,pyo} file in the application wrapper's Resources directory.", __FILE__, __LINE__]; + } + + Py_SetProgramName("/usr/bin/python"); + Py_Initialize(); + PySys_SetArgv(argc, (char**)argv); + + const char* mainFilePathPtr = [mainFilePath UTF8String]; + FILE* mainFile = fopen(mainFilePathPtr, "r"); + int result = PyRun_SimpleFile(mainFile, (char*)[[mainFilePath lastPathComponent] UTF8String]); + + if(result != 0) + [NSException raise: NSInternalInconsistencyException + format: @"%s:%d main() PyRun_SimpleFile failed with file '%@'. See console for errors.", __FILE__, __LINE__, mainFilePath]; + + [pool drain]; + + return result; +} + diff --git a/main.py b/main.py new file mode 100644 index 0000000..1ddbb89 --- /dev/null +++ b/main.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +# Copyright 2009 James Bunton +# Licensed for distribution under the GPL version 2, check COPYING for details + +import objc +import Foundation +import AppKit +from PyObjCTools import AppHelper + +import NotiPodAppDelegate +import NotiPodController + +AppHelper.runEventLoop() + diff --git a/notipod.xcodeproj/project.pbxproj b/notipod.xcodeproj/project.pbxproj new file mode 100644 index 0000000..db5ae1a --- /dev/null +++ b/notipod.xcodeproj/project.pbxproj @@ -0,0 +1,297 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 44; + objects = { + +/* Begin PBXBuildFile section */ + 77631A270C06C501005415CB /* Python.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 77631A260C06C501005415CB /* Python.framework */; }; + 77631A3F0C0748CF005415CB /* main.py in Resources */ = {isa = PBXBuildFile; fileRef = 77631A3E0C0748CF005415CB /* main.py */; }; + 7790198F0C07548A00326F66 /* NotiPodController.py in Resources */ = {isa = PBXBuildFile; fileRef = 7790198E0C07548A00326F66 /* NotiPodController.py */; }; + 77C8C1F90C07829500965286 /* NotiPod.xib in Resources */ = {isa = PBXBuildFile; fileRef = 77C8C1F70C07829500965286 /* NotiPod.xib */; }; + 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */; }; + 8D11072D0486CEB800E47090 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; settings = {ATTRIBUTES = (); }; }; + 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; }; + 93F5D8EB0F930158006FB1E6 /* itunes.py in Resources */ = {isa = PBXBuildFile; fileRef = 93F5D8E90F930158006FB1E6 /* itunes.py */; }; + 93F5D8EC0F930158006FB1E6 /* sync.py in Resources */ = {isa = PBXBuildFile; fileRef = 93F5D8EA0F930158006FB1E6 /* sync.py */; }; + 93F5D91D0F933A00006FB1E6 /* NotiPodAppDelegate.py in Resources */ = {isa = PBXBuildFile; fileRef = 93F5D91C0F933A00006FB1E6 /* NotiPodAppDelegate.py */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 089C165DFE840E0CC02AAC07 /* English */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = English; path = English.lproj/InfoPlist.strings; sourceTree = ""; }; + 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = ""; }; + 29B97316FDCFA39411CA2CEA /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 29B97324FDCFA39411CA2CEA /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = ""; }; + 29B97325FDCFA39411CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; + 32CA4F630368D1EE00C91783 /* notipod_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = notipod_Prefix.pch; sourceTree = ""; }; + 77631A260C06C501005415CB /* Python.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Python.framework; path = /System/Library/Frameworks/Python.framework; sourceTree = ""; }; + 77631A3E0C0748CF005415CB /* main.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = main.py; sourceTree = ""; }; + 7790198E0C07548A00326F66 /* NotiPodController.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = NotiPodController.py; sourceTree = ""; }; + 77C8C1F80C07829500965286 /* English */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = English; path = English.lproj/NotiPod.xib; sourceTree = ""; }; + 8D1107310486CEB800E47090 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 8D1107320486CEB800E47090 /* notipod.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = notipod.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 93F5D8E90F930158006FB1E6 /* itunes.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = itunes.py; sourceTree = ""; }; + 93F5D8EA0F930158006FB1E6 /* sync.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = sync.py; sourceTree = ""; }; + 93F5D91C0F933A00006FB1E6 /* NotiPodAppDelegate.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = NotiPodAppDelegate.py; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 8D11072E0486CEB800E47090 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */, + 77631A270C06C501005415CB /* Python.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 080E96DDFE201D6D7F000001 /* Classes */ = { + isa = PBXGroup; + children = ( + 93F5D91C0F933A00006FB1E6 /* NotiPodAppDelegate.py */, + 7790198E0C07548A00326F66 /* NotiPodController.py */, + ); + name = Classes; + sourceTree = ""; + }; + 1058C7A0FEA54F0111CA2CBB /* Linked Frameworks */ = { + isa = PBXGroup; + children = ( + 77631A260C06C501005415CB /* Python.framework */, + 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */, + ); + name = "Linked Frameworks"; + sourceTree = ""; + }; + 1058C7A2FEA54F0111CA2CBB /* Other Frameworks */ = { + isa = PBXGroup; + children = ( + 29B97324FDCFA39411CA2CEA /* AppKit.framework */, + 29B97325FDCFA39411CA2CEA /* Foundation.framework */, + ); + name = "Other Frameworks"; + sourceTree = ""; + }; + 19C28FACFE9D520D11CA2CBB /* Products */ = { + isa = PBXGroup; + children = ( + 8D1107320486CEB800E47090 /* notipod.app */, + ); + name = Products; + sourceTree = ""; + }; + 29B97314FDCFA39411CA2CEA /* notipod */ = { + isa = PBXGroup; + children = ( + 080E96DDFE201D6D7F000001 /* Classes */, + 29B97315FDCFA39411CA2CEA /* Other Sources */, + 29B97317FDCFA39411CA2CEA /* Resources */, + 29B97323FDCFA39411CA2CEA /* Frameworks */, + 19C28FACFE9D520D11CA2CBB /* Products */, + ); + name = notipod; + sourceTree = ""; + }; + 29B97315FDCFA39411CA2CEA /* Other Sources */ = { + isa = PBXGroup; + children = ( + 93F5D8E90F930158006FB1E6 /* itunes.py */, + 93F5D8EA0F930158006FB1E6 /* sync.py */, + 32CA4F630368D1EE00C91783 /* notipod_Prefix.pch */, + 29B97316FDCFA39411CA2CEA /* main.m */, + 77631A3E0C0748CF005415CB /* main.py */, + ); + name = "Other Sources"; + sourceTree = ""; + }; + 29B97317FDCFA39411CA2CEA /* Resources */ = { + isa = PBXGroup; + children = ( + 77C8C1F70C07829500965286 /* NotiPod.xib */, + 8D1107310486CEB800E47090 /* Info.plist */, + 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */, + ); + name = Resources; + sourceTree = ""; + }; + 29B97323FDCFA39411CA2CEA /* Frameworks */ = { + isa = PBXGroup; + children = ( + 1058C7A0FEA54F0111CA2CBB /* Linked Frameworks */, + 1058C7A2FEA54F0111CA2CBB /* Other Frameworks */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 8D1107260486CEB800E47090 /* notipod */ = { + isa = PBXNativeTarget; + buildConfigurationList = C01FCF4A08A954540054247B /* Build configuration list for PBXNativeTarget "notipod" */; + buildPhases = ( + 8D1107290486CEB800E47090 /* Resources */, + 8D11072C0486CEB800E47090 /* Sources */, + 8D11072E0486CEB800E47090 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = notipod; + productInstallPath = "$(HOME)/Applications"; + productName = notipod; + productReference = 8D1107320486CEB800E47090 /* notipod.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 29B97313FDCFA39411CA2CEA /* Project object */ = { + isa = PBXProject; + buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "notipod" */; + compatibilityVersion = "Xcode 3.0"; + hasScannedForEncodings = 1; + mainGroup = 29B97314FDCFA39411CA2CEA /* notipod */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 8D1107260486CEB800E47090 /* notipod */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 8D1107290486CEB800E47090 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */, + 77631A3F0C0748CF005415CB /* main.py in Resources */, + 7790198F0C07548A00326F66 /* NotiPodController.py in Resources */, + 77C8C1F90C07829500965286 /* NotiPod.xib in Resources */, + 93F5D8EB0F930158006FB1E6 /* itunes.py in Resources */, + 93F5D8EC0F930158006FB1E6 /* sync.py in Resources */, + 93F5D91D0F933A00006FB1E6 /* NotiPodAppDelegate.py in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 8D11072C0486CEB800E47090 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8D11072D0486CEB800E47090 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 089C165DFE840E0CC02AAC07 /* English */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + 77C8C1F70C07829500965286 /* NotiPod.xib */ = { + isa = PBXVariantGroup; + children = ( + 77C8C1F80C07829500965286 /* English */, + ); + name = NotiPod.xib; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + C01FCF4B08A954540054247B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + COPY_PHASE_STRIP = NO; + GCC_DYNAMIC_NO_PIC = NO; + GCC_ENABLE_FIX_AND_CONTINUE = YES; + GCC_MODEL_TUNING = G5; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = notipod_Prefix.pch; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "$(HOME)/Applications"; + PRODUCT_NAME = notipod; + WRAPPER_EXTENSION = app; + ZERO_LINK = YES; + }; + name = Debug; + }; + C01FCF4C08A954540054247B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + GCC_MODEL_TUNING = G5; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = notipod_Prefix.pch; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "$(HOME)/Applications"; + PRODUCT_NAME = notipod; + WRAPPER_EXTENSION = app; + }; + name = Release; + }; + C01FCF4F08A954540054247B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + PREBINDING = NO; + SDKROOT = "$(DEVELOPER_SDK_DIR)/MacOSX10.5.sdk"; + }; + name = Debug; + }; + C01FCF5008A954540054247B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = ( + ppc, + i386, + ); + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + PREBINDING = NO; + SDKROOT = "$(DEVELOPER_SDK_DIR)/MacOSX10.5.sdk"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + C01FCF4A08A954540054247B /* Build configuration list for PBXNativeTarget "notipod" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C01FCF4B08A954540054247B /* Debug */, + C01FCF4C08A954540054247B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + C01FCF4E08A954540054247B /* Build configuration list for PBXProject "notipod" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C01FCF4F08A954540054247B /* Debug */, + C01FCF5008A954540054247B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 29B97313FDCFA39411CA2CEA /* Project object */; +} diff --git a/sync.py b/sync.py new file mode 100755 index 0000000..2ec3d29 --- /dev/null +++ b/sync.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python + +VERSION = "0.1" + +import logging +import optparse +import os +import sys + +import itunes + + +def parse_options(): + parser = optparse.OptionParser(version="%prog " + VERSION, + description = "Synchronise an iTunes playlist with a directory", + usage = "%prog destination playlist [playlist ...]" + ) + parser.add_option("-q", "--quiet", + action="store_true", dest="quiet", default=False) + parser.add_option("-v", "--verbose", + action="store_true", dest="verbose", default=False) + parser.add_option("-n", "--dry-run", + action="store_true", dest="dry_run", default=False) + parser.add_option("--itunes-library", + action="store", dest="itunes_library", default=None) + parser.add_option("--drive-letter", + action="store", dest="drive_letter", default="E") + parser.add_option("--media-dir", + action="store", dest="media_dir", default="iTunes") + + opts, args = parser.parse_args(sys.argv[1:]) + if len(args) < 2: + parser.print_usage() + sys.exit(1) + opts.dest = args[0] + opts.full_dest = os.path.join(opts.dest, opts.media_dir) + opts.playlists = args[1:] + + return opts + +def main(): + opts = parse_options() + + # Set up logging + try: + logging.basicConfig(format="%(levelname)s: %(message)s") + except TypeError: + # Support for Python 2.3 + logging.basicConfig() + if opts.quiet: + logging.getLogger().setLevel(logging.CRITICAL) + elif opts.verbose: + logging.getLogger().setLevel(logging.DEBUG) + else: + logging.getLogger().setLevel(logging.INFO) + + if not os.path.isdir(opts.dest): + logging.fatal("Destination must be specified as an absolute path.") + sys.exit(1) + + logging.info("Loading library") + library = itunes.Library.alloc().initWithFilename_(opts.itunes_library) + + for playlist in opts.playlists: + if not library.has_playlist(playlist): + logging.fatal("Could not find playlist: " + playlist) + sys.exit(1) + + logging.info("Loading playlists") + all_tracks = [] + for playlist in opts.playlists: + tracks = list(library.get_playlist(playlist)) + all_tracks.extend(tracks) + itunes.export_m3u(opts.dry_run, opts.full_dest, + opts.drive_letter, opts.media_dir, + playlist, tracks) + + logging.info("Synchronising") + itunes.sync(opts.dry_run, library.folder, opts.full_dest, all_tracks) + + +if __name__ == "__main__": + main() +