aboutsummaryrefslogblamecommitdiffstats
path: root/src/base/UCovers.pas
blob: 7f378c40889f1e2a65f0ba06f4d0441b8dba28b2 (plain) (tree)
























                                                                        

             









                                                                             







                 
    
      




               

           
 
    





                                      
                      
          
                                                     

                                             

      









                                                             
                                                                                              

                                                   
                                                             





                                             


                                                           





                                                                     


      
                         
 

              








            
                                            






                                                                  
     
                                                       

    

                                                                  
     

                                                   
 
 
                
 




                                                                   
 
 
          
 
                                                      



                            
 



                                              
 
                                       

                          
     


                                                      

                                          
 
 
                  
 


                                    
 


                      
 




                                  
 


                                                    

    










                                                            
   
             
     







                                             
      
 

                                            

    
                                
   
                   
                  
     
                                                                  
 
                                                  



                                                                   
       


                                                                             

                                                                              
             
                                                    

                 
 


                                      
 







                                                                                      
 




















                                                                               

    
                                                                      

                             
     





                                                                    
                                                    

                                               

    
                                                                 
   











                                                        
                                                                    
     
                  



                                                          


      
                                                                
   




                          
     








                                                             
                                                        













                                                                                     
                                                              



























                                                                     
                  











                                                                           
                                             











                                                                        


                                                                          



                                                        
      

             

    









                                                                                         
                                                                                                       
   
                                       


                                       
     





                                   
       
                                                                                               
         
      


























                                                     
 
                      


    
 
{* UltraStar Deluxe - Karaoke Game
 *
 * UltraStar Deluxe is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; see the file COPYING. If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 *
 * $URL$
 * $Id$
 *}

unit UCovers;

{
  TODO:
  - adjust database to new song-loading (e.g. use SongIDs)
  - support for deletion of outdated covers
  - support for update of changed covers
  - use paths relative to the song for removable disks support
    (a drive might have a different drive-name the next time it is connected,
     so "H:/songs/..." will not match "I:/songs/...") 
}

interface

{$IFDEF FPC}
  {$MODE Delphi}
{$ENDIF}

{$I switches.inc}

uses
  SDL,
  SQLite3,
  SQLiteTable3,
  SysUtils,
  Classes,
  UImage,
  UTexture,
  UPath;

type
  ECoverDBException = class(Exception)
  end;

  TCover = class
    private
      ID: int64;
      Filename: IPath;
    public
      constructor Create(ID: int64; Filename: IPath);
      function GetPreviewTexture(): TTexture;
      function GetTexture(): TTexture;
  end;

  TThumbnailInfo = record
    CoverWidth: integer;         // Original width of cover
    CoverHeight: integer;        // Original height of cover
    PixelFormat: TImagePixelFmt; // Pixel-format of thumbnail
  end;
  
  TCoverDatabase = class
    private
      DB: TSQLiteDatabase;
      procedure InitCoverDatabase();
      function CreateThumbnail(const Filename: IPath; var Info: TThumbnailInfo): PSDL_Surface;
      function LoadCover(CoverID: int64): TTexture;
      procedure DeleteCover(CoverID: int64);
      function FindCoverIntern(const Filename: IPath): int64;
      procedure Open();
      function GetVersion(): integer;
      procedure SetVersion(Version: integer);
    public
      constructor Create();
      destructor Destroy; override;
      function AddCover(const Filename: IPath): TCover;
      function FindCover(const Filename: IPath): TCover;
      function CoverExists(const Filename: IPath): boolean;
      function GetMaxCoverSize(): integer;
      procedure SetMaxCoverSize(Size: integer);
  end;

  TBlobWrapper = class(TCustomMemoryStream)
     function Write(const Buffer; Count: Integer): Integer; override;
  end;

var
  Covers: TCoverDatabase;

implementation

uses
  UMain,
  ULog,
  UPlatform,
  UIni,
  Math,
  DateUtils;

