mirror of
https://github.com/fergalmoran/picard.git
synced 2026-01-06 08:34:01 +00:00
PICARD-2550: disc ID lookup from dBpoweramp secure ripping log
This commit is contained in:
70
picard/disc/dbpoweramplog.py
Normal file
70
picard/disc/dbpoweramplog.py
Normal 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))
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
BIN
test/data/dbpoweramp-datatrack.txt
Normal file
BIN
test/data/dbpoweramp-datatrack.txt
Normal file
Binary file not shown.
BIN
test/data/dbpoweramp-utf16le.txt
Normal file
BIN
test/data/dbpoweramp-utf16le.txt
Normal file
Binary file not shown.
58
test/data/dbpoweramp-utf8.txt
Normal file
58
test/data/dbpoweramp-utf8.txt
Normal 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
|
||||
92
test/test_disc_dbpoweramplog.py
Normal file
92
test/test_disc_dbpoweramplog.py
Normal 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)
|
||||
Reference in New Issue
Block a user