From 4aab5036585689b1fd972ee1fd400042d60b278d Mon Sep 17 00:00:00 2001 From: Chris Moeller Date: Thu, 10 Oct 2013 21:49:16 -0700 Subject: [PATCH] Removed APE tag reader from APL plugin and enabled stub APE tag reading of APE tags from APL files in the TagLib plugin --- Frameworks/TagLib/taglib/taglib/fileref.cpp | 3 +- Plugins/APL/APL.xcodeproj/project.pbxproj | 12 - Plugins/APL/APLFile.h | 4 - Plugins/APL/APLFile.m | 3 - Plugins/APL/APLMetadataReader.h | 10 - Plugins/APL/APLMetadataReader.m | 26 --- Plugins/APL/ApeTag.h | 46 ---- Plugins/APL/ApeTag.mm | 237 -------------------- Plugins/TagLib/TagLibMetadataReader.m | 4 +- 9 files changed, 4 insertions(+), 341 deletions(-) delete mode 100644 Plugins/APL/APLMetadataReader.h delete mode 100644 Plugins/APL/APLMetadataReader.m delete mode 100644 Plugins/APL/ApeTag.h delete mode 100644 Plugins/APL/ApeTag.mm diff --git a/Frameworks/TagLib/taglib/taglib/fileref.cpp b/Frameworks/TagLib/taglib/taglib/fileref.cpp index 9902638b7..8c95fcd30 100644 --- a/Frameworks/TagLib/taglib/taglib/fileref.cpp +++ b/Frameworks/TagLib/taglib/taglib/fileref.cpp @@ -139,6 +139,7 @@ StringList FileRef::defaultFileExtensions() l.append("ape"); l.append("opus"); l.append("tak"); + l.append("apl"); return l; } @@ -234,7 +235,7 @@ File *FileRef::create(FileName fileName, bool readAudioProperties, return new ASF::File(fileName, readAudioProperties, audioPropertiesStyle); if(ext == "APE") return new APE::File(fileName, readAudioProperties, audioPropertiesStyle); - if(ext == "TAK") + if(ext == "TAK" || ext == "APL") return new APE::File(fileName, false, audioPropertiesStyle); } diff --git a/Plugins/APL/APL.xcodeproj/project.pbxproj b/Plugins/APL/APL.xcodeproj/project.pbxproj index ffa879883..8196455d4 100644 --- a/Plugins/APL/APL.xcodeproj/project.pbxproj +++ b/Plugins/APL/APL.xcodeproj/project.pbxproj @@ -7,10 +7,8 @@ objects = { /* Begin PBXBuildFile section */ - 99B98A1E0CC7E1CA00C256E9 /* ApeTag.mm in Sources */ = {isa = PBXBuildFile; fileRef = 99B9865F0CC7A20800C256E9 /* ApeTag.mm */; }; 99B98A1F0CC7E1CD00C256E9 /* APLDecoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 8E8D42360CBB0F9800135C1B /* APLDecoder.m */; }; 99B98A200CC7E1CE00C256E9 /* APLFile.m in Sources */ = {isa = PBXBuildFile; fileRef = 99B9863F0CC7A08600C256E9 /* APLFile.m */; }; - 99B98A210CC7E1D100C256E9 /* APLMetadataReader.m in Sources */ = {isa = PBXBuildFile; fileRef = 17DA346D0CC04FCD0003F6B2 /* APLMetadataReader.m */; }; 99B98A260CC7E22500C256E9 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7ADFEA557BF11CA2CBB /* Cocoa.framework */; }; /* End PBXBuildFile section */ @@ -18,8 +16,6 @@ 089C1672FE841209C02AAC07 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; 089C167FFE841241C02AAC07 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = ""; }; 1058C7ADFEA557BF11CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = ""; }; - 17DA346C0CC04FCD0003F6B2 /* APLMetadataReader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = APLMetadataReader.h; sourceTree = ""; }; - 17DA346D0CC04FCD0003F6B2 /* APLMetadataReader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = APLMetadataReader.m; sourceTree = ""; }; 32DBCF630370AF2F00C91783 /* APL_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = APL_Prefix.pch; sourceTree = ""; }; 8D5B49B7048680CD000E48DA /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 8E8D42350CBB0F9800135C1B /* APLDecoder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = APLDecoder.h; sourceTree = ""; }; @@ -27,8 +23,6 @@ 8E8D423C0CBB0FF600135C1B /* Plugin.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; name = Plugin.h; path = ../../Audio/Plugin.h; sourceTree = SOURCE_ROOT; }; 99B9863E0CC7A08600C256E9 /* APLFile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = APLFile.h; sourceTree = ""; }; 99B9863F0CC7A08600C256E9 /* APLFile.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = APLFile.m; sourceTree = ""; }; - 99B9865E0CC7A20800C256E9 /* ApeTag.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ApeTag.h; sourceTree = ""; }; - 99B9865F0CC7A20800C256E9 /* ApeTag.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ApeTag.mm; sourceTree = ""; }; 99B989F40CC7E10400C256E9 /* APL.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = APL.bundle; sourceTree = BUILT_PRODUCTS_DIR; }; D2F7E65807B2D6F200F64583 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = /System/Library/Frameworks/CoreData.framework; sourceTree = ""; }; /* End PBXFileReference section */ @@ -80,12 +74,8 @@ 8E8D423C0CBB0FF600135C1B /* Plugin.h */, 8E8D42350CBB0F9800135C1B /* APLDecoder.h */, 8E8D42360CBB0F9800135C1B /* APLDecoder.m */, - 17DA346C0CC04FCD0003F6B2 /* APLMetadataReader.h */, - 17DA346D0CC04FCD0003F6B2 /* APLMetadataReader.m */, 99B9863E0CC7A08600C256E9 /* APLFile.h */, 99B9863F0CC7A08600C256E9 /* APLFile.m */, - 99B9865E0CC7A20800C256E9 /* ApeTag.h */, - 99B9865F0CC7A20800C256E9 /* ApeTag.mm */, ); name = Classes; sourceTree = ""; @@ -183,10 +173,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 99B98A1E0CC7E1CA00C256E9 /* ApeTag.mm in Sources */, 99B98A1F0CC7E1CD00C256E9 /* APLDecoder.m in Sources */, 99B98A200CC7E1CE00C256E9 /* APLFile.m in Sources */, - 99B98A210CC7E1D100C256E9 /* APLMetadataReader.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Plugins/APL/APLFile.h b/Plugins/APL/APLFile.h index e466b05ab..95ceaeb9e 100644 --- a/Plugins/APL/APLFile.h +++ b/Plugins/APL/APLFile.h @@ -1,12 +1,9 @@ #import -@class ApeTag; - @interface APLFile : NSObject { long startBlock; long endBlock; - ApeTag* tag; NSURL* file; } +createWithFile:(NSString*)f; @@ -14,7 +11,6 @@ -(long)startBlock; // always return 0 or greater! -(long)endBlock; --(ApeTag*) tag; -(NSURL*) file; //writing support to be added in far future diff --git a/Plugins/APL/APLFile.m b/Plugins/APL/APLFile.m index 5dd820e3d..58a68f454 100644 --- a/Plugins/APL/APLFile.m +++ b/Plugins/APL/APLFile.m @@ -1,7 +1,6 @@ #import "APLFile.h" -#import "ApeTag.h" @implementation APLFile +createWithFile:(NSString*)f { return [[APLFile alloc] initWithFile:f]; } @@ -98,7 +97,6 @@ } [scanner release]; //check here for EOF? cocoa does not have this functionality :( - tag = [ApeTag createFromFileRead:f]; [f closeFile]; } return self; @@ -106,7 +104,6 @@ -(long)startBlock { return startBlock; } -(long)endBlock { return endBlock; } --(ApeTag*) tag { return tag; } -(NSURL*) file { return file; } diff --git a/Plugins/APL/APLMetadataReader.h b/Plugins/APL/APLMetadataReader.h deleted file mode 100644 index 071ea84cb..000000000 --- a/Plugins/APL/APLMetadataReader.h +++ /dev/null @@ -1,10 +0,0 @@ - -#import - -#import "Plugin.h" - -@interface APLMetadataReader : NSObject { - -} - -@end diff --git a/Plugins/APL/APLMetadataReader.m b/Plugins/APL/APLMetadataReader.m deleted file mode 100644 index a8e7398ed..000000000 --- a/Plugins/APL/APLMetadataReader.m +++ /dev/null @@ -1,26 +0,0 @@ -// -// APLMetadataReader.m - - -#import "APLMetadataReader.h" -#import "APLDecoder.h" - -#import "APLFile.h" -#import "ApeTag.h" - -@implementation APLMetadataReader - -+ (NSArray *)fileTypes { return [APLDecoder fileTypes]; } -+ (NSArray *)mimeTypes { return [APLDecoder mimeTypes]; } - -+ (NSDictionary *)metadataForURL:(NSURL *)url { - if (![url isFileURL]) return nil; - - APLFile *apl = [APLFile createWithFile:[url path]]; - - ApeTag* tag = [apl tag]; - NSDictionary* res =[tag convertToCogTag]; - return res; -} - -@end diff --git a/Plugins/APL/ApeTag.h b/Plugins/APL/ApeTag.h deleted file mode 100644 index f9a1a3d6e..000000000 --- a/Plugins/APL/ApeTag.h +++ /dev/null @@ -1,46 +0,0 @@ -#import - -//not full support!! and even may have not enougth checks to find a non-supported data! - - - -@interface ApeTagItem: NSObject { - UInt32 flags; - NSString* tag; - NSData* data; -} - -+createTag:(NSString*)t flags:(SInt32)f data:(NSData*)d; --initTag:(NSString*)t flags:(SInt32)f data:(NSData*)d; - -//for writing: --(NSData*) pack; - -//for reading: --(NSString*) tag; --(bool) isString; //returns whether data is UTF-8 string(or implicitly can be converted to) --(NSString*) getString; //returns string, if is not string - this is error, check isString before -@end - - - - -@interface ApeTag : NSObject { - NSMutableArray* fields; -} - -+(id)create; -+(id)createFromFileRead:(NSFileHandle*)f; --(id)init; --(id)initFromFileRead:(NSFileHandle*)f; - --(NSArray*) fields; // returns fields array - --(void) addField:(NSString*)f text:(NSString*)t; --(NSData*) pack; //returns comoplete tag, ready for writing to file/transmitting smwhere - --(void) write:(NSFileHandle*)f; //writes to file output of -pack --(bool) read:(NSFileHandle*)f; //reads from file, returns true on success - --(NSDictionary*) convertToCogTag; -@end diff --git a/Plugins/APL/ApeTag.mm b/Plugins/APL/ApeTag.mm deleted file mode 100644 index 253881014..000000000 --- a/Plugins/APL/ApeTag.mm +++ /dev/null @@ -1,237 +0,0 @@ - -// 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]; - ApeTagItem* item; - int n = 0; - for (item in fields) { - 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]; - id item; - for (item in fields) - { - 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 diff --git a/Plugins/TagLib/TagLibMetadataReader.m b/Plugins/TagLib/TagLibMetadataReader.m index 63b949f52..4f01a8f6e 100644 --- a/Plugins/TagLib/TagLibMetadataReader.m +++ b/Plugins/TagLib/TagLibMetadataReader.m @@ -130,12 +130,12 @@ + (NSArray *)fileTypes { //May be a way to get a list of supported formats - return [NSArray arrayWithObjects:@"ape", @"asf", @"wma", @"ogg", @"opus", @"mpc", @"flac", @"m4a", @"mp3", @"tak", nil]; + return [NSArray arrayWithObjects:@"ape", @"asf", @"wma", @"ogg", @"opus", @"mpc", @"flac", @"m4a", @"mp3", @"tak", @"apl", nil]; } + (NSArray *)mimeTypes { - return [NSArray arrayWithObjects:@"audio/x-ape", @"audio/x-ms-wma", @"application/ogg", @"application/x-ogg", @"audio/x-vorbis+ogg", @"audio/x-musepack", @"audio/x-flac", @"audio/x-m4a", @"audio/mpeg", @"audio/x-mp3", @"audio/x-tak", nil]; + return [NSArray arrayWithObjects:@"audio/x-ape", @"audio/x-ms-wma", @"application/ogg", @"application/x-ogg", @"audio/x-vorbis+ogg", @"audio/x-musepack", @"audio/x-flac", @"audio/x-m4a", @"audio/mpeg", @"audio/x-mp3", @"audio/x-tak", @"audio/x-apl", nil]; } @end