{

This unit is part of MKVExtractGUI2 project
(C) Eugene Pashin, 2010-2011
pashin@rambler.ru

}
{

  * mkvinfo.exe Info Parser
  * Matroska Extraction Helper

  (C) Eugene Pashin, 2010
  pashin@rambler.ru

}
unit siparser;

interface

uses
  Classes, SysUtils, StrUtils, Forms, ConsoleRedirect;

type
  PSegment = ^TSegment;

  TSegment = class
  private
    function Get(Index: integer): TSegment; overload;
    function Get(AName: string): TSegment; overload;
    procedure AddSubSegment(Name, Text: string);
    function GetValue(Name: string): string;
  public
    Name: string;
    Strings: TStringList;
    SubSegments: array of TSegment;
    constructor Create(Text: string); overload;
    constructor Create; overload;
    destructor Free;
    procedure Clear();
    function Count(): integer;
    function ParseFromText(Text: string): boolean;
    property Items[Index: integer]: TSegment read Get; default;
    property Items[Name: string]: TSegment read Get; default;
    property Values[Name: string]: string read GetValue;
  end;

  TMkvInfo = class(TSegment)
  public
    constructor Create();
    destructor Free;
    function Head: TSegment;
    function Segments: TSegment;
  end;

  TMkvExtractor = class
    FMKVFileName: string;
    FPaused: boolean;
  public
    OGM_Chapters: boolean;
    info: TMkvInfo;
    Console: TConsoleRedirector;
    CommandsPassedToConsole: TStringList;
    constructor Create();
    destructor Free;
    function LoadFromFile(FileName: TFileName): boolean;
    function Extract(Mode: string; Tracks: string; Path: string; Simulate: boolean = False): string;
    function IsMkvtoolnixHere: boolean;
    function Progress(): integer;
    function IsProcessActive(): boolean;

    property IsPaused: boolean read FPaused;

    procedure Pause();
    procedure Resume();
    procedure Abort();

  end;

const

  CODEC_EXT: string = // (c) MKVExtractGUI 1.6.4.1
  // в порядке усложнения наименования! //
    'V_MPEG1=mpg'#13 +
    'A_AAC=aac'#13 +
    'A_AC3=ac3'#13 +
    'A_DTS=dts'#13 +
    'A_FLAC=flac'#13 +
    'A_APE=ape'#13 +
    'A_QUICKTIME=qdm'#13 +
    'A_TTA1=tta'#13 +
    'A_WAVPACK4=wv'#13 +
    'A_VORBIS=ogg'#13 +
    'A_REAL=ra'#13 +
    'V_MPEG2=mpg'#13 +
    'V_REAL=rmvb'#13 +
    'V_MS/VFW/FOURCC=avi'#13 +
    'V_MPEG4/ISO/AVC=h264'#13 +
    'S_VOBSUB=idx'#13 +
    'A_MPEG/L3=mp3'#13 +
    'A_MPEG/L2=mp2'#13 +
    'A_MPEG/L1=mpa'#13 +
    'A_PCM/INT/LIT=wav'#13 +
    'S_HDMV/PGS=sup'#13 +
    'S_TEXT/UTF8=srt'#13 +
    'S_TEXT/SSA=ssa'#13 +
    'S_TEXT/ASS=ass'#13 +
    'S_TEXT/USF=usf';

implementation

uses
  utils;

function GetCodecExtByID(ID: string): string;
var
  sl: TStringList;
  i: integer;
begin
  Result := '';
  sl := TStringList.Create;
  sl.Text := CODEC_EXT;
  // перебор с проверкой на частичные вхождения
  for i := 0 to sl.Count - 1 do
    if Pos(ID, sl.Names[i]) > 0 then
      Result := sl.Values[sl.Names[i]];
  sl.Free;
end;

function ExtString(InfoLine: string): string;
var
  p: integer;
begin
  p := Pos('+', InfoLine);
  Result := Trim(AnsiMidStr(InfoLine, p + 1, $FF));
end;

{ TSegment }

