cog/Plugins/APL/ApeTag.mm

240 lines
9.2 KiB
Plaintext

// as simple as it can be - just write a readable tag, no options etc.
#import "ApeTag.h"
#define CURRENT_APE_TAG_VERSION 2000
/*****************************************************************************************
"Standard" APE tag fields
*****************************************************************************************/
#define APE_TAG_FIELD_TITLE @"Title"
#define APE_TAG_FIELD_ARTIST @"Artist"
#define APE_TAG_FIELD_ALBUM @"Album"
#define APE_TAG_FIELD_COMMENT @"Comment"
#define APE_TAG_FIELD_YEAR @"Year"
#define APE_TAG_FIELD_TRACK @"Track"
#define APE_TAG_FIELD_GENRE @"Genre"
#define APE_TAG_FIELD_COVER_ART_FRONT @"Cover Art (front)"
#define APE_TAG_FIELD_NOTES @"Notes"
#define APE_TAG_FIELD_LYRICS @"Lyrics"
#define APE_TAG_FIELD_COPYRIGHT @"Copyright"
#define APE_TAG_FIELD_BUY_URL @"Buy URL"
#define APE_TAG_FIELD_ARTIST_URL @"Artist URL"
#define APE_TAG_FIELD_PUBLISHER_URL @"Publisher URL"
#define APE_TAG_FIELD_FILE_URL @"File URL"
#define APE_TAG_FIELD_COPYRIGHT_URL @"Copyright URL"
#define APE_TAG_FIELD_MJ_METADATA @"Media Jukebox Metadata"
#define APE_TAG_FIELD_TOOL_NAME @"Tool Name"
#define APE_TAG_FIELD_TOOL_VERSION @"Tool Version"
#define APE_TAG_FIELD_PEAK_LEVEL @"Peak Level"
#define APE_TAG_FIELD_REPLAY_GAIN_RADIO @"Replay Gain (radio)"
#define APE_TAG_FIELD_REPLAY_GAIN_ALBUM @"Replay Gain (album)"
#define APE_TAG_FIELD_COMPOSER @"Composer"
#define APE_TAG_FIELD_KEYWORDS @"Keywords"
/*****************************************************************************************
Standard APE tag field values
*****************************************************************************************/
#define APE_TAG_GENRE_UNDEFINED @"Undefined"
/*****************************************************************************************
Footer (and header) flags
*****************************************************************************************/
#define APE_TAG_FLAG_CONTAINS_HEADER (1 << 31)
#define APE_TAG_FLAG_CONTAINS_FOOTER (1 << 30)
#define APE_TAG_FLAG_IS_HEADER (1 << 29)
#define APE_TAG_FLAGS_DEFAULT (APE_TAG_FLAG_CONTAINS_FOOTER)
/*****************************************************************************************
Tag field flags
*****************************************************************************************/
#define TAG_FIELD_FLAG_READ_ONLY (1 << 0)
#define TAG_FIELD_FLAG_DATA_TYPE_MASK (6)
#define TAG_FIELD_FLAG_DATA_TYPE_TEXT_UTF8 (0 << 1)
#define TAG_FIELD_FLAG_DATA_TYPE_BINARY (1 << 1)
#define TAG_FIELD_FLAG_DATA_TYPE_EXTERNAL_INFO (2 << 1)
#define TAG_FIELD_FLAG_DATA_TYPE_RESERVED (3 << 1)
/*****************************************************************************************
The footer at the end of APE tagged files (can also optionally be at the front of the tag)
*****************************************************************************************/
#define APE_TAG_FOOTER_BYTES 32
//------------------------
@implementation ApeTagItem
+createTag:(NSString*)t flags:(SInt32)f data:(NSData*)d{
return [[ApeTagItem alloc] initTag:t flags:f data:d];
}
-initTag:(NSString*)t flags:(SInt32)f data:(NSData*)d{
self = [super init];
if (self)
{
tag = [t copy];
data = [d copy];
flags = f;
}
return self;
}
-(NSData*) pack {
//item header:
NSMutableData* d = [NSMutableData dataWithCapacity:8];
UInt32 len = CFSwapInt32HostToLittle([data length]);
UInt32 flags1 = CFSwapInt32HostToLittle(flags); // ApeTagv1 does not use this, 0 for v2 means only footer and all fields text in utf8 - just right what we want
[d appendBytes:&len length:4]; // lenth of value
[d appendBytes:&flags1 length:4]; // 32 bits of flags
[d appendData:[tag dataUsingEncoding: NSUTF8StringEncoding]]; // item tag
char c = 0; [d appendBytes:&c length:1]; // 0x00 separator after tag
[d appendData:data]; // item value
return d;
}
-(bool) isString { //returns whether data is UTF-8 string(or implicitly can be converted to)
return (flags & TAG_FIELD_FLAG_DATA_TYPE_MASK) == TAG_FIELD_FLAG_DATA_TYPE_TEXT_UTF8;
}
-(NSString*) getString { //returns string, if is not string - this is error, check isString before
if ([self isString]) return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
return nil;
}
-(NSString*) tag { return tag; }
@end
@implementation ApeTag
+(id)create { return [[ApeTag alloc] init];}
+(id)createFromFileRead:(NSFileHandle*)f { return [[ApeTag alloc] initFromFileRead:f]; }
-(id)init {
self = [super init];
if (self)
{
fields = [NSMutableArray array];
}
return self;
}
-(id)initFromFileRead:(NSFileHandle*)f {
self = [super init];
if(self)
{
fields = [NSMutableArray array];
if (![self read:f])
return nil;
}
return self;
}
-(void) addField:(NSString*)f text:(NSString*)t {
[fields addObject:[ApeTagItem createTag:f flags:(TAG_FIELD_FLAG_DATA_TYPE_TEXT_UTF8) data:[t dataUsingEncoding: NSUTF8StringEncoding]]];
}
-(NSArray*) fields { return fields; }
-(NSDictionary*) convertToCogTag {
//NSLog(@"Converting ape tag to cog tag");
NSMutableDictionary* d = [NSMutableDictionary dictionaryWithCapacity:6];
NSEnumerator *e = [fields objectEnumerator];
ApeTagItem* item;
int n = 0;
while ((item = [e nextObject]) != nil) {
if (![[item tag] compare:APE_TAG_FIELD_ARTIST]) { [d setObject:[item getString] forKey:@"artist"]; n++;}
if (![[item tag] compare:APE_TAG_FIELD_ALBUM]) {[d setObject:[item getString] forKey:@"album"]; n++;}
if (![[item tag] compare:APE_TAG_FIELD_TITLE]) {[d setObject:[item getString] forKey:@"title"]; n++;}
if (![[item tag] compare:APE_TAG_FIELD_TRACK]) {[d setObject:[NSNumber numberWithInt:[[item getString] intValue]] forKey:@"track"]; n++;}
if (![[item tag] compare:APE_TAG_FIELD_GENRE]) {[d setObject:[item getString] forKey:@"genre"]; n++;}
if (![[item tag] compare:APE_TAG_FIELD_YEAR]) {[d setObject:[item getString] forKey:@"year"]; n++;}
}
if (n)
return d;
[d release]; d = nil;
return nil;
}
-(NSData*) pack {
int len = 0, num = 0;
NSMutableData* d = [NSMutableData dataWithCapacity:8];
NSEnumerator *e = [fields objectEnumerator];
id item;
while ((item = [e nextObject]) != nil)
{
NSData* i = [item pack];
[d appendData:i];
len += [i length];
num++;
}
UInt32 version = CFSwapInt32HostToLittle(CURRENT_APE_TAG_VERSION);
UInt32 size = CFSwapInt32HostToLittle(len + APE_TAG_FOOTER_BYTES);
UInt32 count = CFSwapInt32HostToLittle(num);
UInt32 flags = CFSwapInt32HostToLittle(APE_TAG_FLAGS_DEFAULT);
[d appendBytes:"APETAGEX" length:8];
[d appendBytes:&version length:4];
[d appendBytes:&size length:4];
[d appendBytes:&count length:4];
[d appendBytes:&flags length:4];
[d appendBytes:"\0\0\0\0\0\0\0\0" length:8]; //reserved
return d;
}
-(void) write:(NSFileHandle*)file {
[file writeData:[self pack]];
}
-(NSString*) readToNull:(NSFileHandle*)f { //!!!
NSMutableData* d = [NSMutableData dataWithCapacity:100]; //it will grow, here should be most expected value (may gain few nanosecond from it =) )
while(true) {
NSData* byte = [f readDataOfLength:1];
if (!byte)
{
[f seekToFileOffset:0-[d length]];
return nil;
}
if (*((const char*)[byte bytes]) == 0) break;
[d appendData:byte];
}
NSString* s = [[NSString alloc] initWithData:d encoding:NSUTF8StringEncoding]; //!handle encoding error (in case binary data starts not from newline, impossible on real apl, but who knows...)
return s;
}
-(bool) read:(NSFileHandle*)f { //reads from file, returns true on success
//reading from file erases previous data:
[fields removeAllObjects];
//!! prefix header may be present and we should read it but lazyness...
NSString* header = @"APETAGEX";
NSData* check = [f readDataOfLength:[header length]];
[f seekToFileOffset:([f offsetInFile]-[check length])];
if (check && [[[NSString alloc] initWithData:check encoding:NSASCIIStringEncoding] isEqualToString:header]) {
NSLog(@"Reading of prefix header not implemented");
return false;
}
//read fields and store 'em
while(true) {
NSData* check = [f readDataOfLength:[header length]];
[f seekToFileOffset:([f offsetInFile]-[check length])];
if (check && [[[NSString alloc] initWithData:check encoding:NSASCIIStringEncoding] isEqualToString:header]) break; // reached footer
NSData* f_len = [f readDataOfLength:4];
NSData* f_flag = [f readDataOfLength:4];
NSString* f_name = [self readToNull:f];
UInt32 len = CFSwapInt32LittleToHost(*(uint32_t*)[f_len bytes]);
UInt32 flags = CFSwapInt32LittleToHost(*(uint32_t*)[f_flag bytes]);
NSData* data = [f readDataOfLength:len];
ApeTagItem* item = [ApeTagItem createTag:f_name flags:flags data:data];
//NSLog(@"Read tag '%@'='%@'", [item tag], [item getString]);
[fields addObject:item];
}
//here we should read footer and check number of fields etc. - but who cares? =)
// just clean up file pointer:
[f readDataOfLength:APE_TAG_FOOTER_BYTES];
return true;
}
@end