1
0
mirror of https://github.com/arsenetar/dupeguru.git synced 2025-03-10 05:34:36 +00:00

Added real iTunes support in dgme (similar to iPhoto support in dgpe).

This commit is contained in:
Virgil Dupras 2012-02-21 10:23:23 -05:00
parent 9457a43993
commit b12b70b0a1
4 changed files with 161 additions and 13 deletions

View File

@ -7,16 +7,21 @@
# http://www.hardcoded.net/licenses/bsd_license # http://www.hardcoded.net/licenses/bsd_license
import logging import logging
from appscript import app, k, CommandError from appscript import app, its, k, CommandError, ApplicationNotFoundError
import plistlib
import time import time
import os.path as op 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.trans import tr
from hscommon.path import Path
from core import directories
from core.app import JobType from core.app import JobType
from core.scanner import ScanType from core.scanner import ScanType
from core_me.app import DupeGuru as DupeGuruBase from core_me.app import DupeGuru as DupeGuruBase
from core_me import fs
from .app import JOBID2TITLE, PyDupeGuruBase from .app import JOBID2TITLE, PyDupeGuruBase
JobType.RemoveDeadTracks = 'jobRemoveDeadTracks' JobType.RemoveDeadTracks = 'jobRemoveDeadTracks'
@ -27,15 +32,160 @@ JOBID2TITLE.update({
JobType.ScanDeadTracks: tr("Scanning the iTunes Library"), 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): class DupeGuruME(DupeGuruBase):
def __init__(self, view, appdata): def __init__(self, view, appdata):
appdata = op.join(appdata, 'dupeGuru Music Edition') appdata = op.join(appdata, 'dupeGuru Music Edition')
DupeGuruBase.__init__(self, view, appdata) DupeGuruBase.__init__(self, view, appdata)
# Use fileclasses set in DupeGuruBase.__init__()
self.directories = Directories(fileclasses=self.directories.fileclasses)
self.dead_tracks = [] 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 remove_dead_tracks(self):
def do(j): def do(j):
a = app('iTunes') a = app(ITUNES)
a.activate(timeout=0) a.activate(timeout=0)
for index, track in enumerate(j.iter_with_progress(self.dead_tracks)): for index, track in enumerate(j.iter_with_progress(self.dead_tracks)):
if index % 100 == 0: if index % 100 == 0:
@ -49,13 +199,10 @@ class DupeGuruME(DupeGuruBase):
def scan_dead_tracks(self): def scan_dead_tracks(self):
def do(j): def do(j):
a = app('iTunes') a = app(ITUNES)
a.activate(timeout=0) a.activate(timeout=0)
try: library = get_itunes_library(a)
[source] = [s for s in a.sources(timeout=0) if s.kind(timeout=0) == k.library] if library is None:
[library] = source.library_playlists(timeout=0)
except ValueError:
logging.warning('Some unexpected iTunes configuration encountered')
return return
self.dead_tracks = [] self.dead_tracks = []
tracks = as_fetch(library.file_tracks, k.file_track) tracks = as_fetch(library.file_tracks, k.file_track)

View File

@ -21,13 +21,13 @@ http://www.hardcoded.net/licenses/bsd_license
{ {
[super fillPopUpMenu]; [super fillPopUpMenu];
NSMenu *m = [addButtonPopUp menu]; 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]; keyEquivalent:@"" atIndex:1];
[mi setTarget:self]; [mi setTarget:self];
} }
- (IBAction)addiTunes:(id)sender - (IBAction)addiTunes:(id)sender
{ {
[self addDirectory:[@"~/Music/iTunes/iTunes Music" stringByExpandingTildeInPath]]; [self addDirectory:@"iTunes Library"];
} }
@end @end

View File

@ -642,7 +642,7 @@
29B97313FDCFA39411CA2CEA /* Project object */ = { 29B97313FDCFA39411CA2CEA /* Project object */ = {
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
LastUpgradeCheck = 0420; LastUpgradeCheck = 0430;
}; };
buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "dupeguru" */; buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "dupeguru" */;
compatibilityVersion = "Xcode 3.2"; compatibilityVersion = "Xcode 3.2";

View File

@ -44,9 +44,10 @@ class DupeGuru(DupeGuruBase):
else: else:
percentage = group.percentage percentage = group.percentage
dupe_count = len(group.dupes) dupe_count = len(group.dupes)
dupe_folder_path = getattr(dupe, 'display_folder_path', dupe.folder_path)
return { return {
'name': dupe.name, 'name': dupe.name,
'folder_path': str(dupe.folder_path), 'folder_path': str(dupe_folder_path),
'size': format_size(size, 2, 2, False), 'size': format_size(size, 2, 2, False),
'duration': format_time(duration, with_hours=False), 'duration': format_time(duration, with_hours=False),
'bitrate': str(bitrate), 'bitrate': str(bitrate),