procedure TSegment.AddSubSegment(Name, Text: string);
begin
  SetLength(SubSegments, Length(SubSegments) + 1);
  SubSegments[Length(SubSegments) - 1] := TSegment.Create(Text);
  SubSegments[Length(SubSegments) - 1].Name := Name;
  Strings.Add(StringReplace(Name, ': ', '=', []));
end;

procedure TSegment.Clear;
var
  i: integer;
begin
  Strings.Clear;
  for i := 0 to Length(SubSegments) - 1 do
    SubSegments[i].Free;
  SetLength(SubSegments, 0);
end;

function TSegment.Count: integer;
begin
  Result := Length(SubSegments);
end;

constructor TSegment.Create;
begin
  Strings := TStringList.Create;
  SetLength(SubSegments, 0);
end;

constructor TSegment.Create(Text: string);
begin
  Create;
  ParseFromText(Text);
end;

destructor TSegment.Free;
begin
  Strings.Free;
  Clear;
end;

function TSegment.Get(AName: string): TSegment;
var
  i: integer;
begin
  Result := nil;
  for i := 0 to Count - 1 do
    if SameText(Items[i].Name, AName) then
    begin
      Result := Items[i];
      Exit;
    end;
end;

function TSegment.Get(Index: integer): TSegment;
begin
  Result := SubSegments[Index];
end;

function TSegment.GetValue(Name: string): string;
begin
  Result := Strings.Values[Name];
end;

function TSegment.ParseFromText(Text: string): boolean;
var
  s, tmp, tmpn: string;
  sl: TStringList;
  i, level, clevel: integer;
begin
  Clear();
  Result := False;
  if Text = '' then
    Exit;
  tmp := '';
  tmpn := '';
  sl := TStringList.Create;
  sl.Text := Trim(Text);
  sl.Add(sl[0]); // enclosing '+'
  clevel := Pos('+', sl[0]);
  for i := 0 to sl.Count - 1 do
  begin
    s := Trim(sl[i]);
    if s = '' then
      Continue;
    level := Pos('+', s);
    if (level = clevel) then
    begin
      if (tmpn <> '') then
      begin
        AddSubSegment(ExtString(tmpn), tmp);
        tmp := '';
      end;
      tmpn := ExtString(s);
    end
    else if (level > clevel) then
      tmp := tmp + #13 + s;
  end;
  sl.Free;
  Result := True;
end;

{ TMkvInfo }

constructor TMkvInfo.Create();
begin
  inherited;
  Name := 'root';
end;

destructor TMkvInfo.Free;
begin
  inherited;
end;

function TMkvInfo.Head: TSegment;
begin
  Result := SubSegments[0];
end;

function TMkvInfo.Segments: TSegment;
begin
  Result := SubSegments[1];
end;

{ TMkvExtractor }

constructor TMkvExtractor.Create;
begin
  FPaused := False;
  info := TMkvInfo.Create;
  CommandsPassedToConsole := TStringList.Create;
  OGM_Chapters := False;
  // Console := TConsoleRedirector.Create;
end;

function TMkvExtractor.Extract(Mode: string; Tracks: string; Path: string;
  Simulate: boolean = False): string;
var
  mkvex, cmd, ogmFlag, TrLang: string;
  FileName, MyPath, codec_id: string;
   tl: TStringList;
  NeedToCreate: boolean;
  t, TrackID: integer;
  seg: TSegment;