const
  COVERDB_FILENAME: UTF8String = 'cover.db';
  COVERDB_VERSION = 01; // 0.1
  COVER_TBL = 'Cover';
  COVER_THUMBNAIL_TBL = 'CoverThumbnail';
  COVER_IDX = 'Cover_Filename_IDX';

// Note: DateUtils.DateTimeToUnix() will throw an exception in FPC
function DateTimeToUnixTime(time: TDateTime): int64;
begin
  Result := Round((time - UnixDateDelta) * SecsPerDay);
end;

// Note: DateUtils.UnixToDateTime() will throw an exception in FPC
function UnixTimeToDateTime(timestamp: int64): TDateTime;
begin
  Result := timestamp / SecsPerDay + UnixDateDelta;
end;


{ TBlobWrapper }

function TBlobWrapper.Write(const Buffer; Count: Integer): Integer;
begin
  SetPointer(Pointer(Buffer), Count);
  Result := Count;
end;


{ TCover }

constructor TCover.Create(ID: int64; Filename: IPath);
begin
  Self.ID := ID;
  Self.Filename := Filename;
end;

function TCover.GetPreviewTexture(): TTexture;
begin
  Result := Covers.LoadCover(ID);
end;

function TCover.GetTexture(): TTexture;
var
  debughelper: UTF8String;
begin
  if not (Assigned(Filename)) or (Filename = nil) then
    Exit;
  debughelper := Filename.ToUTF8(true);
  Result := Texture.LoadTexture(Filename);
end;


{ TCoverDatabase }

constructor TCoverDatabase.Create();
begin
  inherited;

  Open();
  InitCoverDatabase();
end;

destructor TCoverDatabase.Destroy;
begin
  DB.Free;
  inherited;
end;

function TCoverDatabase.GetVersion(): integer;
begin
  Result := DB.GetTableValue('PRAGMA user_version');
end;

procedure TCoverDatabase.SetVersion(Version: integer);
begin
  DB.ExecSQL(Format('PRAGMA user_version = %d', [Version]));
end;

function TCoverDatabase.GetMaxCoverSize(): integer;
begin
  Result := ITextureSizeVals[Ini.TextureSize];
end;

procedure TCoverDatabase.SetMaxCoverSize(Size: integer);
var
  I: integer;
begin
  // search for first valid cover-size > Size
  for I := 0 to Length(ITextureSizeVals)-1 do
  begin
    if (Size <= ITextureSizeVals[I]) then
    begin
      Ini.TextureSize := I;
      Exit;
    end;
  end;

  // fall-back to highest size
  Ini.TextureSize := High(ITextureSizeVals);
end;

procedure TCoverDatabase.Open();
var
  Version: integer;
  Filename: IPath;
begin
  Filename := Platform.GetGameUserPath().Append(COVERDB_FILENAME);

  DB := TSQLiteDatabase.Create(Filename.ToUTF8());
  Version := GetVersion();

  // check version, if version is too old/new, delete database file
  if ((Version <> 0) and (Version <> COVERDB_VERSION)) then
  begin
    Log.LogInfo('Outdated cover-database file found', 'TCoverDatabase.Open');
    // close and delete outdated file
    DB.Free;
    if (not Filename.DeleteFile()) then
      raise ECoverDBException.Create('Could not delete ' + Filename.ToNative);
    // reopen
    DB := TSQLiteDatabase.Create(Filename.ToUTF8());
    Version := 0;
  end;

  // set version number after creation
  if (Version = 0) then
    SetVersion(COVERDB_VERSION);

  // speed-up disk-writing. The default FULL-synchronous mode is too slow.
  // With this option disk-writing is approx. 4 times faster but the database
  // might be corrupted if the OS crashes, although this is very unlikely.
  DB.ExecSQL('PRAGMA synchronous = OFF;');
  
  // the next line rather gives a slow-down instead of a speed-up, so we do not use it
  //DB.ExecSQL('PRAGMA temp_store = MEMORY;');
end;

