unit TagAPE;

interface

uses KOL, ATLCommon;

type
  PTagAPE = ^TTagAPE;
  TAPETag = PTagAPE;
  TTagAPE = object(TTagInfo)
  private
    function ReadFooter(AFileName: KOLstring; ATagRec: Pointer): Boolean;
  public
    function ReadFromFile(const AFileName: KOLstring): Boolean; virtual;
    function SaveToFile(const AFileName: KOLstring): Boolean; virtual;
    function RemoveFromFile(const AFileName: KOLstring): Boolean; virtual;
  end;

  function NewTagAPE: TAPETag;

implementation

uses ATLLib, TagID3v1;

const
  { Tag ID }
  APE_ID = 'APETAGEX';                                                  { APE }

  { Size constants }
  APE_TAG_FOOTER_SIZE = 32;                                  { APE tag footer }
  APE_TAG_HEADER_SIZE = 32;                                  { APE tag header }

  { First version of APE tag }
  APE_VERSION_1_0 = 1000;

  { Max. number of supported tag fields }
  APE_Field_Count = 8;

  { Names of supported tag fields }
  APE_Fields: array [1..APE_FIELD_COUNT] of string = ('Title', 'Artist', 'Album', 'Track', 'Year', 'Genre', 'Comment', 'Copyright');

type
  { APE tag data - for internal use }
  PTagRec = ^TTagRec;
  TTagRec = packed record
    { Real structure of APE footer }
    ID: array [1..8] of Char;                             { Always "APETAGEX" }
    Version: Integer;                                           { Tag version }
    Size: Cardinal;                                { Tag size including footer }
    Fields: Integer;                                       { Number of fields }
    Flags: Integer;                                               { Tag flags }
    Reserved: array [1..8] of Char;                  { Reserved for later use }
    { Extended data }
    DataShift: Byte;                                { Used if ID3v1 tag found }
    Field: array [1..APE_FIELD_COUNT] of string;    { Information from fields }
  end;

function NewTagAPE: TAPETag;
begin
    New(Result, Create);
    Result.FExistsTags:= [etVersion, etTrack, etTitle, etArtist, etAlbum, etYear, etComment, etGenre, etCopyright];
    Result.FTagVendor:= tAPE;
end;

function TTagAPE.ReadFooter(AFileName: KOLstring; ATagRec: Pointer): Boolean;
var SourceFile: PStream;
    Transferred: Integer;
begin
    try
      FillChar(ATagRec^, SizeOf(TTagRec), 0);
      Result:= true;
      { Set read-access and open file }
      SourceFile:= OpenFileRead(AFileName);
      try
        { Check for existing ID3v1 tag }
        PTagRec(ATagRec).DataShift:= SizeOfID3v1Tag(AFileName);
        { Read footer data }
        SourceFile.Seek(- PTagRec(ATagRec).DataShift - APE_TAG_FOOTER_SIZE, spEnd);
        Transferred:= SourceFile.Read(ATagRec^, APE_TAG_FOOTER_SIZE);
        if Transferred < APE_TAG_FOOTER_SIZE then Result:= false;
      finally
        SourceFile.Free;
      end;
    except
      Result:= false;
    end;
end;

function TTagAPE.RemoveFromFile(const AFileName: KOLstring): Boolean;
var TagRec: TTagRec;
    SourceFile: PStream;
    Buf: Pointer;
begin                                                        
    Result:= True;
    if ReadFooter(AFileName, @TagRec) then
      try
        if TagRec.ID <> APE_ID then Exit;
        if (TagRec.Flags shr 31) > 0 then Inc(TagRec.Size, APE_TAG_HEADER_SIZE);

        SourceFile:= OpenFileWrite(AFileName);
        Buf:= nil;
        try
          if TagRec.DataShift>0 then begin
            SourceFile.Seek(-TagRec.DataShift, spEnd);
            Buf:= AllocMem(TagRec.DataShift);
            SourceFile.Read(Buf^, TagRec.DataShift);
          end;
          SourceFile.Size:= SourceFile.Size - TagRec.DataShift - TagRec.Size;
          if TagRec.DataShift>0 then begin
            SourceFile.Seek(0, spEnd);
            SourceFile.Write(Buf^, TagRec.DataShift);
          end;
        finally
          SourceFile.Free;
          DisposeMem(Buf);
        end;
      except
        Result:= false;
      end;
end;

function TTagAPE.SaveToFile(const AFileName: KOLstring): Boolean;
var TagRec, _TagRec: TTagRec;
    TagData, SourceFile: PStream;
    i, ValueSize, Flags: Integer;
    Buf: Pointer;

