Parcourir la source

Metadata: extract ISBN and use it when adding books

- unitmetadata: extend ExtractBookMetadata to include ISBN; parse EPUB identifiers and attempt to detect ISBN; scan pdfinfo lines for ISBN tokens\n- main: pass ISBN out-param and assign to book on import
Codex CLI il y a 4 mois
Parent
commit
586e335de5
4 fichiers modifiés avec 81 ajouts et 58 suppressions
  1. 10 21
      src/main.pas
  2. 24 14
      src/unitbookdialog.lfm
  3. 1 10
      src/unitbookdialog.pas
  4. 46 13
      src/unitmetadata.pas

+ 10 - 21
src/main.pas

@@ -63,7 +63,6 @@ var
   dataXmlPath: String;
   booksDir: String;
   backgroundTile, toolbar: TPicture;
-  backgroundTileBmp: TBitmap;
   coverWidth, coverHeight: Integer;
   optCopyBooks, optRenameBooks, optExtractMeta: Boolean;
   isClosing: Boolean = False;
@@ -124,18 +123,18 @@ var w,h:Integer;
     x,y:Integer;
 begin
   // Safety: if no tile or invalid size, skip custom painting
-  if (backgroundTileBmp = nil) or (backgroundTileBmp.Width <= 0) or (backgroundTileBmp.Height <= 0) then
+  if (backgroundTile = nil) or (backgroundTile.Width <= 0) or (backgroundTile.Height <= 0) then
     Exit;
 
   x:=0;
   y:=0;
-  w:=backgroundTileBmp.Width;
-  h:=backgroundTileBmp.Height;
+  w:=backgroundTile.Width;
+  h:=backgroundTile.Height;
   while x < PanelBackground.Canvas.Width do
   begin
     while y < PanelBackground.Canvas.Height do
     begin
-      PanelBackground.Canvas.Draw(x,y,backgroundTileBmp);
+      PanelBackground.Canvas.Draw(x,y,backgroundTile.Graphic);
       y:=y+h;
     end;
     x:=x+w;
@@ -329,7 +328,6 @@ begin
   FreeAndNil(mGear);
   FreeAndNil(mGearHover);
   FreeAndNil(backgroundTile);
-  FreeAndNil(backgroundTileBmp);
   FreeAndNil(bookList);
   CloseAction := caFree;
 end;
@@ -340,7 +338,7 @@ var
   i    : Integer;
   src  : String;
   dest : String;
-  fname,title,authors,ext : String;
+  fname,title,authors,ext,isbn : String;
   files: TStringList;
 
   function CleanName(const s:String):String;
@@ -358,8 +356,9 @@ var
     dest := src;
     title := '';
     authors := '';
+    isbn := ''; 
     if optExtractMeta then
-      ExtractBookMetadata(src, title, authors);
+      ExtractBookMetadata(src, title, authors, isbn);
 
     if optCopyBooks then
     begin
@@ -393,6 +392,7 @@ var
       if title <> '' then book.Title := title
       else book.Title := ChangeFileExt(ExtractFileName(dest), '');
       if authors <> '' then book.Authors := authors;
+      if isbn <> '' then book.isbn := isbn;
     end
     else
       book.Title := ChangeFileExt(ExtractFileName(dest), '');
@@ -496,19 +496,8 @@ begin
  ActiveControl:=PanelBackground;
 
 
-  backgroundTile:=TPicture.Create;
-  backgroundTile.LoadFromLazarusResource('shelf');
-  // Create opaque bitmap tile to avoid GTK depth assertion when painting
-  backgroundTileBmp := TBitmap.Create;
-  try
-    backgroundTileBmp.Assign(backgroundTile.Graphic);
-    backgroundTileBmp.Transparent := False;
-    {$IFDEF LCLGTK2}
-    backgroundTileBmp.PixelFormat := pf24bit;
-    {$ENDIF}
-  except
-    FreeAndNil(backgroundTileBmp);
-  end;
+ backgroundTile:=TPicture.Create;
+ backgroundTile.LoadFromLazarusResource('shelf');
 
  PanelBackground.DoubleBuffered := True; // reduce flicker
 

+ 24 - 14
src/unitbookdialog.lfm

@@ -31,17 +31,27 @@ object BookEditDialog: TBookEditDialog
     Left = 329
     Height = 29
     Top = 128
-    Width = 400
+    Width = 375
     TabOrder = 2
     Text = 'ISBN'
   end
+  object ButtonLookup: TBitBtn
+    Left = 710
+    Height = 29
+    Top = 128
+    Width = 85
+    Caption = '&Lookup'
+    OnClick = ButtonLookupClick
+    Anchors = [akTop, akRight]
+    TabOrder = 3
+  end
   object EditFilePath: TEdit
     Left = 329
     Height = 29
     Top = 176
     Width = 463
     OnChange = EditFilePathChange
