unit TagID3v1;

interface

uses KOL, TagID3v1Lyrics, ATLCommon;

const MusicGenreCount = 148;
      DefaultGenre = 255;
      MusicGenre: array [0..MusicGenreCount-1] of String = ('Blues', 'Classic Rock', 'Country', 'Dance',
                    'Disco', 'Funk', 'Grunge', 'Hip-Hop', 'Jazz', 'Metal', 'New Age', 'Oldies', 'Other',
                    'Pop', 'R&B', 'Rap', 'Reggae', 'Rock', 'Techno', 'Industrial', 'Alternative', 'Ska',
                    'Death Metal', 'Pranks', 'Soundtrack', 'Euro-Techno', 'Ambient', 'Trip-Hop', 'Vocal',
                    'Jazz+Funk', 'Fusion', 'Trance', 'Classical', 'Instrumental', 'Acid', 'House', 'Game',
                    'Sound Clip', 'Gospel', 'Noise', 'AlternRock', 'Bass', 'Soul', 'Punk', 'Space', 'Meditative',
                    'Instrumental Pop', 'Instrumental Rock', 'Ethnic', 'Gothic', 'Darkwave', 'Techno-Industrial',
                    'Electronic', 'Pop-Folk', 'Eurodance', 'Dream', 'Southern Rock', 'Comedy', 'Cult',
                    'Gangsta', 'Top 40', 'Christian Rap', 'Pop/Funk', 'Jungle', 'Native American', 'Cabaret',
                    'New Wave', 'Psychadelic', 'Rave', 'Showtunes', 'Trailer', 'Lo-Fi', 'Tribal', 'Acid Punk',
                    'Acid Jazz', 'Polka', 'Retro', 'Musical', 'Rock & Roll', 'Hard Rock',
                    { Extended genres }
                    'Folk', 'Folk-Rock', 'National Folk', 'Swing', 'Fast Fusion', 'Bebob', 'Latin', 'Revival',
                    'Celtic', 'Bluegrass', 'Avantgarde', 'Gothic Rock', 'Progessive Rock', 'Psychedelic Rock',
                    'Symphonic Rock', 'Slow Rock', 'Big Band', 'Chorus', 'Easy Listening', 'Acoustic', 'Humour',
                    'Speech', 'Chanson', 'Opera', 'Chamber Music', 'Sonata', 'Symphony', 'Booty Bass', 'Primus',
                    'Porn Groove', 'Satire', 'Slow Jam', 'Club', 'Tango', 'Samba', 'Folklore', 'Ballad',
                    'Power Ballad', 'Rhythmic Soul', 'Freestyle', 'Duet', 'Punk Rock', 'Drum Solo', 'A capella',
                    'Euro-House', 'Dance Hall', 'Goa', 'Drum & Bass', 'Club-House', 'Hardcore', 'Terror',
                    'Indie', 'BritPop', 'Negerpunk', 'Polsk Punk', 'Beat', 'Christian Gangsta Rap',
                    'Heavy Metal', 'Black Metal', 'Crossover', 'Contemporary Christian', 'Christian Rock',
                    'Merengue', 'Salsa', 'Trash Metal', 'Anime', 'JPop', 'Synthpop');

type
  PTagID3v1 = ^TTagID3v1;
  TID3v1Tag = PTagID3v1;
  TTagID3v1 = object(TTagInfo)
  private
    FTagID3v1Lyrics: PTagID3v1Lyrics;
    function ReadTag(const FileName: KOLString; ASetFields: Boolean = True): Boolean;
    function SaveTag(const FileName: KOLString; AUseLYR2: Boolean = True): Boolean;

    function GetTag(const Index: TEasyTags): String; virtual;
    procedure SetTag(const Index: TEasyTags; const Value: String); virtual;
  public
    constructor Create;
    destructor Destroy; virtual;
    procedure ResetData; virtual;

    function ReadFromFile(const AFileName: KOLstring): Boolean; virtual;
    function SaveToFile(const AFileName: KOLstring): Boolean; virtual;
    function RemoveFromFile(const AFileName: KOLstring): Boolean; virtual;

    property Lyrics: PTagID3v1Lyrics read FTagID3v1Lyrics;
  end;

  function NewTagID3v1: TID3v1Tag;
  function SizeOfID3v1Tag(AFileName: KOLString): Integer;

implementation

uses ATLLib;

const
  TAG_VERSION_1_0 = 1;                                { Index for ID3v1.0 tag }
  TAG_VERSION_1_1 = 2;                                { Index for ID3v1.1 tag }

type
  { Real structure of ID3v1 tag }
  TTagRecord = record
    Header: array [1..3] of Char;                //Tag header - must be "TAG"
    Title: array [1..30] of Char;                               // Title data
    Artist: array [1..30] of Char;                             // Artist data
    Album: array [1..30] of Char;                               // Album data
    Year: array [1..4] of Char;                                  // Year data
    Comment: array [1..30] of Char;                           // Comment data
    Genre: Byte;                                                // Genre data
  end;

function SizeOfID3v1Tag(AFileName: KOLString): Integer;
var TagID3v1: TID3v1Tag;
begin
    Result:= 0;
    TagID3v1:= NewTagID3v1;
    if TagID3v1.ReadFromFile(AFileName) then
      Result:= TagID3v1.TagSize + TagID3v1.Lyrics.TagSize;
    TagID3v1.Free;
end;

function NewTagID3v1: TID3v1Tag;
begin
    New(Result, Create);
end;

