From b12b70b0a11fcd08f9868750feb37227aa5f75ce Mon Sep 17 00:00:00 2001 From: Virgil Dupras Date: Tue, 21 Feb 2012 10:23:23 -0500 Subject: [PATCH] Added real iTunes support in dgme (similar to iPhoto support in dgpe). --- cocoa/inter/app_me.py | 165 ++++++++++++++++++-- cocoa/me/DirectoryPanel.m | 4 +- cocoa/me/dupeguru.xcodeproj/project.pbxproj | 2 +- core_me/app.py | 3 +- 4 files changed, 161 insertions(+), 13 deletions(-) diff --git a/cocoa/inter/app_me.py b/cocoa/inter/app_me.py index c7d1413a..408b6658 100644 --- a/cocoa/inter/app_me.py +++ b/cocoa/inter/app_me.py @@ -7,16 +7,21 @@ # http://www.hardcoded.net/licenses/bsd_license import logging -from appscript import app, k, CommandError +from appscript import app, its, k, CommandError, ApplicationNotFoundError +import plistlib import time import os.path as op -from cocoa import as_fetch +from cocoa import as_fetch, proxy +from hscommon import io from hscommon.trans import tr +from hscommon.path import Path +from core import directories from core.app import JobType from core.scanner import ScanType from core_me.app import DupeGuru as DupeGuruBase +from core_me import fs from .app import JOBID2TITLE, PyDupeGuruBase JobType.RemoveDeadTracks = 'jobRemoveDeadTracks' @@ -27,15 +32,160 @@ JOBID2TITLE.update({ JobType.ScanDeadTracks: tr("Scanning the iTunes Library"), }) +ITUNES = 'iTunes' +ITUNES_PATH = Path('iTunes Library') + +def get_itunes_library(a): + try: + [source] = [s for s in a.sources(timeout=0) if s.kind(timeout=0) == k.library] + [library] = source.library_playlists(timeout=0) + return library + except ValueError: + logging.warning('Some unexpected iTunes configuration encountered') + return None + +class ITunesSong(fs.MusicFile): + def __init__(self, song_data): + path = Path(proxy.url2path_(song_data['Location'])) + fs.MusicFile.__init__(self, path) + self.id = song_data['Track ID'] + + def remove_from_library(self): + try: + a = app(ITUNES) + library = get_itunes_library(a) + if library is None: + return + [song] = library.file_tracks[its.database_ID == self.id]() + a.delete(song, timeout=0) + except ValueError: + msg = "Could not find song '{}' in iTunes Library".format(str(self.path)) + raise EnvironmentError(msg) + except (CommandError, RuntimeError) as e: + raise EnvironmentError(str(e)) + + display_folder_path = ITUNES_PATH + +def get_itunes_database_path(): + plisturls = proxy.prefValue_inDomain_('iTunesRecentDatabases', 'com.apple.iApps') + if not plisturls: + raise directories.InvalidPathError() + plistpath = proxy.url2path_(plisturls[0]) + return Path(plistpath) + +def get_itunes_songs(plistpath): + if not io.exists(plistpath): + return [] + plist = plistlib.readPlist(str(plistpath)) + result = [] + for song_data in plist['Tracks'].values(): + if song_data['Track Type'] != 'File': + continue + song = ITunesSong(song_data) + if io.exists(song.path): + result.append(song) + return result + +class Directories(directories.Directories): + def __init__(self, fileclasses): + directories.Directories.__init__(self, fileclasses) + try: + self.itunes_libpath = get_itunes_database_path() + except directories.InvalidPathError: + self.itunes_libpath = None + + def _get_files(self, from_path, j): + if from_path == ITUNES_PATH: + if self.itunes_libpath is None: + return [] + is_ref = self.get_state(from_path) == directories.DirectoryState.Reference + songs = get_itunes_songs(self.itunes_libpath) + for song in songs: + song.is_ref = is_ref + return songs + else: + return directories.Directories._get_files(self, from_path, j) + + @staticmethod + def get_subfolders(path): + if path == ITUNES_PATH: + return [] + else: + return directories.Directories.get_subfolders(path) + + def add_path(self, path): + if path == ITUNES_PATH: + if path not in self: + self._dirs.append(path) + else: + directories.Directories.add_path(self, path) + + def has_itunes_path(self): + return any(path == ITUNES_PATH for path in self._dirs) + + def has_any_file(self): + # If we don't do that, it causes a hangup in the GUI when we click Start Scanning because + # checking if there's any file to scan involves reading the whole library. If we have the + # iTunes library, we assume we have at least one file. + if self.has_itunes_path(): + return True + else: + return directories.Directories.has_any_file(self) + + class DupeGuruME(DupeGuruBase): def __init__(self, view, appdata): appdata = op.join(appdata, 'dupeGuru Music Edition') DupeGuruBase.__init__(self, view, appdata) + # Use fileclasses set in DupeGuruBase.__init__() + self.directories = Directories(fileclasses=self.directories.fileclasses) self.dead_tracks = [] + def _do_delete(self, j, replace_with_hardlinks): + # XXX If I read correctly, Python 3.3 will allow us to go fetch inner function easily, so + # we'll be able to replace "op" below with DupeGuruBase._do_delete.op. + def op(dupe): + j.add_progress() + return self._do_delete_dupe(dupe, replace_with_hardlinks) + + marked = [dupe for dupe in self.results.dupes if self.results.is_marked(dupe)] + j.start_job(self.results.mark_count, tr("Sending dupes to the Trash")) + if any(isinstance(dupe, ITunesSong) for dupe in marked): + j.add_progress(0, desc=tr("Talking to iTunes. Don't touch it!")) + try: + a = app(ITUNES) + a.activate(timeout=0) + except (CommandError, RuntimeError, ApplicationNotFoundError): + pass + self.results.perform_on_marked(op, True) + + def _do_delete_dupe(self, dupe, replace_with_hardlinks): + if isinstance(dupe, ITunesSong): + dupe.remove_from_library() + DupeGuruBase._do_delete_dupe(self, dupe, replace_with_hardlinks) + + def _create_file(self, path): + if (self.directories.itunes_libpath is not None) and (path in self.directories.itunes_libpath[:-1]): + return ITunesSong(path) + return DupeGuruBase._create_file(self, path) + + def copy_or_move(self, dupe, copy, destination, dest_type): + if isinstance(dupe, ITunesSong): + copy = True + return DupeGuruBase.copy_or_move(self, dupe, copy, destination, dest_type) + + def start_scanning(self): + if self.directories.has_itunes_path(): + try: + app(ITUNES) + except ApplicationNotFoundError: + self.view.show_message(tr("The iTunes application couldn't be found.")) + return + DupeGuruBase.start_scanning(self) + def remove_dead_tracks(self): def do(j): - a = app('iTunes') + a = app(ITUNES) a.activate(timeout=0) for index, track in enumerate(j.iter_with_progress(self.dead_tracks)): if index % 100 == 0: @@ -49,13 +199,10 @@ class DupeGuruME(DupeGuruBase): def scan_dead_tracks(self): def do(j): - a = app('iTunes') + a = app(ITUNES) a.activate(timeout=0) - try: - [source] = [s for s in a.sources(timeout=0) if s.kind(timeout=0) == k.library] - [library] = source.library_playlists(timeout=0) - except ValueError: - logging.warning('Some unexpected iTunes configuration encountered') + library = get_itunes_library(a) + if library is None: return self.dead_tracks = [] tracks = as_fetch(library.file_tracks, k.file_track) diff --git a/cocoa/me/DirectoryPanel.m b/cocoa/me/DirectoryPanel.m index 0b91e24d..7b1775d1 100644 --- a/cocoa/me/DirectoryPanel.m +++ b/cocoa/me/DirectoryPanel.m @@ -21,13 +21,13 @@ http://www.hardcoded.net/licenses/bsd_license { [super fillPopUpMenu]; NSMenu *m = [addButtonPopUp menu]; - NSMenuItem *mi = [m insertItemWithTitle:TR(@"Add iTunes Directory") action:@selector(addiTunes:) + NSMenuItem *mi = [m insertItemWithTitle:TR(@"Add iTunes Library") action:@selector(addiTunes:) keyEquivalent:@"" atIndex:1]; [mi setTarget:self]; } - (IBAction)addiTunes:(id)sender { - [self addDirectory:[@"~/Music/iTunes/iTunes Music" stringByExpandingTildeInPath]]; + [self addDirectory:@"iTunes Library"]; } @end diff --git a/cocoa/me/dupeguru.xcodeproj/project.pbxproj b/cocoa/me/dupeguru.xcodeproj/project.pbxproj index 2f6c2745..f29e1260 100644 --- a/cocoa/me/dupeguru.xcodeproj/project.pbxproj +++ b/cocoa/me/dupeguru.xcodeproj/project.pbxproj @@ -642,7 +642,7 @@ 29B97313FDCFA39411CA2CEA /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0420; + LastUpgradeCheck = 0430; }; buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "dupeguru" */; compatibilityVersion = "Xcode 3.2"; diff --git a/core_me/app.py b/core_me/app.py index 73b9a4dd..0906e720 100644 --- a/core_me/app.py +++ b/core_me/app.py @@ -44,9 +44,10 @@ class DupeGuru(DupeGuruBase): else: percentage = group.percentage dupe_count = len(group.dupes) + dupe_folder_path = getattr(dupe, 'display_folder_path', dupe.folder_path) return { 'name': dupe.name, - 'folder_path': str(dupe.folder_path), + 'folder_path': str(dupe_folder_path), 'size': format_size(size, 2, 2, False), 'duration': format_time(duration, with_hours=False), 'bitrate': str(bitrate),