-    TabOrder = 3
+    TabOrder = 4
     Text = 'File Path'
   end
   object ButtonSave: TBitBtn
@@ -52,7 +62,7 @@ object BookEditDialog: TBookEditDialog
     Caption = '&Save'
     OnClick = ButtonSaveClick
     Anchors = [akRight, akBottom]
-    TabOrder = 4
+    TabOrder = 5
   end
   object ButtonCancel: TBitBtn
     Left = 716
@@ -62,23 +72,23 @@ object BookEditDialog: TBookEditDialog
     Caption = '&Cancel'
     OnClick = ButtonCancelClick
     Anchors = [akRight, akBottom]
-    TabOrder = 5
+    TabOrder = 6
   end
   object Panel1: TPanel
     Left = 24
-    Height = 220
+    Height = 250
     Top = 33
-    Width = 182
+    Width = 130
     BorderWidth = 2
     BorderStyle = bsSingle
-    ClientHeight = 218
-    ClientWidth = 180
-    TabOrder = 6
+    ClientHeight = 250
+    ClientWidth = 130
+    TabOrder = 7
     object ImageBookCover: TImage
       Left = 0
-      Height = 224
+      Height = 250
       Top = 0
-      Width = 182
+      Width = 130
       OnClick = ImageBookCoverClick
       Cursor = crHandPoint
       ShowHint = True
@@ -89,8 +99,8 @@ object BookEditDialog: TBookEditDialog
   object ButtonChangeCover: TButton
     Left = 24
     Height = 28
-    Top = 264
-    Width = 182
+    Top = 270
+    Width = 130
     Caption = 'Change Cover...'
     OnClick = ImageBookCoverClick
     TabOrder = 8
@@ -101,7 +111,7 @@ object BookEditDialog: TBookEditDialog
     Top = 224
     Width = 463
     OnChange = EditFilePathChange
-    TabOrder = 7
+    TabOrder = 9
     Text = 'Image Path'
   end
   object Label1: TLabel

+ 1 - 10
src/unitbookdialog.pas

@@ -62,15 +62,6 @@ procedure Tbookeditdialog.Formcreate(Sender: Tobject);
 begin
   ActiveControl:=ButtonSave;
 
-  ButtonLookup := TButton.Create(Self);
-  ButtonLookup.Parent := Self;
-  ButtonLookup.Caption := 'Lookup';
-  ButtonLookup.Top := EditISBN.Top;
-  ButtonLookup.Left := EditISBN.Left + EditISBN.Width + 8;
-  ButtonLookup.Height := EditISBN.Height;
-  ButtonLookup.Anchors := [akTop, akRight];
-  ButtonLookup.OnClick := @ButtonLookupClick;
-
   // Make it obvious the cover can be changed
   ImageBookCover.Cursor := crHandPoint;
   ImageBookCover.ShowHint := True;
@@ -260,7 +251,7 @@ begin
   // Set file path first (auto-cover logic may look for sibling images)
   mBook.FilePath:=EditFilePath.Text;
   // Then set explicit image path to ensure it takes precedence
-  mBook.ImagePath:=editimagepath.Text;
+  mBook.ImagePath:=EditImagePath.Text;
 
   // Ensure UI reflects any new cover choice
   mBook.EnsureScaledToCoverSize;

+ 46 - 13
src/unitmetadata.pas

@@ -7,17 +7,39 @@ interface
 uses
   Classes, SysUtils, FileUtil;
 
-// Extract basic metadata (title, authors) from a book file.
+// Extract basic metadata (title, authors, isbn) from a book file.
 // Supports PDF (via pdfinfo) and EPUB (via unzip and parsing the OPF file).
 // Returns True if any metadata was found.
-function ExtractBookMetadata(const FileName: String; out Title, Authors: String): Boolean;
+function ExtractBookMetadata(const FileName: String; out Title, Authors, Isbn: String): Boolean;
 
 implementation
 
 uses
   Process, DOM, XMLRead, LazUTF8, StrUtils, LazFileUtils, unitLog;
 
