fpcddb.pp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656
  1. {
  2. Copyright (c) 2008 by Michael Van Canneyt
  3. Unit to parse CDDB responses and construct a list
  4. of tracks in a CD.
  5. See the file COPYING.FPC, included in this distribution,
  6. for details about the copyright.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  10. **********************************************************************}
  11. {
  12. Some notes:
  13. Disc.Year and Disc.Genre only have values if proto = 5 or above as specified in the request.
  14. With protocol 5 and under the responses are in ISO-8859-1. In version 6 it's UTF-8
  15. A more complete explanation of the protocol can be found here:
  16. http://ftp.freedb.org/pub/freedb/latest/CDDBPROTO
  17. }
  18. unit fpcddb;
  19. {$mode objfpc}{$H+}
  20. interface
  21. uses
  22. Classes, SysUtils;
  23. Type
  24. TCDDisk = Class;
  25. { TCDTrack }
  26. TCDTrack = Class(TCollectionItem)
  27. private
  28. FDuration: TDateTime;
  29. FExtra: String;
  30. FPerformer: String;
  31. FTitle: String;
  32. function GetPerformer: String;
  33. Public
  34. Procedure Assign(Source : TPersistent); override;
  35. Published
  36. Property Title : String Read FTitle Write FTitle;
  37. Property Performer : String Read GetPerformer Write FPerformer;
  38. Property Extra : String Read FExtra Write FExtra;
  39. Property Duration : TDateTime Read FDuration Write FDuration;
  40. end;
  41. { TCDTracks }
  42. TCDTracks = Class(TCollection)
  43. private
  44. FCDDisk: TCDDisk;
  45. function GetT(AIndex : Integer): TCDTrack;
  46. procedure SetT(AIndex : Integer; const AValue: TCDTrack);
  47. Public
  48. Property CDDisk : TCDDisk Read FCDDisk;
  49. Function AddTrack(Const ATitle,AExtra : String; ADuration : TDateTime) : TCDTrack;
  50. Function AddTrack(Const ATitle,AExtra : String) : TCDTrack;
  51. Function AddTrack(Const ATitle : String) : TCDTrack;
  52. Property Track[AIndex : Integer] : TCDTrack Read GetT Write SetT; default;
  53. end;
  54. { TCDDisk }
  55. TCDDisk = Class(TCollectionItem)
  56. private
  57. FDiskID: Integer;
  58. FExtra: String;
  59. FGenre: String;
  60. FPerformer: String;
  61. FPlayOrder: String;
  62. FTitle: String;
  63. FTracks: TCDTracks;
  64. FYear: Word;
  65. function GetDiskID: String;
  66. procedure SetDiskID(const AValue: String);
  67. procedure SetTracks(const AValue: TCDTracks);
  68. Protected
  69. Function CreateTracks : TCDTracks; virtual;
  70. Public
  71. Constructor Create(ADiskID : Integer);
  72. Constructor Create(ACollection : TCollection); override;
  73. Procedure Assign(Source : TPersistent); override;
  74. Property IntDiscID : Integer Read FDiskID Write FDiskID;
  75. Published
  76. Property PlayOrder : String Read FPlayOrder Write FPlayOrder;
  77. Property Year : Word Read FYear Write FYear; // proto=5
  78. Property Title : String Read FTitle Write FTitle;
  79. Property Performer : String Read FPerformer Write FPerformer;
  80. Property Genre : String Read FGenre write FGenre; //proto=5
  81. Property Extra : String Read FExtra Write FExtra;
  82. Property DiscID : String Read GetDiskID Write SetDiskID;
  83. property Tracks : TCDTracks Read FTracks Write SetTracks;
  84. end;
  85. { TCDDisks }
  86. TCDDisks = Class(TCollection)
  87. private
  88. function GetD(AIndex : Integer): TCDDisk;
  89. procedure SetD(AIndex : Integer; const AValue: TCDDisk);
  90. Public
  91. Function AddDisk(ADiscID : String) : TCDDisk;
  92. Function AddDisk : TCDDisk;
  93. Property Disk[AIndex : Integer] : TCDDisk Read GetD Write SetD; default;
  94. end;
  95. { TCDDBQueryMatch }
  96. TCDDBQueryMatch = Class(TCollectionItem)
  97. private
  98. FCategory: String;
  99. FDiscID: Integer;
  100. FPerformer: String;
  101. FTitle: String;
  102. Public
  103. Procedure Assign(Source : TPersistent); override;
  104. Published
  105. Property DiscID : Integer Read FDiscID Write FDiscID;
  106. Property Category : String Read FCategory Write FCategory;
  107. Property Title : String Read FTitle Write FTitle;
  108. Property Performer : String Read FPerformer Write FPerformer;
  109. end;
  110. { TCDDBQueryMatches }
  111. TCDDBQueryMatches = Class(TCollection)
  112. private
  113. function GetM(AIndex : Integer): TCDDBQueryMatch;
  114. procedure SetM(AIndex : Integer; const AValue: TCDDBQueryMatch);
  115. Public
  116. Function AddMatch(Const ADiscID: Integer; Const ACategory,ATitle, APerformer : String) : TCDDBQueryMatch;
  117. Function AddMatch(Const ADiscID,ACategory,ATitle, APerformer : String) : TCDDBQueryMatch;
  118. Function AddMatch : TCDDBQueryMatch;
  119. Property Match[AIndex : Integer] :TCDDBQueryMatch Read GetM Write SetM; default;
  120. end;
  121. { TCDDBParser }
  122. TCDDBParser = Class(TComponent)
  123. private
  124. FDisks: TCDDisks;
  125. FDisk : TCDDisk;
  126. function ParseExtraDiskData(AData: String): Boolean;
  127. function ParseExtraTrackData(ATrack: TCDTrack; AData: String): Boolean;
  128. procedure SetDisks(const AValue: TCDDisks);
  129. procedure SplitQueryResponse(AResponse: String; var ACategory, ADiscID, ATitle, APerformer: String);
  130. procedure SplitTitle(const ALine: String; var AArtist, ATitle: String;
  131. PreferTitle: boolean);
  132. function StdReplacements(S: String): String;
  133. Protected
  134. Procedure CheckDisk;
  135. function CheckCDDBCmdResult(var S: String): Integer;
  136. Function CreateDisks :TCDDisks; virtual;
  137. Function IsComment(Const L : String) : Boolean;
  138. Function GetTrack(Const TrackNo : Integer) : TCDTrack;
  139. Property Disk : TCDDisk Read FDisk;
  140. Public
  141. Constructor Create(AOwner : TComponent); override;
  142. Destructor Destroy; override;
  143. Function ParseCDDBReadResponse(Response : TStrings; WithHeader : Boolean = True) : Integer;
  144. Function ParseCDDBReadResponse(Response : TStream; WithHeader : Boolean = True) : Integer;
  145. Function ParseCDDBQueryResponse(Response : TStrings; Matches : TCDDBQueryMatches; WithHeader : Boolean = True) : Integer;
  146. Function ParseCDDBQueryResponse(Response : TStream; Matches : TCDDBQueryMatches; WithHeader : Boolean = True) : Integer;
  147. Published
  148. Property Disks : TCDDisks Read FDisks Write SetDisks;
  149. end;
  150. ECDDBParser = Class(Exception);
  151. Function DiscIDToStr(ID : Integer) : String;
  152. Function StrToDiscID(S : String) : Integer;
  153. implementation
  154. Resourcestring
  155. SErrNoDisk = 'No disk active';
  156. SErrInvalidTrackNo = 'Invalid track number: %d';
  157. SErrParsingLine = 'An error occurred while parsing line %d of the response: %s';
  158. SErrCDDBResponse = 'CDDB error in command response: %s';
  159. function DiscIDToStr(ID: Integer): String;
  160. begin
  161. Result:=LowerCase(Format('%.8x',[ID]));
  162. end;
  163. function StrToDiscID(S: String): Integer;
  164. begin
  165. Result:=StrToIntDef('$'+S,-1);
  166. end;
  167. { TCDTrack }
  168. function TCDTrack.GetPerformer: String;
  169. begin
  170. Result:=FPerformer;
  171. If (Result='') and Assigned(Collection) and (Collection is TCDTracks) then
  172. If Assigned(TCDTracks(Collection).CDDisk) then
  173. Result:=TCDTracks(Collection).CDDisk.Performer;
  174. end;
  175. procedure TCDTrack.Assign(Source: TPersistent);
  176. Var
  177. T : TCDTrack;
  178. begin
  179. if (Source is TCDTrack) then
  180. begin
  181. T:=Source as TCDTrack;
  182. FTitle:=T.FTitle;
  183. FExtra:=T.FExtra;
  184. FPerformer:=T.FPerformer;
  185. FDuration:=T.FDuration;
  186. end
  187. else
  188. inherited Assign(Source);
  189. end;
  190. { TCDDisk }
  191. procedure TCDDisk.SetTracks(const AValue: TCDTracks);
  192. begin
  193. if FTracks=AValue then exit;
  194. FTracks.Assign(AValue);
  195. end;
  196. function TCDDisk.GetDiskID: String;
  197. begin
  198. Result:=DiscIDToStr(FdiskID);
  199. end;
  200. procedure TCDDisk.SetDiskID(const AValue: String);
  201. begin
  202. FDiskID:=StrToDiscID(AValue);
  203. end;
  204. function TCDDisk.CreateTracks: TCDTracks;
  205. begin
  206. Result:=TCDTracks.Create(TCDTrack);
  207. end;
  208. constructor TCDDisk.Create(ADiskID: Integer);
  209. begin
  210. FDiskID:=ADiskID;
  211. Create(Nil);
  212. end;
  213. constructor TCDDisk.Create(ACollection: TCollection);
  214. begin
  215. FTracks:=CreateTracks;
  216. FTracks.FCDDisk:=Self;
  217. inherited Create(ACollection);
  218. end;
  219. procedure TCDDisk.Assign(Source: TPersistent);
  220. Var
  221. D : TCDDisk;
  222. begin
  223. if Source is TCDDisk then
  224. begin
  225. D:=Source as TCDDisk;
  226. FTitle:=D.FTitle;
  227. FExtra:=D.FExtra;
  228. FPerformer:=D.FPerformer;
  229. FYear:=D.FYear;
  230. FTracks.Assign(D.FTracks);
  231. FPLayOrder:=D.FPlayOrder;
  232. end
  233. else
  234. inherited Assign(Source);
  235. end;
  236. { TCDTracks }
  237. function TCDTracks.GetT(AIndex : Integer): TCDTrack;
  238. begin
  239. Result:=Items[AIndex] as TCDTrack;
  240. end;
  241. procedure TCDTracks.SetT(AIndex : Integer; const AValue: TCDTrack);
  242. begin
  243. Items[AIndex]:=AValue;
  244. end;
  245. function TCDTracks.AddTrack(const ATitle, AExtra: String; ADuration: TDateTime
  246. ): TCDTrack;
  247. begin
  248. Result:=Add as TCDTrack;
  249. Result.Title:=ATitle;
  250. Result.Extra:=AExtra;
  251. Result.Duration:=ADuration;
  252. end;
  253. function TCDTracks.AddTrack(const ATitle, AExtra: String): TCDTrack;
  254. begin
  255. Result:=AddTrack(ATitle,AExtra,0);
  256. end;
  257. function TCDTracks.AddTrack(const ATitle: String): TCDTrack;
  258. begin
  259. Result:=AddTrack(ATitle,'',0);
  260. end;
  261. { TCDDisks }
  262. function TCDDisks.GetD(AIndex : Integer): TCDDisk;
  263. begin
  264. Result:=Items[AIndex] as TCDDisk;
  265. end;
  266. procedure TCDDisks.SetD(AIndex : Integer; const AValue: TCDDisk);
  267. begin
  268. Items[AIndex]:=AValue;
  269. end;
  270. function TCDDisks.AddDisk(ADiscID: String): TCDDisk;
  271. begin
  272. Result:=Self.AddDisk();
  273. Result.DiscID:=ADiscID;
  274. end;
  275. function TCDDisks.AddDisk: TCDDisk;
  276. begin
  277. Result:=Add as TCDDisk;
  278. end;
  279. { TCDDBParser }
  280. procedure TCDDBParser.SetDisks(const AValue: TCDDisks);
  281. begin
  282. if FDisks=AValue then exit;
  283. FDisks.Assign(AValue);
  284. end;
  285. procedure TCDDBParser.CheckDisk;
  286. begin
  287. If (FDisk=Nil) then
  288. Raise ECDDBParser.Create(SErrNoDisk)
  289. end;
  290. function TCDDBParser.CreateDisks: TCDDisks;
  291. begin
  292. Result:=TCDDisks.Create(TCDDisk);
  293. end;
  294. function TCDDBParser.IsComment(const L: String): Boolean;
  295. begin
  296. Result:=(Length(L)=0) or (L[1]='#');
  297. end;
  298. function TCDDBParser.GetTrack(const TrackNo: Integer): TCDTrack;
  299. begin
  300. If (TrackNo<0) then
  301. Raise ECDDBParser.CreateFmt(SErrInvalidTrackNo,[TrackNo]);
  302. CheckDisk;
  303. If (TrackNo>FDisk.Tracks.Count) then
  304. Raise ECDDBParser.CreateFmt(SErrInvalidTrackNo,[TrackNo]);
  305. If (TrackNo=FDisk.Tracks.Count) then
  306. Result:=FDisk.Tracks.AddTrack('')
  307. else
  308. Result:=FDisk.Tracks[TrackNo]
  309. end;
  310. constructor TCDDBParser.Create(AOwner: TComponent);
  311. begin
  312. inherited Create(AOwner);
  313. FDisks:=CreateDisks;
  314. end;
  315. destructor TCDDBParser.Destroy;
  316. begin
  317. FreeAndNil(FDisks);
  318. inherited Destroy;
  319. end;
  320. Function TCDDBParser.StdReplacements(S : String) : String;
  321. begin
  322. Result:=StringReplace(S,'\n',sLineBreak,[rfReplaceAll]);
  323. end;
  324. Function TCDDBParser.ParseExtraDiskData(AData : String) : Boolean;
  325. begin
  326. FDisk.Extra:=FDisk.Extra+StdReplacements(AData);
  327. end;
  328. Function TCDDBParser.ParseExtraTrackData(ATrack : TCDTrack; AData : String) : Boolean;
  329. begin
  330. ATrack.Extra:=ATrack.Extra+StdReplacements(AData);
  331. end;
  332. Procedure TCDDBParser.SplitTitle(Const ALine: String; Var AArtist, ATitle : String; PreferTitle : boolean);
  333. Var
  334. P,L : Integer;
  335. begin
  336. // Artist / Title
  337. L:=Length(ALine);
  338. P:=Pos('/',ALine);
  339. If (P=0) and Not PreferTitle then
  340. P:=L+1;
  341. AArtist:=Trim(Copy(ALine,1,P-1));
  342. ATitle:=Trim(Copy(ALine,P+1,L-P));
  343. end;
  344. Function TCDDBParser.ParseCDDBReadResponse(Response: TStrings; WithHeader : Boolean = True) : Integer;
  345. Var
  346. I,P : Integer;
  347. L,Args,A,T : String;
  348. TrackID : Integer;
  349. Track : TCDTrack;
  350. begin
  351. Result:=-1;
  352. FDisks.Clear;
  353. If WithHeader and (Response.Count>0) then
  354. begin
  355. L:=Response[0];
  356. If Not (CheckCDDBCmdResult(L) in [200,210]) then
  357. Raise ECDDBParser.CreateFmt(SErrCDDBResponse,[L]);
  358. end;
  359. FDisk:=Nil;
  360. Result:=0;
  361. Try
  362. Try
  363. I:=Ord(WithHeader);
  364. While (I<Response.Count) do
  365. begin
  366. L:=Response[i];
  367. If Not IsComment(L) then
  368. begin
  369. P:=Pos('=',L);
  370. Args:=Copy(L,P+1,Length(L)-P);
  371. L:=Uppercase(Copy(L,1,P-1));
  372. If (L='DISCID') then
  373. FDisk:=FDisks.AddDisk(Args)
  374. else
  375. begin
  376. CheckDisk;
  377. If (L='DTITLE') then
  378. begin
  379. SplitTitle(Args,A,T,True);
  380. FDisk.Title:=T;
  381. FDisk.Performer:=A;
  382. end
  383. else if (L='DYEAR') then
  384. begin
  385. FDisk.Year:=StrToIntDef(Trim(Args),0);
  386. end
  387. else if (L='DGENRE') then
  388. begin
  389. FDisk.Genre:=Trim(Args);
  390. end
  391. else if (L='EXTD') then
  392. ParseExtraDiskData(Args)
  393. else if (Copy(L,1,6)='TTITLE') then
  394. begin
  395. Delete(L,1,6);
  396. TrackID:=StrToIntDef(L,-1);
  397. Track:=GetTrack(TrackID);
  398. SplitTitle(Args,A,T,True);
  399. Track.Title:=T;
  400. Track.Performer:=A;
  401. end
  402. else if (Copy(L,1,6)='EXTT') then
  403. begin
  404. Delete(L,1,6);
  405. TrackID:=StrToIntDef(L,-1);
  406. Track:=GetTrack(TrackID);
  407. ParseExtraTrackData(Track,Args);
  408. end
  409. else if (Copy(L,1,9)='PLAYORDER') then
  410. begin
  411. FDisk.PlayOrder:=Trim(Args);
  412. end;
  413. end;
  414. end;
  415. Inc(I);
  416. end;
  417. except
  418. On E : Exception do
  419. begin
  420. E.Message:=Format(SErrParsingLine,[I,E.MEssage]);
  421. Raise;
  422. end;
  423. end;
  424. Result:=FDisks.Count;
  425. Finally
  426. FDisk:=Nil;
  427. end;
  428. end;
  429. Function TCDDBParser.ParseCDDBReadResponse(Response: TStream; WithHeader : Boolean = True) : Integer;
  430. Var
  431. L : TStringList;
  432. begin
  433. L:=TStringList.Create;
  434. try
  435. L.LoadFromStream(Response);
  436. Result:=ParseCDDBReadResponse(L,WithHeader);
  437. finally
  438. L.Free;
  439. end;
  440. end;
  441. function TCDDBParser.ParseCDDBQueryResponse(Response: TStrings;
  442. Matches: TCDDBQueryMatches; WithHeader: Boolean): Integer;
  443. Var
  444. I,CmdRes : Integer;
  445. L : String;
  446. D,C,T,P : String;
  447. begin
  448. Matches.Clear;
  449. Result:=-1;
  450. If WithHeader and (Response.Count>0) then
  451. begin
  452. L:=Response[0];
  453. CmdRes:=CheckCDDBCmdResult(L);
  454. If (CmdRes=200) then
  455. begin
  456. SplitQueryResponse(L,C,D,T,P);
  457. Matches.AddMatch(D,C,T,P);
  458. Result:=1;
  459. Exit;
  460. end
  461. else if not (CmdRes in [210,211]) then
  462. Raise ECDDBParser.CreateFmt(SerrCDDBResponse,[L]);
  463. end;
  464. For I:=Ord(WithHeader or (CMDRes=211)) to Response.Count-1 do
  465. If (Response[i]<>'.') then
  466. begin
  467. SplitQueryResponse(Response[i],C,D,T,P);
  468. Matches.AddMatch(D,C,T,P);
  469. end;
  470. Result:=Matches.Count;
  471. end;
  472. function TCDDBParser.ParseCDDBQueryResponse(Response: TStream;
  473. Matches: TCDDBQueryMatches; WithHeader: Boolean): Integer;
  474. Var
  475. L : TStringList;
  476. begin
  477. L:=TStringList.Create;
  478. try
  479. L.LoadFromStream(Response);
  480. Result:=ParseCDDBQueryResponse(L,Matches,WithHeader);
  481. finally
  482. L.Free;
  483. end;
  484. end;
  485. Function TCDDBParser.CheckCDDBCmdResult(Var S : String) : Integer;
  486. Var
  487. P : integer;
  488. begin
  489. P:=Pos(' ',S);
  490. If (P=0) then
  491. P:=Length(S)+1;
  492. Result:=StrToIntDef(Copy(S,1,P-1),0);
  493. Delete(S,1,P);
  494. end;
  495. Procedure TCDDBParser.SplitQueryResponse(AResponse :String; Var ACategory, ADiscID, ATitle, APerformer : String);
  496. Var
  497. P : Integer;
  498. begin
  499. P:=Pos(' ',AResponse);
  500. ACategory:=Copy(AResponse,1,P-1);
  501. Delete(AResponse,1,P);
  502. P:=Pos(' ',AResponse);
  503. ADiscId:=Copy(AResponse,1,P-1);
  504. Delete(AResponse,1,P);
  505. SplitTitle(AResponse,APerformer,ATitle,True);
  506. end;
  507. { TCDDBQueryMatches }
  508. function TCDDBQueryMatches.GetM(AIndex : Integer): TCDDBQueryMatch;
  509. begin
  510. Result:=TCDDBQueryMatch(Items[AIndex]);
  511. end;
  512. procedure TCDDBQueryMatches.SetM(AIndex : Integer; const AValue: TCDDBQueryMatch
  513. );
  514. begin
  515. Items[AIndex]:=AValue;
  516. end;
  517. function TCDDBQueryMatches.AddMatch(const ADiscID: Integer; const ACategory,
  518. ATitle, APerformer: String): TCDDBQueryMatch;
  519. begin
  520. Result:=AddMatch();
  521. Result.DiscID:=ADiscID;
  522. Result.Category:=ACategory;
  523. Result.Title:=ATitle;
  524. Result.Performer:=APerformer;
  525. end;
  526. function TCDDBQueryMatches.AddMatch(const ADiscID, ACategory, ATitle, APerformer : String): TCDDBQueryMatch;
  527. begin
  528. Result:=AddMatch(StrToDiscID(ADiscID),ACategory,ATitle,APerformer);
  529. end;
  530. function TCDDBQueryMatches.AddMatch: TCDDBQueryMatch;
  531. begin
  532. Result:=Add as TCDDBQueryMatch;
  533. end;
  534. { TCDDBQueryMatch }
  535. procedure TCDDBQueryMatch.Assign(Source: TPersistent);
  536. Var
  537. M : TCDDBQueryMatch;
  538. begin
  539. if Source is TCDDBQueryMatch then
  540. begin
  541. M:=Source as TCDDBQueryMatch;
  542. FDiscID:=M.FDiscID;
  543. FCategory:=M.FCategory;
  544. FPerformer:=M.FPerformer;
  545. FTitle:=M.FTitle;
  546. end
  547. else
  548. inherited Assign(Source);
  549. end;
  550. end.