/* * SapphireMetaDataSupport.m * Sapphire * * Created by Graham Booker on Apr. 16, 2008. * Copyright 2008 Sapphire Development Team and/or www.nanopi.net * All rights reserved. * * This program is free software; you can redistribute it and/or modify it under the terms of the GNU * General Public License as published by the Free Software Foundation; either version 3 of the License, * or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General * Public License for more details. * * You should have received a copy of the GNU General Public License along with this program; if not, * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #import "SapphireMetaDataSupport.h" #import "SapphireDirectoryMetaData.h" #import "SapphireFileMetaData.h" #import "SapphireJoinedFile.h" #import "SapphireCollectionDirectory.h" #import "SapphireMovie.h" #import "SapphireCast.h" #import "SapphireMovieTranslation.h" #import "SapphireMoviePoster.h" #import "SapphireTVTranslation.h" #import "SapphireTVShow.h" #import "SapphireMetaDataUpgrading.h" #import "SapphireDirector.h" #import "SapphireGenre.h" #import "SapphireDirectorySymLink.h" #import "SapphireFileSymLink.h" #import "SapphireEpisode.h" #import "SapphireXMLData.h" #import "SapphireApplianceController.h" #import "SapphireSeason.h" #import "CoreDataSupportFunctions.h" #define META_VERSION_KEY @"Version" /* Movie Translations */ #define MOVIE_TRAN_VERSION_KEY @"Version" #define MOVIE_TRAN_CURRENT_VERSION 2 /* Translation Keys */ #define MOVIE_TRAN_TRANSLATIONS_KEY @"Translations" #define MOVIE_TRAN_IMDB_LINK_KEY @"IMDB Link" #define MOVIE_TRAN_IMP_LINK_KEY @"IMP Link" #define MOVIE_TRAN_IMP_POSTERS_KEY @"IMP Posters" #define MOVIE_TRAN_SELECTED_POSTER_KEY @"Selected Poster" #define MOVIE_TRAN_AUTO_SELECT_POSTER_KEY @"Default Poster" static NSSet *coverArtExtentions = nil; NSString *searchCoverArtExtForPath(NSString *path) { NSFileManager *fm = [NSFileManager defaultManager]; NSString *directory = [path stringByDeletingLastPathComponent]; NSArray *files = [fm directoryContentsAtPath:directory]; NSString *lastComp = [path lastPathComponent]; /*Search all files*/ NSEnumerator *fileEnum = [files objectEnumerator]; NSString *file = nil; while((file = [fileEnum nextObject]) != nil) { NSString *ext = [file pathExtension]; if([ext length] && [coverArtExtentions containsObject:ext] && [lastComp caseInsensitiveCompare:[file stringByDeletingPathExtension]] == NSOrderedSame) return [directory stringByAppendingPathComponent:file]; } /*Didn't find one*/ return nil; } @implementation SapphireMetaDataSupport + (void)load { coverArtExtentions = [[NSSet alloc] initWithObjects: @"jpg", @"jpeg", @"tif", @"tiff", @"png", @"gif", nil]; } + (SapphireMetaDataSupport *)sharedInstance { static SapphireMetaDataSupport *shared = nil; if(shared == nil) shared = [[SapphireMetaDataSupport alloc] init]; return shared; } - (void) dealloc { [writeTimer invalidate]; writeTimer = nil; [super dealloc]; } + (void)pruneMetaData:(NSManagedObjectContext *)moc { NSPredicate *movieFilePred = [NSPredicate predicateWithFormat:@"movie != nil"]; NSArray *movieFiles = doFetchRequest(SapphireFileMetaDataName, moc, movieFilePred); NSSet *movieIds = [NSSet setWithArray:[movieFiles valueForKeyPath:@"movie.objectID"]]; NSPredicate *movieNoFile = [NSPredicate predicateWithFormat:@"NOT SELF IN %@", movieIds]; NSArray *emptyMovies = doFetchRequest(SapphireMovieName, moc, movieNoFile); SapphireLog(SAPPHIRE_LOG_IMPORT, SAPPHIRE_LOG_LEVEL_DETAIL, @"Pruning Movies %@", [emptyMovies valueForKeyPath:@"title"]); NSEnumerator *objEnum = [emptyMovies objectEnumerator]; NSManagedObject *obj; while((obj = [objEnum nextObject]) != nil) [moc deleteObject:obj]; NSArray *allMovies = doFetchRequest(SapphireMovieName, moc, nil); NSDictionary *pruneKeys = [NSDictionary dictionaryWithObjectsAndKeys: SapphireCastName, @"cast", SapphireGenreName, @"genres", SapphireDirectorName, @"directors", nil]; NSEnumerator *keyEnum = [pruneKeys keyEnumerator]; NSString *key; while((key = [keyEnum nextObject]) != nil) { NSString *objName = [pruneKeys objectForKey:key]; NSArray *itemSet = [allMovies valueForKeyPath:[NSString stringWithFormat:@"@distinctUnionOfSets.%@.objectID", key]]; NSArray *emptyItems = doFetchRequest(objName, moc, [NSPredicate predicateWithFormat:@"NOT SELF IN %@", itemSet]); SapphireLog(SAPPHIRE_LOG_IMPORT, SAPPHIRE_LOG_LEVEL_DETAIL, @"Pruning %@ %@", key, [emptyItems valueForKeyPath:@"name"]); objEnum = [emptyItems objectEnumerator]; while((obj = [objEnum nextObject]) != nil) [moc deleteObject:obj]; } NSPredicate *epFilePred = [NSPredicate predicateWithFormat:@"tvEpisode != nil"]; NSArray *epFiles = doFetchRequest(SapphireFileMetaDataName, moc, epFilePred); NSSet *epIds = [NSSet setWithArray:[epFiles valueForKeyPath:@"tvEpisode.objectID"]]; NSPredicate *epNoFile = [NSPredicate predicateWithFormat:@"NOT SELF IN %@", epIds]; NSArray *emptyEpisodes = doFetchRequest(SapphireEpisodeName, moc, epNoFile); SapphireLog(SAPPHIRE_LOG_IMPORT, SAPPHIRE_LOG_LEVEL_DETAIL, @"Pruning Episodes %@", [emptyEpisodes valueForKeyPath:@"episodeTitle"]); objEnum = [emptyEpisodes objectEnumerator]; while((obj = [objEnum nextObject]) != nil) [moc deleteObject:obj]; NSArray *allEps = doFetchRequest(SapphireEpisodeName, moc, nil); NSSet *seasonIds = [allEps valueForKeyPath:@"@distinctUnionOfObjects.season.objectID"]; NSPredicate *noEpisodes = [NSPredicate predicateWithFormat:@"NOT SELF IN %@", seasonIds]; NSArray *emptySeasons = doFetchRequest(SapphireSeasonName, moc, noEpisodes); SapphireLog(SAPPHIRE_LOG_IMPORT, SAPPHIRE_LOG_LEVEL_DETAIL, @"Pruning Seasons %@", [emptySeasons valueForKeyPath:@"path"]); objEnum = [emptySeasons objectEnumerator]; while((obj = [objEnum nextObject]) != nil) [moc deleteObject:obj]; NSSet *showIds = [allEps valueForKeyPath:@"@distinctUnionOfObjects.tvShow.objectID"]; noEpisodes = [NSPredicate predicateWithFormat:@"NOT SELF IN %@", showIds]; NSArray *emptyShows = doFetchRequest(SapphireTVShowName, moc, noEpisodes); SapphireLog(SAPPHIRE_LOG_IMPORT, SAPPHIRE_LOG_LEVEL_DETAIL, @"Pruning Shows %@", [emptyShows valueForKeyPath:@"name"]); objEnum = [emptyShows objectEnumerator]; while((obj = [objEnum nextObject]) != nil) [moc deleteObject:obj]; } - (void)realWriteMetaData:(NSTimer *)timer { NSManagedObjectContext *context = nil; if([timer isKindOfClass:[NSManagedObjectContext class]]) context = (NSManagedObjectContext *)timer; else context = [timer userInfo]; if(writeTimer != nil) SapphireLog(SAPPHIRE_LOG_METADATA_STORE, SAPPHIRE_LOG_LEVEL_DETAIL, @"Rescheduled write"); [writeTimer invalidate]; writeTimer = nil; NSError *error = nil; locked = NO; BOOL success = NO; @try { success = [context save:&error]; } @catch (NSException * e) { SapphireLog(SAPPHIRE_LOG_METADATA_STORE, SAPPHIRE_LOG_LEVEL_DETAIL, @"Could not save due to exception \"%@\" with reason \"%@\"", [e name], [e reason]); } if(error != nil) { SapphireLog(SAPPHIRE_LOG_METADATA_STORE, SAPPHIRE_LOG_LEVEL_DETAIL, @"Save error \"%@\"", error); NSException *underlying = [[error userInfo] objectForKey:@"NSUnderlyingException"]; SapphireLog(SAPPHIRE_LOG_METADATA_STORE, SAPPHIRE_LOG_LEVEL_DEBUG, @"Underlying is %@ %@ %@ %@", underlying, [underlying name], [underlying reason], [underlying userInfo]); if([[underlying reason] isEqualToString:@"database is locked"]) { SapphireLog(SAPPHIRE_LOG_METADATA_STORE, SAPPHIRE_LOG_LEVEL_DEBUG, @"Detected locked"); locked = YES; } } if(success == NO) { SapphireLog(SAPPHIRE_LOG_METADATA_STORE, SAPPHIRE_LOG_LEVEL_DEBUG, @"Inserted objects is %@", [context insertedObjects]); SapphireLog(SAPPHIRE_LOG_METADATA_STORE, SAPPHIRE_LOG_LEVEL_DEBUG, @"Updated objects is %@", [context updatedObjects]); SapphireLog(SAPPHIRE_LOG_METADATA_STORE, SAPPHIRE_LOG_LEVEL_DEBUG, @"Deleted objects is %@", [context deletedObjects]); interval *= 2; [writeTimer invalidate]; SapphireLog(SAPPHIRE_LOG_METADATA_STORE, SAPPHIRE_LOG_LEVEL_DETAIL, @"Rescheduling write to occurr in %f seconds", interval); @try { NSSet *objSet = [context updatedObjects]; NSEnumerator *objEnum = [objSet objectEnumerator]; NSManagedObject *obj; while((obj = [objEnum nextObject]) != nil) [context refreshObject:obj mergeChanges:YES]; objSet = [context deletedObjects]; objEnum = [objSet objectEnumerator]; while((obj = [objEnum nextObject]) != nil) [context refreshObject:obj mergeChanges:YES]; } @catch (NSException * e) { SapphireLog(SAPPHIRE_LOG_METADATA_STORE, SAPPHIRE_LOG_LEVEL_DETAIL, @"Could not fix save due to exception \"%@\" with reason \"%@\"", [e name], [e reason]); } writeTimer = [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(realWriteMetaData:) userInfo:context repeats:NO]; } else SapphireLog(SAPPHIRE_LOG_METADATA_STORE, SAPPHIRE_LOG_LEVEL_DETAIL, @"Save successful"); } - (BOOL)save:(NSManagedObjectContext *)context; { if(writeTimer == nil) { interval = 1; [self performSelectorOnMainThread:@selector(realWriteMetaData:) withObject:context waitUntilDone:YES]; return (writeTimer == nil); } else return YES; } + (BOOL)save:(NSManagedObjectContext *)context { if(context == nil) return NO; return [[SapphireMetaDataSupport sharedInstance] save:context]; } - (BOOL)wasLocked { return locked; } + (BOOL)wasLocked { return [[SapphireMetaDataSupport sharedInstance] wasLocked]; } + (void)importV1Store:(NSManagedObjectContext *)v1Context intoContext:(NSManagedObjectContext *)context withDisplay:(SapphireMetaDataUpgrading *)display { [display setCurrentFile:@"Upgrading Cast"]; NSDictionary *castLookup = [SapphireCast upgradeV1CastFromContext:v1Context toContext:context]; [display setCurrentFile:@"Upgrading Directors"]; NSDictionary *directorLookup = [SapphireDirector upgradeV1DirectorsFromContext:v1Context toContext:context]; [display setCurrentFile:@"Upgrading Genres"]; NSDictionary *genreLookup = [SapphireGenre upgradeV1GenresFromContext:v1Context toContext:context]; [display setCurrentFile:@"Upgrading Movies"]; NSDictionary *movieLookup = [SapphireMovie upgradeV1MoviesFromContext:v1Context toContext:context withCast:castLookup directors:directorLookup genres:genreLookup]; [display setCurrentFile:@"Upgrading Shows"]; [SapphireTVShow upgradeV1ShowsFromContext:v1Context toContext:context]; [display setCurrentFile:@"Upgrading Directories"]; NSDictionary *dirLookup = [SapphireDirectoryMetaData upgradeV1DirectoriesFromContext:v1Context toContext:context]; [display setCurrentFile:@"Upgrading Files"]; NSDictionary *fileLookup = [SapphireFileMetaData upgradeV1FilesFromContext:v1Context toContext:context withMovies:movieLookup directories:dirLookup]; [display setCurrentFile:@"Upgrading SymLinks"]; [SapphireDirectorySymLink upgradeV1DirLinksFromContext:v1Context toContext:context directories:dirLookup]; [SapphireFileSymLink upgradeV1FileLinksFromContext:v1Context toContext:context directories:dirLookup file:fileLookup]; [display setCurrentFile:@"Upgrading Joined Files"]; [SapphireJoinedFile upgradeV1JoinedFileFromContext:v1Context toContext:context file:fileLookup]; [display setCurrentFile:@"Upgrading Episodes"]; [SapphireEpisode upgradeV1EpisodesFromContext:v1Context toContext:context file:fileLookup]; [display setCurrentFile:@"Upgrading XML"]; [SapphireXMLData upgradeV1XMLFromContext:v1Context toContext:context file:fileLookup]; } + (void)importPlist:(NSString *)configDir intoContext:(NSManagedObjectContext *)context withDisplay:(SapphireMetaDataUpgrading *)display { NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:[configDir stringByAppendingPathComponent:@"metaData.plist"]]; NSMutableDictionary *defer = [NSMutableDictionary dictionaryWithObjectsAndKeys: [NSMutableDictionary dictionary], @"Join", [NSMutableDictionary dictionary], @"Cast", [NSMutableDictionary dictionary], @"Directors", nil]; int version = [[dict objectForKey:META_VERSION_KEY] intValue]; SapphireDirectoryMetaData *newDir = nil; if(version > 2) { NSDictionary *slash = [dict objectForKey:@"/"]; newDir = [SapphireDirectoryMetaData createDirectoryWithPath:@"/" parent:nil inContext:context]; [newDir insertDictionary:slash withDefer:defer andDisplay:display]; } else { newDir = [SapphireDirectoryMetaData createDirectoryWithPath:[NSHomeDirectory() stringByAppendingPathComponent:@"Movies"] parent:nil inContext:context]; [newDir insertDictionary:dict withDefer:defer andDisplay:display]; } [display setCurrentFile:BRLocalizedString(@"Upgrading Joined Files", @"")]; NSDictionary *joinDict = [defer objectForKey:@"Join"]; if(joinDict != nil) { NSEnumerator *joinEunm = [joinDict keyEnumerator]; NSString *joinedPath; while((joinedPath = [joinEunm nextObject]) != nil) { SapphireJoinedFile *joinedFile = [SapphireJoinedFile joinedFileForPath:joinedPath inContext:context]; NSArray *joinArray = [joinDict objectForKey:joinedPath]; NSEnumerator *joinedEnum = [joinArray objectEnumerator]; SapphireFileMetaData *joinFile; while((joinFile = [joinedEnum nextObject]) != nil) joinFile.joinedToFile = joinedFile; } } [display setCurrentFile:BRLocalizedString(@"Upgrading Collection Prefs", @"")]; NSDictionary *options = [dict objectForKey:@"Options"]; NSMutableSet *collections = [NSMutableSet set]; NSArray *custom = [options objectForKey:@"Directories"]; if([custom count]) [collections unionSet:[NSSet setWithArray:custom]]; NSDictionary *hidden = [options objectForKey:@"Hide"]; NSArray *keyArray = [hidden allKeys]; if([keyArray count]) [collections unionSet:[NSSet setWithArray:keyArray]]; NSDictionary *skipped = [options objectForKey:@"Skip"]; keyArray = [skipped allKeys]; if([keyArray count]) [collections unionSet:[NSSet setWithArray:keyArray]]; NSEnumerator *collectionEnum = [collections objectEnumerator]; NSString *collectionPath; while((collectionPath = [collectionEnum nextObject]) != nil) { [SapphireCollectionDirectory collectionAtPath:collectionPath mount:NO skip:[[skipped objectForKey:collectionPath] boolValue] hidden:[[hidden objectForKey:collectionPath] boolValue] manual:[custom containsObject:collectionPath] inContext:context]; } //Set the mount values for all [SapphireCollectionDirectory availableCollectionDirectoriesInContext:context]; [display setCurrentFile:BRLocalizedString(@"Upgrading Movie Translations", @"")]; NSDictionary *movieTranslations = [NSDictionary dictionaryWithContentsOfFile:[configDir stringByAppendingPathComponent:@"movieData.plist"]]; NSDictionary *translations = [movieTranslations objectForKey:MOVIE_TRAN_TRANSLATIONS_KEY]; NSEnumerator *movieEnum = [translations keyEnumerator]; NSString *movie = nil; while((movie = [movieEnum nextObject]) != nil) { NSDictionary *movieDict = [translations objectForKey:movie]; SapphireMovieTranslation *trans = [SapphireMovieTranslation createMovieTranslationWithName:movie inContext:context]; trans.IMPLink = [movieDict objectForKey:MOVIE_TRAN_IMP_LINK_KEY]; NSString *IMDBLink = [movieDict objectForKey:MOVIE_TRAN_IMDB_LINK_KEY]; trans.IMDBLink = IMDBLink; int imdbNumber = [SapphireMovie imdbNumberFromString:IMDBLink]; if(imdbNumber != 0) { SapphireMovie *thisMovie = [SapphireMovie movieWithIMDB:imdbNumber inContext:context]; trans.movie = thisMovie; } NSArray *posters = [movieDict objectForKey:MOVIE_TRAN_IMP_POSTERS_KEY]; NSSet *dupCheck = [NSSet setWithArray:posters]; posters = [dupCheck allObjects]; NSString *selectedPoster = [movieDict objectForKey:MOVIE_TRAN_SELECTED_POSTER_KEY]; int i, count = [posters count]; for(i=0; i