From 6222e25adcc8982981d0720aece2c5134db798a6 Mon Sep 17 00:00:00 2001 From: Christopher Snowhill Date: Fri, 8 Jul 2022 06:26:28 -0700 Subject: [PATCH] Experimental tag support redesign This redesign completely changes how tags are stored in memory. Now all arbitrary tag names are supported, where possible. Some extra work will be needed to support arbitrary tags with TagLib, such as replacing it with a different library. Translation pending for a couple of strings. Signed-off-by: Christopher Snowhill --- Base.lproj/InfoInspector.xib | 108 +++-- .../DataModel.xcdatamodel/contents | 14 +- Playlist/PlaylistEntry.h | 19 + Playlist/PlaylistEntry.m | 397 +++++++++++++++++- Playlist/PlaylistLoader.m | 15 +- Plugins/CueSheet/CueSheetMetadataReader.m | 8 +- Plugins/FFMPEG/FFMPEGDecoder.h | 16 +- Plugins/FFMPEG/FFMPEGDecoder.m | 169 +++----- Plugins/Flac/Flac.xcodeproj/project.pbxproj | 6 + Plugins/Flac/FlacDecoder.h | 14 +- Plugins/Flac/FlacDecoder.m | 128 ++---- .../HighlyComplete/HighlyComplete/HCDecoder.h | 6 - .../HighlyComplete/HCDecoder.mm | 63 +-- Plugins/MIDI/MIDI/MIDIMetadataReader.mm | 19 +- Plugins/OpenMPT/OpenMPT/OMPTMetadataReader.mm | 47 +-- Plugins/Opus/Opus/OpusDecoder.h | 20 +- Plugins/Opus/Opus/OpusDecoder.m | 172 +++----- .../Opus/OpusPlugin.xcodeproj/project.pbxproj | 16 +- Plugins/TagLib/TagLibID3v2Reader.mm | 11 +- Plugins/TagLib/TagLibMetadataReader.m | 11 +- Plugins/Vorbis/VorbisDecoder.h | 19 +- Plugins/Vorbis/VorbisDecoder.m | 162 +++---- .../VorbisPlugin.xcodeproj/project.pbxproj | 16 +- Plugins/vgmstream/vgmstream/VGMDecoder.m | 8 +- Utils/RedundantPlaylistDataStore.m | 14 + en.lproj/InfoInspector.strings | 9 +- es.lproj/InfoInspector.strings | 8 +- 27 files changed, 868 insertions(+), 627 deletions(-) diff --git a/Base.lproj/InfoInspector.xib b/Base.lproj/InfoInspector.xib index a1527beae..eeeb2727c 100644 --- a/Base.lproj/InfoInspector.xib +++ b/Base.lproj/InfoInspector.xib @@ -1,8 +1,8 @@ - + - + @@ -15,16 +15,16 @@ - + - + - + - + @@ -33,7 +33,7 @@ - + @@ -42,7 +42,7 @@ - + @@ -51,7 +51,7 @@ - + @@ -60,16 +60,16 @@ - + - + - + @@ -78,7 +78,7 @@ - + @@ -87,7 +87,7 @@ - + @@ -96,7 +96,7 @@ - + @@ -105,7 +105,7 @@ - + @@ -114,7 +114,7 @@ - + @@ -123,7 +123,7 @@ - + @@ -135,7 +135,7 @@ - + @@ -147,7 +147,7 @@ - + @@ -159,7 +159,7 @@ - + @@ -171,7 +171,7 @@ - + @@ -183,7 +183,7 @@ - + @@ -191,11 +191,11 @@ - + - + @@ -207,7 +207,7 @@ - + @@ -223,7 +223,7 @@ - + @@ -235,7 +235,7 @@ - + @@ -247,7 +247,7 @@ - + @@ -259,7 +259,7 @@ - + @@ -268,7 +268,7 @@ - + @@ -280,7 +280,7 @@ - + @@ -289,7 +289,7 @@ - + @@ -301,7 +301,7 @@ - + @@ -310,7 +310,7 @@ - + @@ -322,7 +322,7 @@ - + @@ -331,7 +331,7 @@ - + @@ -344,7 +344,7 @@ - + @@ -353,7 +353,7 @@ - + @@ -365,7 +365,7 @@ - + @@ -374,7 +374,7 @@ - + @@ -386,7 +386,7 @@ - + @@ -394,8 +394,17 @@ + + + + + + + + + - + @@ -407,6 +416,19 @@ + + + + + + + + + + + + + diff --git a/DataModel.xcdatamodeld/DataModel.xcdatamodel/contents b/DataModel.xcdatamodeld/DataModel.xcdatamodel/contents index 96a65fb5c..0664176a8 100644 --- a/DataModel.xcdatamodeld/DataModel.xcdatamodel/contents +++ b/DataModel.xcdatamodeld/DataModel.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -15,10 +15,7 @@ - - - @@ -30,19 +27,18 @@ - - + + - @@ -55,12 +51,10 @@ - - @@ -70,7 +64,7 @@ - + \ No newline at end of file diff --git a/Playlist/PlaylistEntry.h b/Playlist/PlaylistEntry.h index 02e210fc7..110370abc 100644 --- a/Playlist/PlaylistEntry.h +++ b/Playlist/PlaylistEntry.h @@ -74,6 +74,25 @@ @property(nonatomic, readonly) float rating; +@property(nonatomic) NSString *_Nullable album; +@property(nonatomic) NSString *_Nullable albumartist; +@property(nonatomic) NSString *_Nullable artist; +@property(nonatomic) NSString *_Nullable rawTitle; +@property(nonatomic) NSString *_Nullable genre; +@property(nonatomic) int32_t disc; +@property(nonatomic) int32_t track; +@property(nonatomic) int32_t year; + +@property(nonatomic) NSString *_Nullable date; + +@property(nonatomic) NSString *_Nullable comment; + +@property(nonatomic) NSDictionary *_Nullable metadataBlob; + +- (NSString *_Nullable)readAllValuesAsString:(NSString *_Nonnull)tagName; +- (void)setValue:(NSString *_Nonnull)tagName fromString:(NSString *_Nullable)value; +- (void)addValue:(NSString *_Nonnull)tagName fromString:(NSString *_Nonnull)value; + - (void)setMetadata:(NSDictionary *_Nonnull)metadata; @end diff --git a/Playlist/PlaylistEntry.m b/Playlist/PlaylistEntry.m index fa0ad0b31..ae8f38b65 100644 --- a/Playlist/PlaylistEntry.m +++ b/Playlist/PlaylistEntry.m @@ -16,11 +16,28 @@ #import "SHA256Digest.h" #import "SecondsFormatter.h" +#import + extern NSPersistentContainer *kPersistentContainer; extern NSMutableDictionary *kArtworkDictionary; +static NSMutableDictionary *kMetadataBlobCache = nil; +static NSMutableDictionary *kMetadataCache = nil; + +static void *kCompressionScratchBuffer = NULL; +static void *kDecompressionScratchBuffer = NULL; + @implementation PlaylistEntry (Extension) ++ (void)initialize { + if(!kMetadataBlobCache) { + kMetadataBlobCache = [[NSMutableDictionary alloc] init]; + } + if(!kMetadataCache) { + kMetadataCache = [[NSMutableDictionary alloc] init]; + } +} + // The following read-only keys depend on the values of other properties + (NSSet *)keyPathsForValuesAffectingUrl { @@ -503,8 +520,72 @@ NSURL *_Nullable urlForPath(NSString *_Nullable path) { self.error = YES; self.errorMessage = NSLocalizedStringFromTableInBundle(@"ErrorMetadata", nil, [NSBundle bundleForClass:[self class]], @""); } else { + NSMutableDictionary *metaDict = [[NSMutableDictionary alloc] init]; self.volume = 1; - [self setValuesForKeysWithDictionary:metadata]; + for(NSString *key in metadata) { + NSString *lowerKey = [key lowercaseString]; + id valueObj = [metadata objectForKey:key]; + NSArray *values = nil; + NSString *firstValue = nil; + NSData *dataValue = nil; + if([valueObj isKindOfClass:[NSArray class]]) { + values = (NSArray *)valueObj; + if([values count]) { + firstValue = values[0]; + } + } else if([valueObj isKindOfClass:[NSString class]]) { + firstValue = (NSString *)valueObj; + values = @[firstValue]; + } else if([valueObj isKindOfClass:[NSNumber class]]) { + NSNumber *numberValue = (NSNumber *)valueObj; + firstValue = [numberValue stringValue]; + values = @[firstValue]; + } else if([valueObj isKindOfClass:[NSData class]]) { + dataValue = (NSData *)valueObj; + } + if([lowerKey isEqualToString:@"bitrate"]) { + self.bitrate = [firstValue intValue]; + } else if([lowerKey isEqualToString:@"bitspersample"]) { + self.bitsPerSample = [firstValue intValue]; + } else if([lowerKey isEqualToString:@"channelconfig"]) { + self.channelConfig = [firstValue intValue]; + } else if([lowerKey isEqualToString:@"channels"]) { + self.channels = [firstValue intValue]; + } else if([lowerKey isEqualToString:@"codec"]) { + self.codec = firstValue; + } else if([lowerKey isEqualToString:@"cuesheet"]) { + self.cuesheet = firstValue; + } else if([lowerKey isEqualToString:@"encoding"]) { + self.encoding = firstValue; + } else if([lowerKey isEqualToString:@"endian"]) { + self.endian = firstValue; + } else if([lowerKey isEqualToString:@"floatingpoint"]) { + self.floatingPoint = [firstValue boolValue]; + } else if([lowerKey isEqualToString:@"samplerate"]) { + self.sampleRate = [firstValue floatValue]; + } else if([lowerKey isEqualToString:@"seekable"]) { + self.seekable = [firstValue boolValue]; + } else if([lowerKey isEqualToString:@"totalframes"]) { + self.totalFrames = [firstValue integerValue]; + } else if([lowerKey isEqualToString:@"unsigned"]) { + self.unSigned = [firstValue boolValue]; + } else if([lowerKey isEqualToString:@"replaygain_album_gain"]) { + self.replayGainAlbumGain = [firstValue floatValue]; + } else if([lowerKey isEqualToString:@"replaygain_album_peak"]) { + self.replayGainAlbumPeak = [firstValue floatValue]; + } else if([lowerKey isEqualToString:@"replaygain_track_gain"]) { + self.replayGainTrackGain = [firstValue floatValue]; + } else if([lowerKey isEqualToString:@"replaygain_track_peak"]) { + self.replayGainTrackPeak = [firstValue floatValue]; + } else if([lowerKey isEqualToString:@"volume"]) { + self.volume = [firstValue floatValue]; + } else if([lowerKey isEqualToString:@"albumart"]) { + self.albumArt = dataValue; + } else { + [metaDict setObject:values forKey:lowerKey]; + } + } + self.metadataBlob = metaDict; } [self setMetadataLoaded:YES]; @@ -574,4 +655,318 @@ NSURL *_Nullable urlForPath(NSString *_Nullable path) { } } +@dynamic album; +- (NSString *)album { + return [self readAllValuesAsString:@"album"]; +} + +- (void)setAlbum:(NSString *)album { + [self setValue:@"album" fromString:album]; +} + +@dynamic albumartist; +- (NSString *)albumartist { + NSString *value = [self readAllValuesAsString:@"albumartist"]; + if(!value) { + value = [self readAllValuesAsString:@"album artist"]; + } + if(!value) { + value = [self readAllValuesAsString:@"album_artist"]; + } + return value; +} + +- (void)setAlbumartist:(NSString *)albumartist { + [self setValue:@"albumartist" fromString:albumartist]; + [self setValue:@"album artist" fromString:nil]; + [self setValue:@"album_artist" fromString:nil]; +} + +@dynamic artist; +- (NSString *)artist { + return [self readAllValuesAsString:@"artist"]; +} + +- (void)setArtist:(NSString *)artist { + [self setValue:@"artist" fromString:artist]; +} + +@dynamic rawTitle; +- (NSString *)rawTitle { + return [self readAllValuesAsString:@"title"]; +} + +- (void)setRawTitle:(NSString *)rawTitle { + [self setValue:@"title" fromString:rawTitle]; +} + +@dynamic genre; +- (NSString *)genre { + return [self readAllValuesAsString:@"genre"]; +} + +- (void)setGenre:(NSString *)genre { + [self setValue:@"genre" fromString:genre]; +} + +@dynamic disc; +- (int32_t)disc { + NSString *value = [self readAllValuesAsString:@"discnumber"]; + if(!value) { + value = [self readAllValuesAsString:@"discnum"]; + } + if(!value) { + value = [self readAllValuesAsString:@"disc"]; + } + if(value) { + return [value intValue]; + } else { + return 0; + } +} + +- (void)setDisc:(int32_t)disc { + [self setValue:@"discnumber" fromString:[NSString stringWithFormat:@"%u", disc]]; + [self setValue:@"discnum" fromString:nil]; + [self setValue:@"disc" fromString:nil]; +} + +@dynamic track; +- (int32_t)track { + NSString *value = [self readAllValuesAsString:@"tracknumber"]; + if(!value) { + value = [self readAllValuesAsString:@"tracknum"]; + } + if(!value) { + value = [self readAllValuesAsString:@"track"]; + } + if(value) { + return [value intValue]; + } else { + return 0; + } +} + +@dynamic year; +- (int32_t)year { + NSString *value = [self readAllValuesAsString:@"date"]; + if(!value) { + value = [self readAllValuesAsString:@"recording_date"]; + } + if(!value) { + value = [self readAllValuesAsString:@"year"]; + } + if(value) { + return [value intValue]; + } else { + return 0; + } +} + +- (void)setYear:(int32_t)year { + NSString *svalue = [NSString stringWithFormat:@"%u", year]; + [self setValue:@"year" fromString:svalue]; + [self setValue:@"date" fromString:nil]; + [self setValue:@"recording_date" fromString:nil]; +} + +@dynamic date; +- (NSString *)date { + NSString *value = [self readAllValuesAsString:@"date"]; + if(!value) { + value = [self readAllValuesAsString:@"recording_date"]; + } + if(!value) { + value = [self readAllValuesAsString:@"year"]; + } + return value; +} + +- (void)setDate:(NSString *)date { + [self setValue:@"date" fromString:date]; + [self setValue:@"recording_date" fromString:nil]; + [self setValue:@"year" fromString:nil]; +} + +@dynamic comment; +- (NSString *)comment { + return [self readAllValuesAsString:@"comment"]; +} + +- (void)setComment:(NSString *)comment { + [self setValue:@"comment" fromString:comment]; +} + +- (NSString *_Nullable)readAllValuesAsString:(NSString *_Nonnull)tagName { + NSMutableDictionary *dict = [kMetadataCache objectForKey:self.urlString]; + if(dict) { + NSString *value = [dict objectForKey:tagName]; + if(value) { + return value; + } + } + + id metaObj = self.metadataBlob; + + if(metaObj && [metaObj isKindOfClass:[NSDictionary class]]) { + NSDictionary *metaDict = (NSDictionary *)metaObj; + + NSArray *values = [metaDict objectForKey:tagName]; + + if(values) { + NSString *value = [values componentsJoinedByString:@", "]; + if(!dict) { + dict = [[NSMutableDictionary alloc] init]; + [kMetadataCache setObject:dict forKey:self.urlString]; + } + [dict setObject:value forKey:tagName]; + return value; + } + } + + return nil; +} + +- (void)deleteAllValues { + self.metadataBlob = nil; + [kMetadataCache removeObjectForKey:self.urlString]; +} + +- (void)deleteValue:(NSString *_Nonnull)tagName { + id metaObj = self.metadataBlob; + + if(metaObj && [metaObj isKindOfClass:[NSDictionary class]]) { + NSDictionary *metaDict = (NSDictionary *)metaObj; + NSMutableDictionary *metaDictCopy = [metaDict mutableCopy]; + + [metaDictCopy removeObjectForKey:tagName]; + + self.metadataBlob = [NSDictionary dictionaryWithDictionary:metaDictCopy]; + } +} + +- (void)setValue:(NSString *_Nonnull)tagName fromString:(NSString *_Nullable)value { + if(!value) { + [self deleteValue:tagName]; + return; + } + + NSArray *values = [value componentsSeparatedByString:@", "]; + + id metaObj = self.metadataBlob; + + if(metaObj && [metaObj isKindOfClass:[NSDictionary class]]) { + NSDictionary *metaDict = (NSDictionary *)metaObj; + NSMutableDictionary *metaDictCopy = [metaDict mutableCopy]; + + [metaDictCopy setObject:values forKey:tagName]; + + self.metadataBlob = [NSDictionary dictionaryWithDictionary:metaDictCopy]; + } +} + +- (void)addValue:(NSString *_Nonnull)tagName fromString:(NSString *_Nonnull)value { + NSMutableDictionary *dict = [kMetadataCache objectForKey:self.urlString]; + if(dict) { + [dict removeObjectForKey:tagName]; + } + + id metaObj = self.metadataBlob; + + if(metaObj && [metaObj isKindOfClass:[NSDictionary class]]) { + NSDictionary *metaDict = (NSDictionary *)metaObj; + NSMutableDictionary *metaDictCopy = [metaDict mutableCopy]; + + NSArray *values = [metaDictCopy objectForKey:tagName]; + NSMutableArray *valuesCopy; + if(values) { + valuesCopy = [values mutableCopy]; + } else { + valuesCopy = [[NSMutableArray alloc] init]; + } + [valuesCopy addObject:value]; + values = [NSArray arrayWithArray:valuesCopy]; + [metaDictCopy setObject:values forKey:tagName]; + + self.metadataBlob = [NSDictionary dictionaryWithDictionary:metaDictCopy]; + } +} + +- (NSDictionary *)metadataBlob { + { + NSDictionary *blob = [kMetadataBlobCache objectForKey:self.urlString]; + if(blob) { + return blob; + } + } + + if(self.metadataCompressed == nil || self.metadataDecompressedSize == 0) { + return @{}; + } + + if(!kDecompressionScratchBuffer) { + size_t scratchSize = compression_decode_scratch_buffer_size(COMPRESSION_ZLIB); + kDecompressionScratchBuffer = malloc(scratchSize); + } + + void *decompressionBuffer = malloc(self.metadataDecompressedSize); + + size_t decodedBytes = compression_decode_buffer(decompressionBuffer, self.metadataDecompressedSize, [self.metadataCompressed bytes], [self.metadataCompressed length], kDecompressionScratchBuffer, COMPRESSION_ZLIB); + + NSData *decodedData = [NSData dataWithBytes:decompressionBuffer length:decodedBytes]; + + free(decompressionBuffer); + + NSSet *allowed = [NSSet setWithArray:@[[NSDictionary class], [NSMutableDictionary class], [NSArray class], [NSMutableArray class], [NSString class]]]; + NSError *error = nil; + NSDictionary *dict; + dict = [NSKeyedUnarchiver unarchivedObjectOfClasses:allowed + fromData:decodedData + error:&error]; + + if(!dict) { + dict = @{}; + } + + [kMetadataBlobCache setObject:dict forKey:self.urlString]; + + return dict; +} + +- (void)setMetadataBlob:(NSMutableDictionary *)metadataBlob { + [kMetadataCache removeObjectForKey:self.urlString]; + + if(metadataBlob == nil) { + self.metadataCompressed = nil; + self.metadataDecompressedSize = 0; + [kMetadataBlobCache removeObjectForKey:self.urlString]; + return; + } + + [kMetadataBlobCache setObject:metadataBlob forKey:self.urlString]; + + NSError *error = nil; + NSData *data = [NSKeyedArchiver archivedDataWithRootObject:metadataBlob + requiringSecureCoding:YES + error:&error]; + + if(!kCompressionScratchBuffer) { + size_t scratchBufferSize = compression_encode_scratch_buffer_size(COMPRESSION_ZLIB); + kCompressionScratchBuffer = malloc(scratchBufferSize); + } + + size_t fullSize = [data length]; + size_t compressedSize = fullSize * 3 / 2 + 256; + void *compressedBuffer = malloc(compressedSize); + + size_t compressedOutSize = compression_encode_buffer(compressedBuffer, compressedSize, [data bytes], fullSize, kCompressionScratchBuffer, COMPRESSION_ZLIB); + + NSData *compressData = [NSData dataWithBytes:compressedBuffer length:compressedOutSize]; + + free(compressedBuffer); + + self.metadataCompressed = compressData; + self.metadataDecompressedSize = fullSize; +} + @end diff --git a/Playlist/PlaylistLoader.m b/Playlist/PlaylistLoader.m index 677fe35ce..48374f7d6 100644 --- a/Playlist/PlaylistLoader.m +++ b/Playlist/PlaylistLoader.m @@ -861,6 +861,8 @@ NSURL *_Nullable urlForPath(NSString *_Nullable path); } - (BOOL)addDataStore { + BOOL dataMigrated = [[NSUserDefaults standardUserDefaults] boolForKey:@"metadataMigrated"]; + NSPersistentContainer *pc = playlistController.persistentContainer; if(pc) { NSManagedObjectContext *moc = pc.viewContext; @@ -902,10 +904,17 @@ NSURL *_Nullable urlForPath(NSString *_Nullable path); ++index; } [resultsCopy removeObjectsAtIndexes:pruneSet]; - if([pruneSet count]) { + + if(!dataMigrated) { + for(PlaylistEntry *pe in resultsCopy) { + pe.metadataLoaded = NO; + } + } + + if([pruneSet count] || !dataMigrated) { [playlistController commitPersistentStore]; } - + results = [NSArray arrayWithArray:resultsCopy]; { @@ -917,6 +926,8 @@ NSURL *_Nullable urlForPath(NSString *_Nullable path); [playlistController readQueueFromDataStore]; [playlistController readShuffleListFromDataStore]; + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"metadataMigrated"]; + return YES; } return NO; diff --git a/Plugins/CueSheet/CueSheetMetadataReader.m b/Plugins/CueSheet/CueSheetMetadataReader.m index e38074f73..3f7f0bde6 100644 --- a/Plugins/CueSheet/CueSheetMetadataReader.m +++ b/Plugins/CueSheet/CueSheetMetadataReader.m @@ -85,10 +85,10 @@ if([[track track] intValue]) [cuesheetMetadata setValue:@([[track track] intValue]) forKey:@"track"]; if([track genre]) [cuesheetMetadata setValue:[track genre] forKey:@"genre"]; if([[track year] intValue]) [cuesheetMetadata setValue:@([[track year] intValue]) forKey:@"year"]; - if([track albumGain]) [cuesheetMetadata setValue:@([track albumGain]) forKey:@"replayGainAlbumGain"]; - if([track albumPeak]) [cuesheetMetadata setValue:@([track albumPeak]) forKey:@"replayGainAlbumPeak"]; - if([track trackGain]) [cuesheetMetadata setValue:@([track trackGain]) forKey:@"replayGainTrackGain"]; - if([track trackPeak]) [cuesheetMetadata setValue:@([track trackPeak]) forKey:@"replayGainTrackPeak"]; + if([track albumGain]) [cuesheetMetadata setValue:@([track albumGain]) forKey:@"replaygain_album_gain"]; + if([track albumPeak]) [cuesheetMetadata setValue:@([track albumPeak]) forKey:@"replaygain_album_peak"]; + if([track trackGain]) [cuesheetMetadata setValue:@([track trackGain]) forKey:@"replaygain_track_gain"]; + if([track trackPeak]) [cuesheetMetadata setValue:@([track trackPeak]) forKey:@"replaygain_track_peak"]; return [NSDictionary dictionaryWithDictionary:cuesheetMetadata]; } diff --git a/Plugins/FFMPEG/FFMPEGDecoder.h b/Plugins/FFMPEG/FFMPEGDecoder.h index e999a4d10..cd546d50e 100644 --- a/Plugins/FFMPEG/FFMPEGDecoder.h +++ b/Plugins/FFMPEG/FFMPEGDecoder.h @@ -60,23 +60,13 @@ int64_t ffmpeg_seek(void *opaque, int64_t offset, int whence); BOOL endOfAudio; int metadataIndex; - NSString *artist; - NSString *albumartist; - NSString *album; - NSString *title; - NSString *genre; - NSNumber *year; - NSNumber *track; - NSNumber *disc; - float replayGainAlbumGain; - float replayGainAlbumPeak; - float replayGainTrackGain; - float replayGainTrackPeak; - float volumeScale; int attachedPicIndex; + NSData *albumArt; + NSDictionary *metaDict; + NSDictionary *id3Metadata; } diff --git a/Plugins/FFMPEG/FFMPEGDecoder.m b/Plugins/FFMPEG/FFMPEGDecoder.m index af62db24a..463e1ba1e 100644 --- a/Plugins/FFMPEG/FFMPEGDecoder.m +++ b/Plugins/FFMPEG/FFMPEGDecoder.m @@ -465,21 +465,9 @@ static uint8_t reverse_bits[0x100]; seekedToStart = !seekable; - artist = @""; - albumartist = @""; - album = @""; - title = @""; - genre = @""; - year = @(0); - track = @(0); - disc = @(0); - replayGainAlbumGain = 0.0; - replayGainAlbumPeak = 0.0; - replayGainTrackGain = 0.0; - replayGainTrackPeak = 0.0; - volumeScale = 1.0; + id3Metadata = [[NSDictionary alloc] init]; + metaDict = [NSDictionary dictionary]; albumArt = [NSData data]; - id3Metadata = @{}; metadataUpdated = NO; [self updateMetadata]; @@ -534,21 +522,18 @@ static uint8_t reverse_bits[0x100]; [self close]; } +static void setDictionary(NSMutableDictionary *dict, NSString *tag, NSString *value) { + NSMutableArray *array = [dict valueForKey:tag]; + if(!array) { + array = [[NSMutableArray alloc] init]; + [dict setObject:array forKey:tag]; + } + [array addObject:value]; +} + - (void)updateMetadata { + NSMutableDictionary *_metaDict = [[NSMutableDictionary alloc] init]; const AVDictionaryEntry *tag = NULL; - NSString *_artist = artist; - NSString *_albumartist = albumartist; - NSString *_album = album; - NSString *_title = title; - NSString *_genre = genre; - NSNumber *_year = year; - NSNumber *_track = track; - NSNumber *_disc = disc; - float _replayGainAlbumGain = replayGainAlbumGain; - float _replayGainAlbumPeak = replayGainAlbumPeak; - float _replayGainTrackGain = replayGainTrackGain; - float _replayGainTrackPeak = replayGainTrackPeak; - float _volumeScale = volumeScale; for(size_t i = 0; i < 2; ++i) { AVDictionary *metadata; if(i == 0) { @@ -567,62 +552,45 @@ static uint8_t reverse_bits[0x100]; if(!strcasecmp(tag->key, "streamtitle")) { NSString *artistTitle = guess_encoding_of_string(tag->value); NSArray *splitValues = [artistTitle componentsSeparatedByString:@" - "]; - _artist = @""; - _title = [splitValues objectAtIndex:0]; + NSString *_artist = @""; + NSString *_title = [splitValues objectAtIndex:0]; if([splitValues count] > 1) { _artist = _title; _title = [splitValues objectAtIndex:1]; + setDictionary(_metaDict, @"artist", _artist); + setDictionary(_metaDict, @"title", _title); + } else { + setDictionary(_metaDict, @"title", _title); } } else if(!strcasecmp(tag->key, "icy-url")) { - _album = guess_encoding_of_string(tag->value); - } else if(!strcasecmp(tag->key, "icy-genre") || - !strcasecmp(tag->key, "genre")) { - _genre = guess_encoding_of_string(tag->value); - } else if(!strcasecmp(tag->key, "album")) { - _album = guess_encoding_of_string(tag->value); - } else if(!strcasecmp(tag->key, "album_artist")) { - _albumartist = guess_encoding_of_string(tag->value); - } else if(!strcasecmp(tag->key, "artist")) { - _artist = guess_encoding_of_string(tag->value); + setDictionary(_metaDict, @"album", guess_encoding_of_string(tag->value)); + } else if(!strcasecmp(tag->key, "icy-genre")) { + setDictionary(_metaDict, @"genre", guess_encoding_of_string(tag->value)); } else if(!strcasecmp(tag->key, "title")) { NSString *_tag = guess_encoding_of_string(tag->value); if(i == 0 && formatCtx->nb_chapters > 1) { - _album = _tag; + setDictionary(_metaDict, @"album", _tag); } else { - _title = _tag; + setDictionary(_metaDict, @"title", _tag); } - } else if(!strcasecmp(tag->key, "date") || - !strcasecmp(tag->key, "date_recorded")) { - NSString *dateString = guess_encoding_of_string(tag->value); - _year = @([dateString intValue]); - } else if(!strcasecmp(tag->key, "track")) { - NSString *trackString = guess_encoding_of_string(tag->value); - _track = @([trackString intValue]); - } else if(!strcasecmp(tag->key, "disc")) { - NSString *discString = guess_encoding_of_string(tag->value); - _disc = @([discString intValue]); - } else if(!strcasecmp(tag->key, "replaygain_album_gain")) { - NSString *rgValue = guess_encoding_of_string(tag->value); - _replayGainAlbumGain = [rgValue floatValue]; - } else if(!strcasecmp(tag->key, "replaygain_album_peak")) { - NSString *rgValue = guess_encoding_of_string(tag->value); - _replayGainAlbumPeak = [rgValue floatValue]; - } else if(!strcasecmp(tag->key, "replaygain_track_gain")) { - NSString *rgValue = guess_encoding_of_string(tag->value); - _replayGainTrackGain = [rgValue floatValue]; - } else if(!strcasecmp(tag->key, "replaygain_track_peak")) { - NSString *rgValue = guess_encoding_of_string(tag->value); - _replayGainTrackPeak = [rgValue floatValue]; + } else if(!strcasecmp(tag->key, "date_recorded")) { + setDictionary(_metaDict, @"date", guess_encoding_of_string(tag->value)); } else if(!strcasecmp(tag->key, "replaygain_gain")) { // global or chapter gain - NSString *rgValue = guess_encoding_of_string(tag->value); - if(i == 0) _replayGainAlbumGain = [rgValue floatValue]; - else _replayGainTrackGain = [rgValue floatValue]; + NSString *tagName; + if(i == 0) + tagName = @"replaygain_album_gain"; + else + tagName = @"replaygain_track_gain"; + setDictionary(_metaDict, tagName, guess_encoding_of_string(tag->value)); } else if(!strcasecmp(tag->key, "replaygain_peak")) { // global or chapter peak - NSString *rgValue = guess_encoding_of_string(tag->value); - if(i == 0) _replayGainAlbumPeak = [rgValue floatValue]; - else _replayGainTrackPeak = [rgValue floatValue]; + NSString *tagName; + if(i == 0) + tagName = @"replaygain_album_peak"; + else + tagName = @"replaygain_track_peak"; + setDictionary(_metaDict, tagName, guess_encoding_of_string(tag->value)); } else if(!strcasecmp(tag->key, "iTunNORM")) { NSString *tagString = guess_encoding_of_string(tag->value); NSArray *tag = [tagString componentsSeparatedByString:@" "]; @@ -642,8 +610,11 @@ static uint8_t reverse_bits[0x100]; float volume1 = -log10((double)(hexvalue1) / 1000) * 10; float volume2 = -log10((double)(hexvalue2) / 1000) * 10; float volumeToUse = MIN(volume1, volume2); - _volumeScale = pow(10, volumeToUse / 20); + NSNumber *_volumeScale = @(pow(10, volumeToUse / 20)); + setDictionary(_metaDict, @"volume", [_volumeScale stringValue]); } + } else { + setDictionary(_metaDict, guess_encoding_of_string(tag->key), guess_encoding_of_string(tag->value)); } } } @@ -653,39 +624,28 @@ static uint8_t reverse_bits[0x100]; HTTPSource *httpSource = (HTTPSource *)source; if([httpSource hasMetadata]) { NSDictionary *metadata = [httpSource metadata]; - _genre = [metadata valueForKey:@"genre"]; - _album = [metadata valueForKey:@"album"]; - _artist = [metadata valueForKey:@"artist"]; - _title = [metadata valueForKey:@"title"]; + NSString *_genre = [metadata valueForKey:@"genre"]; + NSString *_album = [metadata valueForKey:@"album"]; + NSString *_artist = [metadata valueForKey:@"artist"]; + NSString *_title = [metadata valueForKey:@"title"]; + + if(_genre && [_genre length]) { + [_metaDict setObject:@[_genre] forKey:@"genre"]; + } + if(_album && [_album length]) { + [_metaDict setObject:@[_album] forKey:@"album"]; + } + if(_artist && [_artist length]) { + [_metaDict setObject:@[_artist] forKey:@"artist"]; + } + if(_title && [_title length]) { + [_metaDict setObject:@[_title] forKey:@"title"]; + } } } - if(![_artist isEqual:artist] || - ![_albumartist isEqual:albumartist] || - ![_album isEqual:album] || - ![_title isEqual:title] || - ![_genre isEqual:genre] || - ![_year isEqual:year] || - ![_track isEqual:track] || - ![_disc isEqual:disc] || - _replayGainAlbumGain != replayGainAlbumGain || - _replayGainAlbumPeak != replayGainAlbumPeak || - _replayGainTrackGain != replayGainTrackGain || - _replayGainTrackPeak != replayGainTrackPeak || - _volumeScale != volumeScale) { - artist = _artist; - albumartist = _albumartist; - album = _album; - title = _title; - genre = _genre; - year = _year; - track = _track; - disc = _disc; - replayGainAlbumGain = _replayGainAlbumGain; - replayGainAlbumPeak = _replayGainAlbumPeak; - replayGainTrackGain = _replayGainTrackGain; - replayGainTrackPeak = _replayGainTrackPeak; - volumeScale = _volumeScale; + if(![_metaDict isEqualToDictionary:metaDict]) { + metaDict = _metaDict; if(![source seekable]) { [self willChangeValueForKey:@"metadata"]; [self didChangeValueForKey:@"metadata"]; @@ -700,7 +660,7 @@ static uint8_t reverse_bits[0x100]; Class tagReader = NSClassFromString(@"TagLibID3v2Reader"); if(tagReader && [tagReader respondsToSelector:@selector(metadataForTag:)]) { NSDictionary *_id3Metadata = [tagReader metadataForTag:tag]; - if(![_id3Metadata isEqualTo:id3Metadata]) { + if(![_id3Metadata isEqualToDictionary:id3Metadata]) { id3Metadata = _id3Metadata; [self willChangeValueForKey:@"metadata"]; [self didChangeValueForKey:@"metadata"]; @@ -710,7 +670,7 @@ static uint8_t reverse_bits[0x100]; - (void)updateArtwork { NSData *_albumArt = [NSData dataWithBytes:lastReadPacket->data length:lastReadPacket->size]; - if(![_albumArt isEqual:albumArt]) { + if(![_albumArt isEqualToData:albumArt]) { albumArt = _albumArt; if(![source seekable]) { [self willChangeValueForKey:@"metadata"]; @@ -1010,7 +970,10 @@ static uint8_t reverse_bits[0x100]; } - (NSDictionary *)metadata { - return [NSDictionary dictionaryByMerging:@{ @"artist": artist, @"albumartist": albumartist, @"album": album, @"title": title, @"genre": genre, @"year": year, @"track": track, @"disc": disc, @"replayGainAlbumGain": @(replayGainAlbumGain), @"replayGainAlbumPeak": @(replayGainAlbumPeak), @"replayGainTrackGain": @(replayGainTrackGain), @"replayGainTrackPeak": @(replayGainTrackPeak), @"volume": @(volumeScale), @"albumArt": albumArt } with:id3Metadata]; + NSDictionary *dict1 = @{ @"albumArt": albumArt }; + NSDictionary *dict2 = [dict1 dictionaryByMergingWith:metaDict]; + NSDictionary *dict3 = [dict2 dictionaryByMergingWith:id3Metadata]; + return dict3; } + (NSArray *)fileTypes { diff --git a/Plugins/Flac/Flac.xcodeproj/project.pbxproj b/Plugins/Flac/Flac.xcodeproj/project.pbxproj index ff5b0866f..6bf15ba6f 100644 --- a/Plugins/Flac/Flac.xcodeproj/project.pbxproj +++ b/Plugins/Flac/Flac.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 17C93F080B8FF67A008627D6 /* FlacDecoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 17C93F040B8FF67A008627D6 /* FlacDecoder.m */; }; + 8301C147287805F500651A6E /* NSDictionary+Merge.m in Sources */ = {isa = PBXBuildFile; fileRef = 8301C146287805F500651A6E /* NSDictionary+Merge.m */; }; 836EF0DA27BB970B00BF35B2 /* libFLAC.8.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 836EF0D927BB970B00BF35B2 /* libFLAC.8.dylib */; }; 83AA660B27B7DAE40098D4B8 /* cuesheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 83AA660A27B7DAE40098D4B8 /* cuesheet.m */; }; 8D5B49B4048680CD000E48DA /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7ADFEA557BF11CA2CBB /* Cocoa.framework */; }; @@ -33,6 +34,8 @@ 17C93F030B8FF67A008627D6 /* FlacDecoder.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = FlacDecoder.h; sourceTree = ""; }; 17C93F040B8FF67A008627D6 /* FlacDecoder.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; path = FlacDecoder.m; sourceTree = ""; }; 32DBCF630370AF2F00C91783 /* Flac_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Flac_Prefix.pch; sourceTree = ""; }; + 8301C145287805F500651A6E /* NSDictionary+Merge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSDictionary+Merge.h"; path = "../../Utils/NSDictionary+Merge.h"; sourceTree = ""; }; + 8301C146287805F500651A6E /* NSDictionary+Merge.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSDictionary+Merge.m"; path = "../../Utils/NSDictionary+Merge.m"; sourceTree = ""; }; 8356BD1927B3CCBB0074E50C /* HTTPSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HTTPSource.h; path = ../HTTPSource/HTTPSource.h; sourceTree = ""; }; 836EF0D927BB970B00BF35B2 /* libFLAC.8.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libFLAC.8.dylib; path = ../../ThirdParty/flac/lib/libFLAC.8.dylib; sourceTree = ""; }; 83747C4A2862DCF40021245F /* Shared.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Shared.xcconfig; sourceTree = ""; }; @@ -89,6 +92,8 @@ 08FB77AFFE84173DC02AAC07 /* Classes */ = { isa = PBXGroup; children = ( + 8301C145287805F500651A6E /* NSDictionary+Merge.h */, + 8301C146287805F500651A6E /* NSDictionary+Merge.m */, 83AA660A27B7DAE40098D4B8 /* cuesheet.m */, 8356BD1927B3CCBB0074E50C /* HTTPSource.h */, 8384912D180816C900E7332D /* Logging.h */, @@ -211,6 +216,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 8301C147287805F500651A6E /* NSDictionary+Merge.m in Sources */, 17C93F080B8FF67A008627D6 /* FlacDecoder.m in Sources */, 83AA660B27B7DAE40098D4B8 /* cuesheet.m in Sources */, ); diff --git a/Plugins/Flac/FlacDecoder.h b/Plugins/Flac/FlacDecoder.h index 09038fa17..417ce98ca 100644 --- a/Plugins/Flac/FlacDecoder.h +++ b/Plugins/Flac/FlacDecoder.h @@ -36,18 +36,8 @@ BOOL streamOpened; BOOL abortFlag; - NSString *artist; - NSString *albumartist; - NSString *album; - NSString *title; - NSString *genre; - NSNumber *year; - NSNumber *track; - NSNumber *disc; - float replayGainAlbumGain; - float replayGainAlbumPeak; - float replayGainTrackGain; - float replayGainTrackPeak; + NSDictionary *metaDict; + NSDictionary *icyMetaDict; NSData *albumArt; diff --git a/Plugins/Flac/FlacDecoder.m b/Plugins/Flac/FlacDecoder.m index 221534823..876682d9e 100644 --- a/Plugins/Flac/FlacDecoder.m +++ b/Plugins/Flac/FlacDecoder.m @@ -12,6 +12,8 @@ #import "HTTPSource.h" +#import "NSDictionary+Merge.h" + extern void grabbag__cuesheet_emit(NSString **out, const FLAC__StreamMetadata *cuesheet, const char *file_reference); @implementation FlacDecoder @@ -178,6 +180,15 @@ FLAC__StreamDecoderWriteStatus WriteCallback(const FLAC__StreamDecoder *decoder, return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE; } +static void setDictionary(NSMutableDictionary *dict, NSString *tag, NSString *value) { + NSMutableArray *array = [dict valueForKey:tag]; + if(!array) { + array = [[NSMutableArray alloc] init]; + [dict setObject:array forKey:tag]; + } + [array addObject:value]; +} + // This callback is only called for STREAMINFO blocks void MetadataCallback(const FLAC__StreamDecoder *decoder, const FLAC__StreamMetadata *metadata, void *client_data) { // Some flacs observed in the wild have multiple STREAMINFO metadata blocks, @@ -223,18 +234,7 @@ void MetadataCallback(const FLAC__StreamDecoder *decoder, const FLAC__StreamMeta } if(metadata->type == FLAC__METADATA_TYPE_VORBIS_COMMENT) { - NSString *_artist = flacDecoder->artist; - NSString *_albumartist = flacDecoder->albumartist; - NSString *_album = flacDecoder->album; - NSString *_title = flacDecoder->title; - NSString *_genre = flacDecoder->genre; - NSNumber *_year = flacDecoder->year; - NSNumber *_track = flacDecoder->track; - NSNumber *_disc = flacDecoder->disc; - float _replayGainAlbumGain = flacDecoder->replayGainAlbumGain; - float _replayGainAlbumPeak = flacDecoder->replayGainAlbumPeak; - float _replayGainTrackGain = flacDecoder->replayGainTrackGain; - float _replayGainTrackPeak = flacDecoder->replayGainTrackPeak; + NSMutableDictionary *_metaDict = [[NSMutableDictionary alloc] init]; NSString *_cuesheet = flacDecoder->cuesheet; const FLAC__StreamMetadata_VorbisComment *vorbis_comment = &metadata->data.vorbis_comment; for(int i = 0; i < vorbis_comment->num_comments; ++i) { @@ -246,74 +246,23 @@ void MetadataCallback(const FLAC__StreamDecoder *decoder, const FLAC__StreamMeta free(_name); free(_value); name = [name lowercaseString]; - if([name isEqualToString:@"artist"]) { - _artist = value; - } else if([name isEqualToString:@"albumartist"]) { - _albumartist = value; - } else if([name isEqualToString:@"album"]) { - _album = value; - } else if([name isEqualToString:@"title"]) { - _title = value; - } else if([name isEqualToString:@"genre"]) { - _genre = value; - } else if([name isEqualToString:@"cuesheet"]) { + if([name isEqualToString:@"cuesheet"]) { _cuesheet = value; flacDecoder->cuesheetFound = YES; - } else if([name isEqualToString:@"date"] || - [name isEqualToString:@"year"]) { - _year = @([value intValue]); - } else if([name isEqualToString:@"tracknumber"] || - [name isEqualToString:@"tracknum"] || - [name isEqualToString:@"track"]) { - _track = @([value intValue]); - } else if([name isEqualToString:@"discnumber"] || - [name isEqualToString:@"discnum"] || - [name isEqualToString:@"disc"]) { - _disc = @([value intValue]); - } else if([name isEqualToString:@"replaygain_album_gain"]) { - _replayGainAlbumGain = [value floatValue]; - } else if([name isEqualToString:@"replaygain_album_peak"]) { - _replayGainAlbumPeak = [value floatValue]; - } else if([name isEqualToString:@"replaygain_track_gain"]) { - _replayGainTrackGain = [value floatValue]; - } else if([name isEqualToString:@"replaygain_track_peak"]) { - _replayGainTrackPeak = [value floatValue]; } else if([name isEqualToString:@"waveformatextensible_channel_mask"]) { if([value hasPrefix:@"0x"]) { char *end; const char *_value = [value UTF8String] + 2; flacDecoder->channelConfig = (uint32_t)strtoul(_value, &end, 16); } + } else { + setDictionary(_metaDict, name, value); } } } - if(![_artist isEqual:flacDecoder->artist] || - ![_albumartist isEqual:flacDecoder->albumartist] || - ![_album isEqual:flacDecoder->album] || - ![_title isEqual:flacDecoder->title] || - ![_genre isEqual:flacDecoder->genre] || - ![_cuesheet isEqual:flacDecoder->cuesheet] || - ![_year isEqual:flacDecoder->year] || - ![_track isEqual:flacDecoder->track] || - ![_disc isEqual:flacDecoder->disc] || - _replayGainAlbumGain != flacDecoder->replayGainAlbumGain || - _replayGainAlbumPeak != flacDecoder->replayGainAlbumPeak || - _replayGainTrackGain != flacDecoder->replayGainTrackGain || - _replayGainTrackPeak != flacDecoder->replayGainTrackPeak) { - flacDecoder->artist = _artist; - flacDecoder->albumartist = _albumartist; - flacDecoder->album = _album; - flacDecoder->title = _title; - flacDecoder->genre = _genre; - flacDecoder->cuesheet = _cuesheet; - flacDecoder->year = _year; - flacDecoder->track = _track; - flacDecoder->disc = _disc; - flacDecoder->replayGainAlbumGain = _replayGainAlbumGain; - flacDecoder->replayGainAlbumPeak = _replayGainAlbumPeak; - flacDecoder->replayGainTrackGain = _replayGainTrackGain; - flacDecoder->replayGainTrackPeak = _replayGainTrackPeak; + if(![_metaDict isEqualToDictionary:flacDecoder->metaDict]) { + flacDecoder->metaDict = _metaDict; if(![flacDecoder->source seekable]) { [flacDecoder willChangeValueForKey:@"metadata"]; @@ -348,18 +297,8 @@ void ErrorCallback(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorS isOggFlac = YES; } - artist = @""; - albumartist = @""; - album = @""; - title = @""; - genre = @""; - year = @(0); - track = @(0); - disc = @(0); - replayGainAlbumGain = 0.0; - replayGainAlbumPeak = 0.0; - replayGainTrackGain = 0.0; - replayGainTrackPeak = 0.0; + metaDict = [NSDictionary dictionary]; + icyMetaDict = [NSDictionary dictionary]; albumArt = [NSData data]; cuesheetFound = NO; cuesheet = @""; @@ -461,20 +400,28 @@ void ErrorCallback(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorS Class sourceClass = [source class]; if([sourceClass isEqual:NSClassFromString(@"HTTPSource")]) { HTTPSource *httpSource = (HTTPSource *)source; + NSMutableDictionary *_icyMetaDict = [[NSMutableDictionary alloc] init]; if([httpSource hasMetadata]) { NSDictionary *metadata = [httpSource metadata]; NSString *_genre = [metadata valueForKey:@"genre"]; NSString *_album = [metadata valueForKey:@"album"]; NSString *_artist = [metadata valueForKey:@"artist"]; NSString *_title = [metadata valueForKey:@"title"]; - if(![_genre isEqualToString:genre] || - ![_album isEqualToString:album] || - ![_artist isEqualToString:artist] || - ![_title isEqualToString:title]) { - genre = _genre; - album = _album; - artist = _artist; - title = _title; + + if(_genre && [_genre length]) { + setDictionary(_icyMetaDict, @"genre", _genre); + } + if(_album && [_album length]) { + setDictionary(_icyMetaDict, @"album", _album); + } + if(_artist && [_artist length]) { + setDictionary(_icyMetaDict, @"artist", _artist); + } + if(_title && [_title length]) { + setDictionary(_icyMetaDict, @"title", _title); + } + if(![_icyMetaDict isEqualToDictionary:icyMetaDict]) { + icyMetaDict = _icyMetaDict; [self willChangeValueForKey:@"metadata"]; [self didChangeValueForKey:@"metadata"]; } @@ -557,7 +504,10 @@ void ErrorCallback(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorS } - (NSDictionary *)metadata { - return @{ @"artist": artist, @"albumartist": albumartist, @"album": album, @"title": title, @"genre": genre, @"year": year, @"track": track, @"disc": disc, @"replayGainAlbumGain": @(replayGainAlbumGain), @"replayGainAlbumPeak": @(replayGainAlbumPeak), @"replayGainTrackGain": @(replayGainTrackGain), @"replayGainTrackPeak": @(replayGainTrackPeak), @"cuesheet": cuesheet, @"albumArt": albumArt }; + NSDictionary *dict1 = @{ @"albumArt": albumArt, @"cuesheet": cuesheet }; + NSDictionary *dict2 = [dict1 dictionaryByMergingWith:metaDict]; + NSDictionary *dict3 = [dict2 dictionaryByMergingWith:icyMetaDict]; + return dict3; } + (NSArray *)fileTypes { diff --git a/Plugins/HighlyComplete/HighlyComplete/HCDecoder.h b/Plugins/HighlyComplete/HighlyComplete/HCDecoder.h index 1ae1497ca..ce01303ec 100644 --- a/Plugins/HighlyComplete/HighlyComplete/HCDecoder.h +++ b/Plugins/HighlyComplete/HighlyComplete/HCDecoder.h @@ -24,12 +24,6 @@ int tagLengthMs; int tagFadeMs; - float replayGainAlbumGain; - float replayGainAlbumPeak; - float replayGainTrackGain; - float replayGainTrackPeak; - float volume; - int type; int sampleRate; diff --git a/Plugins/HighlyComplete/HighlyComplete/HCDecoder.mm b/Plugins/HighlyComplete/HighlyComplete/HCDecoder.mm index a81555e1a..432a219a2 100644 --- a/Plugins/HighlyComplete/HighlyComplete/HCDecoder.mm +++ b/Plugins/HighlyComplete/HighlyComplete/HCDecoder.mm @@ -220,6 +220,20 @@ static int parse_time_crap(NSString *value) { return totalSeconds; } +static void setDictionary(NSMutableDictionary *dict, NSString *tag, NSString *value) { + NSMutableArray *array = [dict valueForKey:tag]; + if(!array) { + array = [[NSMutableArray alloc] init]; + [dict setObject:array forKey:tag]; + } + if([array count]) { + NSString *existing = array[0]; + array[0] = [existing stringByAppendingFormat:@"\r\n%@", value]; + } else { + [array addObject:value]; + } +} + static int psf_info_meta(void *context, const char *name, const char *value) { struct psf_info_meta_state *state = (struct psf_info_meta_state *)context; @@ -232,46 +246,18 @@ static int psf_info_meta(void *context, const char *name, const char *value) { if([taglc isEqualToString:@"game"]) { taglc = @"album"; - } else if([taglc isEqualToString:@"date"]) { - taglc = @"year"; - } else if([taglc isEqualToString:@"album artist"]) { - taglc = @"albumartist"; - } else if([taglc isEqualToString:@"tracknumber"]) { - taglc = @"track"; - } else if([taglc isEqualToString:@"discnumber"]) { - taglc = @"disc"; } - if([taglc hasPrefix:@"replaygain_"]) { - if([taglc hasPrefix:@"replaygain_album_"]) { - if([taglc hasSuffix:@"gain"]) - state->albumGain = [svalue floatValue]; - else if([taglc hasSuffix:@"peak"]) - state->albumPeak = [svalue floatValue]; - } else if([taglc hasPrefix:@"replaygain_track_"]) { - if([taglc hasSuffix:@"gain"]) - state->trackGain = [svalue floatValue]; - else if([taglc hasSuffix:@"peak"]) - state->trackPeak = [svalue floatValue]; - } - } else if([taglc isEqualToString:@"volume"]) { - state->volume = [svalue floatValue]; - } else if([taglc isEqualToString:@"length"]) { + if([taglc isEqualToString:@"length"]) { state->tag_length_ms = parse_time_crap(svalue); + setDictionary(state->info, @"psf_length", svalue); } else if([taglc isEqualToString:@"fade"]) { state->tag_fade_ms = parse_time_crap(svalue); + setDictionary(state->info, @"psf_fade", svalue); } else if([taglc isEqualToString:@"utf8"]) { state->utf8 = true; - } else if([taglc isEqualToString:@"title"] || - [taglc isEqualToString:@"albumartist"] || - [taglc isEqualToString:@"artist"] || - [taglc isEqualToString:@"album"] || - [taglc isEqualToString:@"genre"]) { - [state->info setObject:svalue forKey:taglc]; - } else if([taglc isEqualToString:@"year"] || - [taglc isEqualToString:@"track"] || - [taglc isEqualToString:@"disc"]) { - [state->info setObject:@([svalue intValue]) forKey:taglc]; + } else { + setDictionary(state->info, taglc, svalue); } return 0; @@ -1218,12 +1204,6 @@ static int usf_info(void *context, const char *name, const char *value) { tagFadeMs = (int)ceil(defaultFade * 1000.0); } - replayGainAlbumGain = info.albumGain; - replayGainAlbumPeak = info.albumPeak; - replayGainTrackGain = info.trackGain; - replayGainTrackPeak = info.trackPeak; - volume = info.volume; - metadataList = info.info; framesLength = [self retrieveFrameCount:tagLengthMs]; @@ -1573,11 +1553,6 @@ static int usf_info(void *context, const char *name, const char *value) { @"totalFrames": @(totalFrames), @"bitrate": @(0), @"seekable": @(YES), - @"replayGainAlbumGain": @(replayGainAlbumGain), - @"replayGainAlbumPeak": @(replayGainAlbumPeak), - @"replayGainTrackGain": @(replayGainTrackGain), - @"replayGainTrackPeak": @(replayGainTrackPeak), - @"volume": @(volume), @"codec": codec, @"endian": @"host", @"encoding": @"synthesized" }; diff --git a/Plugins/MIDI/MIDI/MIDIMetadataReader.mm b/Plugins/MIDI/MIDI/MIDIMetadataReader.mm index 6d3f317bf..6fde95f49 100644 --- a/Plugins/MIDI/MIDI/MIDIMetadataReader.mm +++ b/Plugins/MIDI/MIDI/MIDIMetadataReader.mm @@ -26,6 +26,15 @@ return 1.0f; } +static void setDictionary(NSMutableDictionary *dict, NSString *tag, NSString *value) { + NSMutableArray *array = [dict valueForKey:tag]; + if(!array) { + array = [[NSMutableArray alloc] init]; + [dict setObject:array forKey:tag]; + } + [array addObject:value]; +} + + (NSDictionary *)metadataForURL:(NSURL *)url { id audioSourceClass = NSClassFromString(@"AudioSource"); id source = [audioSourceClass audioSourceForURL:url]; @@ -62,9 +71,7 @@ midi_meta_data_item item; bool remap_display_name = !metadata.get_item("title", item); - NSArray *allowedKeys = @[@"title", @"artist", @"albumartist", @"album", @"genre"]; - - NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:10]; + NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; for(size_t i = 0; i < metadata.get_count(); ++i) { const midi_meta_data_item &item = metadata[i]; @@ -72,11 +79,7 @@ if(![name isEqualToString:@"type"]) { if(remap_display_name && [name isEqualToString:@"display_name"]) name = @"title"; - if([allowedKeys containsObject:name]) { - [dict setObject:guess_encoding_of_string(item.m_value.c_str()) forKey:name]; - } else if([name isEqualToString:@"year"]) { - [dict setObject:@([guess_encoding_of_string(item.m_value.c_str()) intValue]) forKey:name]; - } + setDictionary(dict, name, guess_encoding_of_string(item.m_value.c_str())); } } diff --git a/Plugins/OpenMPT/OpenMPT/OMPTMetadataReader.mm b/Plugins/OpenMPT/OpenMPT/OMPTMetadataReader.mm index 66205ec41..b5922805e 100644 --- a/Plugins/OpenMPT/OpenMPT/OMPTMetadataReader.mm +++ b/Plugins/OpenMPT/OpenMPT/OMPTMetadataReader.mm @@ -27,6 +27,15 @@ return 1.0f; } +static void setDictionary(NSMutableDictionary *dict, NSString *tag, NSString *value) { + NSMutableArray *array = [dict valueForKey:tag]; + if(!array) { + array = [[NSMutableArray alloc] init]; + [dict setObject:array forKey:tag]; + } + [array addObject:value]; +} + + (NSDictionary *)metadataForURL:(NSURL *)url { id audioSourceClass = NSClassFromString(@"AudioSource"); id source = [audioSourceClass audioSourceForURL:url]; @@ -55,41 +64,25 @@ std::map ctls; openmpt::module *mod = new openmpt::module(data, std::clog, ctls); - NSString *title = nil; - NSString *artist = nil; - // NSString * comment = nil; - NSString *date = nil; - NSString *type = nil; + NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; std::vector keys = mod->get_metadata_keys(); for(std::vector::iterator key = keys.begin(); key != keys.end(); ++key) { - if(*key == "title") - title = guess_encoding_of_string(mod->get_metadata(*key).c_str()); - else if(*key == "artist") - artist = guess_encoding_of_string(mod->get_metadata(*key).c_str()); - /*else if ( *key == "message" ) - comment = guess_encoding_of_string(mod->get_metadata( *key ).c_str());*/ - else if(*key == "date") - date = guess_encoding_of_string(mod->get_metadata(*key).c_str()); - else if(*key == "type_long") - type = guess_encoding_of_string(mod->get_metadata(*key).c_str()); + NSString *tag = guess_encoding_of_string((*key).c_str()); + NSString *value = guess_encoding_of_string(mod->get_metadata(*key).c_str()); + if(*key == "type") + continue; + else if(*key == "type_long") { + setDictionary(dict, @"codec", value); + } else { + setDictionary(dict, tag, value); + } } delete mod; - if(title == nil) - title = @""; - if(artist == nil) - artist = @""; - /*if (comment == nil) - comment = @"";*/ - if(date == nil) - date = @""; - if(type == nil) - type = @""; - - return @{ @"title": title, @"artist": artist, /*@"comment": comment,*/ @"year": @([date intValue]), @"codec": type }; + return dict; } catch(std::exception & /*e*/) { return 0; } diff --git a/Plugins/Opus/Opus/OpusDecoder.h b/Plugins/Opus/Opus/OpusDecoder.h index b983d84dc..a74b29657 100644 --- a/Plugins/Opus/Opus/OpusDecoder.h +++ b/Plugins/Opus/Opus/OpusDecoder.h @@ -23,23 +23,13 @@ int channels; long totalFrames; - NSString *artist; - NSString *albumartist; - NSString *album; - NSString *title; - NSString *genre; - NSNumber *year; - NSNumber *track; - NSNumber *disc; - float replayGainAlbumGain; - float replayGainTrackGain; - - NSString *icygenre; - NSString *icyalbum; - NSString *icyartist; - NSString *icytitle; + NSDictionary *metaDict; + NSDictionary *icyMetaDict; NSData *albumArt; + + float replayGainAlbumGain; + float replayGainTrackGain; } @end diff --git a/Plugins/Opus/Opus/OpusDecoder.m b/Plugins/Opus/Opus/OpusDecoder.m index fc09fbbd7..d625f9524 100644 --- a/Plugins/Opus/Opus/OpusDecoder.m +++ b/Plugins/Opus/Opus/OpusDecoder.m @@ -16,6 +16,8 @@ #import "NSDictionary+Merge.h" +#import + @implementation OpusFile static const int MAXCHANNELS = 8; @@ -121,113 +123,60 @@ opus_int64 sourceTell(void *_stream) { [self willChangeValueForKey:@"properties"]; [self didChangeValueForKey:@"properties"]; - artist = @""; - albumartist = @""; - album = @""; - title = @""; - genre = @""; - icygenre = @""; - icyalbum = @""; - icyartist = @""; - icytitle = @""; - year = @(0); - track = @(0); - disc = @(0); + metaDict = [NSDictionary dictionary]; + icyMetaDict = [NSDictionary dictionary]; albumArt = [NSData data]; + [self updateMetadata]; return YES; } -- (NSString *)parseTag:(NSString *)tag fromTags:(const OpusTags *)tags { - NSMutableArray *tagStrings = [[NSMutableArray alloc] init]; - - int tagCount = opus_tags_query_count(tags, [tag UTF8String]); - - for(int i = 0; i < tagCount; ++i) { - const char *value = opus_tags_query(tags, [tag UTF8String], i); - [tagStrings addObject:guess_encoding_of_string(value)]; +static void setDictionary(NSMutableDictionary *dict, NSString *tag, NSString *value) { + NSMutableArray *array = [dict valueForKey:tag]; + if(!array) { + array = [[NSMutableArray alloc] init]; + [dict setObject:array forKey:tag]; } - - return [tagStrings componentsJoinedByString:@", "]; + [array addObject:value]; } - (void)updateMetadata { - const OpusTags *tags = op_tags(opusRef, -1); + const struct OpusTags *tags = op_tags(opusRef, -1); + NSMutableDictionary *_metaDict = [[NSMutableDictionary alloc] init]; + NSData *_albumArt = albumArt; if(tags) { - NSString *_artist = [self parseTag:@"artist" fromTags:tags]; - NSString *_albumartist = [self parseTag:@"albumartist" fromTags:tags]; - NSString *_album = [self parseTag:@"album" fromTags:tags]; - NSString *_title = [self parseTag:@"title" fromTags:tags]; - NSString *_genre = [self parseTag:@"genre" fromTags:tags]; + for(int i = 0; i < tags->comments; ++i) { + FLAC__StreamMetadata_VorbisComment_Entry entry = { .entry = (FLAC__byte *)tags->user_comments[i], .length = tags->comment_lengths[i] }; + char *name, *value; + if(FLAC__metadata_object_vorbiscomment_entry_to_name_value_pair(entry, &name, &value)) { + NSString *tagName = guess_encoding_of_string(name); + NSString *tagValue = guess_encoding_of_string(value); + free(name); + free(value); - NSString *_yearDate = [self parseTag:@"date" fromTags:tags]; - NSString *_yearYear = [self parseTag:@"year" fromTags:tags]; + tagName = [tagName lowercaseString]; - NSNumber *_year = @(0); - if([_yearDate length]) - _year = @([_yearDate intValue]); - else if([_yearYear length]) - _year = @([_yearYear intValue]); - - NSString *_trackNumber = [self parseTag:@"tracknumber" fromTags:tags]; - NSString *_trackNum = [self parseTag:@"tracknum" fromTags:tags]; - NSString *_trackTrack = [self parseTag:@"track" fromTags:tags]; - - NSNumber *_track = @(0); - if([_trackNumber length]) - _track = @([_trackNumber intValue]); - else if([_trackNum length]) - _track = @([_trackNum intValue]); - else if([_trackTrack length]) - _track = @([_trackTrack intValue]); - - NSString *_discNumber = [self parseTag:@"discnumber" fromTags:tags]; - NSString *_discNum = [self parseTag:@"discnum" fromTags:tags]; - NSString *_discDisc = [self parseTag:@"disc" fromTags:tags]; - - NSNumber *_disc = @(0); - if([_discNumber length]) - _disc = @([_discNumber intValue]); - else if([_discNum length]) - _disc = @([_discNum intValue]); - else if([_discDisc length]) - _disc = @([_discDisc intValue]); - - NSData *_albumArt = [NSData data]; - - size_t count = opus_tags_query_count(tags, "METADATA_BLOCK_PICTURE"); - if(count) { - const char *pictureTag = opus_tags_query(tags, "METADATA_BLOCK_PICTURE", 0); - OpusPictureTag _pic = { 0 }; - if(opus_picture_tag_parse(&_pic, pictureTag) >= 0) { - if(_pic.format == OP_PIC_FORMAT_PNG || - _pic.format == OP_PIC_FORMAT_JPEG || - _pic.format == OP_PIC_FORMAT_GIF) { - _albumArt = [NSData dataWithBytes:_pic.data length:_pic.data_length]; + if([tagName isEqualToString:@"metadata_block_picture"]) { + OpusPictureTag _pic = { 0 }; + if(opus_picture_tag_parse(&_pic, [tagValue UTF8String]) >= 0) { + if(_pic.format == OP_PIC_FORMAT_PNG || + _pic.format == OP_PIC_FORMAT_JPEG || + _pic.format == OP_PIC_FORMAT_GIF) { + _albumArt = [NSData dataWithBytes:_pic.data length:_pic.data_length]; + } + opus_picture_tag_clear(&_pic); + } + } else { + setDictionary(_metaDict, tagName, tagValue); } - opus_picture_tag_clear(&_pic); } } - if(![_artist isEqual:artist] || - ![_albumartist isEqual:albumartist] || - ![_album isEqual:album] || - ![_title isEqual:title] || - ![_genre isEqual:genre] || - ![_year isEqual:year] || - ![_track isEqual:year] || - ![_disc isEqual:disc] || - ![_albumArt isEqual:albumArt]) { - artist = _artist; - albumartist = _albumartist; - album = _album; - title = _title; - genre = _genre; - year = _year; - track = _track; - disc = _disc; + if(![_albumArt isEqualToData:albumArt] || + ![_metaDict isEqualToDictionary:metaDict]) { + metaDict = _metaDict; albumArt = _albumArt; [self willChangeValueForKey:@"metadata"]; @@ -239,31 +188,35 @@ opus_int64 sourceTell(void *_stream) { - (void)updateIcyMetadata { if([source seekable]) return; - NSString *_genre = icygenre; - NSString *_album = icyalbum; - NSString *_artist = icyartist; - NSString *_title = icytitle; + NSMutableDictionary *_icyMetaDict = [[NSMutableDictionary alloc] init]; Class sourceClass = [source class]; if([sourceClass isEqual:NSClassFromString(@"HTTPSource")]) { HTTPSource *httpSource = (HTTPSource *)source; if([httpSource hasMetadata]) { NSDictionary *metadata = [httpSource metadata]; - _genre = [metadata valueForKey:@"genre"]; - _album = [metadata valueForKey:@"album"]; - _artist = [metadata valueForKey:@"artist"]; - _title = [metadata valueForKey:@"title"]; + NSString *_genre = [metadata valueForKey:@"genre"]; + NSString *_album = [metadata valueForKey:@"album"]; + NSString *_artist = [metadata valueForKey:@"artist"]; + NSString *_title = [metadata valueForKey:@"title"]; + + if(_genre && [_genre length]) { + setDictionary(_icyMetaDict, @"genre", _genre); + } + if(_album && [_album length]) { + setDictionary(_icyMetaDict, @"album", _album); + } + if(_artist && [_artist length]) { + setDictionary(_icyMetaDict, @"artist", _artist); + } + if(_title && [_title length]) { + setDictionary(_icyMetaDict, @"title", _title); + } } } - if(![_genre isEqual:icygenre] || - ![_album isEqual:icyalbum] || - ![_artist isEqual:icyartist] || - ![_title isEqual:icytitle]) { - icygenre = _genre; - icyalbum = _album; - icyartist = _artist; - icytitle = _title; + if(![_icyMetaDict isEqualToDictionary:icyMetaDict]) { + icyMetaDict = _icyMetaDict; [self willChangeValueForKey:@"metadata"]; [self didChangeValueForKey:@"metadata"]; } @@ -338,15 +291,18 @@ opus_int64 sourceTell(void *_stream) { @"totalFrames": @(totalFrames), @"bitrate": @(bitrate), @"seekable": @([source seekable] && seekable), - @"replayGainAlbumGain": @(replayGainAlbumGain), - @"replayGainTrackGain": @(replayGainTrackGain), + @"replaygain_album_gain": @(replayGainAlbumGain), + @"replaygain_track_gain": @(replayGainTrackGain), @"codec": @"Opus", @"endian": @"host", @"encoding": @"lossy" }; } - (NSDictionary *)metadata { - return [@{ @"artist": artist, @"albumartist": albumartist, @"album": album, @"title": title, @"genre": genre, @"year": year, @"track": track, @"disc": disc, @"albumArt": albumArt } dictionaryByMergingWith:@{ @"genre": icygenre, @"album": icyalbum, @"artist": icyartist, @"title": icytitle }]; + NSDictionary *dict1 = @{ @"albumArt": albumArt }; + NSDictionary *dict2 = [dict1 dictionaryByMergingWith:metaDict]; + NSDictionary *dict3 = [dict2 dictionaryByMergingWith:icyMetaDict]; + return dict3; } + (NSArray *)fileTypes { diff --git a/Plugins/Opus/OpusPlugin.xcodeproj/project.pbxproj b/Plugins/Opus/OpusPlugin.xcodeproj/project.pbxproj index e0fcdb202..14c5d0bec 100644 --- a/Plugins/Opus/OpusPlugin.xcodeproj/project.pbxproj +++ b/Plugins/Opus/OpusPlugin.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 8301C14928780C1A00651A6E /* libFLAC.8.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 8301C14828780C1A00651A6E /* libFLAC.8.dylib */; }; 83186316285CEC91001422CC /* NSDictionary+Merge.m in Sources */ = {isa = PBXBuildFile; fileRef = 83186315285CEC91001422CC /* NSDictionary+Merge.m */; }; 836EF0CF27BB952F00BF35B2 /* libopusfile.0.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 836EF0CE27BB952F00BF35B2 /* libopusfile.0.dylib */; }; 8375B04017FFEA400092A79F /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8375B03F17FFEA400092A79F /* Cocoa.framework */; }; @@ -27,6 +28,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 8301C14828780C1A00651A6E /* libFLAC.8.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libFLAC.8.dylib; path = ../../ThirdParty/flac/lib/libFLAC.8.dylib; sourceTree = ""; }; 83186314285CEC91001422CC /* NSDictionary+Merge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSDictionary+Merge.h"; path = "../../../Utils/NSDictionary+Merge.h"; sourceTree = ""; }; 83186315285CEC91001422CC /* NSDictionary+Merge.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSDictionary+Merge.m"; path = "../../../Utils/NSDictionary+Merge.m"; sourceTree = ""; }; 833F68411CDBCABC00AFB9F0 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -53,6 +55,7 @@ buildActionMask = 2147483647; files = ( 8375B04017FFEA400092A79F /* Cocoa.framework in Frameworks */, + 8301C14928780C1A00651A6E /* libFLAC.8.dylib in Frameworks */, 836EF0CF27BB952F00BF35B2 /* libopusfile.0.dylib in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -90,6 +93,7 @@ 8375B03E17FFEA400092A79F /* Frameworks */ = { isa = PBXGroup; children = ( + 8301C14828780C1A00651A6E /* libFLAC.8.dylib */, 836EF0CE27BB952F00BF35B2 /* libopusfile.0.dylib */, 8375B03F17FFEA400092A79F /* Cocoa.framework */, 8375B04117FFEA400092A79F /* Other Frameworks */, @@ -340,10 +344,14 @@ ../../ThirdParty/opusfile/include, ../../ThirdParty/opus/include, ../../ThirdParty/ogg/include, + ../../ThirdParty/flac/include, ); INFOPLIST_FILE = "Opus/OpusPlugin-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles"; - LIBRARY_SEARCH_PATHS = ../../ThirdParty/opusfile/lib; + LIBRARY_SEARCH_PATHS = ( + ../../ThirdParty/opusfile/lib, + ../../ThirdParty/flac/lib, + ); PRODUCT_BUNDLE_IDENTIFIER = net.kode54.opus; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; @@ -364,10 +372,14 @@ ../../ThirdParty/opusfile/include, ../../ThirdParty/opus/include, ../../ThirdParty/ogg/include, + ../../ThirdParty/flac/include, ); INFOPLIST_FILE = "Opus/OpusPlugin-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles"; - LIBRARY_SEARCH_PATHS = ../../ThirdParty/opusfile/lib; + LIBRARY_SEARCH_PATHS = ( + ../../ThirdParty/opusfile/lib, + ../../ThirdParty/flac/lib, + ); PRODUCT_BUNDLE_IDENTIFIER = net.kode54.opus; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; diff --git a/Plugins/TagLib/TagLibID3v2Reader.mm b/Plugins/TagLib/TagLibID3v2Reader.mm index 51f3f5a20..daaacfbdd 100644 --- a/Plugins/TagLib/TagLibID3v2Reader.mm +++ b/Plugins/TagLib/TagLibID3v2Reader.mm @@ -87,10 +87,10 @@ rgAlbumPeak = tag->rgAlbumPeak(); rgTrackGain = tag->rgTrackGain(); rgTrackPeak = tag->rgTrackPeak(); - [dict setObject:@(rgAlbumGain) forKey:@"replayGainAlbumGain"]; - [dict setObject:@(rgAlbumPeak) forKey:@"replayGainAlbumPeak"]; - [dict setObject:@(rgTrackGain) forKey:@"replayGainTrackGain"]; - [dict setObject:@(rgTrackPeak) forKey:@"replayGainTrackPeak"]; + [dict setObject:@(rgAlbumGain) forKey:@"replaygain_album_gain"]; + [dict setObject:@(rgAlbumPeak) forKey:@"replaygain_album_peak"]; + [dict setObject:@(rgTrackGain) forKey:@"replaygain_track_gain"]; + [dict setObject:@(rgTrackPeak) forKey:@"replaygain_track_peak"]; soundcheck = tag->soundcheck(); if(!soundcheck.isEmpty()) { @@ -128,6 +128,9 @@ if(!cuesheet.isEmpty()) [dict setObject:[NSString stringWithUTF8String:cuesheet.toCString(true)] forKey:@"cuesheet"]; + if(!comment.isEmpty()) + [dict setObject:[NSString stringWithUTF8String:comment.toCString(true)] forKey:@"comment"]; + // Try to load the image. NSData *image = nil; diff --git a/Plugins/TagLib/TagLibMetadataReader.m b/Plugins/TagLib/TagLibMetadataReader.m index 9cf9c407b..7c665227a 100644 --- a/Plugins/TagLib/TagLibMetadataReader.m +++ b/Plugins/TagLib/TagLibMetadataReader.m @@ -101,10 +101,10 @@ rgAlbumPeak = tag->rgAlbumPeak(); rgTrackGain = tag->rgTrackGain(); rgTrackPeak = tag->rgTrackPeak(); - [dict setObject:@(rgAlbumGain) forKey:@"replayGainAlbumGain"]; - [dict setObject:@(rgAlbumPeak) forKey:@"replayGainAlbumPeak"]; - [dict setObject:@(rgTrackGain) forKey:@"replayGainTrackGain"]; - [dict setObject:@(rgTrackPeak) forKey:@"replayGainTrackPeak"]; + [dict setObject:@(rgAlbumGain) forKey:@"replaygain_album_gain"]; + [dict setObject:@(rgAlbumPeak) forKey:@"replaygain_album_peak"]; + [dict setObject:@(rgTrackGain) forKey:@"replaygain_track_gain"]; + [dict setObject:@(rgTrackPeak) forKey:@"replaygain_track_peak"]; soundcheck = tag->soundcheck(); if(!soundcheck.isEmpty()) { @@ -141,6 +141,9 @@ if(!cuesheet.isEmpty()) [dict setObject:[NSString stringWithUTF8String:cuesheet.toCString(true)] forKey:@"cuesheet"]; + + if(!comment.isEmpty()) + [dict setObject:[NSString stringWithUTF8String:comment.toCString(true)] forKey:@"comment"]; } // Try to load the image. diff --git a/Plugins/Vorbis/VorbisDecoder.h b/Plugins/Vorbis/VorbisDecoder.h index 3c5806e71..61c271ea0 100644 --- a/Plugins/Vorbis/VorbisDecoder.h +++ b/Plugins/Vorbis/VorbisDecoder.h @@ -32,23 +32,8 @@ float frequency; long totalFrames; - NSString *artist; - NSString *albumartist; - NSString *album; - NSString *title; - NSString *genre; - NSNumber *year; - NSNumber *track; - NSNumber *disc; - float replayGainAlbumGain; - float replayGainAlbumPeak; - float replayGainTrackGain; - float replayGainTrackPeak; - - NSString *icygenre; - NSString *icyalbum; - NSString *icyartist; - NSString *icytitle; + NSDictionary *metaDict; + NSDictionary *icyMetaDict; NSData *albumArt; } diff --git a/Plugins/Vorbis/VorbisDecoder.m b/Plugins/Vorbis/VorbisDecoder.m index 30302c795..410ed5641 100644 --- a/Plugins/Vorbis/VorbisDecoder.m +++ b/Plugins/Vorbis/VorbisDecoder.m @@ -16,6 +16,8 @@ #import "NSDictionary+Merge.h" +#import + @implementation VorbisDecoder static const int MAXCHANNELS = 8; @@ -101,111 +103,58 @@ long sourceTell(void *datasource) { [self willChangeValueForKey:@"properties"]; [self didChangeValueForKey:@"properties"]; - artist = @""; - albumartist = @""; - album = @""; - title = @""; - genre = @""; - icygenre = @""; - icyalbum = @""; - icyartist = @""; - icytitle = @""; - year = @(0); - track = @(0); - disc = @(0); + metaDict = [NSDictionary dictionary]; + icyMetaDict = [NSDictionary dictionary]; albumArt = [NSData data]; + [self updateMetadata]; return YES; } -- (NSString *)parseTag:(NSString *)tag fromTags:(vorbis_comment *)tags { - NSMutableArray *tagStrings = [[NSMutableArray alloc] init]; - - int tagCount = vorbis_comment_query_count(tags, [tag UTF8String]); - - for(int i = 0; i < tagCount; ++i) { - const char *value = vorbis_comment_query(tags, [tag UTF8String], i); - [tagStrings addObject:guess_encoding_of_string(value)]; +static void setDictionary(NSMutableDictionary *dict, NSString *tag, NSString *value) { + NSMutableArray *array = [dict valueForKey:tag]; + if(!array) { + array = [[NSMutableArray alloc] init]; + [dict setObject:array forKey:tag]; } - - return [tagStrings componentsJoinedByString:@", "]; + [array addObject:value]; } - (void)updateMetadata { vorbis_comment *tags = ov_comment(&vorbisRef, -1); + NSMutableDictionary *_metaDict = [[NSMutableDictionary alloc] init]; + NSData *_albumArt = albumArt; if(tags) { - NSString *_artist = [self parseTag:@"artist" fromTags:tags]; - NSString *_albumartist = [self parseTag:@"albumartist" fromTags:tags]; - NSString *_album = [self parseTag:@"album" fromTags:tags]; - NSString *_title = [self parseTag:@"title" fromTags:tags]; - NSString *_genre = [self parseTag:@"genre" fromTags:tags]; + for(int i = 0; i < tags->comments; ++i) { + FLAC__StreamMetadata_VorbisComment_Entry entry = { .entry = (FLAC__byte *)tags->user_comments[i], .length = tags->comment_lengths[i] }; + char *name, *value; + if(FLAC__metadata_object_vorbiscomment_entry_to_name_value_pair(entry, &name, &value)) { + NSString *tagName = guess_encoding_of_string(name); + NSString *tagValue = guess_encoding_of_string(value); + free(name); + free(value); - NSString *_yearDate = [self parseTag:@"date" fromTags:tags]; - NSString *_yearYear = [self parseTag:@"year" fromTags:tags]; + tagName = [tagName lowercaseString]; - NSNumber *_year = @(0); - if([_yearDate length]) - _year = @([_yearDate intValue]); - else if([_yearYear length]) - _year = @([_yearYear intValue]); - - NSString *_trackNumber = [self parseTag:@"tracknumber" fromTags:tags]; - NSString *_trackNum = [self parseTag:@"tracknum" fromTags:tags]; - NSString *_trackTrack = [self parseTag:@"track" fromTags:tags]; - - NSNumber *_track = @(0); - if([_trackNumber length]) - _track = @([_trackNumber intValue]); - else if([_trackNum length]) - _track = @([_trackNum intValue]); - else if([_trackTrack length]) - _track = @([_trackTrack intValue]); - - NSString *_discNumber = [self parseTag:@"discnumber" fromTags:tags]; - NSString *_discNum = [self parseTag:@"discnum" fromTags:tags]; - NSString *_discDisc = [self parseTag:@"disc" fromTags:tags]; - - NSNumber *_disc = @(0); - if([_discNumber length]) - _disc = @([_discNumber intValue]); - else if([_discNum length]) - _disc = @([_discNum intValue]); - else if([_discDisc length]) - _disc = @([_discDisc intValue]); - - NSData *_albumArt = [NSData data]; - - size_t count = vorbis_comment_query_count(tags, "METADATA_BLOCK_PICTURE"); - if(count) { - const char *pictureTag = vorbis_comment_query(tags, "METADATA_BLOCK_PICTURE", 0); - flac_picture_t *picture = flac_picture_parse_from_base64(pictureTag); - if(picture) { - if(picture->binary && picture->binary_length) { - _albumArt = [NSData dataWithBytes:picture->binary length:picture->binary_length]; + if([tagName isEqualToString:@"metadata_block_picture"]) { + flac_picture_t *picture = flac_picture_parse_from_base64([tagValue UTF8String]); + if(picture) { + if(picture->binary && picture->binary_length) { + _albumArt = [NSData dataWithBytes:picture->binary length:picture->binary_length]; + } + flac_picture_free(picture); + } + } else { + setDictionary(_metaDict, tagName, tagValue); } - flac_picture_free(picture); } } - if(![_artist isEqual:artist] || - ![_albumartist isEqual:albumartist] || - ![_album isEqual:album] || - ![_title isEqual:title] || - ![_genre isEqual:genre] || - ![_year isEqual:year] || - ![_track isEqual:year] || - ![_disc isEqual:disc] || - ![_albumArt isEqual:albumArt]) { - artist = _artist; - albumartist = _albumartist; - album = _album; - title = _title; - genre = _genre; - year = _year; - track = _track; - disc = _disc; + if(![_albumArt isEqualToData:albumArt] || + ![_metaDict isEqualToDictionary:metaDict]) { + metaDict = _metaDict; albumArt = _albumArt; [self willChangeValueForKey:@"metadata"]; @@ -217,31 +166,35 @@ long sourceTell(void *datasource) { - (void)updateIcyMetadata { if([source seekable]) return; - NSString *_genre = icygenre; - NSString *_album = icyalbum; - NSString *_artist = icyartist; - NSString *_title = icytitle; + NSMutableDictionary *_icyMetaDict = [[NSMutableDictionary alloc] init]; Class sourceClass = [source class]; if([sourceClass isEqual:NSClassFromString(@"HTTPSource")]) { HTTPSource *httpSource = (HTTPSource *)source; if([httpSource hasMetadata]) { NSDictionary *metadata = [httpSource metadata]; - _genre = [metadata valueForKey:@"genre"]; - _album = [metadata valueForKey:@"album"]; - _artist = [metadata valueForKey:@"artist"]; - _title = [metadata valueForKey:@"title"]; + NSString *_genre = [metadata valueForKey:@"genre"]; + NSString *_album = [metadata valueForKey:@"album"]; + NSString *_artist = [metadata valueForKey:@"artist"]; + NSString *_title = [metadata valueForKey:@"title"]; + + if(_genre && [_genre length]) { + setDictionary(_icyMetaDict, @"genre", _genre); + } + if(_album && [_album length]) { + setDictionary(_icyMetaDict, @"album", _album); + } + if(_artist && [_artist length]) { + setDictionary(_icyMetaDict, @"artist", _artist); + } + if(_title && [_title length]) { + setDictionary(_icyMetaDict, @"title", _title); + } } } - if(![_genre isEqual:icygenre] || - ![_album isEqual:icyalbum] || - ![_artist isEqual:icyartist] || - ![_title isEqual:icytitle]) { - icygenre = _genre; - icyalbum = _album; - icyartist = _artist; - icytitle = _title; + if(![_icyMetaDict isEqualToDictionary:icyMetaDict]) { + icyMetaDict = _icyMetaDict; [self willChangeValueForKey:@"metadata"]; [self didChangeValueForKey:@"metadata"]; } @@ -325,7 +278,10 @@ long sourceTell(void *datasource) { } - (NSDictionary *)metadata { - return [@{ @"artist": artist, @"albumartist": albumartist, @"album": album, @"title": title, @"genre": genre, @"year": year, @"track": track, @"disc": disc, @"albumArt": albumArt } dictionaryByMergingWith:@{ @"genre": icygenre, @"album": icyalbum, @"artist": icyartist, @"title": icytitle }]; + NSDictionary *dict1 = @{ @"albumArt": albumArt }; + NSDictionary *dict2 = [dict1 dictionaryByMergingWith:metaDict]; + NSDictionary *dict3 = [dict2 dictionaryByMergingWith:icyMetaDict]; + return dict3; } + (NSArray *)fileTypes { diff --git a/Plugins/Vorbis/VorbisPlugin.xcodeproj/project.pbxproj b/Plugins/Vorbis/VorbisPlugin.xcodeproj/project.pbxproj index 734274b1f..8a425bcc4 100644 --- a/Plugins/Vorbis/VorbisPlugin.xcodeproj/project.pbxproj +++ b/Plugins/Vorbis/VorbisPlugin.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 17C93D360B8FDA66008627D6 /* VorbisDecoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 17C93D340B8FDA66008627D6 /* VorbisDecoder.m */; }; + 8301C14B287810F300651A6E /* libFLAC.8.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 8301C14A287810F300651A6E /* libFLAC.8.dylib */; }; 83186313285CEBD2001422CC /* NSDictionary+Merge.m in Sources */ = {isa = PBXBuildFile; fileRef = 83186312285CEBD2001422CC /* NSDictionary+Merge.m */; }; 836EF0D627BB969D00BF35B2 /* libvorbisfile.3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 836EF0D427BB969D00BF35B2 /* libvorbisfile.3.dylib */; }; 836EF0DF27BB987000BF35B2 /* libvorbis.0.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 836EF0DE27BB987000BF35B2 /* libvorbis.0.dylib */; }; @@ -24,6 +25,7 @@ 17C93D330B8FDA66008627D6 /* VorbisDecoder.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = VorbisDecoder.h; sourceTree = ""; }; 17C93D340B8FDA66008627D6 /* VorbisDecoder.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; path = VorbisDecoder.m; sourceTree = ""; }; 32DBCF630370AF2F00C91783 /* VorbisPlugin_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VorbisPlugin_Prefix.pch; sourceTree = ""; }; + 8301C14A287810F300651A6E /* libFLAC.8.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libFLAC.8.dylib; path = ../../ThirdParty/flac/lib/libFLAC.8.dylib; sourceTree = ""; }; 83186311285CEBD2001422CC /* NSDictionary+Merge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSDictionary+Merge.h"; path = "../../Utils/NSDictionary+Merge.h"; sourceTree = ""; }; 83186312285CEBD2001422CC /* NSDictionary+Merge.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSDictionary+Merge.m"; path = "../../Utils/NSDictionary+Merge.m"; sourceTree = ""; }; 8356BD1C27B46A2D0074E50C /* HTTPSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HTTPSource.h; path = ../HTTPSource/HTTPSource.h; sourceTree = ""; }; @@ -45,6 +47,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 8301C14B287810F300651A6E /* libFLAC.8.dylib in Frameworks */, 8D5B49B4048680CD000E48DA /* Cocoa.framework in Frameworks */, 836EF0DF27BB987000BF35B2 /* libvorbis.0.dylib in Frameworks */, 836EF0D627BB969D00BF35B2 /* libvorbisfile.3.dylib in Frameworks */, @@ -103,6 +106,7 @@ 1058C7ACFEA557BF11CA2CBB /* Linked Frameworks */ = { isa = PBXGroup; children = ( + 8301C14A287810F300651A6E /* libFLAC.8.dylib */, 836EF0DE27BB987000BF35B2 /* libvorbis.0.dylib */, 836EF0D427BB969D00BF35B2 /* libvorbisfile.3.dylib */, 1058C7ADFEA557BF11CA2CBB /* Cocoa.framework */, @@ -295,10 +299,14 @@ "$(inherited)", ../../ThirdParty/vorbis/include, ../../ThirdParty/ogg/include, + ../../ThirdParty/flac/include, ); INFOPLIST_FILE = Info.plist; INSTALL_PATH = "$(HOME)/Library/Bundles"; - LIBRARY_SEARCH_PATHS = ../../ThirdParty/vorbis/lib; + LIBRARY_SEARCH_PATHS = ( + ../../ThirdParty/vorbis/lib, + ../../ThirdParty/flac/lib, + ); PRODUCT_BUNDLE_IDENTIFIER = org.cogx.vorbis; PRODUCT_NAME = VorbisPlugin; SDKROOT = macosx; @@ -324,10 +332,14 @@ "$(inherited)", ../../ThirdParty/vorbis/include, ../../ThirdParty/ogg/include, + ../../ThirdParty/flac/include, ); INFOPLIST_FILE = Info.plist; INSTALL_PATH = "$(HOME)/Library/Bundles"; - LIBRARY_SEARCH_PATHS = ../../ThirdParty/vorbis/lib; + LIBRARY_SEARCH_PATHS = ( + ../../ThirdParty/vorbis/lib, + ../../ThirdParty/flac/lib, + ); PRODUCT_BUNDLE_IDENTIFIER = org.cogx.vorbis; PRODUCT_NAME = VorbisPlugin; SDKROOT = macosx; diff --git a/Plugins/vgmstream/vgmstream/VGMDecoder.m b/Plugins/vgmstream/vgmstream/VGMDecoder.m index 78ba9ac25..5f49855c4 100644 --- a/Plugins/vgmstream/vgmstream/VGMDecoder.m +++ b/Plugins/vgmstream/vgmstream/VGMDecoder.m @@ -157,10 +157,10 @@ static NSString *get_description_tag(const char *description, const char *tag, c @"floatingPoint": @(NO), @"channels": @(channels), @"seekable": @(YES), - @"replayGainAlbumGain": rgAlbumGain, - @"replayGainAlbumPeak": rgAlbumPeak, - @"replayGainTrackGain": rgTrackGain, - @"replayGainTrackPeak": rgTrackPeak, + @"replaygain_album_gain": rgAlbumGain, + @"replaygain_album_peak": rgAlbumPeak, + @"replaygain_track_gain": rgTrackGain, + @"replaygain_track_peak": rgTrackPeak, @"codec": codec, @"endian": @"host", @"encoding": @"lossy/lossless" }; diff --git a/Utils/RedundantPlaylistDataStore.m b/Utils/RedundantPlaylistDataStore.m index 904ba6264..e1df1a300 100644 --- a/Utils/RedundantPlaylistDataStore.m +++ b/Utils/RedundantPlaylistDataStore.m @@ -51,6 +51,17 @@ } } +- (NSArray *)coalesceArray:(NSArray *)input { + if(input == nil) return input; + + NSMutableArray *array = [[NSMutableArray alloc] init]; + for(NSString *string in input) { + [array addObject:[self coalesceString:string]]; + } + + return [NSArray arrayWithArray:array]; +} + - (NSDictionary *)coalesceEntryInfo:(NSDictionary *)entryInfo { if(entryInfo == nil) return entryInfo; @@ -63,6 +74,9 @@ } else if([obj isKindOfClass:[NSData class]]) { NSData *dataObj = (NSData *)obj; [ret setObject:[self coalesceArt:dataObj] forKey:key]; + } else if([obj isKindOfClass:[NSArray class]]) { + NSArray *arrayObj = (NSArray *)obj; + [ret setObject:[self coalesceArray:arrayObj] forKey:key]; } else { [ret setObject:obj forKey:key]; } diff --git a/en.lproj/InfoInspector.strings b/en.lproj/InfoInspector.strings index 48f90b4af..0a67f6400 100644 --- a/en.lproj/InfoInspector.strings +++ b/en.lproj/InfoInspector.strings @@ -14,9 +14,6 @@ /* Class = "NSTextFieldCell"; title = "Length:"; ObjectID = "16"; */ "16.title" = "Length:"; -/* Class = "NSTextFieldCell"; title = "Year:"; ObjectID = "18"; */ -"18.title" = "Year:"; - /* Class = "NSTextFieldCell"; title = "Genre:"; ObjectID = "20"; */ "20.title" = "Genre:"; @@ -79,3 +76,9 @@ /* Class = "NSTextFieldCell"; title = "Codec:"; ObjectID = "QPg-Mb-Urn"; */ "QPg-Mb-Urn.title" = "Codec:"; + +/* Class = "NSTextFieldCell"; title = "Date:"; ObjectID = "17"; */ +"18.title" = "Date:"; + +/* Class = "NSTextFieldCell"; title = "Comment:"; ObjectID = "cd3-Qt-hCm"; */ +"Ule-N3-dKW.title" = "Comment:"; diff --git a/es.lproj/InfoInspector.strings b/es.lproj/InfoInspector.strings index 5e772b625..1d476ea7f 100644 --- a/es.lproj/InfoInspector.strings +++ b/es.lproj/InfoInspector.strings @@ -16,9 +16,6 @@ /* Class = "NSTextFieldCell"; title = "Length:"; ObjectID = "16"; */ "16.title" = "Duración:"; -/* Class = "NSTextFieldCell"; title = "Year:"; ObjectID = "18"; */ -"18.title" = "Año:"; - /* Class = "NSTextFieldCell"; title = "Genre:"; ObjectID = "20"; */ "20.title" = "Género:"; @@ -109,3 +106,8 @@ /* Class = "NSTextFieldCell"; title = "N/A"; ObjectID = "Yby-OU-cqP"; */ "Yby-OU-cqP.title" = "n/d"; +/* Class = "NSTextFieldCell"; title = "Date:"; ObjectID = "17"; */ +"18.title" = "Date:"; + +/* Class = "NSTextFieldCell"; title = "Comment:"; ObjectID = "cd3-Qt-hCm"; */ +"Ule-N3-dKW.title" = "Comment:";