begin
  MyPath := ExtractFilePath(Application.ExeName);
  mkvex := Format('%smkvextract --ui-language en', [MyPath]);

  cmd := '';

  tl := TStringList.Create;
  tl.CommaText := Tracks;
  for t := 0 to tl.Count - 1 do
  begin
    if Trim(tl[t]) = '' then
      Continue;

    TrackID := StrToIntDef(tl[t], 1);

    if Mode = 'tracks' then
    begin
      seg := info.Segments['Segment tracks'][TrackID - 1];
      if cmd = '' then
        cmd := Format('%s %s "%s"', [mkvex, Mode, FMKVFileName]);
      codec_id := seg.Values['Codec ID'];
      TrLang := IfThen(seg.Values['Language'] <> '', seg.Values['Language'], 'eng');
      FileName := Format('%s_track%d_%s.%s', [ChangeFileExt(ExtractFileName(FMKVFileName), ''), TrackID, TrLang, GetCodecExtByID(codec_id)]);
      cmd := cmd + Format(' %d:"%s"', [TrackID, Path + FileName]);
    end
    else if Mode = 'attachments' then
    begin
      seg := info.Segments['Attachments'][TrackID - 1];
      if cmd = '' then
        cmd := Format('%s %s "%s"', [mkvex, Mode, FMKVFileName]);
      FileName := seg.Values['File name'];
      cmd := cmd + Format(' %d:"%s"', [TrackID, Path + FileName]);
    end
    else if (Mode = 'chapters') or (Mode = 'tags') then
    begin
      FileName := Format('%s_%s.???', [ChangeFileExt(ExtractFileName(FMKVFileName), ''), Mode]);
      if OGM_Chapters then
      begin
        FileName := ChangeFileExt(FileName, '.ogm');
        ogmFlag := ' --simple';
      end
      else
      begin
        FileName := ChangeFileExt(FileName, '.xml');
        ogmFlag := '';
      end;
      if cmd = '' then
        cmd := Format('%s %s "%s" --redirect-output "%s" %s', [mkvex, Mode, FMKVFileName, Path + FileName, ogmFlag]);
    end;
  end;
  tl.Free;

  // check for Existing File?
  // ...

  if not Simulate then
  begin
    if Assigned(Console) then
    begin
      NeedToCreate := Console.Terminated;
    end
    else
      NeedToCreate := True;
    if NeedToCreate then
    begin
      Console := TConsoleRedirector.Create();
      Console.FreeOnTerminate := True;
    end;

    Console.Run(Mode + '=' + cmd);
    Console.Resume();
  end;
  CommandsPassedToConsole.Add(cmd);

  Result := FileName;

end;

destructor TMkvExtractor.Free;
begin
  info.Free;
  CommandsPassedToConsole.Free;
end;

function TMkvExtractor.LoadFromFile(FileName: TFileName): boolean;
var
  cmd: string;
  txt: string;
begin
  Result := False;
  if FileExists(FileName) then
  begin
    FMKVFileName := '';
    cmd := Format('%smkvinfo.exe --ui-language en "%s"', [ExtractFilePath(Application.ExeName),
      FileName]);
    txt := ExecuteConsoleApp(cmd);
    if info.ParseFromText(txt) then
      Result := info.Count > 0;
    if Result then
      FMKVFileName := FileName;
  end;
end;

function TMkvExtractor.IsMkvtoolnixHere: boolean;
var
  MyPath: string;
begin
  MyPath := ExtractFilePath(Application.ExeName);
  Result := FileExists(MyPath + 'mkvinfo.exe') and FileExists(MyPath + 'mkvextract.exe');
end;

function TMkvExtractor.IsProcessActive: boolean;
begin
  Result := not Console.Terminated;
end;

function TMkvExtractor.Progress: integer;
const
  SGN = 'Progress:';
var
  p: integer;
  s, s1: string;
begin
  s := Console.Output;
  p := PosEx(SGN, s, Length(s) - Length(SGN) * 2);
  if p > 0 then
  begin
    s1 := Trim(MidStr(s, p + Length(SGN), $FF));
    Result := StrToIntDef(LeftStr(s1, Length(s1) - 1), 0);
  end
  else
    Result := 0;
end;

procedure TMkvExtractor.Abort;
begin
  FPaused := False;
  if Assigned(Console) then
    Console.Abort();
end;

procedure TMkvExtractor.Pause;
begin
  if Assigned(Console) then
    Console.SuspendProcess();
  FPaused := True;
end;

procedure TMkvExtractor.Resume;
begin
  if Assigned(Console) then
    Console.ResumeProcess();
  FPaused := False;
end;

end.