function TTagID3v1.ReadFromFile(const AFileName: KOLString): Boolean;
begin
    ResetData;
    Result:= ReadTag(AFileName) and FTagID3v1Lyrics.ReadFromFile(AFileName);
end;

function TTagID3v1.ReadTag(const FileName: KOLString; ASetFields: Boolean = True): Boolean;
var TagData: TTagRecord;
    SourceFile: PStream;
    Tr, Ver: Byte;
begin
    try
      Result:= True;
      SourceFile:= OpenFileRead(FileName);
      try
        // Read id3v1 tag
        SourceFile.Seek(SourceFile.Size - 128, spBegin);
        SourceFile.Read(TagData, 128);

        if TagData.Header = 'TAG' then begin
          FExists:= True;
          FTagSize:= 128;
          if ASetFields then begin

            if ((TagData.Comment[29] = #0) and (TagData.Comment[30] <> #0)) or
               ((TagData.Comment[29] = #32) and (TagData.Comment[30] <> #32)) then Ver:= TAG_VERSION_1_1
            else Ver:= TAG_VERSION_1_0;
            SetTag(etVersion, Int2Str(Ver));

            Title:= TrimRight(TagData.Title);
            Artist:= TrimRight(TagData.Artist);
            Album:= TrimRight(TagData.Album);
            Year:= TrimRight(TagData.Year);

            if Ver = TAG_VERSION_1_0 then Comment:= TrimRight(TagData.Comment)
            else begin
               Comment:= TrimRight(Copy(TagData.Comment, 1, 28));
               Tr:= Ord(TagData.Comment[30]);
               if Tr>0 then Track:= Int2Str(Tr);
            end;
            GenreID:= TagData.Genre;
          end;
        end;
(*      // Read LYRICS2 tag        *)

      finally
        SourceFile.Free;
      end;
    except
      Result:= False;
    end;
end;

function TTagID3v1.SaveToFile(const AFileName: KOLString): Boolean;
begin
    Result:= RemoveFromFile(AFileName) and SaveTag(AFileName) and FTagID3v1Lyrics.SaveToFile(AFileName);
end;

function TTagID3v1.SaveTag(const FileName: KOLString; AUseLYR2: Boolean = True): Boolean;
var _Tag: TTagRecord;
    SourceFile: PStream;
    Tr: Byte;
  procedure WriteField(sID, sValue: string);
  var iLen: integer;
      S: String;
  begin
      if Trim(sValue) <> '' then begin
        iLen:= Length(sValue);
        S:= sID + StrRepeat('0', 5 - Length(Int2Str(iLen))) + Int2Str(iLen) + sValue;
        SourceFile.Write(S[1], Length(S));
      end;
  end;
begin
    Result:= True;
    try
      SourceFile:= OpenFileWrite(FileName);
      try
(*        // Write lyrics2  *)

        // Write id3v1
        SourceFile.Seek(SourceFile.Size, spBegin);

        FillChar(_Tag, SizeOf(TTagRecord), 0);
        _Tag.Header:= 'TAG';

        Move(Title[1], _Tag.Title, Min(Length(Title), 30));
        Move(Artist[1], _Tag.Artist, Min(Length(Artist), 30));
        Move(Album[1], _Tag.Album, Min(Length(Album), 30));
        Move(Year[1], _Tag.Year, Min(Length(Year), 4));
        Move(Comment[1], _Tag.Comment, Min(Length(Comment), 30));

        Tr:= Str2Int(Track);
        if Tr > 0 then begin
           _Tag.Comment[29]:= #0;
           _Tag.Comment[30]:= Chr(Tr);
        end;
        _Tag.Genre:= FGenreID;

        SourceFile.Write(_Tag, SizeOf(_Tag));
        FExists:= True;
      finally
        SourceFile.Free;
      end;
    except
      Result:= False;
    end;
end;

function TTagID3v1.RemoveFromFile(const AFileName: KOLString): Boolean;
var SourceFile: PStream;
begin
    try
      Result:= FTagID3v1Lyrics.RemoveFromFile(AFileName);
      Result:= Result and ReadTag(AFileName, False);
      if FExists then
        try
          SourceFile:= OpenFileWrite(AFileName);
          SourceFile.Size:= SourceFile.Size - 128;
          FExists:= False;
        finally
           SourceFile.Free;
        end;
    except
      Result:= False;
    end;
end;

constructor TTagID3v1.Create;
begin
    FTagID3v1Lyrics:= NewTagID3v1Lyrics;
    inherited;
    FExistsTags:= [etVersion, etTrack, etTitle, etArtist, etAlbum, etYear, etComment, etGenre];
    FTagVendor:= tID3v1;
end;

destructor TTagID3v1.Destroy;
begin
    FTagID3v1Lyrics.Free;
    inherited;
end;

procedure TTagID3v1.ResetData;
begin
    inherited;
    FTagID3v1Lyrics.ResetData;
end;

function TTagID3v1.GetTag(const Index: TEasyTags): String;
begin
    case Index of
      etGenre: begin
        Result:= '';
        if (FGenreID < MusicGenreCount) then Result:= MusicGenre[FGenreID]
      end;
      else Result:= inherited GetTag(Index);
    end;
end;

procedure TTagID3v1.SetTag(const Index: TEasyTags; const Value: String);
var i: integer;
begin
    case Index of
      etGenre: begin
        FGenreID:= 255;
        for i:= 0 to MusicGenreCount - 1 do
          if UpperCase(MusicGenre[i]) = UpperCase(Value) then begin
             FGenreID:= i;
             Break;
          end
      end;
      else inherited;
    end;

end;

end.