begin
    FillChar(TagRec, SizeOf(TagRec), 0);
    TagRec.Field[1]:= Title;
    TagRec.Field[2]:= Artist;
    TagRec.Field[3]:= Album;
    TagRec.Field[4]:= Track;
    TagRec.Field[5]:= Year;
    TagRec.Field[6]:= Genre;
    TagRec.Field[7]:= Comment;
    TagRec.Field[8]:= Copyright;

    Result:= RemoveFromFile(AFileName);

    if Result then begin
      Buf:= nil;
      TagData:= NewMemoryStream;
      SourceFile:= OpenFileWrite(AFileName);
      try
        for i:= 1 to APE_FIELD_COUNT do
          if TagRec.Field[i] <> '' then begin
            ValueSize:= Length(TagRec.Field[i]) + 1;
            Flags:= 0;
            TagData.Write(ValueSize, SizeOf(ValueSize));
            TagData.Write(Flags, SizeOf(Flags));
            TagData.WriteStr(APE_Fields[i] + #0);
            TagData.WriteStr(TagRec.Field[i] + #0);
          end;

        { Build tag footer }
        TagRec.ID:= APE_ID;
        TagRec.Version:= APE_VERSION_1_0;
        TagRec.Size:= APE_TAG_FOOTER_SIZE;
        for i:= 1 to APE_FIELD_COUNT do
          if TagRec.Field[i] <> '' then
          begin
            Inc(TagRec.Size, Length(APE_Fields[i] + TagRec.Field[i]) + 10);
            Inc(TagRec.Fields);
          end;

        TagData.Write(TagRec, APE_TAG_FOOTER_SIZE);

        ReadFooter(AFileName, @_TagRec);
        Buf:= AllocMem(_TagRec.DataShift);
        if _TagRec.DataShift>0 then begin
          SourceFile.Seek(-_TagRec.DataShift, spEnd);
          SourceFile.Read(Buf^, _TagRec.DataShift);
          SourceFile.Size:= SourceFile.Size - _TagRec.DataShift;
        end;
        { Add created tag to file }
        SourceFile.Seek(0, spEnd); TagData.Position:= 0;
        Stream2Stream(SourceFile, TagData, TagData.Size);

        if _TagRec.DataShift>0 then begin
          SourceFile.Seek(0, spEnd);
          SourceFile.Write(Buf^, _TagRec.DataShift);
        end;
        Result:= True;
      finally
        SourceFile.Free;
        TagData.Free;
        DisposeMem(Buf);
      end;
    end;
end;

function TTagAPE.ReadFromFile(const AFileName: KOLstring): Boolean;
  procedure SetTagItem(const AFieldName, AFieldValue: string; var ATag: TTagRec);
  var i: Byte;
  begin
    for i:= 1 to APE_FIELD_COUNT do
      if AnsiUpperCase(AFieldName) = UpperCase(APE_Fields[i]) then
        if ATag.Version > APE_VERSION_1_0 then ATag.Field[i]:= Utf8ToAnsi(AFieldValue)
        else ATag.Field[i]:= AFieldValue;
  end;

var TagRec: TTagRec;
    SourceFile: PStream;
    FieldName: string;
    FieldValue: array [1..250] of Char;
    NextChar: Char;
    i, ValueSize, ValuePosition, FieldFlags: Integer;
begin
    ResetData;
    Result:= ReadFooter(AFileName, @TagRec);
    { Process data if loaded and footer valid }
    if Result and (TagRec.ID = APE_ID) then begin
      FExists:= true;
      { Fill properties with footer data }
      SetTag(etVersion, Int2Str(TagRec.Version));
      FTagSize:= TagRec.Size;
      { Get information from fields }
      try
        { Set read-access, open file }
        SourceFile:= OpenFileRead(AFileName);
        try
          SourceFile.Seek(SourceFile.Size - TagRec.DataShift - TagRec.Size, spBegin);
          { Read all stored fields }
          for i:= 1 to TagRec.Fields do begin
            FillChar(FieldValue, SizeOf(FieldValue), 0);
            SourceFile.Read(ValueSize, SizeOf(ValueSize));
            SourceFile.Read(FieldFlags, SizeOf(FieldFlags));
            FieldName:= '';
            repeat
              SourceFile.Read(NextChar, SizeOf(NextChar));
              FieldName:= FieldName + NextChar;
            until Ord(NextChar) = 0;
            ValuePosition:= SourceFile.Position;
            SourceFile.Read(FieldValue, ValueSize mod SizeOf(FieldValue));
            SetTagItem(Trim(FieldName), Trim(FieldValue), TagRec);
            SourceFile.Seek(ValuePosition + ValueSize, spBegin);
          end;
        finally
          SourceFile.Free;
        end;
      except
      end;

      Title:= TagRec.Field[1];
      Artist:= TagRec.Field[2];
      Album:= TagRec.Field[3];
      Track:= Parse(TagRec.Field[4], '/');
      Year:= TagRec.Field[5];
      Genre:= TagRec.Field[6];
      Comment:= TagRec.Field[7];
      Copyright:= TagRec.Field[8];
    end;
end;

end.