procedure TCoverDatabase.InitCoverDatabase();
begin
  DB.ExecSQL('CREATE TABLE IF NOT EXISTS ['+COVER_TBL+'] (' +
               '[ID] INTEGER  NOT NULL PRIMARY KEY AUTOINCREMENT, ' +
               '[Filename] TEXT  UNIQUE NOT NULL, ' +
               '[Date] INTEGER  NOT NULL, ' +
               '[Width] INTEGER  NOT NULL, ' +
               '[Height] INTEGER  NOT NULL ' +
             ')');

  DB.ExecSQL('CREATE INDEX IF NOT EXISTS ['+COVER_IDX+'] ON ['+COVER_TBL+'](' +
               '[Filename]  ASC' +
             ')');

  DB.ExecSQL('CREATE TABLE IF NOT EXISTS ['+COVER_THUMBNAIL_TBL+'] (' +
               '[ID] INTEGER  NOT NULL PRIMARY KEY, ' +
               '[Format] INTEGER  NOT NULL, ' +
               '[Width] INTEGER  NOT NULL, ' +
               '[Height] INTEGER  NOT NULL, ' +
               '[Data] BLOB  NULL' +
             ')');
end;

function TCoverDatabase.FindCoverIntern(const Filename: IPath): int64;
var
  FileUTF8String: UTF8String;
begin
  if Filename = nil then
    Result := 0
  else
  begin
    FileUTF8String:=Filename.ToUTF8;
    Result := DB.GetTableValue('SELECT [ID] FROM ['+COVER_TBL+'] ' +
                             'WHERE [Filename] = ?',
                             [FileUTF8String]);
  end;
end;

function TCoverDatabase.FindCover(const Filename: IPath): TCover;
var
  CoverID: int64;
begin
  Result := nil;
  try
    CoverID := FindCoverIntern(Filename);
    if (CoverID > 0) then
      Result := TCover.Create(CoverID, Filename);
  except on E: Exception do
    Log.LogError(E.Message, 'TCoverDatabase.FindCover');
  end;
end;

function TCoverDatabase.CoverExists(const Filename: IPath): boolean;
begin
  Result := false;
  try
    Result := (FindCoverIntern(Filename) > 0);
  except on E: Exception do
    Log.LogError(E.Message, 'TCoverDatabase.CoverExists');
  end;
end;

function TCoverDatabase.AddCover(const Filename: IPath): TCover;
var
  CoverID: int64;
  Thumbnail: PSDL_Surface;
  CoverData: TBlobWrapper;
  FileDate: TDateTime;
  Info: TThumbnailInfo;
begin
  Result := nil;

  //if (not FileExists(Filename)) then
  //  Exit;

  // TODO: replace '\' with '/' in filename
  FileDate := Now(); //FileDateToDateTime(FileAge(Filename));

  Thumbnail := CreateThumbnail(Filename, Info);
  if not (assigned(Thumbnail)) or (Thumbnail = nil) then
    Exit;

  CoverData := TBlobWrapper.Create;
  CoverData.Write(Thumbnail^.pixels, Thumbnail^.h * Thumbnail^.pitch);

  try
    // Note: use a transaction to speed-up file-writing.
    // Without data written by the first INSERT might be moved at the second INSERT. 
    DB.BeginTransaction();

    // add general cover info
    DB.ExecSQL('INSERT INTO ['+COVER_TBL+'] ' +
               '([Filename], [Date], [Width], [Height]) VALUES' +
               '(?, ?, ?, ?)',
               [Filename.ToUTF8, DateTimeToUnixTime(FileDate),
                Info.CoverWidth, Info.CoverHeight]);

    // get auto-generated cover ID
    CoverID := DB.GetLastInsertRowID();

    // add thumbnail info
    DB.ExecSQL('INSERT INTO ['+COVER_THUMBNAIL_TBL+'] ' +
               '([ID], [Format], [Width], [Height], [Data]) VALUES' +
               '(?, ?, ?, ?, ?)',
               [CoverID, Ord(Info.PixelFormat),
                Thumbnail^.w, Thumbnail^.h, CoverData]);

    Result := TCover.Create(CoverID, Filename);
  except on E: Exception do
    Log.LogError(E.Message, 'TCoverDatabase.AddCover');
  end;

  DB.Commit();
  CoverData.Free;
  SDL_FreeSurface(Thumbnail);
