{* 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.