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 000000000..d62a95623 Binary files /dev/null and b/test/data/dbpoweramp-datatrack.txt differ diff --git a/test/data/dbpoweramp-utf16le.txt b/test/data/dbpoweramp-utf16le.txt new file mode 100644 index 000000000..5dddd1cee Binary files /dev/null and b/test/data/dbpoweramp-utf16le.txt differ diff --git a/test/data/dbpoweramp-utf8.txt b/test/data/dbpoweramp-utf8.txt new file mode 100644 index 000000000..8331e4510 --- /dev/null +++ b/test/data/dbpoweramp-utf8.txt @@ -0,0 +1,58 @@ +dBpoweramp 2022-09-02 Digital Audio Extraction Log from Donnerstag, 22. September 2022 08:03 + +Drive & Settings +---------------- + +Ripping with drive 'E: [TSSTcorp - CDDVDW SE-218BB ]', Drive offset: 6, Overread Lead-in/out: No +AccurateRip: Active, Using C2: No, Cache: 1024 KB, FUA Cache Invalidate: No +Pass 1 Drive Speed: Max, Pass 2 Drive Speed: Max +Bad Sector Re-rip:: Drive Speed: Max, Maximum Re-reads: 34 + +Encoder: m4a FDK (AAC) -cli_encoder="C:\Program Files\dBpoweramp\encoder\m4a FDK (AAC)\fdkaac.exe" -cli_cmd="-b 224000 -w 22050 -p 2 -G 0 --ignorelength -S -o {qt}[outfile]{qt} - " -selection="1,14" -cli_cmd_sf0=" -p 2 -G 0" -selection_sf0="0,0" + +Extraction Log +-------------- + +Track 1: Ripped LBA 0 to 24914 (5:32) in 0:30. Filename: C:\Users\Developer\Music\pornophonique\Brave New World\01 pornophonique - Coming Home.m4a + AccurateRip: Accurate (confidence 3) [Pass 1] + CRC32: E924FB20 AccurateRip CRC: 3182EB6A (CRCv2) [DiscID: 008-000adae1-00473706-5407c408-1] + AccurateRip Verified Confidence 3 [CRCv2 3182eb6a] + +Track 2: Ripped LBA 24914 to 43461 (4:07) in 0:20. Filename: C:\Users\Developer\Music\pornophonique\Brave New World\02 pornophonique - Save Game.m4a + AccurateRip: Accurate (confidence 3) [Pass 1] + CRC32: F19A6AFC AccurateRip CRC: 79A374E7 (CRCv2) [DiscID: 008-000adae1-00473706-5407c408-2] + AccurateRip Verified Confidence 3 [CRCv2 79a374e7] + +Track 3: Ripped LBA 43461 to 60740 (3:50) in 0:17. Filename: C:\Users\Developer\Music\pornophonique\Brave New World\03 pornophonique - Voices in My Head.m4a + AccurateRip: Accurate (confidence 3) [Pass 1] + CRC32: 300DF15C AccurateRip CRC: 77DE3AFE (CRCv2) [DiscID: 008-000adae1-00473706-5407c408-3] + AccurateRip Verified Confidence 3 [CRCv2 77de3afe] + +Track 4: Ripped LBA 60740 to 82940 (4:56) in 0:20. Filename: C:\Users\Developer\Music\pornophonique\Brave New World\04 pornophonique - The Songs We Sang Together.m4a + AccurateRip: Accurate (confidence 3) [Pass 1] + CRC32: 1657126F AccurateRip CRC: 4D824E2F (CRCv2) [DiscID: 008-000adae1-00473706-5407c408-4] + AccurateRip Verified Confidence 3 [CRCv2 4d824e2f] + +Track 5: Ripped LBA 82940 to 99850 (3:45) in 0:14. Filename: C:\Users\Developer\Music\pornophonique\Brave New World\05 pornophonique - Night Will Fall.m4a + AccurateRip: Accurate (confidence 3) [Pass 1] + CRC32: 66EFEA59 AccurateRip CRC: D0D39055 (CRCv2) [DiscID: 008-000adae1-00473706-5407c408-5] + AccurateRip Verified Confidence 3 [CRCv2 d0d39055] + +Track 6: Ripped LBA 99850 to 114907 (3:20) in 0:12. Filename: C:\Users\Developer\Music\pornophonique\Brave New World\06 pornophonique - Brave New World.m4a + AccurateRip: Accurate (confidence 3) [Pass 1] + CRC32: 5D7B712B AccurateRip CRC: CD69B7FC (CRCv2) [DiscID: 008-000adae1-00473706-5407c408-6] + AccurateRip Verified Confidence 3 [CRCv2 cd69b7fc] + +Track 7: Ripped LBA 114907 to 135408 (4:33) in 0:16. Filename: C:\Users\Developer\Music\pornophonique\Brave New World\07 pornophonique - Wave After Wave.m4a + AccurateRip: Accurate (confidence 3) [Pass 1] + CRC32: 694CDD79 AccurateRip CRC: FE6DBCBF (CRCv2) [DiscID: 008-000adae1-00473706-5407c408-7] + AccurateRip Verified Confidence 3 [CRCv2 fe6dbcbf] + +Track 8: Ripped LBA 135408 to 149173 (3:03) in 0:10. Filename: C:\Users\Developer\Music\pornophonique\Brave New World\08 pornophonique - Awakening.m4a + AccurateRip: Accurate (confidence 3) [Pass 1] + CRC32: 629CC273 AccurateRip CRC: 7EB7F715 (CRCv2) [DiscID: 008-000adae1-00473706-5407c408-8] + AccurateRip Verified Confidence 3 [CRCv2 7eb7f715] + +-------------- + +8 Tracks Ripped Accurately diff --git a/test/test_disc_dbpoweramplog.py b/test/test_disc_dbpoweramplog.py new file mode 100644 index 000000000..cf067208e --- /dev/null +++ b/test/test_disc_dbpoweramplog.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +# +# Picard, the next-generation MusicBrainz tagger +# +# Copyright (C) 2022 Laurent Monin +# 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. + + +from typing import Iterator + +from test.picardtestcase import ( + PicardTestCase, + get_test_data_path, +) + +from picard.disc.dbpoweramplog import ( + filter_toc_entries, + toc_from_file, +) +from picard.disc.utils import ( + NotSupportedTOCError, + TocEntry, +) + + +test_log = ( + 'TEST LOG', + 'Track 1: Ripped LBA 0 to 24914 (5:32) in 0:30. Filename: ', + '', + 'Track 2: Ripped LBA 24914 to 43461 (4:07) in 0:20. Filename: ', + 'Track 3: Ripped LBA 43461 to 60740 (3:50) in 0:17. Filename: ', + '', + 'foo', +) + +test_entries = [ + TocEntry(1, 0, 24913), + TocEntry(2, 24914, 43460), + TocEntry(3, 43461, 60739), +] + + +class TestFilterTocEntries(PicardTestCase): + + def test_filter_toc_entries(self): + result = filter_toc_entries(iter(test_log)) + self.assertTrue(isinstance(result, Iterator)) + entries = list(result) + self.assertEqual(test_entries, entries) + + def test_no_gaps_in_track_numbers(self): + log = test_log[:2] + test_log[4:] + with self.assertRaisesRegex(NotSupportedTOCError, '^Non consecutive track numbers'): + list(filter_toc_entries(log)) + + +class TestTocFromFile(PicardTestCase): + + def _test_toc_from_file(self, logfile): + test_log = get_test_data_path(logfile) + toc = toc_from_file(test_log) + self.assertEqual((1, 8, 149323, 150, 25064, 43611, 60890, 83090, 100000, 115057, 135558), toc) + + def test_toc_from_file_utf8(self): + self._test_toc_from_file('dbpoweramp-utf8.txt') + + def test_toc_from_file_utf16le(self): + self._test_toc_from_file('dbpoweramp-utf16le.txt') + + def test_toc_from_file_with_datatrack(self): + test_log = get_test_data_path('dbpoweramp-datatrack.txt') + toc = toc_from_file(test_log) + self.assertEqual((1, 13, 239218, 150, 16988, 32954, 48647, 67535, 87269, 104221, 121441, 138572, 152608, 170362, 187838, 215400), toc) + + def test_toc_from_empty_file(self): + test_log = get_test_data_path('eac-empty.log') + with self.assertRaises(NotSupportedTOCError): + toc_from_file(test_log)