Search code examples
delphipascal

Loading MP3 files into ListView. When multiple items are select, show <Multi>, need ability to save to each file even with <multi> showing


In this project, we are opening MP3 files using the component 3delite.hu =- ID3v2 Library, and adding the MP3 files metadata to a ListView.

What I would like to accomplish.
When selecting multiple items in the listview, the edit fields will show <multi>, meaning that multiple items have been selected. If you try and edit anything and save it, this will result in the files getting the wrong data sent to them.
What I would like to ask is this.
How can you select multiple items, edit certain areas, and have it update each file individually?
Below is the code for this project.

LoadIt.pas

unit LoadIt;

interface


uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ExtCtrls, ComCtrls, StdCtrls, ID3v1Library, ID3v2Library, UITypes, StrUtils;

type
  TForm1 = class(TForm)
    SidePanel: TPanel;
    MainPanel: TPanel;
    ListView1: TListView;
    OpenDialog1: TOpenDialog;
    strTitle: TEdit;
    strArtist: TEdit;
    Label1: TLabel;
    Label2: TLabel;
    strAlbum: TEdit;
    Label3: TLabel;
    strTrack: TEdit;
    Label5: TLabel;
    TopPanel: TPanel;
    Button8: TButton;
    Edit1: TEdit;
    Label4: TLabel;
    strPath: TEdit;
    saveButton: TButton;
    Memo1: TMemo;
    SelectedLabel: TLabel;
    Button1: TButton;
    Button2: TButton;
    procedure FormCreate(Sender: TObject);
    procedure Button8Click(Sender: TObject);
    procedure ListView1SelectItem(Sender: TObject; Item: TListItem;
      Selected: Boolean);
    procedure saveButtonClick(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;
  ID3v1Tag: TID3v1Tag = nil;
  ID3v2Tag: TID3v2Tag = nil;
  CurrentAPICIndex: Integer = - 1;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
var
  Column: TListColumn;
begin
  TopPanel.Align := alTop;
  SidePanel.Align := alLeft;
  MainPanel.Align := alClient;
  ListView1.Align := alClient;
  ID3v1Tag := TID3v1Tag.Create;
  ID3v2Tag := TID3v2Tag.Create;

  Column := ListView1.Columns.Add;
  Column.Caption := 'Track ID';
  Column.Alignment := taLeftJustify;
  Column.Width := 60;

    Column := ListView1.Columns.Add;
  Column.Caption := 'Artist';
  Column.Alignment := taLeftJustify;
  Column.Width := 200;

    Column := ListView1.Columns.Add;
  Column.Caption := 'Album';
  Column.Alignment := taLeftJustify;
  Column.Width := 200;

  Column := ListView1.Columns.Add;
  Column.Caption := 'Title';
  Column.Alignment := taLeftJustify;
  Column.Width := 200;

   Column := ListView1.Columns.Add;
  Column.Caption := 'File ALocation';
  Column.Alignment := taLeftJustify;
  Column.Width := 0;


end;

procedure AddFileToTree(ListView: TListView; FileName: String);
var
  SR: TSearchRec;
  ListItem: TListItem;
begin
  // FileSize := 0;
  if (FindFirst(FileName, faAnyFile, SR) = 0) then
  begin
    // FileSize := SR.Size;
    // FileDate := FileDateToDateTime(SR.Time);
    ListItem := ListView.Items.Add;
    // get the MP3 File name
 // Track Number
ListItem.Caption := ID3v2Tag.GetUnicodeText('TRCK');  //0

   // get the Artist
ListItem.SubItems.Add(ID3v2Tag.GetUnicodeText('TPE1'));   // 1

   // get the Album
ListItem.SubItems.Add(ID3v2Tag.GetUnicodeText('TALB'));   // 2

   // get the Song title
ListItem.SubItems.Add(ID3v2Tag.GetUnicodeText('TIT2'));   // 3

   // get the path to the MP3 file
ListItem.SubItems.Add(FileName);    // 4

//  First we get the MP3 File name
ListItem.SubItems.Add(extractfilename(FileName));  //5
  end;
end;

// This section was coded by: Remy Lebeau
// https://stackoverflow.com/a/78172902/2031172
procedure TForm1.ListView1SelectItem(Sender: TObject; Item: TListItem;
  Selected: Boolean);
var
  ls: TListItem;
  sTrack, sArtist, sSong, sAlbum, sPath: string;

  procedure CheckForMulti(var selectedStr: string; const value: string);
  begin
    if selectedStr = '' then
      selectedStr := value
    else if selectedStr <> value then
      selectedStr := '<multi>';
  end;

begin
  ls := ListView1.Selected;
  while Assigned(ls) do
  begin
    CheckForMulti(sTrack,  ls.Caption);
    CheckForMulti(sArtist, ls.SubItems[0]);
    CheckForMulti(sSong,   ls.SubItems[1]);
    CheckForMulti(sAlbum,  ls.SubItems[2]);
    CheckForMulti(sPath,  ls.SubItems[3]);
    ls := ListView1.GetNextItem(ls, sdAll, [isSelected]);
  end;
  strTrack.Text  := sTrack;
  strArtist.Text := sArtist;
  strTitle.Text   := sSong;
  strAlbum.Text  := sAlbum;
  strPath.Text  := sPath;
end;

procedure TForm1.saveButtonClick(Sender: TObject);
var
    ErrorCode, i: Integer;
    LanguageID: TLanguageID;
    FileName : string;
    ListItem: TListItem;
    getString : string;
    LoadIt : integer;
begin

    if ID3v2Tag.MajorVersion = 2 then begin
        if MessageDlg('The tag in the file is an ID3v2.2 tag, if you click ''Yes'' it will be saved as an ID3v2.3 or ID3v2.4 tag. Do you want to save the tag?', mtConfirmation, [mbYes, mbCancel], 0) <> mrYes then begin
            Exit;
        end;
    end;


       ID3v2Tag.SetUnicodeText('TPE1', strArtist.Text);

       ID3v2Tag.SetUnicodeText('TALB', strAlbum.Text);

       ID3v2Tag.SetUnicodeText('TIT2', strTitle.Text);

       ID3v2Tag.SetUnicodeText('TRCK', strTrack.Text);

      ErrorCode := ID3v2Tag.SaveToFile(strPath.text);
   //  ErrorCode := ID3v2Tag.SaveToFile(strPath.Text{$IFDEF DEBUG}, False{$ENDIF});

    if ErrorCode <> ID3V2LIBRARY_SUCCESS then begin
        MessageDlg('Error saving ID3v2 tag, error code: ' + IntToStr(ErrorCode) + #13#10 + ID3v2TagErrorCode2String(ErrorCode), mtError, [mbOk], 0);
    end;
 end;



procedure TForm1.Button1Click(Sender: TObject);
var
  Item: TListItem;
  test: System.String;
begin
  memo1.Lines.Clear;
  Item := ListView1.Selected;
  while Item <> nil do
  begin
  // From Remy Lebeau - https://stackoverflow.com/a/14189378/2031172
  Memo1.SelStart := Memo1.GetTextLen;
  Memo1.SelLength := 0;
    //memo1.lines.Add(Item.SubItems[3]);
    // Added in this to created a comma delemited list.
    //  This should make it easier to iterate through.
    Memo1.SelText :=  Item.SubItems[3] + ',';
    Item := ListView1.GetNextItem(Item, sdAll, [isSelected]);
  end;
  // Remove the last character (comma) in the string.
  // Mike Jablonski - https://stackoverflow.com/a/29102436/2031172
  test := Memo1.Text;
  test := Copy(test,1,length(test)-1);
  Memo1.Text := test;
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  Memo1.Clear;
end;

procedure TForm1.Button8Click(Sender: TObject);
var
  i: integer;
  LoadIt: integer;
begin
ListView1.Clear;
  if OpenDialog1.execute then
    for i := 0 to OpenDialog1.Files.Count - 1 do
      if SameText(ExtractFileExt(OpenDialog1.Files[i]), '.mp3') then
      begin
        // Bug fix here. Changing from the original TEdit to the OpenDialog.Files[i] fixed song titles in list.
        LoadIt := ID3v2Tag.LoadFromFile(OpenDialog1.Files[i]);
        AddFileToTree(ListView1, OpenDialog1.Files[i]);
      end;
end;

end.

LoadIt.dfm

object Form1: TForm1
  Left = 0
  Top = 0
  Caption = 'Form1'
  ClientHeight = 375
  ClientWidth = 688
  Color = clBtnFace
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -11
  Font.Name = 'Tahoma'
  Font.Style = []
  OnCreate = FormCreate
  TextHeight = 13
  object SidePanel: TPanel
    Left = -4
    Top = 128
    Width = 189
    Height = 252
    TabOrder = 0
    object Label1: TLabel
      Left = 4
      Top = 40
      Width = 38
      Height = 19
      Caption = 'Artist'
      Font.Charset = DEFAULT_CHARSET
      Font.Color = clWindowText
      Font.Height = -16
      Font.Name = 'Tahoma'
      Font.Style = []
      ParentFont = False
    end
    object Label2: TLabel
      Left = 4
      Top = 95
      Width = 31
      Height = 19
      Caption = 'Title'
      Font.Charset = DEFAULT_CHARSET
      Font.Color = clWindowText
      Font.Height = -16
      Font.Name = 'Tahoma'
      Font.Style = []
      ParentFont = False
    end
    object Label3: TLabel
      Left = 4
      Top = 68
      Width = 47
      Height = 19
      Caption = 'Album'
      Font.Charset = DEFAULT_CHARSET
      Font.Color = clWindowText
      Font.Height = -16
      Font.Name = 'Tahoma'
      Font.Style = []
      ParentFont = False
    end
    object Label5: TLabel
      Left = 4
      Top = 120
      Width = 56
      Height = 19
      Caption = 'Track #'
      Font.Charset = DEFAULT_CHARSET
      Font.Color = clWindowText
      Font.Height = -16
      Font.Name = 'Tahoma'
      Font.Style = []
      ParentFont = False
    end
    object strTitle: TEdit
      Left = 61
      Top = 93
      Width = 121
      Height = 21
      TabOrder = 0
      Text = 'Song Title'
    end
    object strArtist: TEdit
      Left = 61
      Top = 39
      Width = 121
      Height = 21
      TabOrder = 1
      Text = 'Artist Name'
    end
    object strAlbum: TEdit
      Left = 61
      Top = 66
      Width = 121
      Height = 21
      TabOrder = 2
      Text = 'Artist Name'
    end
    object strTrack: TEdit
      Left = 61
      Top = 120
      Width = 121
      Height = 21
      TabOrder = 3
      Text = 'Artist Name'
    end
    object saveButton: TButton
      Left = 21
      Top = 156
      Width = 59
      Height = 22
      Caption = 'Save...'
      Font.Charset = DEFAULT_CHARSET
      Font.Color = clWindowText
      Font.Height = -11
      Font.Name = 'Tahoma'
      Font.Style = []
      ParentFont = False
      TabOrder = 4
      OnClick = saveButtonClick
    end
  end
  object MainPanel: TPanel
    Left = 200
    Top = 130
    Width = 441
    Height = 201
    TabOrder = 1
    object ListView1: TListView
      Left = 104
      Top = 27
      Width = 200
      Height = 150
      Columns = <>
      MultiSelect = True
      RowSelect = True
      SortType = stText
      TabOrder = 0
      ViewStyle = vsReport
      OnSelectItem = ListView1SelectItem
    end
  end
  object TopPanel: TPanel
    Left = 8
    Top = 8
    Width = 649
    Height = 114
    TabOrder = 2
    object Label4: TLabel
      Left = 352
      Top = 15
      Width = 31
      Height = 19
      Caption = 'Path'
      Font.Charset = DEFAULT_CHARSET
      Font.Color = clWindowText
      Font.Height = -16
      Font.Name = 'Tahoma'
      Font.Style = []
      ParentFont = False
    end
    object SelectedLabel: TLabel
      Left = 81
      Top = 45
      Width = 89
      Height = 13
      Caption = 'All Selected Paths.'
    end
    object Button8: TButton
      Left = 9
      Top = 12
      Width = 59
      Height = 22
      Caption = 'Select...'
      Font.Charset = DEFAULT_CHARSET
      Font.Color = clWindowText
      Font.Height = -11
      Font.Name = 'Tahoma'
      Font.Style = []
      ParentFont = False
      TabOrder = 0
      OnClick = Button8Click
    end
    object Edit1: TEdit
      Left = 74
      Top = 13
      Width = 265
      Height = 21
      Font.Charset = DEFAULT_CHARSET
      Font.Color = clWindowText
      Font.Height = -11
      Font.Name = 'Tahoma'
      Font.Style = []
      ParentFont = False
      TabOrder = 1
    end
    object strPath: TEdit
      Left = 389
      Top = 12
      Width = 244
      Height = 21
      TabOrder = 2
      Text = 'strPath'
    end
    object Memo1: TMemo
      Left = 74
      Top = 64
      Width = 505
      Height = 41
      ScrollBars = ssVertical
      TabOrder = 3
    end
    object Button1: TButton
      Left = 192
      Top = 40
      Width = 91
      Height = 25
      Caption = 'Get All Selected'
      TabOrder = 4
      OnClick = Button1Click
    end
    object Button2: TButton
      Left = 308
      Top = 40
      Width = 75
      Height = 25
      Caption = 'Clear Paths'
      TabOrder = 5
      OnClick = Button2Click
    end
  end
  object OpenDialog1: TOpenDialog
    Filter = 'MP3|*.mp3'
    Options = [ofHideReadOnly, ofAllowMultiSelect, ofEnableSizing]
    Left = 598
    Top = 324
  end
end

Project1.dpr

program Project1;

uses
  Forms,
  LoadIt in 'LoadIt.pas' {Form1};

{$R *.res}

begin
  Application.Initialize;
  Application.MainFormOnTaskbar := True;
  Application.CreateForm(TForm1, Form1);
  Application.Run;

end.

Solution

  • For what you are attempting, you need to keep track of the changes being made (ie, have a look at the TEdit.Modified property) and then loop through the selected items saving those changes to each individual file as needed.

    Also, AddFileToTree() is leaking the TSearchRec resources as it is not calling FindClose(). But, since you are not actually using the TSearchRec for anything useful, you should just use FileExists() instead. Or better, just load the file unconditionally and then check whether ID3v2Tag.LoadFromFile() succeeded or failed.

    Try something more like this:

    unit LoadIt;
    
    interface
    
    uses
      Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
      Dialogs, ExtCtrls, ComCtrls, StdCtrls, ID3v1Library, ID3v2Library, UITypes, StrUtils;
    
    type
      TForm1 = class(TForm)
        SidePanel: TPanel;
        MainPanel: TPanel;
        ListView1: TListView;
        OpenDialog1: TOpenDialog;
        strTitle: TEdit;
        strArtist: TEdit;
        Label1: TLabel;
        Label2: TLabel;
        strAlbum: TEdit;
        Label3: TLabel;
        strTrack: TEdit;
        Label5: TLabel;
        TopPanel: TPanel;
        Button8: TButton;
        Edit1: TEdit;
        Label4: TLabel;
        strPath: TEdit;
        saveButton: TButton;
        Memo1: TMemo;
        SelectedLabel: TLabel;
        Button1: TButton;
        Button2: TButton;
        procedure FormCreate(Sender: TObject);
        procedure FormDeestroy(Sender: TObject);
        procedure Button8Click(Sender: TObject);
        procedure ListView1SelectItem(Sender: TObject; Item: TListItem;
          Selected: Boolean);
        procedure saveButtonClick(Sender: TObject);
        procedure Button1Click(Sender: TObject);
        procedure Button2Click(Sender: TObject);
      private
        { Private declarations }
        ID3v1Tag: TID3v1Tag;
        ID3v2Tag: TID3v2Tag;
        procedure AddFileToTree(FileName: String);
      public
        { Public declarations }
      end;
    
    var
      Form1: TForm1;
      CurrentAPICIndex: Integer = - 1;
    
    implementation
    
    {$R *.dfm}
    
    procedure TForm1.FormCreate(Sender: TObject);
    var
      Column: TListColumn;
    begin
      ID3v1Tag := TID3v1Tag.Create;
      ID3v2Tag := TID3v2Tag.Create;
    
      TopPanel.Align := alTop;
      SidePanel.Align := alLeft;
      MainPanel.Align := alClient;
      ListView1.Align := alClient;
    
      Column := ListView1.Columns.Add;
      Column.Caption := 'Track ID';
      Column.Alignment := taLeftJustify;
      Column.Width := 60;
    
      Column := ListView1.Columns.Add;
      Column.Caption := 'Artist';
      Column.Alignment := taLeftJustify;
      Column.Width := 200;
    
      Column := ListView1.Columns.Add;
      Column.Caption := 'Album';
      Column.Alignment := taLeftJustify;
      Column.Width := 200;
    
      Column := ListView1.Columns.Add;
      Column.Caption := 'Title';
      Column.Alignment := taLeftJustify;
      Column.Width := 200;
    
      Column := ListView1.Columns.Add;
      Column.Caption := 'File Allocation';
      Column.Alignment := taLeftJustify;
      Column.Width := 0;
    end;
    
    procedure TForm1.FormDestroy(Sender: TObject);
    begin
      ID3v1Tag.Free;
      ID3v2Tag.Free;
    end;
    
    procedure TForm1.AddFileToTree(FileName: String);
    var
      ErrorCode: integer;
      ListItem: TListItem;
    begin
      ErrorCode := ID3v2Tag.LoadFromFile(FileName);
      if ErrorCode <> ID3V2LIBRARY_SUCCESS then begin
        MessageDlg('Error loading ID3v2 tag from file: ' + FileName + #13#10 + 'Error code: ' + IntToStr(ErrorCode) + #13#10 + ID3v2TagErrorCode2String(ErrorCode), mtError, [mbOk], 0);
        Exit;
      end;
    
      ListItem := ListView1.Items.Add;
    
      // get the MP3 File name
      // Track Number
      ListItem.Caption := ID3v2Tag.GetUnicodeText('TRCK');  //0
    
      // get the Artist
      ListItem.SubItems.Add(ID3v2Tag.GetUnicodeText('TPE1'));   // 1
    
      // get the Album
      ListItem.SubItems.Add(ID3v2Tag.GetUnicodeText('TALB'));   // 2
    
      // get the Song title
      ListItem.SubItems.Add(ID3v2Tag.GetUnicodeText('TIT2'));   // 3
    
      // get the path to the MP3 file
      ListItem.SubItems.Add(FileName);// 4
    
      //  First we get the MP3 File name
      ListItem.SubItems.Add(ExtractFileName(FileName));  //5
    end;
    
    procedure TForm1.ListView1SelectItem(Sender: TObject; Item: TListItem;
      Selected: Boolean);
    var
      ListItem: TListItem;
      sTrack, sArtist, sSong, sAlbum, sPath: string;
    
      procedure CheckForMulti(var selectedStr: string; const value: string);
      begin
        if selectedStr = '' then
          selectedStr := value
        else if selectedStr <> value then
          selectedStr := '<multi>';
      end;
    
    begin
      ListItem := ListView1.Selected;
      while ListItem <> nil do
      begin
        CheckForMulti(sTrack,  ListItem.Caption);
        CheckForMulti(sArtist, ListItem.SubItems[0]);
        CheckForMulti(sSong,   ListItem.SubItems[1]);
        CheckForMulti(sAlbum,  ListItem.SubItems[2]);
        CheckForMulti(sPath,   ListItem.SubItems[3]);
        ListItem := ListView1.GetNextItem(ListItem, sdAll, [isSelected]);
      end;
    
      if not strTrack.Modified then begin
        strTrack.Text      := sTrack;
        strTrack.Modified  := False;
      end;
    
      if not strArtist.Modified then begin
        strArtist.Text     := sArtist;
        strArtist.Modified := False;
      end;
    
      if not strTitle.Modified then begin
        strTitle.Text      := sSong;
        strTitle.Modified  := False;
      end;
    
      if not strAlbum.Modified then begin
        strAlbum.Text      := sAlbum;
        strAlbum.Modified  := False;
      end;
    
      strPath.Text := sPath;
    end;
    
    procedure TForm1.saveButtonClick(Sender: TObject);
    var
      FileName : string;
      ListItem: TListItem;
      ErrorCode: Integer;
    begin
    
      if (not strTrack.Modified) and
         (not strArtist.Modified) and
         (not strTitle.Modified) and
         (not strAlbum.Modified) then
      begin
        MessageDlg('No changes to save', mtInformation, [mbOk], 0);
        Exit;
      end;
    
      if ID3v2Tag.MajorVersion = 2 then begin
        if MessageDlg('The tag in the file is an ID3v2.2 tag, if you click ''Yes'' it will be saved as an ID3v2.3 or ID3v2.4 tag. Do you want to save the tag?', mtConfirmation, [mbYes, mbCancel], 0) <> mrYes then begin
          Exit;
        end;
      end;
    
      ListItem := ListView1.Selected;
      while ListItem <> nil do
      begin
        if strTrack.Modified then
          sTrack := strTrack.Text
        else
          sTrack := ListItem.Caption;
    
        if strArtist.Modified then
          sArtist := strArtist.Text
        else
          sArtist := ListItem.SubItems[0];
    
        if strTitle.Modified then
          sSong := strTitle.Text
        else
          sSong := ListItem.SubItems[1];
    
        if strAlbum.Modified then
          sAlbum := strAlbum.Text
        else
          sAlbum := ListItem.SubItems[2];
    
        ID3v2Tag.SetUnicodeText('TPE1', sArtist);
        ID3v2Tag.SetUnicodeText('TALB', sAlbum);
        ID3v2Tag.SetUnicodeText('TIT2', sSong);
        ID3v2Tag.SetUnicodeText('TRCK', sTrack);
    
        FileName := ListItem.SubItems[3];
    
        ErrorCode := ID3v2Tag.SaveToFile(FileName);
        //  ErrorCode := ID3v2Tag.SaveToFile(FileName{$IFDEF DEBUG}, False{$ENDIF});
    
        if ErrorCode <> ID3V2LIBRARY_SUCCESS then begin
          MessageDlg('Error saving ID3v2 tag, error code: ' + IntToStr(ErrorCode) + #13#10 + ID3v2TagErrorCode2String(ErrorCode), mtError, [mbOk], 0);
        end;
    
        ListItem := ListView1.GetNextItem(ListItem, sdAll, [isSelected]);
      end;
    
      strTrack.Modified  := False;
      strArtist.Modified := False;
      strTitle.Modified  := False;
      strAlbum.Modified  := False;
    end;
    
    procedure TForm1.Button1Click(Sender: TObject);
    var
      ListItem: TListItem;
      test: String;
    begin
      test := '';
      ListItem := ListView1.Selected;
      if ListItem <> nil then
      begin
        do
          test := test + ListItem.SubItems[3] + ',';
          ListItem := ListView1.GetNextItem(ListItem, sdAll, [isSelected]);
        until ListItem = nil;
        // Remove the last character (comma) in the string.
        SetLength(test, Length(test)-1);
      end;
      Memo1.Text := test;
      
      { alternatively: 
    
      SL := TStringList.Create;
      try
        ListItem := ListView1.Selected;
        while ListItem <> nil do
        begin
          SL.Add(ListItem.SubItems[3]);
          ListItem := ListView1.GetNextItem(ListItem, sdAll, [isSelected]);
        end;
        Memo1.Text := SL.CommaText;
      finally
        SL.Free;
      end;
      }
    end;
    
    procedure TForm1.Button2Click(Sender: TObject);
    begin
      Memo1.Clear;
    end;
    
    procedure TForm1.Button8Click(Sender: TObject);
    var
      i: integer;
      FileName: string;
    begin
      ListView1.Clear;
      if OpenDialog1.Execute then
      begin
        for i := 0 to OpenDialog1.Files.Count - 1 do
        begin
          FileName := OpenDialog1.Files[i];
          if SameText(ExtractFileExt(FileName), '.mp3') then
            AddFileToTree(FileName);
        end;
      end;
    end;
    
    end.