240 lines
9.2 KiB
Plaintext
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
|