end;

function TCoverDatabase.LoadCover(CoverID: int64): TTexture;
var
  Width, Height: integer;
  PixelFmt: TImagePixelFmt;
  Data: PChar;
  DataSize: integer;
  Filename: IPath;
  Table: TSQLiteUniTable;
begin
  Table := nil;

  try
    Table := DB.GetUniTable(Format(
      'SELECT C.[Filename], T.[Format], T.[Width], T.[Height], T.[Data] ' +
      'FROM ['+COVER_TBL+'] C ' +
        'INNER JOIN ['+COVER_THUMBNAIL_TBL+'] T ' +
        'USING(ID) ' +
      'WHERE [ID] = %d', [CoverID]));

    Filename := Path(Table.FieldAsString(0));
    PixelFmt := TImagePixelFmt(Table.FieldAsInteger(1));
    Width    := Table.FieldAsInteger(2);
    Height   := Table.FieldAsInteger(3);

    Data := Table.FieldAsBlobPtr(4, DataSize);
    if (Data <> nil) and
       (PixelFmt = ipfRGB) then
    begin
      Result := Texture.CreateTexture(Data, Filename, Width, Height, 24)
    end
    else
    begin
      // FillChar() does not decrement the ref-count of ref-counted fields
      // -> reset Name field manually
      Result.Name := nil;
      FillChar(Result, SizeOf(TTexture), 0);
    end;
  except on E: Exception do
    Log.LogError(E.Message, 'TCoverDatabase.LoadCover');
  end;

  Table.Free;
end;

procedure TCoverDatabase.DeleteCover(CoverID: int64);
begin
  DB.ExecSQL(Format('DELETE FROM ['+COVER_TBL+'] WHERE [ID] = %d', [CoverID]));
  DB.ExecSQL(Format('DELETE FROM ['+COVER_THUMBNAIL_TBL+'] WHERE [ID] = %d', [CoverID]));
end;

(**
 * Returns a pointer to an array of bytes containing the texture data in the
 * requested size
 *)
function TCoverDatabase.CreateThumbnail(const Filename: IPath; var Info: TThumbnailInfo): PSDL_Surface;
var
  //TargetAspect, SourceAspect: double;
  //TargetWidth, TargetHeight: integer;
  Thumbnail: PSDL_Surface;
  MaxSize: integer;
begin
  Result := nil;

  MaxSize := GetMaxCoverSize();

  Thumbnail := LoadImage(Filename);
  if (not assigned(Thumbnail)) then
  begin
    Log.LogError('Could not load cover: "'+ Filename.ToNative +'"', 'TCoverDatabase.AddCover');
    Exit;
  end;

  // Convert pixel format as needed
  AdjustPixelFormat(Thumbnail, TEXTURE_TYPE_PLAIN);

  Info.CoverWidth  := Thumbnail^.w;
  Info.CoverHeight := Thumbnail^.h;
  Info.PixelFormat := ipfRGB;

  (* TODO: keep aspect ratio
  TargetAspect := Width / Height;
  SourceAspect := TexSurface.w / TexSurface.h;

  // Scale texture to covers dimensions (keep aspect)
  if (SourceAspect >= TargetAspect) then
  begin
    TargetWidth := Width;
    TargetHeight := Trunc(Width / SourceAspect);
  end
  else
  begin
    TargetHeight := Height;
    TargetWidth := Trunc(Height * SourceAspect);
  end;
  *)

  // TODO: do not scale if image is smaller
  ScaleImage(Thumbnail, MaxSize, MaxSize);

  Result := Thumbnail;
end;

end.