From 593728e61641fbf09e55220d4df98a79c189a202 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Fri, 23 Sep 2022 08:25:28 +0200 Subject: [PATCH] PICARD-2550: disc ID lookup from dBpoweramp secure ripping log --- picard/disc/dbpoweramplog.py | 70 ++++++++++++++++++++++ picard/tagger.py | 26 +++++--- picard/ui/mainwindow.py | 2 +- test/data/dbpoweramp-datatrack.txt | Bin 0 -> 11044 bytes test/data/dbpoweramp-utf16le.txt | Bin 0 -> 7440 bytes test/data/dbpoweramp-utf8.txt | 58 ++++++++++++++++++ test/test_disc_dbpoweramplog.py | 92 +++++++++++++++++++++++++++++ 7 files changed, 239 insertions(+), 9 deletions(-) create mode 100644 picard/disc/dbpoweramplog.py create mode 100644 test/data/dbpoweramp-datatrack.txt create mode 100644 test/data/dbpoweramp-utf16le.txt create mode 100644 test/data/dbpoweramp-utf8.txt create mode 100644 test/test_disc_dbpoweramplog.py diff --git a/picard/disc/dbpoweramplog.py b/picard/disc/dbpoweramplog.py new file mode 100644 index 000000000..81588e1cd --- /dev/null +++ b/picard/disc/dbpoweramplog.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +# +# Picard, the next-generation MusicBrainz tagger +# +# Copyright (C) 2022 Philipp Wolfer +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +import re + +from picard.disc.utils import ( + NotSupportedTOCError, + TocEntry, + calculate_mb_toc_numbers, +) + + +RE_TOC_ENTRY = re.compile( + r"^Track (?P\d+):\s+Ripped LBA (?P\d+) to (?P\d+)") + + +def filter_toc_entries(lines): + """ + Take iterator of lines, return iterator of toc entries + """ + last_track_num = 0 + for line in lines: + m = RE_TOC_ENTRY.match(line) + if m: + track_num = int(m['num']) + if last_track_num + 1 != track_num: + raise NotSupportedTOCError(f'Non consecutive track numbers ({last_track_num} => {track_num}) in dBPoweramp log. Likely a partial rip, disc ID cannot be calculated') + last_track_num = track_num + yield TocEntry(track_num, int(m['start_sector']), int(m['end_sector'])-1) + + +ENCODING_BOMS = { + b'\xff\xfe': 'utf-16-le', + b'\xfe\xff': 'utf-16-be', + b'\00\00\xff\xfe': 'utf-32-le', + b'\00\00\xfe\xff': 'utf-32-be', +} + + +def _detect_encoding(path): + with open(path, 'rb') as f: + first_bytes = f.read(4) + for bom, encoding in ENCODING_BOMS.items(): + if first_bytes.startswith(bom): + return encoding + return 'utf-8' + + +def toc_from_file(path): + """Reads dBpoweramp log files, generates MusicBrainz disc TOC listing for use as discid.""" + encoding = _detect_encoding(path) + with open(path, 'r', encoding=encoding) as f: + return calculate_mb_toc_numbers(filter_toc_entries(f)) diff --git a/picard/tagger.py b/picard/tagger.py index 0f76da448..839a0c6e1 100644 --- a/picard/tagger.py +++ b/picard/tagger.py @@ -108,6 +108,7 @@ from picard.const.sys import ( from picard.dataobj import DataObject from picard.disc import ( Disc, + dbpoweramplog, eaclog, whipperlog, ) @@ -1171,7 +1172,9 @@ class Tagger(QtWidgets.QApplication): def lookup_discid_from_logfile(self): file_chooser = QtWidgets.QFileDialog(self.window) file_chooser.setNameFilters([ + _("All supported log files") + " (*.log, *.txt)", _("EAC / XLD / Whipper log files") + " (*.log)", + _("dBpoweramp log files") + " (*.txt)", _("All files") + " (*)", ]) if file_chooser.exec_(): @@ -1184,16 +1187,23 @@ class Tagger(QtWidgets.QApplication): traceback=self._debug) def _parse_disc_ripping_log(self, disc, path): - try: - log.debug('Trying to parse "%s" as EAC / XLD log...', path) - toc = eaclog.toc_from_file(path) - except Exception: + log_readers = ( + eaclog.toc_from_file, + whipperlog.toc_from_file, + dbpoweramplog.toc_from_file, + ) + for reader in log_readers: + module_name = reader.__module__ try: - log.debug('Trying to parse "%s" as Whipper log...', path) - toc = whipperlog.toc_from_file(path) + log.debug('Trying to parse "%s" with %s...', path, module_name) + toc = reader(path) + break except Exception: - log.warning('Failed parsing ripping log "%s"', path, exc_info=True) - raise + log.debug('Failed parsing ripping log "%s" with %s', path, module_name, exc_info=True) + else: + msg = N_('Failed parsing ripping log "%s"') + log.warning(msg, path) + raise Exception(_(msg) % path) disc.put(toc) @property diff --git a/picard/ui/mainwindow.py b/picard/ui/mainwindow.py index c8e10a41f..55125329d 100644 --- a/picard/ui/mainwindow.py +++ b/picard/ui/mainwindow.py @@ -922,7 +922,7 @@ class MainWindow(QtWidgets.QMainWindow, PreserveGeometry): def _set_cd_lookup_from_file_actions(self, drives): if self.cd_lookup_menu.actions(): self.cd_lookup_menu.addSeparator() - action = self.cd_lookup_menu.addAction(_('From EAC / XLD / Whipper &log file...')) + action = self.cd_lookup_menu.addAction(_('From CD ripper &log file...')) if not drives: self._update_cd_lookup_default_action(action) action.setData('logfile:eac') diff --git a/test/data/dbpoweramp-datatrack.txt b/test/data/dbpoweramp-datatrack.txt new file mode 100644 index 0000000000000000000000000000000000000000..d62a95623a6fd04c0139f12c3fff0b4924de44c3 GIT binary patch literal 11044 zcmd6tYi}Ay6o%(>rTzz$4^1kS6j-j7`oUaWMM;{fcAH9Z6afnkietMtu@hDO>uuj> zX2*NM3rhq9t$<~AF6W$i=FFLy{qNttD&#OhuoXDeJdmziUeE zo0&P&=Y#T{>;IwoXhQwJ)cwGGQaUw^^-J1?QlIJfOuth-xm4ZM!IKNy+O@6e%>1Bq zT6?VDV?DW2`b4=$Hdic7Nz$~8s|MxlzuN`7SE|!AfwUmalD|vNTwB z`MQ2nO;@^Rk`JkGb@#!%)m@lq<)L&vP#V`Ob0Ue4^*xqeb8EY@(tFagZ~oBdrT!hO zL|OV(C4E;{$-FiXt=8Bhmj67$ZtF;Ra|`bupeRZ$6?G&8aJ(bDW|a{Y2`>k3T$j?RIs02!fLoZF}G z^>nEAj@0u{_4`PcJCx8*CB#ALOix zovL(34|018J5Aoma#Kk~PM&6^b*YOrC(BjTLPh1h7g^>B$y3A=o!A~8t2Y=Qj!-v( z85_<6bPZz;i^4Ven({A!K2Q~?R9zQbF}BG`O3Qj#&&`G9$c=1A7`^~jA*!!Cyu`Wc zr`@^Io{N0E#n{p~v~n^BP|Epxy!xqn@v~SNspPrR@oYSu>35>16-isNJ^+#z%e#Jk zqFO2MJlNxiU2I5SF86Rr--kUIg%@faZ{$W7@&@vDSjbR?Ngrhx$X^XjaXruRz)LU@cvUB6EJZq2`Jy{Dm($?{;4QZYB ztY|j9(`=aIS?f#LPLk}{Zpup7nf4y3_5<15d5vVe;8tZ{-?QTXSFufWJXGAl>V*jN zts;@BQqFa4hw!B%di*UV5ID1ASuW_`4pYStaB4OzXHw8~_6=-pNbCpt)bN~H69QF)OGY_gK- zi1H)rPuB6;?55T=(;lg9R_(irOqv@c9J?)gZLir>PxB&^)>6XLJbT93EhW}YGQJpj z?@M%oWmYl%T#*TTBdo4JZ-s1j9@i(E`LApDG&+`2&vlXAj6;nGVn{|rOSSBd?A8X! z#%@`%hmmGetJx?g9^o_sd)>dDgy5KBPd4_d>j{yc_ulOV@4YWE3G?wPvNr<@rRZ%(|zw`V+`o_KZ1i2`M+D%;g(ca(QF=*_WP zmU4E(L-kx$wjSwo|LT%RWVt_2GVM8M*WLMf@6TBcr8y6jXCF96!y=sLpWST3pJSc% zNq3Y&Yg^o+m%p-O%-c&hGQQX$69s4dV5|{l8x*_*uThl0p5!~X zy3H~yb`(&n1>IEDt-8y(r*0Sj{M~gmckQm+gX_7mYr6}6^3p`>uVLXx(>1 zH{f&2xDmgr?#^}jeZ_ObJ<_fjI^5NEN$Ush1MYo#u9#bMsOQpFyRn%D?mODG+8gdo zdah~jG5Xv_O|;3OO~vi;9k{RESN7Rw=ApZ1WX^q)$lsaPqMOg_*9o zhUh1)A9%WTpLyy|t@0XM_h{2~>W`?RD(jcOUtj@T-YL9{Y-DU*pQT zGxyr8mOm!?U*V@dI`>#rD@4hdpUl27BYXI_OV5YoDQ~)d&)Rp1%n{GBr;0`$S``s_ z)7z$5)uRZ>XYzc`{mPoD&2{wDD9z)0>lV|`j8=QmkP-6q9!va)17@hwG8vh3KjOoC zBuC8LO>{`-m3}|dcEhC^XT8wGi#@Kct#`<{$7G#}j7Q_dbBkoON)2iGOQ@37ict<< z^oWO~uUSJ{Jy!#-%@Azf8Jg3Xd|Ym9uE2#)=xpExA*0KRW9z-3x6OJ7ME*VZ9XwaX zbKmluXRQ47iP6(?Ke$(n*mEtsraT((txVM{LnM0mvBer+W^!w8rWR7m)^CBmuKSId zmBVk4eB|?LMz0R?T_=6=WT(&C5Jc#b{tro8!IhI1&nD`7OP?Pl)DEyv(de1}$~T{B zd19r};*>w#@3j6!mU(>l6Kl_&dNeYZ=%~DrFDCpWJ&Rzch|RJ?qf-xiOFrSbK3Zv{ zdgrzEtzXVOEmvTL0`e(~tYSiqc@7gDTZB7AL;2wm>N8lm;TfRjlymZ^aIIK`aJ6g| zs>(yBwV1o>i%4$s;*#umqG zSW>(`Bw}}rY=K2#)bsKYEkeeLjUS*{>cJcofTvV>n&MdZSufrX6^Q&Ugz~A!v0oH2AOr@7;l*&ss z!2SwS4P+yWFa{Y^#7HpLb%oHkK5Vme9q_9@87qQbxZ5 zzdCl8RFU0hwdZo|T#EM4yyWqwNG$T2eC53&y(f0{;D-pk{90OX8qX` z;5s%*;V%T+vBhhG53x~t{olAQZ4}qCt8IA|5~+9zt^>=@Ww@S3vBz~}I97%V*OJ!0 zIm_8aR?7;m1N2;n>yI#n_6JoZ>E3hb{^0o?ZDOme;9R?50ZtX(%P+=xjAVnDDMuO> zzwO{W*f7px+ZO{O)hAwy`CAOEJYLM-)2Q}1*G{)V9Y-v##-WA&8bLw?O7Up0GH7`}{k>yUp0zYjq4 zEn`o39&xGe(%b$hGZ>FRcEh;2ipO^?Y98l{-xsE<^L^VF0;KdNE1zr&NFyo)_u z_N>&d#q1uf#P~F#J;sahh~7O_lcy*bo6n=%pYUa&K9A6I9m-d~!DfZY%dm0GJEZ6G z+KW)GQ_6_AN!r)>+d_G?QIvPUxa>N|u0ECMVwA@#Q9g_56z2h4plZEhI9C-EaLwa9 z$_$$;AJ^gh1m5=NG*!H%vtpf*rmb1Qx#Ag8qw8EKrNMbN%NskMr!Dw