PICARD-2550: disc ID lookup from dBpoweramp secure ripping log

This commit is contained in:
Philipp Wolfer
2022-09-23 08:25:28 +02:00
parent e1428c9340
commit 593728e616
7 changed files with 239 additions and 9 deletions

View File

@@ -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<num>\d+):\s+Ripped LBA (?P<start_sector>\d+) to (?P<end_sector>\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))

View File

@@ -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

View File

@@ -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')

Binary file not shown.

Binary file not shown.

View File

@@ -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

View File

@@ -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)