-function ExtractPDFMetadata(const FileName: String; out Title, Authors: String): Boolean;
+function NormalizeISBN(const S: String): String;
+var
+  i: Integer; ch: Char; acc: String; src: String;
+begin
+  // strip common prefixes
+  src := StringReplace(S, 'urn:isbn:', '', [rfIgnoreCase]);
+  src := StringReplace(src, 'isbn:', '', [rfIgnoreCase]);
+  src := StringReplace(src, 'isbn', '', [rfIgnoreCase]);
+  acc := '';
+  for i := 1 to Length(src) do
+  begin
+    ch := src[i];
+    if (ch >= '0') and (ch <= '9') then acc += ch
+    else if (ch = 'x') or (ch = 'X') then acc += ch
+    else if (ch = '-') or (ch = ' ') then Continue
+    else if (ch = #9) then Continue
+    else ;
+  end;
+  if (Length(acc) = 10) and (acc[Length(acc)] in ['x','X']) then acc[Length(acc)] := 'X';
+  if (Length(acc) = 13) or (Length(acc) = 10) then Result := acc else Result := '';
+end;
+
+function ExtractPDFMetadata(const FileName: String; out Title, Authors, Isbn: String): Boolean;
 var
   proc: TProcess;
   sl: TStringList;
@@ -29,6 +51,7 @@ begin
   Result := False;
   Title := '';
   Authors := '';
+  Isbn := '';
   exe := FindDefaultExecutablePath('pdfinfo');
   if exe = '' then exe := 'pdfinfo';
   LogInfoFmt('pdfinfo tool: %s', [exe]);
@@ -57,10 +80,12 @@ begin
           Title := Trim(Copy(line, 7, MaxInt));
         if (Authors = '') and (AnsiStartsStr('Author:', line) or AnsiStartsStr('Authors:', line)) then
           Authors := Trim(Copy(line, Pos(':', line) + 1, MaxInt));
+        if Isbn = '' then
+          Isbn := NormalizeISBN(line);
       end;
-      Result := (Title <> '') or (Authors <> '');
-      LogInfoFmt('PDF metadata parsed: title="%s" authors="%s" result=%s',
-        [Title, Authors, BoolToStr(Result, True)]);
+      Result := (Title <> '') or (Authors <> '') or (Isbn <> '');
+      LogInfoFmt('PDF metadata parsed: title="%s" authors="%s" isbn="%s" result=%s',
+        [Title, Authors, Isbn, BoolToStr(Result, True)]);
     except
       on E: Exception do
       begin
@@ -75,7 +100,7 @@ begin
   end;
 end;
 
-function ExtractEPUBMetadata(const FileName: String; out Title, Authors: String): Boolean;
+function ExtractEPUBMetadata(const FileName: String; out Title, Authors, Isbn: String): Boolean;
 var
   proc: TProcess;
   sl: TStringList;
@@ -90,6 +115,7 @@ begin
   Result := False;
   Title := '';
   Authors := '';
+  Isbn := '';
   exe := FindDefaultExecutablePath('unzip');
   if exe = '' then exe := 'unzip';
   LogInfoFmt('unzip tool: %s', [exe]);
@@ -189,6 +215,12 @@ begin
             if Authors <> '' then Authors := Authors + ', ';
             Authors := Authors + UTF8Encode(Trim(node.TextContent));
           end;
+          if (lname = 'dc:identifier') or (lname = 'identifier') then
+          begin
+            if Isbn = '' then Isbn := NormalizeISBN(UTF8Encode(Trim(node.TextContent)));
+            if (Isbn = '') and (node is TDOMElement) then
+              Isbn := NormalizeISBN(UTF8Encode(TDOMElement(node).GetAttribute('opf:scheme')));
+          end;
         end;
       end;
     finally
@@ -197,24 +229,25 @@ begin
   finally
     stream.Free;
   end;
-  Result := (Title <> '') or (Authors <> '');
-  LogInfoFmt('EPUB metadata parsed: title="%s" authors="%s" result=%s',
-    [Title, Authors, BoolToStr(Result, True)]);
+  Result := (Title <> '') or (Authors <> '') or (Isbn <> '');
+  LogInfoFmt('EPUB metadata parsed: title="%s" authors="%s" isbn="%s" result=%s',
+    [Title, Authors, Isbn, BoolToStr(Result, True)]);
 end;
 
-function ExtractBookMetadata(const FileName: String; out Title, Authors: String): Boolean;
+function ExtractBookMetadata(const FileName: String; out Title, Authors, Isbn: String): Boolean;
 var
   ext: String;
 begin
   ext := LowerCase(ExtractFileExt(FileName));
   if ext = '.pdf' then
-    Result := ExtractPDFMetadata(FileName, Title, Authors)
+    Result := ExtractPDFMetadata(FileName, Title, Authors, Isbn)
   else if ext = '.epub' then
-    Result := ExtractEPUBMetadata(FileName, Title, Authors)
+    Result := ExtractEPUBMetadata(FileName, Title, Authors, Isbn)
   else
   begin
     Title := '';
     Authors := '';
+    Isbn := '';
     Result := False;
   end;
 end;