реструктуризация файлов, добавление вебчатов

This commit is contained in:
PC1\PTyTb
2025-08-14 10:50:33 +03:00
parent 04b5259737
commit 3ac578b6e6
79 changed files with 10256 additions and 1284 deletions
+651
View File
@@ -0,0 +1,651 @@
unit uDataBase;
interface
uses
System.SysUtils, System.Classes, FireDAC.Comp.Client, FireDAC.Stan.Param,
FMX.Grid, FireDAC.Stan.Def, FireDAC.Stan.Intf, FireDAC.Stan.Option,
FireDAC.Stan.Error, FireDAC.UI.Intf, FireDAC.Phys.Intf, FireDAC.Stan.Pool,
FireDAC.Stan.Async, FireDAC.Phys, FireDAC.Phys.SQLite, FireDAC.Phys.SQLiteDef,
FireDAC.Stan.ExprFuncs, FireDAC.Phys.SQLiteWrapper.Stat, FireDAC.FMXUI.Wait,
FireDAC.DatS, FireDAC.DApt.Intf, FireDAC.DApt, Data.DB, FireDAC.Comp.DataSet,
FireDAC.Comp.UI, uRecords, System.Generics.Collections, System.Rtti, System.TypInfo;
type
TSettingsDatabase = class
private
FConnection: TFDConnection;
procedure InitializeDatabase;
function GetSetting(const Name: string): string;
procedure SetSetting(const Name, Value: string);
function GetColumnsDefinition(Grid: TStringGrid): string;
function GetColumnsList(Grid: TStringGrid): string;
function GetValuesPlaceholders(Grid: TStringGrid): string;
function CheckTableExists(const TableName: string): Boolean;
procedure EnsureTableForRecord(const TableName: string; RecordTypeInfo: PTypeInfo);
// function GetFieldDefinition(Field: TRttiField): string;
function GetSQLType(Field: TRttiField): string;
function TableHasColumn(const TableName, ColumnName: string): Boolean;
public
FChannel: string;
constructor Create(const DatabasePath: string);
destructor Destroy; override;
function ReadSetting(const Name: string; Default: string = ''): string;
procedure WriteSetting(const Name, Value: string);
function getLoginData: TLogin;
procedure SaveGridToTable(const TableName: string; Grid: TStringGrid);
procedure LoadGridFromTable(const TableName: string; Grid: TStringGrid);
procedure LoadUsers(var users: TList<TUser>);
procedure addGroupResponse(Name, Respons: string);
procedure getGroupResponse(aName: string; const lbResponse: TStrings);
procedure getGroupName(const lbName: TStrings);
procedure delGroupName(aName: string);
procedure delGroupResponse(aName, aResponse: string);
procedure SaveRecordArray<T>(const TableName: string; const Items: array of T);
procedure LoadRecordArray<T>(const TableName: string; var Items: TArray<T>);
end;
implementation
uses uGeneral;
constructor TSettingsDatabase.Create(const DatabasePath: string);
begin
FConnection := TFDConnection.Create(nil);
try
FConnection.DriverName := 'SQLite';
FConnection.Params.Database := DatabasePath;
FConnection.Connected := True;
InitializeDatabase;
except
on E: Exception do
begin
FreeAndNil(FConnection);
raise Exception.Create('Îøèáêà ïðè ïîäêëþ÷åíèè ê áàçå äàííûõ: ' + E.Message);
end;
end;
end;
procedure TSettingsDatabase.delGroupName(aName: string);
var
Query: TFDQuery;
begin
Query := TFDQuery.Create(nil);
try
Query.Connection := FConnection;
Query.SQL.Text := 'DELETE FROM GroupResponse WHERE Name = :name';
Query.ParamByName('name').AsString := aName;
Query.ExecSQL;
finally
Query.Free;
end;
end;
procedure TSettingsDatabase.delGroupResponse(aName, aResponse: string);
var
Query: TFDQuery;
begin
Query := TFDQuery.Create(nil);
try
Query.Connection := FConnection;
Query.SQL.Text := 'DELETE FROM GroupResponse WHERE Name = :name AND Response = :response';
Query.ParamByName('name').AsString := aName;
Query.ParamByName('response').AsString := aResponse;
Query.ExecSQL;
finally
Query.Free;
end;
end;
destructor TSettingsDatabase.Destroy;
begin
if Assigned(FConnection) then
FConnection.Connected := False;
FreeAndNil(FConnection);
inherited;
end;
procedure TSettingsDatabase.InitializeDatabase;
var
FDQuery: TFDQuery;
FieldExists: Boolean;
begin
FConnection.ExecSQL('CREATE TABLE IF NOT EXISTS params (' +
' name TEXT PRIMARY KEY,' + ' value TEXT' + ');');
FConnection.ExecSQL('CREATE TABLE IF NOT EXISTS users (' +
' id TEXT PRIMARY KEY,' + ' login TEXT,' + ' DisplayName TEXT,' +
' created_at DATETIME,' + ' follow_at DATETIME,' + ' isVip TEXT,' +
' isModer TEXT,' + ' isO TEXT,' + ' streamer TEXT' + ');');
FDQuery := TFDQuery.Create(nil);
try
FDQuery.Connection := FConnection;
FieldExists := False;
FDQuery.SQL.Text := 'PRAGMA table_info(users)';
FDQuery.Open;
while not FDQuery.EOF do
begin
if FDQuery.FieldByName('name').AsString = 'streamer' then
begin
FieldExists := True;
Break;
end;
FDQuery.Next;
end;
FDQuery.Close;
if not FieldExists then
FConnection.ExecSQL('ALTER TABLE users ADD COLUMN streamer TEXT');
finally
FDQuery.Free;
end;
FConnection.ExecSQL('CREATE TABLE IF NOT EXISTS GroupResponse (ID INTEGER PRIMARY KEY, Name TEXT, Response TEXT);');
end;
procedure TSettingsDatabase.LoadUsers(var users: TList<TUser>);
var
Query: TFDQuery;
UserItem: TUser;
begin
Query := TFDQuery.Create(nil);
try
Query.Connection := FConnection;
Query.SQL.Text := 'SELECT * FROM users WHERE streamer = :streamer';
Query.ParamByName('streamer').AsString := FChannel;
Query.Open;
while not Query.EOF do
begin
UserItem.id := Query.FieldByName('id').AsString;
UserItem.login := Query.FieldByName('login').AsString;
UserItem.DisplayName := Query.FieldByName('DisplayName').AsString;
UserItem.created_at := Query.FieldByName('created_at').AsDateTime;
UserItem.follow_at := Query.FieldByName('follow_at').AsDateTime;
UserItem.isVip := Query.FieldByName('isVip').AsString = 'True';
UserItem.isModer := Query.FieldByName('isModer').AsString = 'True';
UserItem.isO := Query.FieldByName('isO').AsString = 'True';
UserItem.isO_today := False;
users.Add(UserItem);
Query.Next;
end;
finally
Query.Free;
end;
end;
function TSettingsDatabase.GetColumnsDefinition(Grid: TStringGrid): string;
var
Col: Integer;
begin
Result := '';
for Col := 0 to Grid.ColumnCount - 1 do
begin
if Result <> '' then
Result := Result + ', ';
Result := Result + 'col' + IntToStr(Col) + ' TEXT';
end;
end;
function TSettingsDatabase.GetColumnsList(Grid: TStringGrid): string;
var
Col: Integer;
begin
Result := '';
for Col := 0 to Grid.ColumnCount - 1 do
begin
if Result <> '' then
Result := Result + ', ';
Result := Result + 'col' + IntToStr(Col);
end;
end;
procedure TSettingsDatabase.getGroupName(const lbName: TStrings);
var
Query: TFDQuery;
begin
Query := TFDQuery.Create(nil);
try
Query.Connection := FConnection;
lbName.Clear;
Query.SQL.Text := 'SELECT DISTINCT Name FROM GroupResponse';
Query.Open;
while not Query.EOF do
begin
lbName.Add(Query.FieldByName('Name').AsString);
Query.Next;
end;
finally
Query.Free;
end;
end;
procedure TSettingsDatabase.getGroupResponse(aName: string; const lbResponse: TStrings);
var
Query: TFDQuery;
begin
Query := TFDQuery.Create(nil);
try
Query.Connection := FConnection;
lbResponse.Clear;
Query.SQL.Text := 'SELECT Response FROM GroupResponse WHERE Name = :name';
Query.ParamByName('name').AsString := aName;
Query.Open;
while not Query.EOF do
begin
lbResponse.Add(Query.FieldByName('Response').AsString);
Query.Next;
end;
finally
Query.Free;
end;
end;
function TSettingsDatabase.getLoginData: TLogin;
begin
Result.TTV_Token_Bot := ReadSetting('edtBotToken');
Result.TTV_Token_Strimer := ReadSetting('edtBotTokenStreamer');
Result.TTV_Name_Bot := ReadSetting('edtBotName');
Result.TTV_Name_Strimer := ReadSetting('edtChannel');
Result.TTV_ClientID := ReadSetting('edtBotClientID');
Result.DA_ClientID := ReadSetting('edtDAClientID');
Result.DA_Client_Sictert := ReadSetting('edtDAClientSecret');
Result.DA_RedirectURL := ReadSetting('edtDARedirectURL');
Result.DA_Code := ReadSetting('edtDACode');
Result.AI_Gigachat_ClientID := ReadSetting('edtGPTClientID');
Result.AI_Gigachat_AutorizationCode := ReadSetting('edtGPTAC');
Result.AI_ChatGPT_Token := ReadSetting('edtGPTATChatGPT');
Result.AI_DeepSeek_Token := ReadSetting('edtGPTATDeepSeek');
end;
function TSettingsDatabase.GetValuesPlaceholders(Grid: TStringGrid): string;
var
Col: Integer;
begin
Result := '';
for Col := 0 to Grid.ColumnCount - 1 do
begin
if Result <> '' then
Result := Result + ', ';
Result := Result + ':col' + IntToStr(Col);
end;
end;
procedure TSettingsDatabase.addGroupResponse(Name, Respons: string);
var
Query: TFDQuery;
begin
Query := TFDQuery.Create(nil);
try
Query.Connection := FConnection;
Query.SQL.Text := 'INSERT INTO GroupResponse (Name, Response) VALUES (:name, :response)';
Query.ParamByName('name').AsString := Name;
Query.ParamByName('response').AsString := Respons;
Query.ExecSQL;
finally
Query.Free;
end;
end;
function TSettingsDatabase.CheckTableExists(const TableName: string): Boolean;
var
Query: TFDQuery;
begin
Result := False;
Query := TFDQuery.Create(nil);
try
Query.Connection := FConnection;
Query.SQL.Text := 'SELECT COUNT(*) FROM sqlite_master WHERE type=''table'' AND name=:TableName';
Query.ParamByName('TableName').AsString := TableName;
Query.Open;
Result := (Query.Fields[0].AsInteger > 0);
finally
Query.Free;
end;
end;
procedure TSettingsDatabase.LoadGridFromTable(const TableName: string; Grid: TStringGrid);
var
Query: TFDQuery;
Col, Row: Integer;
begin
if not CheckTableExists(TableName) then Exit;
Query := TFDQuery.Create(nil);
try
Query.Connection := FConnection;
Query.SQL.Text := 'SELECT * FROM ' + TableName;
Query.Open;
Grid.RowCount := 0;
while not Query.EOF do
begin
Row := Grid.RowCount;
Grid.RowCount := Grid.RowCount + 1;
for Col := 0 to Grid.ColumnCount - 1 do
Grid.Cells[Col, Row] := Query.FieldByName('col' + IntToStr(Col)).AsString;
Query.Next;
end;
finally
Query.Free;
end;
end;
procedure TSettingsDatabase.SaveGridToTable(const TableName: string; Grid: TStringGrid);
var
Query: TFDQuery;
Col, Row: Integer;
begin
FConnection.ExecSQL('DROP TABLE IF EXISTS ' + TableName);
FConnection.ExecSQL('CREATE TABLE ' + TableName + ' (id INTEGER PRIMARY KEY AUTOINCREMENT, ' + GetColumnsDefinition(Grid) + ');');
Query := TFDQuery.Create(nil);
try
Query.Connection := FConnection;
for Row := 0 to Grid.RowCount - 1 do
begin
Query.SQL.Text := 'INSERT INTO ' + TableName + ' (' + GetColumnsList(Grid) + ') VALUES (' + GetValuesPlaceholders(Grid) + ')';
for Col := 0 to Grid.ColumnCount - 1 do
Query.ParamByName('col' + IntToStr(Col)).AsString := Grid.Cells[Col, Row];
Query.ExecSQL;
end;
finally
Query.Free;
end;
end;
function TSettingsDatabase.GetSetting(const Name: string): string;
var
Query: TFDQuery;
begin
Query := TFDQuery.Create(nil);
try
Query.Connection := FConnection;
Query.SQL.Text := 'SELECT value FROM params WHERE name = :name';
Query.ParamByName('name').AsString := Name;
Query.Open;
if not Query.IsEmpty then
Result := Query.FieldByName('value').AsString
else
Result := '';
finally
Query.Free;
end;
end;
procedure TSettingsDatabase.SetSetting(const Name, Value: string);
begin
FConnection.ExecSQL('INSERT OR REPLACE INTO params (name, value) VALUES (:name, :value)', [Name, Value]);
end;
function TSettingsDatabase.ReadSetting(const Name: string; Default: string = ''): string;
begin
Result := GetSetting(Name);
if Result = '' then
Result := Default;
end;
procedure TSettingsDatabase.WriteSetting(const Name, Value: string);
begin
SetSetting(Name, Value);
end;
function TSettingsDatabase.TableHasColumn(const TableName, ColumnName: string): Boolean;
var
Query: TFDQuery;
begin
Query := TFDQuery.Create(nil);
try
Query.Connection := FConnection;
Query.SQL.Text := Format('PRAGMA table_info(%s)', [TableName]);
Query.Open;
Result := False;
while not Query.EOF do
begin
if SameText(Query.FieldByName('name').AsString, ColumnName) then
Exit(True);
Query.Next;
end;
finally
Query.Free;
end;
end;
function TSettingsDatabase.GetSQLType(Field: TRttiField): string;
begin
case Field.FieldType.TypeKind of
tkInteger, tkInt64:
Result := 'INTEGER';
tkFloat:
Result := 'REAL';
tkUString, tkString, tkWString, tkLString:
Result := 'TEXT';
tkEnumeration:
if Field.FieldType = TypeInfo(Boolean) then
Result := 'BOOLEAN'
else
Result := 'INTEGER';
else
raise Exception.CreateFmt('Unsupported type for field %s', [Field.Name]);
end;
end;
procedure TSettingsDatabase.EnsureTableForRecord(const TableName: string; RecordTypeInfo: PTypeInfo);
var
Context: TRttiContext;
RttiType: TRttiType;
Field: TRttiField;
FieldDefs: string;
Query: TFDQuery;
begin
if not CheckTableExists(TableName) then
begin
Context := TRttiContext.Create;
RttiType := Context.GetType(RecordTypeInfo);
FieldDefs := '';
for Field in RttiType.GetFields do
begin
if FieldDefs <> '' then
FieldDefs := FieldDefs + ', ';
FieldDefs := FieldDefs + Field.Name + ' ' + GetSQLType(Field);
end;
FConnection.ExecSQL(Format('CREATE TABLE %s (%s)', [TableName, FieldDefs]));
end
else
begin
Context := TRttiContext.Create;
RttiType := Context.GetType(RecordTypeInfo);
Query := TFDQuery.Create(nil);
try
Query.Connection := FConnection;
for Field in RttiType.GetFields do
begin
if not TableHasColumn(TableName, Field.Name) then
begin
Query.SQL.Text := Format('ALTER TABLE %s ADD COLUMN %s %s', [TableName, Field.Name, GetSQLType(Field)]);
Query.ExecSQL;
end;
end;
finally
Query.Free;
end;
end;
end;
procedure TSettingsDatabase.SaveRecordArray<T>(const TableName: string; const Items: array of T);
var
Context: TRttiContext;
RttiType: TRttiType;
Fields: TArray<TRttiField>;
Query: TFDQuery;
Rec: T;
Field: TRttiField;
FieldNames, Placeholders: string;
i, j: Integer;
Value: TValue;
Param: TFDParam;
begin
if Length(Items) = 0 then
begin
FConnection.ExecSQL('DELETE FROM ' + TableName);
exit;
end;
EnsureTableForRecord(TableName, TypeInfo(T));
Context := TRttiContext.Create;
RttiType := Context.GetType(TypeInfo(T));
Fields := RttiType.GetFields;
Query := TFDQuery.Create(nil);
try
Query.Connection := FConnection;
// Ïîñòðîåíèå ñïèñêà ïîëåé è ïëåéñõîëäåðîâ
FieldNames := '';
Placeholders := '';
for j := 0 to High(Fields) do
begin
if j > 0 then
begin
FieldNames := FieldNames + ', ';
Placeholders := Placeholders + ', ';
end;
FieldNames := FieldNames + Fields[j].Name;
Placeholders := Placeholders + ':' + Fields[j].Name;
end;
Query.SQL.Text := 'INSERT INTO ' + TableName + ' (' + FieldNames + ') VALUES (' + Placeholders + ')';
// Ïîäãîòàâëèâàåì çàïðîñ (ñîçäàåò ïàðàìåòðû)
//Query.Prepare;
// Óñòàíàâëèâàåì ðàçìåð ìàññèâà ïàðàìåòðîâ
Query.Params.ArraySize := Length(Items);
// Íà÷àëî òðàíçàêöèè
FConnection.StartTransaction;
try
// Î÷èñòêà òàáëèöû ïåðåä âñòàâêîé íîâûõ äàííûõ
FConnection.ExecSQL('DELETE FROM ' + TableName);
i := 0;
for Rec in Items do
begin
for Field in Fields do
begin
Value := Field.GetValue(@Rec);
// Íàõîäèì ïàðàìåòð
Param := Query.FindParam(Field.Name);
if not Assigned(Param) then
raise Exception.CreateFmt('Parameter "%s" not found in query', [Field.Name]);
// Îáðàáîòêà NULL-çíà÷åíèé
if Value.IsEmpty then
begin
Param.Clear(i);
end
else
begin
// Îáðàáîòêà ïî òèïó
case Field.FieldType.TypeKind of
tkInteger, tkInt64:
Param.AsIntegers[i] := Value.AsInteger;
tkFloat:
if (Field.FieldType = TypeInfo(TDateTime)) or
(Field.FieldType = TypeInfo(TDate)) or
(Field.FieldType = TypeInfo(TTime)) then
Param.AsDateTimes[i] := Value.AsExtended
else
Param.AsFloats[i] := Value.AsExtended;
tkEnumeration:
if Field.FieldType = TypeInfo(Boolean) then
Param.AsBooleans[i] := Value.AsBoolean
else
Param.AsIntegers[i] := Value.AsOrdinal;
tkUString, tkString, tkWString, tkLString:
Param.AsStrings[i] := Value.AsString;
else
Param.AsStrings[i] := Value.ToString;
end;
end;
end;
Inc(i);
end;
// Ïàêåòíîå âûïîëíåíèå
Query.Execute(Length(Items), 0);
FConnection.Commit;
except
FConnection.Rollback;
raise;
end;
finally
Query.Free;
end;
end;
procedure TSettingsDatabase.LoadRecordArray<T>(const TableName: string; var Items: TArray<T>);
var
Context: TRttiContext;
RttiType: TRttiType;
Fields: TArray<TRttiField>;
Query: TFDQuery;
Rec: T;
RecList: TList<T>;
Field: TRttiField;
begin
if not CheckTableExists(TableName) then
begin
SetLength(Items, 0);
Exit;
end;
EnsureTableForRecord(TableName, TypeInfo(T));
Context := TRttiContext.Create;
try
RttiType := Context.GetType(TypeInfo(T));
Fields := RttiType.GetFields;
Query := TFDQuery.Create(nil);
RecList := TList<T>.Create;
try
Query.Connection := FConnection;
Query.SQL.Text := Format('SELECT * FROM %s', [TableName]);
Query.Open;
while not Query.EOF do
begin
Rec := Default(T);
for Field in Fields do
begin
if Query.FindField(Field.Name) <> nil then
Field.SetValue(@Rec, TValue.FromVariant(Query.FieldByName(Field.Name).Value));
end;
RecList.Add(Rec);
Query.Next;
end;
Items := RecList.ToArray;
finally
Query.Free;
RecList.Free;
end;
finally
Context.Free;
end;
end;
end.
+158
View File
@@ -0,0 +1,158 @@
unit uMyTimer;
interface
uses
System.Classes, System.SyncObjs, System.SysUtils;
type
TTimerExec = procedure(Sender: TObject; const txt: string; o: Boolean) of object;
TMyTimerThread = class(TThread)
private
FEvent: TEvent;
FCriticalSection: TCriticalSection;
FInterval: Integer;
FText: string;
FFlagO: Boolean;
FEnabled: Boolean;
FOnTimerExec: TTimerExec;
procedure SyncTimerEvent;
protected
procedure Execute; override;
public
constructor Create(AIntervalMinutes: Integer; const AText: string; AFlagO: Boolean);
destructor Destroy; override;
procedure StartT;
procedure StopT;
procedure TerminateAndDestroy;
procedure Update(AIntervalMinutes: Integer; const AText: string; AFlagO: Boolean);
property OnTimerExec: TTimerExec read FOnTimerExec write FOnTimerExec;
end;
implementation
{ TMyTimerThread }
constructor TMyTimerThread.Create(AIntervalMinutes: Integer; const AText: string; AFlagO: Boolean);
begin
inherited Create(True);
FreeOnTerminate := False;
FEvent := TEvent.Create(nil, True, False, '');
FCriticalSection := TCriticalSection.Create;
FInterval := AIntervalMinutes * 60 * 1000;
FText := AText;
FFlagO := AFlagO;
FEnabled := False;
end;
destructor TMyTimerThread.Destroy;
begin
StopT;
Terminate;
FEvent.SetEvent;
if not Suspended then
WaitFor;
FreeAndNil(FEvent);
FreeAndNil(FCriticalSection);
inherited;
end;
procedure TMyTimerThread.Execute;
var
WaitResult: TWaitResult;
LocalInterval: Integer;
begin
while not Terminated do
begin
FCriticalSection.Enter;
try
if FEnabled then
LocalInterval := FInterval
else
LocalInterval := INFINITE;
finally
FCriticalSection.Leave;
end;
WaitResult := FEvent.WaitFor(LocalInterval);
FCriticalSection.Enter;
try
if FEnabled and (WaitResult = wrTimeout) then
begin
if Assigned(FOnTimerExec) then
SyncTimerEvent;
end;
finally
FCriticalSection.Leave;
end;
FEvent.ResetEvent;
end;
end;
procedure TMyTimerThread.StartT;
begin
FCriticalSection.Enter;
try
FEnabled := True;
Suspended:=false;
finally
FCriticalSection.Leave;
end;
if Suspended then
Start;
FEvent.SetEvent;
end;
procedure TMyTimerThread.StopT;
begin
FCriticalSection.Enter;
try
FEnabled := False;
Suspended:=true;
finally
FCriticalSection.Leave;
end;
FEvent.SetEvent;
end;
procedure TMyTimerThread.SyncTimerEvent;
var
LText: string;
LFlag: Boolean;
begin
FCriticalSection.Enter;
try
LText := FText;
LFlag := FFlagO;
finally
FCriticalSection.Leave;
end;
if Assigned(FOnTimerExec) then
FOnTimerExec(Self, LText, LFlag);
end;
procedure TMyTimerThread.TerminateAndDestroy;
begin
StopT;
Terminate;
Free;
end;
procedure TMyTimerThread.Update(AIntervalMinutes: Integer; const AText: string; AFlagO: Boolean);
begin
FCriticalSection.Enter;
try
FInterval := AIntervalMinutes * 60 * 1000;
FText := AText;
FFlagO := AFlagO;
finally
FCriticalSection.Leave;
end;
FEvent.SetEvent;
end;
end.
+13
View File
@@ -0,0 +1,13 @@
object OBS_Doc_Player: TOBS_Doc_Player
OnCreate = WebModuleCreate
OnDestroy = WebModuleDestroy
Actions = <
item
Default = True
Name = 'DefaultHandler'
PathInfo = '/'
OnAction = WebModule1DefaultHandlerAction
end>
Height = 230
Width = 415
end
+273
View File
@@ -0,0 +1,273 @@
unit uOBS_Doc_Player;
interface
uses
System.SysUtils, System.Classes, Web.HTTPApp, FMX.Types,
FMX.Controls3D, FMX.Objects3D, FMX.Controls, FMX.Forms, FMX.StdCtrls,
FMX.Edit, FMX.ListBox, uPlayerThread, bass_simple;
type
TOBS_Doc_Player = class(TWebModule)
procedure WebModule1DefaultHandlerAction(Sender: TObject;
Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
procedure TrackBar1Change(Sender: TObject);
procedure WebModuleCreate(Sender: TObject);
procedure WebModuleDestroy(Sender: TObject);
private
{ Private declarations }
function gethtml(): string;
procedure AddToListBox(Text: String);
procedure DelFromListBox(Text: String);
procedure add(const aTitle: string);
procedure del(const aTitle: string);
{ Private declarations }
public
{ Public declarations }
end;
var
OBS_Doc_Player: TComponentClass = TOBS_Doc_Player;
Button1: TButton;
Button2: TButton;
ProgressBar1: TProgressBar;
TrackBar1: TTrackBar;
ListBox1: TListBox;
b: TBassSimple;
player: TPlayerThread;
mVolume: Integer;
isplay: string;
mySoundPath: string;
implementation
{%CLASSGROUP 'FMX.Controls.TControl'}
{$R *.dfm}
{ TOBS_Doc_Player }
function GetPathToTestExe: string; // âåðíåò ïàïêó romaming
begin
Result := GetEnvironmentVariable('APPDATA');
if Result <> '' then
Result := IncludeTrailingPathDelimiter(Result);
end;
procedure TOBS_Doc_Player.add(const aTitle: string);
begin
AddToListBox(aTitle);
end;
procedure TOBS_Doc_Player.AddToListBox(Text: String);
begin
if ListBox1.Items.IndexOf(Text) = -1 then
ListBox1.Items.add(Text);
end;
procedure TOBS_Doc_Player.Button1Click(Sender: TObject);
begin
b.Pause;
if isplay = '0' then
isplay := '1'
else
isplay := '0'
end;
procedure TOBS_Doc_Player.Button2Click(Sender: TObject);
begin
player.Skip;
end;
procedure TOBS_Doc_Player.del(const aTitle: string);
begin
DelFromListBox(aTitle);
end;
procedure TOBS_Doc_Player.DelFromListBox(Text: String);
var
i: Integer;
begin
Application.ProcessMessages;
i := ListBox1.Items.IndexOf(Text);
if i <> -1 then
if i <= ListBox1.Items.Count - 1 then
ListBox1.Items.Delete(i);
Application.ProcessMessages;
end;
function TOBS_Doc_Player.gethtml: string;
var
s: string;
i: Integer;
playIcon: string;
begin
// Îïðåäåëÿåì èêîíêó â çàâèñèìîñòè îò ñîñòîÿíèÿ
if isplay = '1' then
playIcon := 'fa-pause'
else
playIcon := 'fa-play';
// Ñîáèðàåì ýëåìåíòû ListBox â ñòðîêó
s := '';
for i := 0 to ListBox1.Items.Count - 1 do
s := s + '<li>' + ListBox1.Items[i] + '</li>';
// Ãåíåðàöèÿ HTML-ñòðàíèöû ñ îáíîâëåííûìè çíà÷åíèÿìè
Result := '<html>' + '<head><title>Web Radio</title>' +
'<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">'
+ '<style>' + 'body {' + 'background-color: #1d1f21;' + 'color: #ffffff;' +
'font-family: "Arial", sans-serif;' + 'text-align: center;' + 'margin: 0;' +
'padding: 20px;' + 'display: flex;' + 'flex-direction: column;' +
'align-items: center;' + '}' +
'.controls {' + 'display: flex;' + 'gap: 20px;' + 'align-items: center;' +
'margin: 20px 0;' + '}' +
'.icon-btn {' + 'cursor: pointer;' + 'font-size: 24px;' + 'color: #4e8b31;'
+ 'transition: transform 0.2s, color 0.2s;' + 'background: none;' +
'border: none;' + 'padding: 10px;' + '}' +
'.icon-btn:hover {' + 'color: #5f9c42;' + 'transform: scale(1.1);' + '}' +
'.volume-container {' + 'margin: 20px 0;' + '}' +
'ul {' + 'list-style-type: none;' + 'padding: 0;' + 'color: #ccc;' +
'max-width: 500px;' + 'width: 100%;' + '}' +
'li {' + 'background-color: #333;' + 'margin: 5px;' + 'padding: 15px;' +
'border-radius: 5px;' + 'text-align: left;' + '}' +
'#currentVolume {' + 'font-size: 16px;' + 'color: #ddd;' +
'margin-bottom: 10px;' + '}' + '</style>' +
'<script>' + 'function setVolume(val) {' +
' fetch("/setVolume?value=" + val); ' +
' document.getElementById("currentVolume").innerText = "Ãðîìêîñòü: " + val + "%";'
+ '} ' +
'function btn(val) {' + ' if (val == 1) {' +
' const icon = document.getElementById("playIcon");' +
' icon.classList.toggle("fa-play");' +
' icon.classList.toggle("fa-pause");' + ' fetch("/button1");' + ' }'
+ ' if (val == 2) fetch("/button2");' + '}' + '</script>' + '</head>' +
'<body>' + '<div class="controls">' +
'<i id="playIcon" class="icon-btn fas ' + playIcon +
'" onclick="btn(1)"></i>' +
'<i class="icon-btn fas fa-forward" onclick="btn(2)"></i>' + '</div>' +
'<div class="volume-container">' + '<label id="currentVolume">Ãðîìêîñòü: ' +
IntToStr(mVolume) + '%</label>' +
'<input type="range" min="0" max="100" value="' + IntToStr(mVolume) +
'" onchange="setVolume(this.value)" />' + '</div>' +
'<ul id="songList">' + s + '</ul>' +
'<script>' + 'function updateSongList() {' + ' fetch("/getSongs")' +
' .then(response => response.json())' + ' .then(data => {' +
' const listElement = document.getElementById("songList");' +
' listElement.innerHTML = "";' + ' data.forEach(song => {' +
' const li = document.createElement("li");' +
' li.textContent = song;' + ' listElement.appendChild(li);' +
' });' + ' })' +
' .catch(error => console.error("Error fetching songs:", error));' + '}'
+ 'setInterval(updateSongList, 3000);' + '</script>' + '</body>' +
'</html>';
end;
procedure TOBS_Doc_Player.TrackBar1Change(Sender: TObject);
begin
mVolume := Round(TrackBar1.Value);
b.Volume := mVolume;
end;
procedure TOBS_Doc_Player.WebModule1DefaultHandlerAction(Sender: TObject;
Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
var
VolumeStr: string;
VolumeValue, i: Integer;
begin
if Request.PathInfo = '/getSongs' then
begin
// Âîçâðàùàåì ñïèñîê ïåñåí â ôîðìàòå JSON
var
songList: string := '[';
for i := 0 to ListBox1.Items.Count - 1 do
begin
songList := songList + '"' + ListBox1.Items[i] + '"';
if i < ListBox1.Items.Count - 1 then
songList := songList + ',';
end;
songList := songList + ']';
Response.Content := songList; // Îòïðàâëÿåì JSON
Response.ContentType := 'application/json;charset=utf8';
Handled := True;
Exit;
end;
if Request.PathInfo = '/setVolume' then
begin
// Ïîëó÷àåì çíà÷åíèå èç ïàðàìåòðà 'value'
VolumeStr := Request.QueryFields.Values['value'];
// Ïðîáóåì ïðåîáðàçîâàòü â öåëîå ÷èñëî
if TryStrToInt(VolumeStr, VolumeValue) then
begin
// Åñëè óäàëîñü ïðåîáðàçîâàòü, îáíîâëÿåì ïåðåìåííóþ mVolume
mVolume := VolumeValue;
// Îáíîâëÿåì çâóê íà ñåðâåðå (åñëè íóæíî, íàïðèìåð, ÷åðåç áèáëèîòåêó bass)
b.Volume := mVolume;
// Âîçâðàùàåì îáíîâëåííîå ñîñòîÿíèå íà ñòðàíèöó
Response.Content := gethtml;
end
else
begin
// Åñëè íå óäàëîñü ïðåîáðàçîâàòü, âîçâðàùàåì îøèáêó
Response.Content := 'Invalid volume value';
end;
Handled := True; // Çàïðîñ îáðàáîòàí
Exit;
end;
if Request.PathInfo = '/button1' then
begin
Button1Click(Sender); // Âûçîâ ïðîöåäóðû äëÿ êíîïêè 1
Response.Content := gethtml;
Handled := True; // Óêàçûâàåì, ÷òî çàïðîñ áûë îáðàáîòàí
Exit;
end;
if Request.PathInfo = '/button2' then
begin
Button2Click(Sender); // Âûçîâ ïðîöåäóðû äëÿ êíîïêè 2
Response.Content := gethtml;
Handled := True; // Óêàçûâàåì, ÷òî çàïðîñ áûë îáðàáîòàí
Exit;
end;
Response.Content := gethtml;
Handled := True;
end;
procedure TOBS_Doc_Player.WebModuleCreate(Sender: TObject);
begin
mySoundPath := GetPathToTestExe + 'TTW_Bot\ytSongs';
b := TBassSimple.Create(0);
player := TPlayerThread.Create(b, mySoundPath);
player.OnAddAd := add;
player.OnSkip := del;
mVolume := 0;
ListBox1 := TListBox.Create(self);
player.Start;
isplay := '1';
end;
procedure TOBS_Doc_Player.WebModuleDestroy(Sender: TObject);
begin
player.Free;
b.Free;
ListBox1.Free;
end;
end.
+183
View File
@@ -0,0 +1,183 @@
unit uPlayerThread;
interface
uses
Classes, SysUtils, SyncObjs, Generics.Collections, bass_simple,
System.IOUtils, Types;
type
TOnError = procedure(const Msg, FileName: string) of object;
TOnSkip = procedure(const FileName: string) of object;
TPlayerThread = class(TThread)
private
b: tbasssimple;
FFilesQueue: TList<string>;
FQueueCS: TCriticalSection;
FCurrentFile: string;
FOnError: TOnError;
FOnSkip: TOnSkip;
FOnAddAd: TOnSkip;
FMusicFolder: string;
FIsPlaying: Boolean;
procedure PlayCurrentFile;
procedure ScanFolder;
procedure OnPlayHandler(Sender: TObject);
procedure OnStopHandler(Sender: TObject);
procedure OnEndPlayHandler(Sender: TObject);
protected
procedure Execute; override;
public
constructor Create(var ab: tbasssimple; const aFolder: string);
destructor Destroy; override;
procedure Skip;
property OnError: TOnError read FOnError write FOnError;
property OnSkip: TOnSkip read FOnSkip write FOnSkip;
property OnAddAd: TOnSkip read FOnAddAd write FOnAddAd;
end;
implementation
constructor TPlayerThread.Create(var ab: tbasssimple; const aFolder: string);
begin
inherited Create(True);
b := ab;
FMusicFolder := aFolder;
FIsPlaying := False;
b.OnPlay := OnPlayHandler;
b.OnStop := OnStopHandler;
b.OnEndPlay := OnEndPlayHandler;
FFilesQueue := TList<string>.Create;
FQueueCS := TCriticalSection.Create;
end;
destructor TPlayerThread.Destroy;
begin
FQueueCS.Enter;
try
FFilesQueue.Free;
finally
FQueueCS.Leave;
end;
FQueueCS.Free;
inherited;
end;
procedure TPlayerThread.Execute;
begin
while not Terminated do
begin
ScanFolder;
if (not FIsPlaying) then
begin
FQueueCS.Enter;
try
if (FFilesQueue.Count > 0) then
begin
FCurrentFile := FFilesQueue[0];
FFilesQueue.Delete(0);
end;
finally
FQueueCS.Leave;
end;
PlayCurrentFile;
end;
Sleep(1000);
end;
end;
procedure TPlayerThread.PlayCurrentFile;
begin
if (FCurrentFile = '') or (not FileExists(FCurrentFile)) then
begin
if Assigned(FOnError) then
FOnError('Ôàéë íå íàéäåí', FCurrentFile);
Exit;
end;
// Ñîçäàåì íîâûé ïîòîê è íà÷èíàåì âîñïðîèçâåäåíèå
TThread.Synchronize(nil,
procedure
begin
b.Play(FCurrentFile);
end);
FIsPlaying := True;
end;
procedure TPlayerThread.ScanFolder;
var
Files: TStringDynArray;
FileName: string;
begin
Files := TDirectory.GetFiles(FMusicFolder, '*.mp3');
FQueueCS.Enter;
try
for FileName in Files do
if FFilesQueue.IndexOf(FileName) = -1 then
begin
Sleep(5000);
FFilesQueue.Add(FileName);
if Assigned(FOnAddAd) then
TThread.Synchronize(nil,
procedure
begin
FOnAddAd(ExtractFileName(FileName));
end);
end;
finally
FQueueCS.Leave;
end;
end;
procedure TPlayerThread.OnPlayHandler(Sender: TObject);
begin
FIsPlaying := True;
end;
procedure TPlayerThread.OnStopHandler(Sender: TObject);
begin
FIsPlaying := False;
end;
procedure TPlayerThread.OnEndPlayHandler(Sender: TObject);
begin
try
b.Stop;
b.FreeStream;
if FileExists(FCurrentFile) then
begin
DeleteFile(FCurrentFile);
if Assigned(FOnSkip) then
FOnSkip(ExtractFileName(FCurrentFile));
end;
FCurrentFile := '';
FIsPlaying := False;
except
end;
end;
procedure TPlayerThread.Skip;
begin
if FIsPlaying then
begin
b.Stop;
b.FreeStream;
if FileExists(FCurrentFile) then
begin
DeleteFile(FCurrentFile);
if Assigned(FOnSkip) then
FOnSkip(ExtractFileName(FCurrentFile));
end;
FCurrentFile := '';
FIsPlaying := False;
end;
end;
end.
+482
View File
@@ -0,0 +1,482 @@
unit uRecords;
interface
type
TRLog = record
rTime: ttime;
rType: string;
rModule: string;
rMethod: string;
rMessage: string;
end;
type
TCounter = record
counterName: string;
trigger: string;
count: integer;
auto: integer;
end;
type
TBanWord = record
regexp: string;
end;
type
TListTimer = record
Enable: integer;
interval: integer;
o: integer;
mess: string;
end;
type
TOBSKandinsky = record
port: integer;
end;
type
TOBSNotify = record
Picture: string;
Sound: string;
ColorBlock: string;
SolidBorder: integer;
Paddings: integer;
ColorBorder: integer;
ColorBackground: integer;
HeaderText: string;
HeaderColorFont: integer;
HeaderSizeFont: integer;
HeaderStyleFont: integer;
MessText: string;
MessColorFont: integer;
MessSizeFont: integer;
MessStyleFont: integer;
TimeMess: integer;
TypeEvent: integer;
TypeEdit: string;
port: integer;
end;
type
TOBSChat = record
ColorBlock: string;
ColorBorder: integer;
ColorBackground: integer;
ColorFont: integer;
SolidBorder: integer;
Paddings: integer;
SizeFont: integer;
MaxCountMess: integer;
TimeMess: integer;
port: integer;
freez:integer;
StyleFont: integer;
end;
type
TRandomCounters = record
rndName: string;
Ot: integer;
ToValue: integer;
end;
type
TListCommands = record
R1: string;
R2: string;
end;
type
TConst = record
GeneralPath: string;
AppDataPath: string;
DBPath: string;
fontsPath: string;
imgsPath: string;
soundsPath: string;
stlPath: string;
ytSongsPath: string;
SilentPlay: string;
ytPlay: string;
cfg1: string;
end;
type
TLogin = record
TTV_Token_Bot: string;
TTV_Token_Strimer: string;
TTV_Name_Bot: string;
TTV_Name_Strimer: string;
TTV_ClientID: string;
DA_ClientID: string;
DA_Client_Sictert: string;
DA_RedirectURL: string;
DA_Code: string;
AI_Gigachat_ClientID: string;
AI_Gigachat_AutorizationCode: string;
AI_ChatGPT_Token: string;
AI_DeepSeek_Token: string;
end;
var
RLogin: TLogin;
type
TUser = record
id: string;
login: string;
DisplayName: string;
created_at: TDateTime;
follow_at: TDateTime;
isVip: boolean;
isModer: boolean;
isO: boolean;
isO_today: boolean;
end;
TImage = record
Url1x: string;
Url2x: string;
Url4x: string;
end;
{ TReward = record
Id: string;
ChannelId: string;
Title: string;
Prompt: string;
Cost: Integer;
IsUserInputRequired: Boolean;
IsSubOnly: Boolean;
Image: TImage;
DefaultImage: TImage;
BackgroundColor: string;
IsEnabled: Boolean;
IsPaused: Boolean;
IsInStock: Boolean;
MaxPerStream: record
IsEnabled: Boolean;
MaxPerStream: Integer;
end;
ShouldRedemptionsSkipRequestQueue: Boolean;
end; }
{ TRedemption = record
Id: string;
User: TUser;
ChannelId: string;
RedeemedAt: string;
Reward: TReward;
UserInput: string;
Status: string;
end; }
{ TData = record
Timestamp: string;
Topic: string; // Äîáàâëåíî ïîëå Topic
MessageType: string; // Äîáàâëåíî ïîëå MessageType
Redemption: TRedemption;
end; }
{ TRewardRedeemed = record
&Type: string;
Data: TData;
end; }
type
TCustomRevards = record
id: string;
title: string;
promt: string;
cost: integer;
is_user_input_required: boolean;
end;
{ type
THistRevards = record
revard: TRewardRedeemed;
user: user;
is_ok: boolean;
end; }
type
TTwitchChatMessage = record
BadgeInfo: string;
Badges: string;
ClientNonce: string;
Color: string;
DisplayName: string;
Emotes: string;
FirstMsg: integer;
Flags: string;
id: string;
Moder: integer;
ReturningChatter: integer;
RoomId: string;
Subscriber: integer;
TmiSentTs: Int64;
Turbo: integer;
UserId: string;
UserType: string;
Vip: integer;
Username: string;
Channel: string;
Message: string;
end;
type
TBadgeVersion = record
id: string;
ImageUrl1x: string;
ImageUrl2x: string;
ImageUrl4x: string;
title: string;
Description: string;
ClickAction: string;
ClickUrl: string;
end;
TChatBadge = record
SetId: string;
Versions: TArray<TBadgeVersion>;
end;
TEmotes = record
id: string;
name: string;
images: TImage;
tier: string;
emote_type: string;
emote_set_id: string;
format: TArray<string>;
scale: TArray<string>;
theme_mode: TArray<string>;
end;
TStyleChat = record
Nick: string;
Context: string;
MaxMsgCount: integer;
TimeMsg: integer;
FontSize: integer;
FontFamily: string;
FontColor: string;
BorderColor: string;
BColor: string;
BorderSize: integer;
BlockColor: string;
BlockPadding: integer;
end;
TFont = record
Font: string;
Size: integer;
Color: string;
end;
TStyleEvent = record
title: string;
Context: string;
Url: string;
SoundURL: string;
RequireInteraction: boolean;
Timestamp: TDateTime;
TimeMsg: integer;
FontTitle: TFont;
FontContext: TFont;
BorderColor: string;
BorderSize: integer;
BlockColor: string;
end;
type
TBTTVr = record
id: string;
code: string;
end;
type
T7TVr = record
id: string;
code: string;
Url: string;
end;
TCondition = record
broadcaster_user_id: string;
reward_id: string;
end;
TTransport = record
method: string;
end;
TSubscriptionPoints = record
id: string;
subscription_type: string;
// "type" çàðåçåðâèðîâàíî â Delphi, ïîýòîìó èñïîëüçóåì äðóãîå èìÿ
version: string;
status: string;
cost: integer;
condition: TCondition;
transport: TTransport;
created_at: string;
end;
TMaxPerStream = record
is_enabled: boolean;
value: integer;
end;
TMaxPerUserPerStream = record
is_enabled: boolean;
value: integer;
end;
TGlobalCooldown = record
is_enabled: boolean;
seconds: integer;
end;
TReward = record
id: string;
title: string;
cost: integer;
prompt: string;
end;
TEventReward = record
id: string;
broadcaster_user_id: string;
broadcaster_user_login: string;
broadcaster_user_name: string;
user_id: string;
user_login: string;
user_name: string;
user_input: string;
revard: TReward;
end;
TCustomRewardEvent = record
subscription: TSubscriptionPoints;
event: TEventReward;
end;
TMetadata = record
message_id: string;
message_type: string;
message_timestamp: string;
subscription_type: string;
end;
TSession = record
id: string;
status: string;
connected_at: string;
keepalive_timeout_seconds: integer;
reconnect_url: string;
end;
TPayload = record
session: TSession;
end;
TWelcomMessage = record
payload: TPayload;
end;
TEventFollow = record
user_id: string;
user_login: string;
user_name: string;
broadcaster_user_id: string;
broadcaster_user_login: string;
broadcaster_user_name: string;
followed_at: string;
end;
TEventSub = record
user_id: string;
user_login: string;
user_name: string;
broadcaster_user_id: string;
broadcaster_user_login: string;
broadcaster_user_name: string;
tier: string;
is_gift: boolean;
end;
TEventGift = record
user_id: string;
user_login: string;
user_name: string;
broadcaster_user_id: string;
broadcaster_user_login: string;
broadcaster_user_name: string;
total: integer;
tier: string;
cumulative_total: integer; // null if anonymous or not shared by the user
is_anonymous: boolean;
end;
TEventRaid = record
from_broadcaster_user_id: string;
from_broadcaster_user_login: string;
from_broadcaster_user_name: string;
to_broadcaster_user_id: string;
to_broadcaster_user_login: string;
to_broadcaster_user_name: string;
viewers: integer;
end;
TFollowEvent = record
subscription: TSubscriptionPoints;
event: TEventFollow;
end;
TSubEvent = record
subscription: TSubscriptionPoints;
event: TEventSub;
end;
TGiftEvent = record
subscription: TSubscriptionPoints;
event: TEventGift;
end;
TRaidEvent = record
subscription: TSubscriptionPoints;
event: TEventRaid;
end;
TBotAppCfg = record
TTV_ClientID: string;
AI_GigaChat_AC: string;
AI_Gigachat_ClientID: string;
AI_ChatGPT_Token: string;
AI_DeepSeec_Token: string;
DA_ClientID: string;
DA_Sicret: string;
DA_URL: string;
end;
implementation
end.
+4049
View File
File diff suppressed because it is too large Load Diff
+98
View File
@@ -0,0 +1,98 @@
unit uSoundManager;
interface
uses classes, ShellAPI, bass_simple, windows, System.SysUtils;
type
TSongMachine = class(TObject)
private
public
constructor Create;
destructor Destroy;
procedure PlayPublic(AFileName: string; aVolume: string);
procedure PlaySilent(AFileName: string; aVolume: string);
end;
implementation
uses uGeneral;
var
mp: TBassSimple;
{ SongMachine }
constructor TSongMachine.Create;
begin
mp := TBassSimple.Create(0);
end;
destructor TSongMachine.Destroy;
begin
mp.FreeStream;
mp.Free;
end;
function TimeToSeconds(const timeStr: string): Integer;
var
minutes, seconds: Integer;
begin
if TryStrToInt(Copy(timeStr, 1, 2), minutes) and
TryStrToInt(Copy(timeStr, 4, 5), seconds) then
begin
result := minutes * 60 + seconds;
end
else
begin
result := -1;
end;
end;
procedure TSongMachine.PlaySilent(AFileName: string; aVolume: string);
var
sec: string;
mm: TBassSimple;
begin
try
if not FileExists(AFileName) then
begin
TTW_Bot.toLog( 'TSongMachine', 'PlayPublic', 'Íåò ôàéëà ' + AFileName,2);
exit;
end;
mm := TBassSimple.Create(0);
try
mm.OpenFile(AFileName);
sec := inttostr(TimeToSeconds(mm.TimeLength) + 1);
finally
mm.FreeStream;
mm.Free;
end;
ShellExecute(0, 'open', PChar(myConst.SilentPlay),
PChar(Format('%s %s "%s"', [sec, aVolume, AFileName])), nil, SW_HIDE);
except
on e: Exception do
TTW_Bot.toLog( 'TSongMachine', 'PlaySilent', e.message,2)
end;
end;
procedure TSongMachine.PlayPublic(AFileName: string; aVolume: string);
begin
try
if not FileExists(AFileName) then
begin
TTW_Bot.toLog( 'TSongMachine', 'PlayPublic', 'Íåò ôàéëà ' + AFileName,2);
exit;
end;
mp.Volume := strtoint(aVolume);
mp.Play(AFileName)
except
on e: Exception do
TTW_Bot.toLog( 'TSongMachine', 'PlaySilent', e.message,2)
end;
end;
end.
+212
View File
@@ -0,0 +1,212 @@
unit uTTS;
interface
uses
Windows, SysUtils, Classes, MMSystem;
type
TPlayFinishedEvent = procedure(Sender: TObject) of object;
type
TTTS = class
private
FPiperExePath: string;
FVoicesFolder: string;
FCurrentModel: string;
FOnPlayFinished: TPlayFinishedEvent;
procedure DoPlayFinished;
function RunProcessCaptureOutput(const Exe, Args, InputText: string; out OutputText: string): Boolean;
public
constructor Create(const APiperExePath, AVoicesFolder: string);
/// <summary>Ñïèñîê ìîäåëåé (ôàéëû .onnx) â ïàïêå voices</summary>
function GetModelsList: TStringList;
/// <summary>Âûáðàòü ìîäåëü îçâó÷èâàíèÿ (èìÿ ôàéëà .onnx)</summary>
procedure SetModel(const ModelFileName: string);
/// <summary>Îçâó÷èòü òåêñò âûáðàííîé ìîäåëüþ</summary>
procedure TextToSpeech(const aText: string; isDeleteFile: Boolean = False);
property OnPlayFinished: TPlayFinishedEvent read FOnPlayFinished write FOnPlayFinished;
end;
implementation
procedure TTTS.DoPlayFinished;
begin
if Assigned(FOnPlayFinished) then
FOnPlayFinished(Self);
end;
constructor TTTS.Create(const APiperExePath, AVoicesFolder: string);
begin
inherited Create;
FPiperExePath := APiperExePath;
FVoicesFolder := AVoicesFolder;
FCurrentModel := ''; // Ïîêà íå âûáðàíà ìîäåëü
end;
function TTTS.GetModelsList: TStringList;
var
SR: TSearchRec;
begin
Result := TStringList.Create;
if not DirectoryExists(FVoicesFolder) then Exit;
if FindFirst(FVoicesFolder + PathDelim + '*.onnx', faAnyFile, SR) = 0 then
begin
repeat
Result.Add(SR.Name);
until FindNext(SR) <> 0;
FindClose(SR);
end;
end;
procedure TTTS.SetModel(const ModelFileName: string);
var
FullPath: string;
begin
FullPath := IncludeTrailingPathDelimiter(FVoicesFolder) + ModelFileName;
if not FileExists(FullPath) then
raise Exception.CreateFmt('Ìîäåëü íå íàéäåíà: %s', [FullPath]);
FCurrentModel := FullPath;
end;
function TTTS.RunProcessCaptureOutput(const Exe, Args, InputText: string; out OutputText: string): Boolean;
var
SecAttr: TSecurityAttributes;
StdOutRead, StdOutWrite: THandle;
StdInRead, StdInWrite: THandle;
StartupInfo: TStartupInfo;
ProcInfo: TProcessInformation;
Buffer: array [0..4095] of AnsiChar;
BytesRead: Cardinal;
ReadOK: BOOL;
Stream: TStringStream;
InheritHandles: Boolean;
BytesWritten: Cardinal;
Utf8Bytes: TBytes;
begin
Result := False;
OutputText := '';
Stream := TStringStream.Create('', TEncoding.UTF8);
try
SecAttr.nLength := SizeOf(SecAttr);
SecAttr.bInheritHandle := True;
SecAttr.lpSecurityDescriptor := nil;
if not CreatePipe(StdOutRead, StdOutWrite, @SecAttr, 0) then Exit;
try
if not SetHandleInformation(StdOutRead, HANDLE_FLAG_INHERIT, 0) then Exit;
if not CreatePipe(StdInRead, StdInWrite, @SecAttr, 0) then Exit;
try
if not SetHandleInformation(StdInWrite, HANDLE_FLAG_INHERIT, 0) then Exit;
ZeroMemory(@StartupInfo, SizeOf(StartupInfo));
StartupInfo.cb := SizeOf(StartupInfo);
StartupInfo.hStdOutput := StdOutWrite;
StartupInfo.hStdError := StdOutWrite;
StartupInfo.hStdInput := StdInRead;
StartupInfo.dwFlags := STARTF_USESTDHANDLES or STARTF_USESHOWWINDOW;
StartupInfo.wShowWindow := SW_HIDE;
InheritHandles := True;
if not CreateProcess(PChar(Exe), PChar('"' + Exe + '" ' + Args), nil, nil,
InheritHandles, CREATE_NO_WINDOW, nil, nil, StartupInfo, ProcInfo) then Exit;
CloseHandle(StdOutWrite);
CloseHandle(StdInRead);
if InputText <> '' then
begin
Utf8Bytes := TEncoding.UTF8.GetBytes(InputText + #10);
WriteFile(StdInWrite, Utf8Bytes[0], Length(Utf8Bytes), BytesWritten, nil);
end;
CloseHandle(StdInWrite);
repeat
ReadOK := ReadFile(StdOutRead, Buffer, SizeOf(Buffer), BytesRead, nil);
if ReadOK and (BytesRead > 0) then
Stream.Write(Buffer, BytesRead);
until not ReadOK or (BytesRead = 0);
WaitForSingleObject(ProcInfo.hProcess, INFINITE);
CloseHandle(ProcInfo.hThread);
CloseHandle(ProcInfo.hProcess);
OutputText := Stream.DataString;
Result := True;
finally
CloseHandle(StdInWrite);
CloseHandle(StdInRead);
end;
finally
CloseHandle(StdOutRead);
CloseHandle(StdOutWrite);
end;
finally
Stream.Free;
end;
end;
procedure TTTS.TextToSpeech(const aText: string; isDeleteFile: Boolean);
var
Args, Text, Output, WavFile: string;
Lines: TStringList;
i: Integer;
begin
if (FPiperExePath = '') or (FCurrentModel = '') then
raise Exception.Create('Piper.exe èëè ìîäåëü íå óêàçàíû.');
Text := Trim(aText);
if Text = '' then Exit;
// Ôîðìèðóåì àðãóìåíòû - ïóñòü Piper ñîõðàíÿåò wav â òåêóùóþ ïàïêó ñ óíèêàëüíûì èìåíåì
Args := '--model "' + FCurrentModel + '" -f o.wav';
if not RunProcessCaptureOutput(FPiperExePath, Args, Text, Output) then
begin
Exit;
end;
Lines := TStringList.Create;
try
Lines.Text := Output;
WavFile := '';
for i := Lines.Count - 1 downto 0 do
if (Pos('.wav', LowerCase(Lines[i])) > 0) and FileExists(Trim(Lines[i])) then
begin
WavFile := Trim(Lines[i]);
Break;
end;
if WavFile <> '' then
begin
// Çàïóñêàåì â îòäåëüíîì ïîòîêå, ÷òîáû îòñëåäèòü îêîí÷àíèå
TThread.CreateAnonymousThread(
procedure
begin
PlaySound(PChar(WavFile), 0, SND_FILENAME); // áåç SND_ASYNC — æä¸ì îêîí÷àíèÿ
if isDeleteFile then
DeleteFile(WavFile);
TThread.Synchronize(nil,
procedure
begin
DoPlayFinished;
end
);
end
).Start;
end;
finally
Lines.Free;
end;
end;
end.
+293
View File
@@ -0,0 +1,293 @@
unit uWebServerChat;
interface
uses classes, StrUtils, DateUtils, System.JSON, System.Generics.Collections,
IdBaseComponent, IdComponent, IdCustomTCPServer, IdContext,
IdCustomHTTPServer, uRecords, System.IOUtils, IdGlobalProtocols,
IdHTTPServer, System.SysUtils;
type
TTwitchMessage = record
Nickname: string;
Content: string;
Timestamp: TDateTime;
TimeMsg: Integer;
end;
type
TTTW_Chat = class(TObject)
msgStyle: TStyleChat;
fFontsList: tstringlist;
IdHTTPServer1: TIdHTTPServer;
procedure IdHTTPServer1CommandGet(AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
private
FBColor: string;
Messages: TThreadList<TTwitchMessage>;
FDeleteByTime: Boolean; // Ðåæèì óäàëåíèÿ: ïî âðåìåíè (true) èëè êîëè÷åñòâó (false)
FMaxMsgCount: Integer; // Ìàêñèìàëüíîå êîëè÷åñòâî ñîîáùåíèé
function GenerateHTML: string;
function GenerateJSON: string;
procedure CleanupOldMessages;
public
constructor Create(FontList: tstrings; aPort:integer; aColor:string);
destructor Destroy;
procedure addMessage(newMsg: TStyleChat);
procedure ActiveServer(aEn: boolean);
procedure SetDeleteMode(DeleteByTime: Boolean; MaxMsgCount: Integer); // Óñòàíîâêà ðåæèìà óäàëåíèÿ
procedure changeBackground(aColor:string);
end;
var
Timestamp2: string;
implementation
uses uGeneral;
{ TTTW_Chat }
procedure TTTW_Chat.SetDeleteMode(DeleteByTime: Boolean; MaxMsgCount: Integer);
begin
FDeleteByTime := DeleteByTime;
FMaxMsgCount := MaxMsgCount;
end;
procedure TTTW_Chat.ActiveServer(aEn: boolean);
begin
IdHTTPServer1.Active := aEn;
end;
procedure TTTW_Chat.addMessage(newMsg: TStyleChat);
var
Msg: TTwitchMessage;
begin
Msg.Nickname := newMsg.Nick;
Msg.Content := newMsg.Context;
Msg.Timestamp := now;
Msg.TimeMsg := newMsg.TimeMsg;
msgStyle := newMsg;
with Messages.LockList do
try
if not FDeleteByTime then
begin
// Óäàëåíèå ñòàðûõ ñîîáùåíèé ïðè ïðåâûøåíèè ëèìèòà
while Count >= FMaxMsgCount do
Delete(0);
end;
Add(Msg);
finally
Messages.UnlockList;
end;
end;
procedure TTTW_Chat.changeBackground(aColor: string);
begin
FBColor:=aColor;
end;
procedure TTTW_Chat.CleanupOldMessages;
var
MsgList: TList<TTwitchMessage>;
I: integer;
ExpiryTime: TDateTime;
begin
if not FDeleteByTime then
Exit; // Âûõîäèì, åñëè óäàëåíèå ïî âðåìåíè îòêëþ÷åíî
MsgList := Messages.LockList;
try
for I := MsgList.Count - 1 downto 0 do
begin
ExpiryTime := Now - (MsgList[I].TimeMsg / 86400); // Èñïîëüçóåì çíà÷åíèå èç ñîîáùåíèÿ
if MsgList[I].Timestamp < ExpiryTime then
MsgList.Delete(I);
end;
finally
Messages.UnlockList;
end;
end;
constructor TTTW_Chat.Create(FontList: tstrings; aPort:integer;AColor:string);
var
I: integer;
begin
FBColor := AColor;
Messages := TThreadList<TTwitchMessage>.Create;
IdHTTPServer1 := TIdHTTPServer.Create;
IdHTTPServer1.DefaultPort := aPort;
IdHTTPServer1.OnCommandGet := IdHTTPServer1CommandGet;
fFontsList := tstringlist.Create;
for I := 0 to FontList.Count - 1 do
fFontsList.Add(FontList[I]);
FDeleteByTime := True; // Ïî óìîë÷àíèþ óäàëåíèå ïî âðåìåíè
FMaxMsgCount := 100; // Çíà÷åíèå ïî óìîë÷àíèþ
end;
destructor TTTW_Chat.Destroy;
begin
Messages.Free;
IdHTTPServer1.Active := false;
IdHTTPServer1.Free;
fFontsList.Free;
end;
function TTTW_Chat.GenerateHTML: string;
var
I: integer;
s, s1: string;
DeleteByTimeJS: string;
begin
DeleteByTimeJS := LowerCase(BoolToStr(FDeleteByTime)); // Ïðåîáðàçóåì áóëåâî çíà÷åíèå â ñòðîêó 'true' èëè 'false'
s := 'body { background: ' + FBColor + '; }' + #13#10;
for I := 41 to fFontsList.Count - 1 do
begin
s1 := StringReplace(fFontsList[I], '.ttf', '', [rfReplaceAll]);
s := s + '@font-face { font-family: ''' + s1 + '''; src: url(fonts/' + fFontsList[I] + '); }' + #13#10;
end;
Result := '<!DOCTYPE html><html><head>' +
'<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">' +
'<meta http-equiv="Pragma" content="no-cache"><meta http-equiv="Expires" content="0">' +
'<title>Messages</title><style>' + s +
'.message { margin:5px; border-radius:5px; transition: opacity 1s linear; display: flex; align-items: center; }' +
'.message-icon { width: 1.5em; height: 1.5em; margin-right: 0.5em; }' + // Ñòèëü äëÿ èêîíêè
'</style><script>' +
'let existingMessages = new Map(); let fetching = false;' +
'function fetchMessages() {' +
' if (fetching) return; fetching = true;' +
' fetch("/messages").then(response => response.json()).then(data => {' +
' const container = document.getElementById("messages");' +
' const newIds = new Set();' +
' data.forEach(msg => {' +
' const msgId = "msg-" + msg.timestamp;' +
' newIds.add(msgId);' +
' if (!existingMessages.has(msgId)) {' +
' const div = document.createElement("div");' +
' div.className = "message";' +
' div.id = msgId;' +
' div.style = `background-color:${msg.color}; font-family:${msg.family}; ' +
' padding:${msg.padding}px; border: ${msg.sizeBorder}px solid ${msg.colorBorder}; ' +
' color:${msg.colorText}; font-size:${msg.fontSize}px;`;' +
' div.innerHTML = `' +
// ' <img src="${msg.iconUrl}" class="message-icon">' + // Äîáàâëåíà èêîíêà
' <div><b>${msg.nickname}:</b> ${msg.content}</div>`;' +
' div.style.opacity = "1";' +
' container.appendChild(div);' +
' existingMessages.set(msgId, div);' +
' if (deleteByTime) {' + // Óñòàíàâëèâàåì òàéìåð òîëüêî åñëè âêëþ÷åíî óäàëåíèå ïî âðåìåíè
' setTimeout(() => {' +
' div.style.opacity = "0";' +
' setTimeout(() => { div.remove(); existingMessages.delete(msgId); }, 1000);' +
' }, msg.timeMsg * 1000);' +
' }' +
' }' +
' });' +
' existingMessages.forEach((div, msgId) => {' +
' if (!newIds.has(msgId)) { div.remove(); existingMessages.delete(msgId); }' +
' });' +
' }).finally(() => { fetching = false; });' +
'}' +
'setInterval(fetchMessages, 500); fetchMessages();' +
'</script></head><body><div id="messages"></div></body></html>';
end;
function TTTW_Chat.GenerateJSON: string;
var
MsgList: TList<TTwitchMessage>;
Msg: TTwitchMessage;
JSONArray: TJSONArray;
JSONObject: TJSONObject;
begin
JSONArray := TJSONArray.Create;
try
MsgList := Messages.LockList;
try
for Msg in MsgList do
begin
JSONObject := TJSONObject.Create;
JSONObject.AddPair('nickname', Msg.Nickname);
JSONObject.AddPair('content', Msg.Content);
JSONObject.AddPair('timestamp',
TJSONNumber.Create(DateTimeToUnix(Msg.Timestamp)));
JSONObject.AddPair('color', msgStyle.BlockColor); // Îñòàâëÿåì HEX-öâåò
JSONObject.AddPair('bcolor', msgStyle.BColor); // Îñòàâëÿåì HEX-öâåò
JSONObject.AddPair('fontSize', TJSONNumber.Create(msgStyle.FontSize));
JSONObject.AddPair('colorText', msgStyle.FontColor);
JSONObject.AddPair('colorBorder', msgStyle.BorderColor);
JSONObject.AddPair('sizeBorder',
TJSONNumber.Create(msgStyle.BorderSize));
JSONObject.AddPair('padding', TJSONNumber.Create(msgStyle.BorderSize));
JSONObject.AddPair('family', msgStyle.FontFamily);
JSONObject.AddPair('timeMsg', TJSONNumber.Create(Msg.TimeMsg)); // Äîáàâëÿåì âðåìÿ
// Óïðàâëÿåò òîëüêî áëîêîì, íå òåêñòîì
JSONArray.Add(JSONObject);
end;
finally
Messages.UnlockList;
end;
Result := JSONArray.ToString;
finally
JSONArray.Free;
end;
end;
procedure TTTW_Chat.IdHTTPServer1CommandGet(AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
var
HtmlContent: string;
FontFileName: string;
FontFilePath: string;
MIMEType: string;
FS: TFileStream;
begin
CleanupOldMessages;
if ARequestInfo.Document = '/messages' then
begin
AResponseInfo.ContentType := 'application/json; charset=utf-8';
AResponseInfo.ContentText := GenerateJSON;
end
else if Pos('/fonts/', ARequestInfo.Document) = 1 then
// Ïðîâåðÿåì çàïðîñ ê øðèôòàì
begin
// Èçâëåêàåì èìÿ ôàéëà èç URL
FontFileName := TPath.GetFileName(ARequestInfo.Document);
// Ôîðìèðóåì ïîëíûé ïóòü ê ôàéëó (ïàïêà fonts äîëæíà áûòü ðÿäîì ñ èñïîëíÿåìûì ôàéëîì)
FontFilePath := myConst.fontsPath + FontFileName;
// Ïðîâåðÿåì ñóùåñòâîâàíèå ôàéëà
if FileExists(FontFilePath) then
begin
MIMEType := 'font/ttf';
// Íàñòðàèâàåì îòâåò
AResponseInfo.ContentType := MIMEType;
try
FS := TFileStream.Create(FontFilePath, fmOpenRead + fmShareDenyWrite);
AResponseInfo.ContentStream := FS;
AResponseInfo.ResponseNo := 200;
except
FS.Free;
AResponseInfo.ResponseNo := 500;
end;
end;
end
else
begin
AResponseInfo.CacheControl := 'no-cache, no-store, must-revalidate';
AResponseInfo.Pragma := 'no-cache';
AResponseInfo.Expires := 0;
Timestamp2 := IntToStr(DateTimeToUnix(now));
AResponseInfo.ContentType := 'text/html; charset=utf-8';
AResponseInfo.ContentText := GenerateHTML;
end;
end;
end.