Merge branch 'develop'

This commit is contained in:
JoeLametta
2019-10-27 13:30:33 +00:00
76 changed files with 2186 additions and 1035 deletions

View File

@@ -1,6 +1,13 @@
dist: xenial
sudo: required
language: bash
language: python
python:
- "2.7"
virtualenv:
system_site_packages: false
cache: pip
env:
- FLAKE8=false
@@ -9,21 +16,18 @@ env:
install:
# Dependencies
- sudo apt-get -qq update
- sudo pip install --upgrade -qq pip
- sudo apt-get -qq install cdparanoia cdrdao flac gir1.2-glib-2.0 libcdio-dev libiso9660-dev libsndfile1-dev python-cddb python-gi python-musicbrainzngs python-mutagen python-setuptools sox swig libcdio-utils
- sudo pip install pycdio==0.21 requests
- pip install --upgrade -qq pip
- sudo apt-get -qq install cdparanoia cdrdao flac gir1.2-glib-2.0 libcdio-dev libgirepository1.0-dev libiso9660-dev libsndfile1-dev sox swig libcdio-utils
# newer version of pydcio requires newer version of libcdio than travis has
- pip install pycdio==0.21
# install rest of dependencies
- pip install -r requirements.txt
# Testing dependencies
- sudo apt-get -qq install python-twisted-core
- sudo pip install flake8
# Build bundled C utils
- cd src
- sudo make install
- cd ..
- pip install twisted flake8
# Installing
- sudo python setup.py install
- python setup.py install
script:
- if [ ! "$FLAKE8" = true ]; then python -m unittest discover; fi

View File

@@ -2,7 +2,66 @@
## [Unreleased](https://github.com/whipper-team/whipper/tree/HEAD)
[Full Changelog](https://github.com/whipper-team/whipper/compare/v0.7.3...HEAD)
[Full Changelog](https://github.com/whipper-team/whipper/compare/v0.8.0...HEAD)
## [v0.8.0](https://github.com/whipper-team/whipper/tree/v0.8.0) (2019-10-27)
[Full Changelog](https://github.com/whipper-team/whipper/compare/v0.7.3...v0.8.0)
**Fixed bugs:**
- whipper bails out if MusicBrainz release group doesnt have a type [\#396](https://github.com/whipper-team/whipper/issues/396)
- object has no attribute 'working\_directory' when running cd info [\#375](https://github.com/whipper-team/whipper/issues/375)
- Failure to rip CD: "ValueError: could not convert string to float: " [\#374](https://github.com/whipper-team/whipper/issues/374)
- "AttributeError: Program instance has no attribute '\_presult'" when ripping [\#369](https://github.com/whipper-team/whipper/issues/369)
- Drive analysis fails [\#361](https://github.com/whipper-team/whipper/issues/361)
- Eliminate warning "eject: CD-ROM tray close command failed" [\#354](https://github.com/whipper-team/whipper/issues/354)
- Flac file permissions [\#284](https://github.com/whipper-team/whipper/issues/284)
**Closed issues:**
- Separate out Release in log into two value map [\#416](https://github.com/whipper-team/whipper/issues/416)
- Network issue [\#412](https://github.com/whipper-team/whipper/issues/412)
- RequestsDependencyWarning: urllib3 \(1.25.2\) or chardet \(3.0.4\) doesn't match a supported version [\#400](https://github.com/whipper-team/whipper/issues/400)
- Run script after rip [\#394](https://github.com/whipper-team/whipper/issues/394)
- Add git/mercurial dependency to the README [\#386](https://github.com/whipper-team/whipper/issues/386)
- Include MusicBrainz Release ID in the log file [\#381](https://github.com/whipper-team/whipper/issues/381)
- Rip while entering MusicBrainz data [\#360](https://github.com/whipper-team/whipper/issues/360)
- Doesn't eject - "eject: unable to eject" \(but manual eject works\) [\#355](https://github.com/whipper-team/whipper/issues/355)
- Note in the whipper output/log if development version was used [\#337](https://github.com/whipper-team/whipper/issues/337)
- fedora 29, whipper 0.72, Error While Executing Any Command [\#332](https://github.com/whipper-team/whipper/issues/332)
- read-toc progress information [\#299](https://github.com/whipper-team/whipper/issues/299)
- ripping fails frequently, but not repeatably [\#290](https://github.com/whipper-team/whipper/issues/290)
- Look into adding more MusicBrainz identifiers to ripped files [\#200](https://github.com/whipper-team/whipper/issues/200)
**Merged pull requests:**
- Fix ripping discs with less than ten tracks [\#418](https://github.com/whipper-team/whipper/pull/418) ([mtdcr](https://github.com/mtdcr))
- Make getFastToc\(\) fast again [\#417](https://github.com/whipper-team/whipper/pull/417) ([mtdcr](https://github.com/mtdcr))
- Use ruamel.yaml for formatting and outputting rip .log file [\#415](https://github.com/whipper-team/whipper/pull/415) ([itismadness](https://github.com/itismadness))
- Handle missing self.options for whipper cd info [\#410](https://github.com/whipper-team/whipper/pull/410) ([JoeLametta](https://github.com/JoeLametta))
- Fix erroneous result message for whipper drive analyze [\#409](https://github.com/whipper-team/whipper/pull/409) ([JoeLametta](https://github.com/JoeLametta))
- Report eject's failures as logger warnings [\#408](https://github.com/whipper-team/whipper/pull/408) ([JoeLametta](https://github.com/JoeLametta))
- Set FLAC files permissions to 0644 [\#407](https://github.com/whipper-team/whipper/pull/407) ([JoeLametta](https://github.com/JoeLametta))
- Fix offset find command [\#406](https://github.com/whipper-team/whipper/pull/406) ([vmx](https://github.com/vmx))
- Make whipper not break on missing release type [\#398](https://github.com/whipper-team/whipper/pull/398) ([Freso](https://github.com/Freso))
- Set default for eject to: success [\#392](https://github.com/whipper-team/whipper/pull/392) ([gorgobacka](https://github.com/gorgobacka))
- Use eject value of the class again [\#391](https://github.com/whipper-team/whipper/pull/391) ([gorgobacka](https://github.com/gorgobacka))
- Convert documentation from epydoc to reStructuredText [\#387](https://github.com/whipper-team/whipper/pull/387) ([JoeLametta](https://github.com/JoeLametta))
- Include MusicBrainz Release URL in log output [\#382](https://github.com/whipper-team/whipper/pull/382) ([Freso](https://github.com/Freso))
- Specify supported version\(s\) of Python in setup.py [\#378](https://github.com/whipper-team/whipper/pull/378) ([Freso](https://github.com/Freso))
- Fix critical regressions introduced in 3e79032 and 16b0d8d [\#371](https://github.com/whipper-team/whipper/pull/371) ([JoeLametta](https://github.com/JoeLametta))
- Use git to get whipper's version [\#370](https://github.com/whipper-team/whipper/pull/370) ([Freso](https://github.com/Freso))
- Handle artist MBIDs as multivalue tags [\#367](https://github.com/whipper-team/whipper/pull/367) ([Freso](https://github.com/Freso))
- Add Track, Release Group, and Work MBIDs to ripped files [\#366](https://github.com/whipper-team/whipper/pull/366) ([Freso](https://github.com/Freso))
- Refresh MusicBrainz JSON responses used for testing [\#365](https://github.com/whipper-team/whipper/pull/365) ([Freso](https://github.com/Freso))
- Clean up MusicBrainz nomenclature [\#364](https://github.com/whipper-team/whipper/pull/364) ([Freso](https://github.com/Freso))
- Fix misaligned output in command.mblookup [\#363](https://github.com/whipper-team/whipper/pull/363) ([Freso](https://github.com/Freso))
- Update accuraterip-checksum [\#362](https://github.com/whipper-team/whipper/pull/362) ([Freso](https://github.com/Freso))
- Require Developer Certificate of Origin sign-off [\#358](https://github.com/whipper-team/whipper/pull/358) ([JoeLametta](https://github.com/JoeLametta))
- Address warnings/errors from various static analysis tools [\#357](https://github.com/whipper-team/whipper/pull/357) ([JoeLametta](https://github.com/JoeLametta))
- Clarify format option for disc template [\#353](https://github.com/whipper-team/whipper/pull/353) ([rekh127](https://github.com/rekh127))
- Refactor cdrdao toc/table functions into Task and provide progress output [\#345](https://github.com/whipper-team/whipper/pull/345) ([jtl999](https://github.com/jtl999))
- accuraterip-checksum: convert to python C extension [\#274](https://github.com/whipper-team/whipper/pull/274) ([mtdcr](https://github.com/mtdcr))
## [v0.7.3](https://github.com/whipper-team/whipper/tree/v0.7.3) (2018-12-14)
[Full Changelog](https://github.com/whipper-team/whipper/compare/v0.7.2...v0.7.3)
@@ -15,7 +74,6 @@
- Possible HTOA error [\#281](https://github.com/whipper-team/whipper/issues/281)
- Disc template KeyError [\#279](https://github.com/whipper-team/whipper/issues/279)
- Enhanced CD causes computer to freeze. [\#256](https://github.com/whipper-team/whipper/issues/256)
- pycdio & libcdio issues [\#238](https://github.com/whipper-team/whipper/issues/238)
- Unicode issues [\#215](https://github.com/whipper-team/whipper/issues/215)
- whipper offset find exception [\#208](https://github.com/whipper-team/whipper/issues/208)
- ZeroDivisionError: float division by zero [\#202](https://github.com/whipper-team/whipper/issues/202)
@@ -26,7 +84,9 @@
- On Ubuntu 18.10 cd-paranoia binary is called cdparanoia [\#347](https://github.com/whipper-team/whipper/issues/347)
- WARNING:whipper.common.program:network error: NetworkError\(\) [\#338](https://github.com/whipper-team/whipper/issues/338)
- Can not install [\#314](https://github.com/whipper-team/whipper/issues/314)
- use standard logging [\#303](https://github.com/whipper-team/whipper/issues/303)
- Write musicbrainz\_discid tag when disc is unknown [\#280](https://github.com/whipper-team/whipper/issues/280)
- pycdio & libcdio issues [\#238](https://github.com/whipper-team/whipper/issues/238)
- Write .toc files in addition to .cue files to support cdrdao and non-compliant .cue sheets [\#214](https://github.com/whipper-team/whipper/issues/214)
**Merged pull requests:**
@@ -198,7 +258,6 @@
- Add flake8 testing to CI [\#151](https://github.com/whipper-team/whipper/pull/151) ([Freso](https://github.com/Freso))
- Clean up files in misc/ [\#150](https://github.com/whipper-team/whipper/pull/150) ([Freso](https://github.com/Freso))
- Update .gitignore [\#148](https://github.com/whipper-team/whipper/pull/148) ([Freso](https://github.com/Freso))
- Fix references to morituri. [\#109](https://github.com/whipper-team/whipper/pull/109) ([Freso](https://github.com/Freso))
## [v0.5.1](https://github.com/whipper-team/whipper/tree/v0.5.1) (2017-04-24)
[Full Changelog](https://github.com/whipper-team/whipper/compare/v0.5.0...v0.5.1)
@@ -271,6 +330,7 @@
**Merged pull requests:**
- Fix references to morituri. [\#109](https://github.com/whipper-team/whipper/pull/109) ([Freso](https://github.com/Freso))
- Small cleanups of setup.py [\#102](https://github.com/whipper-team/whipper/pull/102) ([Freso](https://github.com/Freso))
- Persist False value for defeats\_cache correctly [\#98](https://github.com/whipper-team/whipper/pull/98) ([ribbons](https://github.com/ribbons))
- Update suggested commands given by `drive list` [\#97](https://github.com/whipper-team/whipper/pull/97) ([ribbons](https://github.com/ribbons))

View File

@@ -1,55 +1,55 @@
Coverage.py 4.5.2 text report against whipper v0.7.3
Coverage.py 4.5.4 text report against whipper v0.8.0
$ coverage run --branch --omit='whipper/test/*' --source=whipper -m unittest discover
$ coverage report -m
Name Stmts Miss Branch BrPart Cover Missing
-----------------------------------------------------------------------------
whipper/__init__.py 10 2 4 2 71% 9, 11, 8->9, 10->11
whipper/__init__.py 15 5 4 2 63% 9-12, 16, 18, 15->16, 17->18
whipper/__main__.py 7 7 2 0 0% 4-14
whipper/command/__init__.py 0 0 0 0 100%
whipper/command/accurip.py 43 43 18 0 0% 21-92
whipper/command/basecommand.py 69 53 30 0 16% 56-114, 121-130, 133, 136, 139, 142-145
whipper/command/cd.py 224 186 58 0 13% 71-79, 84-193, 196, 208, 231-284, 291-318, 321-491
whipper/command/accurip.py 41 41 18 0 0% 21-90
whipper/command/basecommand.py 69 29 30 8 53% 70, 72, 76, 82-88, 98-102, 107-114, 127, 129, 133, 139, 142-145, 68->70, 71->72, 75->76, 80->82, 96->98, 106->107, 126->127, 128->129
whipper/command/cd.py 227 189 60 0 13% 72-80, 85-196, 199, 212, 236-288, 295-322, 325-496
whipper/command/drive.py 57 57 10 0 0% 21-107
whipper/command/image.py 38 38 6 0 0% 21-76
whipper/command/main.py 68 68 22 0 0% 4-115
whipper/command/mblookup.py 28 28 8 0 0% 1-41
whipper/command/offset.py 110 110 32 0 0% 21-221
whipper/command/main.py 68 68 22 0 0% 4-116
whipper/command/mblookup.py 29 3 8 2 86% 21-23, 35->37, 37->28
whipper/command/offset.py 110 110 32 0 0% 21-219
whipper/common/__init__.py 0 0 0 0 100%
whipper/common/accurip.py 133 5 54 5 95% 121, 130, 139-141, 116->121, 125->130, 155->158, 246->252, 255->261
whipper/common/cache.py 105 50 34 6 44% 66-90, 96, 99, 107-112, 115-116, 132, 144-148, 171-178, 202-207, 212-228, 95->96, 98->99, 131->132, 142->152, 143->144, 170->171
whipper/common/accurip.py 132 5 54 5 95% 119, 125, 134-136, 114->119, 120->125, 150->153, 241->247, 250->256
whipper/common/cache.py 104 49 34 6 44% 66-90, 96, 99, 108-111, 114-115, 131, 143-147, 170-177, 201-206, 211-227, 95->96, 98->99, 130->131, 141->151, 142->143, 169->170
whipper/common/checksum.py 26 14 2 0 43% 41-42, 45-46, 49-64
whipper/common/common.py 150 28 38 6 78% 51-52, 119-120, 143-144, 162-169, 181, 275-280, 287-292, 329-333, 118->119, 131->134, 180->181, 190->197, 271->275, 327->335
whipper/common/config.py 92 8 18 4 89% 105-106, 124-125, 131, 141, 143, 145, 130->131, 140->141, 142->143, 144->145
whipper/common/common.py 150 28 38 6 78% 51-52, 119-120, 143-144, 162-169, 181, 274-279, 286-291, 328-332, 118->119, 131->134, 180->181, 190->197, 271->274, 326->334
whipper/common/config.py 91 8 18 4 89% 105-106, 124-125, 131, 141, 143, 145, 130->131, 140->141, 142->143, 144->145
whipper/common/directory.py 21 8 10 2 55% 29, 39, 44-51, 28->29, 38->39
whipper/common/drive.py 31 20 6 0 35% 35-40, 44-50, 54-60, 64-71
whipper/common/encode.py 44 23 2 0 46% 37-38, 41-42, 45-46, 53-56, 59-60, 63-64, 76-77, 80-81, 84-91
whipper/common/mbngs.py 159 53 58 7 66% 38-39, 45, 90-96, 157-158, 163-164, 208, 211, 214, 237-239, 248, 268-322, 156->157, 162->163, 207->208, 210->211, 213->214, 236->237, 245->248
whipper/common/mbngs.py 174 52 66 7 70% 38-39, 45, 93-99, 174-175, 180-181, 227, 233, 258-260, 269, 289-344, 159->158, 173->174, 179->180, 226->227, 232->233, 257->258, 266->269
whipper/common/path.py 24 0 8 3 91% 42->45, 52->57, 62->67
whipper/common/program.py 337 259 110 5 20% 85-87, 93-100, 109-141, 150-155, 158, 162-166, 211, 222-223, 225-229, 245-260, 268-380, 391-442, 450-458, 461-476, 487-527, 539-556, 559-577, 580-590, 593-601, 77->80, 208->211, 221->222, 224->225, 231->235
whipper/common/renamer.py 102 2 16 1 97% 135, 158, 60->68
whipper/common/task.py 77 19 14 2 75% 47-52, 86-87, 90-93, 101, 114-115, 122, 128, 134, 140, 146, 84->86, 98->101
whipper/common/program.py 346 268 112 5 19% 85-87, 93-104, 113-147, 156-161, 164, 169-173, 218, 229-230, 232-236, 253-268, 276-387, 398-456, 464-472, 476-491, 502-542, 554-571, 574-592, 595-605, 608-616, 76->79, 215->218, 228->229, 231->232, 238->242
whipper/common/renamer.py 102 2 16 1 97% 133, 156, 58->66
whipper/common/task.py 77 19 14 2 75% 47-52, 86-87, 91-94, 102, 115-116, 123, 129, 135, 141, 147, 84->86, 99->102
whipper/extern/__init__.py 0 0 0 0 100%
whipper/extern/asyncsub.py 130 71 66 12 40% 15-17, 32, 37-38, 47-84, 89-102, 115, 122, 134, 145, 151, 156-160, 164-176, 14->15, 35->37, 45->47, 110->113, 114->115, 121->122, 133->134, 139->141, 141->152, 144->145, 148->151, 163->164
whipper/extern/freedb.py 104 83 38 1 17% 49, 57-58, 61, 64, 84-162, 170-222, 56->57
whipper/extern/freedb.py 104 83 38 1 17% 49, 57-58, 61, 64, 84-163, 171-223, 56->57
whipper/extern/task/__init__.py 0 0 0 0 100%
whipper/extern/task/task.py 277 116 54 11 54% 57, 61, 81, 87, 153-155, 174-176, 184-200, 218-221, 241-242, 283-284, 287-293, 308-309, 317-319, 328-335, 341-357, 361, 364, 371-388, 399-400, 403-406, 410, 413, 428, 431-433, 449, 461, 506-511, 520-525, 536-544, 547-555, 558-559, 567, 572-574, 56->57, 60->61, 69->71, 152->153, 166->exit, 217->218, 231->233, 236->exit, 496->498, 533->536, 571->572
whipper/extern/task/task.py 271 115 54 11 53% 54, 58, 79, 87, 153-155, 174-176, 184-200, 218-221, 242-243, 284-285, 288-294, 309-310, 318-320, 329-336, 342-359, 363, 366, 373-390, 401-402, 405-408, 412, 415, 430, 433-435, 451, 463, 509-514, 521-526, 535-543, 546-554, 557-558, 566, 571-573, 53->54, 57->58, 66->68, 152->153, 166->exit, 217->218, 231->233, 236->exit, 498->500, 532->535, 570->571
whipper/image/__init__.py 0 0 0 0 100%
whipper/image/cue.py 91 9 20 3 89% 99, 116-117, 132-134, 159, 187, 205, 98->99, 115->116, 131->132
whipper/image/image.py 117 94 18 0 17% 49-57, 65-67, 74-107, 121-153, 156-172, 183-213
whipper/image/table.py 398 22 114 16 93% 237, 346-347, 499, 578, 663-664, 684-685, 694-697, 701-702, 747, 793-794, 796-797, 841-842, 847-849, 180->183, 498->499, 532->536, 555->558, 577->578, 585->592, 683->684, 692->698, 693->694, 722->726, 726->721, 746->747, 792->793, 795->796, 840->841, 846->847
whipper/image/image.py 116 93 18 0 17% 49-57, 65-67, 74-107, 121-154, 157-173, 184-214
whipper/image/table.py 395 18 114 16 93% 238, 497, 576, 661-662, 682-683, 692-695, 746, 792-793, 795-796, 840-841, 846-848, 181->184, 496->497, 530->534, 553->556, 575->576, 583->590, 681->682, 690->696, 691->692, 720->724, 724->719, 745->746, 791->792, 794->795, 839->840, 845->846
whipper/image/toc.py 203 16 60 10 90% 134, 261-262, 278-281, 339-341, 363-365, 385, 409, 439, 130->134, 212->220, 260->261, 277->278, 287->292, 323->330, 338->339, 362->363, 372->376, 404->409
whipper/program/__init__.py 0 0 0 0 100%
whipper/program/arc.py 38 15 12 4 58% 26-28, 32, 37-41, 48-54, 22->26, 31->32, 36->37, 43->48
whipper/program/cdparanoia.py 315 185 86 3 39% 48-50, 59-60, 124-126, 163-166, 199-200, 242-256, 259-310, 313-351, 354-358, 361-397, 452-504, 509-554, 587-590, 593, 600, 606, 611-616, 123->124, 599->600, 603->606
whipper/program/cdrdao.py 59 36 14 2 34% 26-56, 63-69, 79-81, 85-87, 95, 102, 78->79, 84->85
whipper/program/arc.py 3 0 0 0 100%
whipper/program/cdparanoia.py 309 180 78 2 39% 48-50, 59-60, 124-126, 198-199, 239-253, 256-306, 309-347, 350-354, 357-393, 447-500, 505-552, 586-589, 592, 599, 607-612, 123->124, 598->599
whipper/program/cdrdao.py 114 75 34 2 28% 33-58, 80-86, 90-105, 108-137, 140-144, 147-161, 168-171, 181-183, 187-189, 180->181, 186->187
whipper/program/flac.py 9 5 0 0 44% 12-19
whipper/program/sox.py 17 4 4 2 71% 18-19, 23-24, 17->18, 22->23
whipper/program/soxi.py 28 2 2 1 90% 36, 49, 48->49
whipper/program/utils.py 16 10 2 0 33% 11-12, 19-20, 30-35
whipper/program/utils.py 23 16 2 0 28% 12-17, 25-31, 42-47
whipper/result/__init__.py 0 0 0 0 100%
whipper/result/logger.py 148 148 48 0 0% 1-242
whipper/result/result.py 56 13 6 0 69% 112-116, 134, 144-145, 154-161
whipper/result/logger.py 144 23 40 16 78% 68, 84-92, 112, 123, 128, 130, 134-135, 143, 202, 240, 244-245, 252-253, 67->68, 83->84, 111->112, 122->123, 127->128, 129->130, 133->134, 142->143, 201->202, 213->217, 217->222, 222->226, 226->230, 234->244, 236->240, 249->252
whipper/result/result.py 57 13 6 0 70% 115-119, 137, 148-149, 158-165
-----------------------------------------------------------------------------
TOTAL 3961 1910 1104 108 49%
TOTAL 3997 1766 1108 129 53%

View File

@@ -1,31 +1,31 @@
FROM debian:buster
RUN apt-get update \
&& apt-get install -y cdrdao python-gobject-2 python-musicbrainzngs python-mutagen python-setuptools \
python-cddb python-requests libsndfile1-dev flac sox \
&& apt-get install -y cdrdao git python-gobject-2 python-musicbrainzngs python-mutagen \
python-setuptools python-requests libsndfile1-dev flac sox \
libiso9660-dev python-pip swig make pkgconf \
eject locales \
autoconf libtool curl \
&& pip install pycdio==2.0.0
&& pip install pycdio==2.1.0
# libcdio-paranoia / libcdio-utils are wrongfully packaged in Debian, thus built manually
# see https://github.com/whipper-team/whipper/pull/237#issuecomment-367985625
RUN curl -o - 'https://ftp.gnu.org/gnu/libcdio/libcdio-2.0.0.tar.gz' | tar zxf - \
&& cd libcdio-2.0.0 \
RUN curl -o - 'https://ftp.gnu.org/gnu/libcdio/libcdio-2.1.0.tar.bz2' | tar jxf - \
&& cd libcdio-2.1.0 \
&& autoreconf -fi \
&& ./configure --disable-dependency-tracking --disable-cxx --disable-example-progs --disable-static \
&& make install \
&& cd .. \
&& rm -rf libcdio-2.0.0
&& rm -rf libcdio-2.1.0
# Install cd-paranoia from tarball
RUN curl -o - 'https://ftp.gnu.org/gnu/libcdio/libcdio-paranoia-10.2+0.94+2.tar.gz' | tar zxf - \
&& cd libcdio-paranoia-10.2+0.94+2 \
RUN curl -o - 'https://ftp.gnu.org/gnu/libcdio/libcdio-paranoia-10.2+2.0.0.tar.bz2' | tar jxf - \
&& cd libcdio-paranoia-10.2+2.0.0 \
&& autoreconf -fi \
&& ./configure --disable-dependency-tracking --disable-example-progs --disable-static \
&& make install \
&& cd .. \
&& rm -rf libcdio-paranoia-10.2+0.94+2
&& rm -rf libcdio-paranoia-10.2+2.0.0
RUN ldconfig
@@ -45,8 +45,7 @@ RUN echo "LC_ALL=en_US.UTF-8" >> /etc/environment \
# install whipper
RUN mkdir /whipper
COPY . /whipper/
RUN cd /whipper/src && make && make install \
&& cd /whipper && python2 setup.py install \
RUN cd /whipper && python2 setup.py install \
&& rm -rf /whipper \
&& whipper -v

View File

@@ -8,12 +8,14 @@
[![GitHub Issues](https://img.shields.io/github/issues/whipper-team/whipper.svg)](https://github.com/whipper-team/whipper/issues)
[![GitHub contributors](https://img.shields.io/github/contributors/whipper-team/whipper.svg)](https://github.com/whipper-team/whipper/graphs/contributors)
Whipper is a Python 2.7 CD-DA ripper based on the [morituri project](https://github.com/thomasvs/morituri) (_CDDA ripper for *nix systems aiming for accuracy over speed_). It enhances morituri which development seems to have halted merging old ignored pull requests, improving it with bugfixes and new features.
Whipper is a Python 2.7 CD-DA ripper based on the [morituri project](https://github.com/thomasvs/morituri) (_CDDA ripper for *nix systems aiming for accuracy over speed_). It started just as a fork of morituri - which development seems to have halted - merging old ignored pull requests, improving it with bugfixes and new features. Nowadays whipper's codebase diverges significantly from morituri's one.
Whipper is currently developed and tested _only_ on Linux distributions but _may_ work fine on other *nix OSes too.
In order to track whipper's latest changes it's advised to check its commit history (README and [CHANGELOG](#changelog) files may not be comprehensive).
We've nearly completed porting the codebase to Python 3 (Python 2 won't be supported anymore in future releases). If you would like to follow the progress of the port e/o help us with it, please check [pull request #411](https://github.com/whipper-team/whipper/pull/411).
## Table of content
- [Rationale](#rationale)
@@ -34,6 +36,8 @@ In order to track whipper's latest changes it's advised to check its commit hist
- [Logger plugins](#logger-plugins)
- [License](#license)
- [Contributing](#contributing)
- [Developer Certificate of Origin (DCO)](#developer-certificate-of-origin-dco)
- [DCO Sign-Off Methods](#dco-sign-off-methods)
- [Bug reports & feature requests](#bug-reports--feature-requests)
- [Credits](#credits)
- [Links](#links)
@@ -51,7 +55,8 @@ https://web.archive.org/web/20160528213242/https://thomas.apestaart.org/thomas/t
- Performs Test & Copy rips
- Verifies rip accuracy using the [AccurateRip database](http://www.accuraterip.com/)
- Uses [MusicBrainz](https://musicbrainz.org/doc/About) for metadata lookup
- Supports reading the [pre-emphasis](http://wiki.hydrogenaud.io/index.php?title=Pre-emphasis) flag embedded into some CDs (and correctly tags the resulting rip). _Currently whipper only reports the pre-emphasis flag value stored in the TOC._
- Supports reading the [pre-emphasis](http://wiki.hydrogenaud.io/index.php?title=Pre-emphasis) flag embedded into some CDs (and correctly tags the resulting rip)
- _Currently whipper only reports the pre-emphasis flag value stored in the TOC_
- Detects and rips _non digitally silent_ [Hidden Track One Audio](http://wiki.hydrogenaud.io/index.php?title=HTOA) (HTOA)
- Provides batch ripping capabilities
- Provides templates for file and directory naming
@@ -72,11 +77,11 @@ Whipper still isn't available as an official package in every Linux distribution
You can easily install whipper without needing to care about the required dependencies by making use of the automatically built images hosted on Docker Hub:
`docker pull joelametta/whipper`
`docker pull whipperteam/whipper`
Alternatively, in case you prefer building Docker images locally, just issue the following command (it relies on the [Dockerfile](https://github.com/whipper-team/whipper/blob/master/Dockerfile) included in whipper's repository):
`docker build -t whipper/whipper`
`docker build -t whipperteam/whipper`
It's recommended to create an alias for a convenient usage:
@@ -84,7 +89,7 @@ It's recommended to create an alias for a convenient usage:
alias whipper="docker run -ti --rm --device=/dev/cdrom \
-v ~/.config/whipper:/home/worker/.config/whipper \
-v ${PWD}/output:/output \
whipper/whipper"
whipperteam/whipper"
```
You should put this e.g. into your `.bash_aliases`. Also keep in mind to substitute the path definitions to something that fits to your needs (e.g. replace `… -v ${PWD}/output:/output …` with `… -v ${HOME}/ripped:/output \ …`).
@@ -106,6 +111,8 @@ This is a noncomprehensive summary which shows whipper's packaging status (unoff
[![Packaging status](https://repology.org/badge/vertical-allrepos/whipper.svg)](https://repology.org/metapackage/whipper)
Someone also packaged whipper as snap: [unofficial snap on snapcraft](https://snapcraft.io/whipper).
In case you decide to install whipper using an unofficial repository just keep in mind it is your responsibility to verify that the provided content is safe to use.
## Building
@@ -117,8 +124,8 @@ If you are building from a source tarball or checkout, you can choose to use whi
Whipper relies on the following packages in order to run correctly and provide all the supported features:
- [cd-paranoia](https://www.gnu.org/software/libcdio/), for the actual ripping
- To avoid bugs it's advised to use `cd-paranoia` version **10.2+0.94+2-2**
- The package named `libcdio-utils`, available on Debian and Ubuntu, is affected by a bug: it doesn't include the `cd-paranoia` binary (needed by whipper). For more details see: [#888053 (Debian)](https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=888053), [#1750264 (Ubuntu)](https://bugs.launchpad.net/ubuntu/+source/libcdio/+bug/1750264).
- To avoid bugs it's advised to use `cd-paranoia` versions ≥ **10.2+0.94+2-2**
- The package named `libcdio-utils`, available on Debian and Ubuntu, is affected by a bug (except for Debian testing/sid): it doesn't include the `cd-paranoia` binary (needed by whipper). For more details see: [#888053 (Debian)](https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=888053), [#889803 (Debian)](https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=889803) and [#1750264 (Ubuntu)](https://bugs.launchpad.net/ubuntu/+source/libcdio/+bug/1750264).
- [cdrdao](http://cdrdao.sourceforge.net/), for session, TOC, pre-gap, and ISRC extraction
- [GObject Introspection](https://wiki.gnome.org/Projects/GObjectIntrospection), to provide GLib-2.0 methods used by `task.py`
- [PyGObject](https://pypi.org/project/PyGObject/), required by `task.py`
@@ -127,7 +134,8 @@ Whipper relies on the following packages in order to run correctly and provide a
- [python-setuptools](https://pypi.python.org/pypi/setuptools), for installation, plugins support
- [python-requests](https://pypi.python.org/pypi/requests), for retrieving AccurateRip database entries
- [pycdio](https://pypi.python.org/pypi/pycdio/), for drive identification (required for drive offset and caching behavior to be stored in the configuration file).
- To avoid bugs it's advised to use `pycdio` **0.20** or **0.21** with `libcdio`**0.90****0.94* or `pycdio` **2.0.0** with `libcdio` **2.0.0**. All other combinations won't probably work.
- To avoid bugs it's advised to use the most recent `pycdio` version with the corresponding `libcdio` release or, if stuck to old pycdio versions, **0.20**/**0.21** with `libcdio` **0.90****0.94**. All other combinations won't probably work.
- [ruamel.yaml](https://pypi.org/project/ruamel.yaml/), for generating well formed YAML report logfiles
- [libsndfile](http://www.mega-nerd.com/libsndfile/), for reading wav files
- [flac](https://xiph.org/flac/), for reading flac files
- [sox](http://sox.sourceforge.net/), for track peak detection
@@ -140,6 +148,8 @@ Some dependencies aren't available in the PyPI. They can be probably installed u
- [libsndfile](http://www.mega-nerd.com/libsndfile/)
- [flac](https://xiph.org/flac/)
- [sox](http://sox.sourceforge.net/)
- [git](https://git-scm.com/) or [mercurial](https://www.mercurial-scm.org/)
- Required either when running whipper without installing it or when building it from its source code (code cloned from a git/mercurial repository).
PyPI installable dependencies are listed in the [requirements.txt](https://github.com/whipper-team/whipper/blob/master/requirements.txt) file and can be installed issuing the following command:
@@ -303,8 +313,8 @@ Licensed under the [GNU GPLv3 license](http://www.gnu.org/licenses/gpl-3.0).
```Text
Copyright (C) 2009 Thomas Vander Stichele
Copyright (C) 2016-2018 The Whipper Team: JoeLametta, Frederik Olesen,
Samantha Baldwin, Merlijn Wajer, et al.
Copyright (C) 2016-2019 The Whipper Team: JoeLametta, Samantha Baldwin,
Merlijn Wajer, Frederik “Freso” S. Olesen, et al.
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
@@ -328,6 +338,65 @@ repository](https://github.com/whipper-team/whipper). Where possible,
please include tests for new or changed functionality. You can run tests
with `python -m unittest discover` from your source checkout.
### Developer Certificate of Origin (DCO)
To make a good faith effort to ensure licensing criteria are met, this project requires the Developer Certificate of Origin (DCO) process to be followed.
The Developer Certificate of Origin (DCO) is a document that certifies you own and/or have the right to contribute the work and license it appropriately. The DCO is used instead of a _much more annoying_
[CLA (Contributor License Agreement)](https://en.wikipedia.org/wiki/Contributor_License_Agreement). With the DCO, you retain copyright of your own work :). The DCO originated in the Linux community, and is used by other projects like Git and Docker.
The DCO agreement is shown below and it's also available online: [HERE](https://developercertificate.org/).
```
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
1 Letterman Drive
Suite D4700
San Francisco, CA, 94129
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
```
#### DCO Sign-Off Methods
The DCO requires a sign-off message in the following format appear on each commit in the pull request:
```
Signed-off-by: Full Name <email>
```
The DCO text can either be manually added to your commit body, or you can add either `-s` or `--signoff` to your usual Git commit commands. If you forget to add the sign-off you can also amend a previous commit with the sign-off by running `git commit --amend -s`.
### Bug reports & feature requests
Please use the [issue tracker](https://github.com/whipper-team/whipper/issues) to report any bugs or to file feature requests.
@@ -349,8 +418,9 @@ Thanks to:
- [Thomas Vander Stichele](https://github.com/thomasvs)
- [Joe Lametta](https://github.com/JoeLametta)
- [Merlijn Wajer](https://github.com/MerlijnWajer)
- [Samantha Baldwin](https://github.com/RecursiveForest)
- [Frederik “Freso” S. Olesen](https://github.com/Freso)
- [Merlijn Wajer](https://github.com/MerlijnWajer)
And to all the [contributors](https://github.com/whipper-team/whipper/graphs/contributors).
@@ -365,5 +435,6 @@ You can find us and talk about the project on:
- [Redacted thread (official)](https://redacted.ch/forums.php?action=viewthread&threadid=150)
Other relevant links:
- [Whipper - Hydrogenaudio Knowledgebase](https://wiki.hydrogenaud.io/index.php?title=Whipper)
- [Repology: versions for whipper](https://repology.org/metapackage/whipper/versions)
- [Unattended ripping using whipper (by Thomas McWork)](https://github.com/thomas-mc-work/most-possible-unattended-rip)

4
TODO
View File

@@ -56,7 +56,7 @@ HARD
- write xbmc/plex plugin
SPECIFIC ALBUMS ISSUES
SPECIFIC RELEASES ISSUES
- on ana, Goldfrapp tells me I have offset 0!
@@ -67,7 +67,7 @@ SPECIFIC ALBUMS ISSUES
NO DECISION YET
- possibly figure out how to name albums with credited artist; look at gorky and spiritualized electric mainline
- possibly figure out how to name releases with credited artist; look at gorky and spiritualized electric mainline
- check if cdda2wav or icedax analyze pregaps correctly

View File

@@ -64,4 +64,4 @@ if len(line) > 11:
line = line[:-2] + '"'
lines.append(line)
print "\n".join(lines)
print("\n".join(lines))

View File

@@ -3,3 +3,5 @@ mutagen
pycdio>0.20
PyGObject
requests
ruamel.yaml
setuptools_scm

View File

@@ -0,0 +1,35 @@
#!/usr/bin/env python
# SPDX-License-Identifier: GPL-3.0-only
import accuraterip
import sys
if len(sys.argv) == 2 and sys.argv[1] == '--version':
print('accuraterip-checksum version 2.0')
exit(0)
use_v1 = None
if len(sys.argv) == 4:
offset = 0
use_v1 = False
elif len(sys.argv) == 5:
offset = 1
if sys.argv[1] == '--accuraterip-v1':
use_v1 = True
elif sys.argv[1] == '--accuraterip-v2':
use_v1 = False
if use_v1 is None:
print('Syntax: accuraterip-checksum [--version / --accuraterip-v1 / --accuraterip-v2 (default)] filename track_number total_tracks')
exit(1)
filename = sys.argv[offset + 1]
track_number = int(sys.argv[offset + 2])
total_tracks = int(sys.argv[offset + 3])
v1, v2 = accuraterip.compute(filename, track_number, total_tracks)
if use_v1:
print('%08X' % v1)
else:
print('%08X' % v2)

View File

@@ -1,15 +1,21 @@
from setuptools import setup, find_packages
from whipper import __version__ as whipper_version
from setuptools import setup, find_packages, Extension
setup(
name="whipper",
version=whipper_version,
use_scm_version=True,
description="a secure cd ripper preferring accuracy over speed",
author=['Thomas Vander Stichele', 'The Whipper Team'],
maintainer=['The Whipper Team'],
url='https://github.com/whipper-team/whipper',
license='GPL3',
python_requires='>=2.7,<3',
packages=find_packages(),
setup_requires=['setuptools_scm'],
ext_modules=[
Extension('accuraterip',
libraries=['sndfile'],
sources=['src/accuraterip-checksum.c'])
],
entry_points={
'console_scripts': [
'whipper = whipper.command.main:main'
@@ -18,4 +24,7 @@ setup(
data_files=[
('share/metainfo', ['com.github.whipper_team.Whipper.metainfo.xml']),
],
scripts=[
'scripts/accuraterip-checksum',
],
)

1
src/.gitignore vendored
View File

@@ -1 +0,0 @@
accuraterip-checksum

View File

@@ -1,47 +0,0 @@
# See LICENSE file for copyright and license details.
include config.mk
SRC = accuraterip-checksum.c
OBJ = ${SRC:.c=.o}
all: options accuraterip-checksum
options:
@echo accuraterip-checksum build options:
@echo "CFLAGS = ${CFLAGS}"
@echo "LDFLAGS = ${LDFLAGS}"
@echo "CC = ${CC}"
.c.o:
@echo CC $<
@${CC} -c ${CFLAGS} $<
accuraterip-checksum: ${OBJ}
@echo CC -o $@
@${CC} -o $@ ${OBJ} ${LDFLAGS}
clean:
@echo cleaning
@rm -f accuraterip-checksum ${OBJ} accuraterip-checksum-${VERSION}.tar.gz
dist: clean
@echo creating dist tarball
@mkdir -p accuraterip-checksum-${VERSION}
@cp -R Makefile README.md config.mk \
${SRC} accuraterip-checksum-${VERSION}
@tar -cf accuraterip-checksum-${VERSION}.tar accuraterip-checksum-${VERSION}
@gzip accuraterip-checksum-${VERSION}.tar
@rm -rf accuraterip-checksum-${VERSION}
install: all
@echo installing executable file to ${DESTDIR}${PREFIX}/bin
@mkdir -p ${DESTDIR}${PREFIX}/bin
@cp -f accuraterip-checksum ${DESTDIR}${PREFIX}/bin
@chmod 755 ${DESTDIR}${PREFIX}/bin/accuraterip-checksum
uninstall:
@echo removing executable file from ${DESTDIR}${PREFIX}/bin
@rm -f ${DESTDIR}${PREFIX}/bin/accuraterip-checksum
.PHONY: all options clean dist install uninstall

View File

@@ -1,43 +1,45 @@
accuraterip-checksum
====================
# accuraterip-checksum
# Description:
A C99 commandline program to compute the AccurateRip checksum of singletrack WAV files.
Implemented according to
## Description
A C99 command line program to compute the [AccurateRip](http://accuraterip.com/) checksum of single track WAV files, i.e. WAV files which contain only a single track of an audio CD.
Such files can for example be generated by [Exact Audio Copy](http://exactaudiocopy.de/) and various other CD ripping programs, as listed e.g. [here](http://accuraterip.com/software.htm) and [here](https://wiki.hydrogenaud.io/index.php?title=AccurateRip).
http://www.hydrogenaudio.org/forums/index.php?showtopic=97603
Implemented according to [this thread on HydrogenAudio](http://www.hydrogenaudio.org/forums/index.php?showtopic=97603).
# Syntax:
accuraterip-checksum [--version / --accuraterip-v1 / --accuraterip-v2 (default)] filename track_number total_tracks
## Usage
Calculate AccurateRip v2 checksum of track number ```TRACK``` which is contained in WAV file ```TRACK_FILE```, and which was ripped from a disc with a total track count of ```TOTAL_TRACKS```:
# Output:
By default, the V2 (AccurateRip version 2) checksum will be printed.
You can also obtain the V1 checksum with the "--accuraterip-v1" parameter.
accuraterip-checksum TRACK_FILE TRACK TOTAL_TRACKS
You can obtain the version of accuraterip-checksum using the "--version" parameter. This is not to be confused with the AccurateRip version!
Explicitly choose AccurateRip checksum version, where ```VERSION``` is 1 or 2:
The version of accuraterip-checksum should be added to audio files which are tagged using the output of accuraterip-checksum. If any severe bugs are ever found in accuraterip-checksum, this will allow you to identify files which were tagged using affected version.
accuraterip-checksum --accuraterip-vVERSION TRACK_FILE TRACK TOTAL_TRACKS
Show accuraterip-checksum program version (this is **not** the AccurateRip checksum version!):
# Compiling:
accuraterip-checksum --version
The version of accuraterip-checksum should be added to the tags of audio files which were processed using the output of accuraterip-checksum:
If any severe bugs are ever found in accuraterip-checksum this will allow you to identify files which were tagged using affected version.
## Dependencies
libsndfile is used for reading the WAV files.
Therefore, on Ubuntu 12.04, make sure you have the following packages installed:
Therefore, on Ubuntu, make sure you have the following packages installed:
libsndfile1
For compiling you need:
libsndfile1 (should be installed by default)
libsndfile1-dev
The configuration files of an Eclipse project are included.
You can use "EGit" (Eclipse git) to import the whole repository.
It should as well ask you to import the project configuration then.
# Author:
## Author
Leo Bogert (http://leo.bogert.de)
# Version:
1.4
## Version
1.5
# Donations:
## Donations
bitcoin:14kPd2QWsri3y2irVFX6wC33vv7FqTaEBh
# License:
## License
GPLv3

View File

@@ -1,12 +1,10 @@
/*
============================================================================
Name : accuraterip-checksum.c
Author : Leo Bogert (http://leo.bogert.de)
Git : http://leo.bogert.de/accuraterip-checksum
Version : See global variable "version"
Copyright : GPL
Description : A C99 commandline program to compute the AccurateRip checksum of singletrack WAV files.
Implemented according to http://www.hydrogenaudio.org/forums/index.php?showtopic=97603
Authors : Leo Bogert (http://leo.bogert.de), Andreas Oberritter
License : GPLv3
Description : A Python C extension to compute the AccurateRip checksum of WAV or FLAC tracks.
Implemented according to http://www.hydrogenaudio.org/forums/index.php?showtopic=97603
============================================================================
*/
@@ -17,10 +15,10 @@
#include <string.h>
#include <stdint.h>
#include <sndfile.h>
#include <Python.h>
const char *const version = "1.4";
bool check_fileformat(const SF_INFO* sfinfo) {
static bool check_fileformat(const SF_INFO *sfinfo)
{
#ifdef DEBUG
printf("Channels: %i\n", sfinfo->channels);
printf("Format: %X\n", sfinfo->format);
@@ -30,27 +28,25 @@ bool check_fileformat(const SF_INFO* sfinfo) {
printf("Seekable: %i\n", sfinfo->seekable);
#endif
if(sfinfo->channels != 2) return false;
if((sfinfo->format & SF_FORMAT_TYPEMASK & SF_FORMAT_WAV) != SF_FORMAT_WAV) return false;
if((sfinfo->format & SF_FORMAT_SUBMASK & SF_FORMAT_PCM_16) != SF_FORMAT_PCM_16) return false;
//if((sfinfo->format & SF_FORMAT_ENDMASK & SF_ENDIAN_LITTLE) != SF_ENDIAN_LITTLE) return false;
if(sfinfo->samplerate != 44100) return false;
switch (sfinfo->format & SF_FORMAT_TYPEMASK) {
case SF_FORMAT_WAV:
case SF_FORMAT_FLAC:
return (sfinfo->channels == 2) &&
(sfinfo->samplerate == 44100) &&
((sfinfo->format & SF_FORMAT_SUBMASK) == SF_FORMAT_PCM_16);
}
return true;
return false;
}
size_t get_full_audiodata_size(const SF_INFO* sfinfo) {
// 16bit = samplesize, 8 bit = bitcount in byte
return sfinfo->frames * sfinfo->channels * (16 / 8);
}
uint32_t* load_full_audiodata(SNDFILE* sndfile, const SF_INFO* sfinfo) {
uint32_t* data = (uint32_t*)malloc(get_full_audiodata_size(sfinfo));
static void *load_full_audiodata(SNDFILE *sndfile, const SF_INFO *sfinfo, size_t size)
{
void *data = malloc(size);
if(data == NULL)
return NULL;
if(sf_readf_short(sndfile, (short*)data, sfinfo->frames) != sfinfo->frames) {
if(sf_readf_short(sndfile, data, sfinfo->frames) != sfinfo->frames) {
free(data);
return NULL;
}
@@ -58,170 +54,100 @@ uint32_t* load_full_audiodata(SNDFILE* sndfile, const SF_INFO* sfinfo) {
return data;
}
uint32_t compute_v1_checksum(const uint32_t* audio_data, const size_t audio_data_size, const int track_number, const int total_tracks) {
#define DWORD uint32_t
static void compute_checksums(const uint32_t *audio_data, size_t audio_data_size, size_t track_number, size_t total_tracks, uint32_t *v1, uint32_t *v2)
{
uint32_t csum_hi = 0;
uint32_t csum_lo = 0;
uint32_t AR_CRCPosCheckFrom = 0;
size_t Datauint32_tSize = audio_data_size / sizeof(uint32_t);
uint32_t AR_CRCPosCheckTo = Datauint32_tSize;
const size_t SectorBytes = 2352; // each sector
uint32_t MulBy = 1;
size_t i;
const DWORD *pAudioData = audio_data; // this should point entire track audio data
int DataSize = audio_data_size; // size of the data
int TrackNumber = track_number; // actual track number on disc, note that for the first & last track the first and last 5 sectors are skipped
int AudioTrackCount = total_tracks; // CD track count
if (track_number == 1) // first?
AR_CRCPosCheckFrom += ((SectorBytes * 5) / sizeof(uint32_t));
if (track_number == total_tracks) // last?
AR_CRCPosCheckTo -= ((SectorBytes * 5) / sizeof(uint32_t));
//---------AccurateRip CRC checks------------
DWORD AR_CRC = 0, AR_CRCPosMulti = 1;
DWORD AR_CRCPosCheckFrom = 0;
DWORD AR_CRCPosCheckTo = DataSize / sizeof(DWORD);
#define SectorBytes 2352 // each sector
if (TrackNumber == 1) // first?
AR_CRCPosCheckFrom+= ((SectorBytes * 5) / sizeof(DWORD));
if (TrackNumber == AudioTrackCount) // last?
AR_CRCPosCheckTo-=((SectorBytes * 5) / sizeof(DWORD));
int DataDWORDSize = DataSize / sizeof(DWORD);
for (int i = 0; i < DataDWORDSize; i++)
{
if (AR_CRCPosMulti >= AR_CRCPosCheckFrom && AR_CRCPosMulti <= AR_CRCPosCheckTo)
AR_CRC+=(AR_CRCPosMulti * pAudioData[i]);
AR_CRCPosMulti++;
}
return AR_CRC;
}
uint32_t compute_v2_checksum(const uint32_t* audio_data, const size_t audio_data_size, const int track_number, const int total_tracks) {
#define DWORD uint32_t
#define __int64 uint64_t
const DWORD *pAudioData = audio_data; // this should point entire track audio data
int DataSize = audio_data_size; // size of the data
int TrackNumber = track_number; // actual track number on disc, note that for the first & last track the first and last 5 sectors are skipped
int AudioTrackCount = total_tracks; // CD track count
//---------AccurateRip CRC checks------------
DWORD AR_CRCPosCheckFrom = 0;
DWORD AR_CRCPosCheckTo = DataSize / sizeof(DWORD);
#define SectorBytes 2352 // each sector
if (TrackNumber == 1) // first?
AR_CRCPosCheckFrom+= ((SectorBytes * 5) / sizeof(DWORD));
if (TrackNumber == AudioTrackCount) // last?
AR_CRCPosCheckTo-=((SectorBytes * 5) / sizeof(DWORD));
int DataDWORDSize = DataSize / sizeof(DWORD);
DWORD AC_CRCNEW = 0;
DWORD MulBy = 1;
for (int i = 0; i < DataDWORDSize; i++)
{
if (MulBy >= AR_CRCPosCheckFrom && MulBy <= AR_CRCPosCheckTo)
{
DWORD Value = pAudioData[i];
uint64_t CalcCRCNEW = (uint64_t)Value * (uint64_t)MulBy;
DWORD LOCalcCRCNEW = (DWORD)(CalcCRCNEW & (uint64_t)0xFFFFFFFF);
DWORD HICalcCRCNEW = (DWORD)(CalcCRCNEW / (uint64_t)0x100000000);
AC_CRCNEW+=HICalcCRCNEW;
AC_CRCNEW+=LOCalcCRCNEW;
for (i = 0; i < Datauint32_tSize; i++) {
if (MulBy >= AR_CRCPosCheckFrom && MulBy <= AR_CRCPosCheckTo) {
uint64_t product = (uint64_t)audio_data[i] * (uint64_t)MulBy;
csum_hi += (uint32_t)(product >> 32);
csum_lo += (uint32_t)(product);
}
MulBy++;
MulBy++;
}
return AC_CRCNEW;
*v1 = csum_lo;
*v2 = csum_lo + csum_hi;
}
void print_syntax_to_stderr() {
fprintf(stderr, "Syntax: accuraterip-checksum [--version / --accuraterip-v1 / --accuraterip-v2 (default)] filename track_number total_tracks\n");
}
static PyObject *accuraterip_compute(PyObject *self, PyObject *args)
{
const char *filename;
unsigned int track_number;
unsigned int total_tracks;
uint32_t v1, v2;
void *audio_data;
size_t size;
SF_INFO sfinfo;
SNDFILE *sndfile = NULL;
int main(int argc, const char** argv) {
int arg_offset;
bool use_v1;
if (!PyArg_ParseTuple(args, "sII", &filename, &track_number, &total_tracks))
goto err;
switch(argc) {
case 2:
if(strcmp(argv[1], "--version") != 0) {
print_syntax_to_stderr();
return EXIT_FAILURE;
}
printf("accuraterip-checksum version %s\n", version);
return EXIT_SUCCESS;
case 4:
arg_offset = 0;
use_v1 = false;
break;
case 5:
arg_offset = 1;
if(!strcmp(argv[1], "--accuraterip-v1")) {
use_v1 = true;
} else if(!strcmp(argv[1], "--accuraterip-v2")) {
use_v1 = false;
} else {
print_syntax_to_stderr();
return EXIT_FAILURE;
}
break;
default:
print_syntax_to_stderr();
return EXIT_FAILURE;
}
const char* filename = argv[1 + arg_offset];
const char* track_number_string = argv[2 + arg_offset];
const char* total_tracks_string = argv[3 + arg_offset];
const int track_number = atoi(track_number_string);
const int total_tracks = atoi(total_tracks_string);
if(track_number < 1 || track_number > total_tracks) {
if (track_number < 1 || track_number > total_tracks) {
fprintf(stderr, "Invalid track_number!\n");
return EXIT_FAILURE;
goto err;
}
if(total_tracks < 1 || total_tracks > 99) {
if (total_tracks < 1 || total_tracks > 99) {
fprintf(stderr, "Invalid total_tracks!\n");
return EXIT_FAILURE;
goto err;
}
#ifdef DEBUG
printf("Reading %s\n", filename);
#endif
SF_INFO sfinfo;
sfinfo.channels = 0;
sfinfo.format = 0;
sfinfo.frames = 0;
sfinfo.samplerate = 0;
sfinfo.sections = 0;
sfinfo.seekable = 0;
SNDFILE* sndfile = sf_open(filename, SFM_READ, &sfinfo);
if(sndfile == NULL) {
memset(&sfinfo, 0, sizeof(sfinfo));
sndfile = sf_open(filename, SFM_READ, &sfinfo);
if (sndfile == NULL) {
fprintf(stderr, "sf_open failed! sf_error==%i\n", sf_error(NULL));
return EXIT_FAILURE;
goto err;
}
if(!check_fileformat(&sfinfo)) {
if (!check_fileformat(&sfinfo)) {
fprintf(stderr, "check_fileformat failed!\n");
sf_close(sndfile);
return EXIT_FAILURE;
goto err;
}
uint32_t* audio_data = load_full_audiodata(sndfile, &sfinfo);
if(audio_data == NULL) {
size = sfinfo.frames * sfinfo.channels * sizeof(uint16_t);
audio_data = load_full_audiodata(sndfile, &sfinfo, size);
if (audio_data == NULL) {
fprintf(stderr, "load_full_audiodata failed!\n");
sf_close(sndfile);
return EXIT_FAILURE;
goto err;
}
const int checksum = use_v1 ?
compute_v1_checksum(audio_data, get_full_audiodata_size(&sfinfo), track_number, total_tracks)
: compute_v2_checksum(audio_data, get_full_audiodata_size(&sfinfo), track_number, total_tracks);
printf("%08X\n", checksum);
sf_close(sndfile);
compute_checksums(audio_data, size, track_number, total_tracks, &v1, &v2);
free(audio_data);
sf_close(sndfile);
return EXIT_SUCCESS;
return Py_BuildValue("II", v1, v2);
err:
if (sndfile)
sf_close(sndfile);
return Py_BuildValue("OO", Py_None, Py_None);
}
static PyMethodDef accuraterip_methods[] = {
{ "compute", accuraterip_compute, METH_VARARGS, "Compute AccurateRip v1 and v2 checksums" },
{ NULL, NULL, 0, NULL },
};
PyMODINIT_FUNC initaccuraterip(void)
{
Py_InitModule("accuraterip", accuraterip_methods);
}

View File

@@ -1,11 +0,0 @@
VERSION = 1.4
# paths
PREFIX = /usr/local
# flags
CFLAGS = -std=c99
LDFLAGS = -lsndfile
# compiler and linker
CC = cc

View File

@@ -2,7 +2,14 @@ import logging
import os
import sys
__version__ = '0.7.3'
from pkg_resources import (get_distribution,
DistributionNotFound, RequirementParseError)
try:
__version__ = get_distribution(__name__).version
except (DistributionNotFound, RequirementParseError):
# not installed as package or is being run from source/git checkout
from setuptools_scm import get_version
__version__ = get_version()
level = logging.INFO
if 'WHIPPER_DEBUG' in os.environ:

View File

@@ -59,9 +59,7 @@ retrieves and display accuraterip data from the given URL
assert len(r.checksums) == r.num_tracks
assert len(r.confidences) == r.num_tracks
entry = {}
entry["confidence"] = r.confidences[track]
entry["response"] = i + 1
entry = {"confidence": r.confidences[track], "response": i + 1}
checksum = r.checksums[track]
if checksum in checksums:
checksums[checksum].append(entry)

View File

@@ -25,7 +25,7 @@ logger = logging.getLogger(__name__)
# options) to the child command.
class BaseCommand():
class BaseCommand:
"""
Register and handle whipper command arguments with ArgumentParser.

View File

@@ -42,7 +42,7 @@ DEFAULT_DISC_TEMPLATE = u'%r/%A - %d/%A - %d'
TEMPLATE_DESCRIPTION = '''
Tracks are named according to the track template, filling in the variables
and adding the file extension. Variables exclusive to the track template are:
and adding the file extension. Variables exclusive to the track template are:
- %t: track number
- %a: track artist
- %n: track title
@@ -51,12 +51,12 @@ and adding the file extension. Variables exclusive to the track template are:
Disc files (.cue, .log, .m3u) are named according to the disc template,
filling in the variables and adding the file extension. Variables for both
disc and track template are:
- %A: album artist
- %S: album sort name
- %A: release artist
- %S: release sort name
- %d: disc title
- %y: release year
- %r: release type, lowercase
- %R: Release type, normal case
- %R: release type, normal case
- %x: audio extension, lowercase
- %X: audio extension, uppercase
@@ -66,6 +66,7 @@ disc and track template are:
class _CD(BaseCommand):
eject = True
# XXX: Pylint, parameters differ from overridden 'add_arguments' method
@staticmethod
def add_arguments(parser):
parser.add_argument('-R', '--release-id',
@@ -94,7 +95,6 @@ class _CD(BaseCommand):
utils.unmount_device(self.device)
# first, read the normal TOC, which is fast
logger.info("reading TOC...")
self.ittoc = self.program.getFastToc(self.runner, self.device)
# already show us some info based on this
@@ -134,20 +134,23 @@ class _CD(BaseCommand):
return -1
# Change working directory before cdrdao's task
if self.options.working_directory is not None:
if getattr(self.options, 'working_directory', False):
os.chdir(os.path.expanduser(self.options.working_directory))
out_bpath = self.options.output_directory.decode('utf-8')
# Needed to preserve cdrdao's tocfile
out_fpath = self.program.getPath(out_bpath,
self.options.disc_template,
self.mbdiscid,
self.program.metadata)
if hasattr(self.options, 'output_directory'):
out_bpath = self.options.output_directory.decode('utf-8')
# Needed to preserve cdrdao's tocfile
out_fpath = self.program.getPath(out_bpath,
self.options.disc_template,
self.mbdiscid,
self.program.metadata)
else:
out_fpath = None
# now, read the complete index table, which is slower
offset = getattr(self.options, 'offset', 0)
self.itable = self.program.getTable(self.runner,
self.ittoc.getCDDBDiscId(),
self.ittoc.getMusicBrainzDiscId(),
self.device, self.options.offset,
out_fpath)
self.device, offset, out_fpath)
assert self.itable.getCDDBDiscId() == self.ittoc.getCDDBDiscId(), \
"full table's id %s differs from toc id %s" % (
@@ -157,17 +160,13 @@ class _CD(BaseCommand):
"full table's mb id %s differs from toc id mb %s" % (
self.itable.getMusicBrainzDiscId(),
self.ittoc.getMusicBrainzDiscId())
assert self.itable.accuraterip_path() == \
self.ittoc.accuraterip_path(), \
"full table's AR URL %s differs from toc AR URL %s" % (
self.itable.accuraterip_url(), self.ittoc.accuraterip_url())
if self.program.metadata:
self.program.metadata.discid = self.ittoc.getMusicBrainzDiscId()
# result
self.program.result.cdrdaoVersion = cdrdao.getCDRDAOVersion()
self.program.result.cdrdaoVersion = cdrdao.version()
self.program.result.cdparanoiaVersion = \
cdparanoia.getCdParanoiaVersion()
info = drive.getDeviceInfo(self.device)
@@ -186,24 +185,29 @@ class _CD(BaseCommand):
_, self.program.result.vendor, self.program.result.model, \
self.program.result.release = \
cdio.Device(self.device).get_hwinfo()
self.program.result.metadata = self.program.metadata
self.doCommand()
if self.options.eject in ('success', 'always'):
if (self.options.eject == 'success' and self.eject or
self.options.eject == 'always'):
utils.eject_device(self.device)
return None
def doCommand(self):
pass
class Info(_CD):
summary = "retrieve information about the currently inserted CD"
description = ("Display MusicBrainz, CDDB/FreeDB, and AccurateRip"
description = ("Display MusicBrainz, CDDB/FreeDB, and AccurateRip "
"information for the currently inserted CD.")
eject = False
# Requires opts.device
# XXX: Pylint, parameters differ from overridden 'add_arguments' method
def add_arguments(self):
_CD.add_arguments(self.parser)
@@ -227,6 +231,7 @@ Log files will log the path to tracks relative to this directory.
# Requires opts.record
# Requires opts.device
# XXX: Pylint, parameters differ from overridden 'add_arguments' method
def add_arguments(self):
loggers = list(result.getLoggers())
default_offset = None
@@ -245,7 +250,6 @@ Log files will log the path to tracks relative to this directory.
default='whipper',
help=("logger to use (choose from: '%s" %
"', '".join(loggers) + "')"))
# FIXME: get from config
self.parser.add_argument('-o', '--offset',
action="store", dest="offset",
default=default_offset,
@@ -414,6 +418,7 @@ Log files will log the path to tracks relative to this directory.
len(self.itable.tracks),
extra))
break
# FIXME: catching too general exception (Exception)
except Exception as e:
logger.debug('got exception %r on try %d', e, tries)
@@ -441,17 +446,17 @@ Log files will log the path to tracks relative to this directory.
logger.debug('HTOA peak %r is equal to the SILENT '
'threshold, disregarding', trackResult.peak)
self.itable.setFile(1, 0, None,
self.ittoc.getTrackStart(1), number)
self.itable.getTrackStart(1), number)
logger.debug('unlinking %r', trackResult.filename)
os.unlink(trackResult.filename)
trackResult.filename = None
logger.info('HTOA discarded, contains digital silence')
else:
self.itable.setFile(1, 0, trackResult.filename,
self.ittoc.getTrackStart(1), number)
self.itable.getTrackStart(1), number)
else:
self.itable.setFile(number, 1, trackResult.filename,
self.ittoc.getTrackLength(number), number)
self.itable.getTrackLength(number), number)
self.program.saveRipResult()
@@ -480,7 +485,7 @@ Log files will log the path to tracks relative to this directory.
self.program.write_m3u(discName)
try:
self.program.verifyImage(self.runner, self.ittoc)
self.program.verifyImage(self.runner, self.itable)
except accurip.EntryNotFound:
logger.warning('AccurateRip entry not found')

View File

@@ -70,7 +70,7 @@ Verifies the image from the given .cue files against the AccurateRip database.
class Image(BaseCommand):
summary = "handle images"
description = """
Handle disc images. Disc images are described by a .cue file.
Handle disc images. Disc images are described by a .cue file.
Disc images can be verified.
"""
subcommands = {

View File

@@ -45,6 +45,7 @@ def main():
logger.critical("SystemError: %s", e)
if (isinstance(e, common.EjectError) and
cmd.options.eject in ('failure', 'always')):
# XXX: Pylint, instance of 'SystemError' has no 'device' member
eject_device(e.device)
return 255
except RuntimeError as e:
@@ -52,7 +53,7 @@ def main():
return 1
except KeyboardInterrupt:
return 2
except ImportError as e:
except ImportError:
raise
except task.TaskException as e:
if isinstance(e.exception, ImportError):
@@ -74,11 +75,11 @@ def main():
class Whipper(BaseCommand):
description = """whipper is a CD ripping utility focusing on accuracy over speed.
whipper gives you a tree of subcommands to work with.
You can get help on subcommands by using the -h option to the subcommand.
"""
description = (
"whipper is a CD ripping utility focusing on accuracy over speed.\n\n"
"whipper gives you a tree of subcommands to work with.\n"
"You can get help on subcommands by using the -h option "
"to the subcommand.\n")
no_add_help = True
subcommands = {
'accurip': accurip.AccuRip,
@@ -101,10 +102,10 @@ You can get help on subcommands by using the -h option to the subcommand.
help="show this help message and exit")
self.parser.add_argument('-e', '--eject',
action="store", dest="eject",
default="always",
default="success",
choices=('never', 'failure',
'success', 'always'),
help="when to eject disc (default: always)")
help="when to eject disc (default: success)")
def handle_arguments(self):
if self.options.help:

View File

@@ -29,16 +29,18 @@ Example disc id: KnpGsLhvH.lPrNc1PBL21lb9Bg4-"""
print('- Release %d:' % (i + 1, ))
print(' Artist: %s' % md.artist.encode('utf-8'))
print(' Title: %s' % md.title.encode('utf-8'))
print(' Type: %s' % md.releaseType.encode('utf-8')) # noqa: E501
print(' Type: %s' % unicode(md.releaseType).encode('utf-8')) # noqa: E501
print(' URL: %s' % md.url)
print(' Tracks: %d' % len(md.tracks))
if md.catalogNumber:
print(' Cat no: %s' % md.catalogNumber)
if md.barcode:
print(' Barcode: %s' % md.barcode)
print(' Barcode: %s' % md.barcode)
for j, track in enumerate(md.tracks):
print(' Track %2d: %s - %s' % (
j + 1, track.artist.encode('utf-8'),
track.title.encode('utf-8')
))
return None

View File

@@ -85,16 +85,16 @@ CD in the AccurateRip database."""
# first get the Table Of Contents of the CD
t = cdrdao.ReadTOCTask(device)
table = t.table
runner.run(t)
table = t.toc.table
logger.debug("CDDB disc id: %r", table.getCDDBDiscId())
responses = None
try:
responses = accurip.get_db_entry(table.accuraterip_path())
except accurip.EntryNotFound:
logger.warning("AccurateRip entry not found: drive offset "
"can't be determined, try again with another disc")
return
return None
if responses:
logger.debug('%d AccurateRip responses found.', len(responses))
@@ -133,7 +133,7 @@ CD in the AccurateRip database."""
logger.warning('cannot rip with offset %d...', offset)
continue
logger.debug('AR checksums calculated: %s %s', archecksums)
logger.debug('AR checksums calculated: %s', archecksums)
c, i = match(archecksums, 1, responses)
if c:
@@ -170,6 +170,8 @@ CD in the AccurateRip database."""
logger.error('no matching offset found. '
'Consider trying again with a different disc')
return None
def _arcs(self, runner, table, track, offset):
# rips the track with the given offset, return the arcs checksums
logger.debug('ripping track %r with offset %d...', track, offset)
@@ -188,17 +190,13 @@ CD in the AccurateRip database."""
track, offset)
runner.run(t)
v1 = arc.accuraterip_checksum(
path, track, len(table.tracks), wave=True, v2=False
)
v2 = arc.accuraterip_checksum(
path, track, len(table.tracks), wave=True, v2=True
)
v1, v2 = arc.accuraterip_checksum(path, track, len(table.tracks))
os.unlink(path)
return ("%08x" % v1, "%08x" % v2)
return "%08x" % v1, "%08x" % v2
def _foundOffset(self, device, offset):
@staticmethod
def _foundOffset(device, offset):
print('\nRead offset of device is: %d.' % offset)
info = drive.getDeviceInfo(device)

View File

@@ -110,19 +110,14 @@ def calculate_checksums(track_paths):
logger.debug('checksumming %d tracks', track_count)
# This is done sequentially because it is very fast.
for i, path in enumerate(track_paths):
v1_sum = accuraterip_checksum(
path, i+1, track_count, wave=True, v2=False
)
if not v1_sum:
v1_sum, v2_sum = accuraterip_checksum(path, i+1, track_count)
if v1_sum is None:
logger.error('could not calculate AccurateRip v1 checksum '
'for track %d %r', i + 1, path)
v1_checksums.append(None)
else:
v1_checksums.append("%08x" % v1_sum)
v2_sum = accuraterip_checksum(
path, i+1, track_count, wave=True, v2=True
)
if not v2_sum:
if v2_sum is None:
logger.error('could not calculate AccurateRip v2 checksum '
'for track %d %r', i + 1, path)
v2_checksums.append(None)
@@ -236,7 +231,7 @@ def print_report(result):
"""
Print AccurateRip verification results.
"""
for i, track in enumerate(result.tracks):
for _, track in enumerate(result.tracks):
status = 'rip NOT accurate'
conf = '(not found)'
db = 'notfound'

View File

@@ -39,7 +39,7 @@ class Persister:
Call persist to store the object to disk; it will get stored if it
changed from the on-disk object.
@ivar object: the persistent object
:ivar object: the persistent object
"""
def __init__(self, path=None, default=None):
@@ -93,10 +93,10 @@ class Persister:
self.object = default
if not self._path:
return None
return
if not os.path.exists(self._path):
return None
return
handle = open(self._path)
import pickle
@@ -104,12 +104,11 @@ class Persister:
try:
self.object = pickle.load(handle)
logger.debug('loaded persisted object from %r', self._path)
# FIXME: catching too general exception (Exception)
except Exception as e:
# TODO: restrict kind of caught exceptions?
# can fail for various reasons; in that case, pretend we didn't
# load it
logger.debug(e)
pass
def delete(self):
self.object = None
@@ -128,7 +127,7 @@ class PersistedCache:
try:
os.makedirs(self.path)
except OSError as e:
if e.errno != 17: # FIXME
if e.errno != os.errno.EEXIST: # FIXME: errno 17 is 'File Exists'
raise
def _getPath(self, key):
@@ -163,7 +162,7 @@ class ResultCache:
Retrieve the persistable RipResult either from our cache (from a
previous, possibly aborted rip), or return a new one.
@rtype: L{Persistable} for L{result.RipResult}
:rtype: :any:`Persistable` for :any:`result.RipResult`
"""
presult = self._pcache.get(cddbdiscid)

View File

@@ -47,7 +47,7 @@ class CRC32Task(etask.Task):
def _crc32(self):
if not self.is_wave:
fd, tmpf = tempfile.mkstemp()
_, tmpf = tempfile.mkstemp()
try:
subprocess.check_call(['flac', '-d', self.path, '-fo', tmpf])

View File

@@ -56,11 +56,11 @@ def msfToFrames(msf):
"""
Converts a string value in MM:SS:FF to frames.
@param msf: the MM:SS:FF value to convert
@type msf: str
:param msf: the MM:SS:FF value to convert
:type msf: str
@rtype: int
@returns: number of frames
:rtype: int
:returns: number of frames
"""
if ':' not in msf:
return int(msf)
@@ -76,7 +76,7 @@ def framesToMSF(frames, frameDelimiter=':'):
f = frames % FRAMES_PER_SECOND
frames -= f
s = (frames / FRAMES_PER_SECOND) % 60
frames -= s * 60
frames -= s * FRAMES_PER_SECOND
m = frames / FRAMES_PER_SECOND / 60
return "%02d:%02d%s%02d" % (m, s, frameDelimiter, f)
@@ -104,19 +104,19 @@ def formatTime(seconds, fractional=3):
If it is greater than 0, we will show seconds and fractions of seconds.
As a side consequence, there is no way to show seconds without fractions.
@param seconds: the time in seconds to format.
@type seconds: int or float
@param fractional: how many digits to show for the fractional part of
:param seconds: the time in seconds to format.
:type seconds: int or float
:param fractional: how many digits to show for the fractional part of
seconds.
@type fractional: int
:type fractional: int
@rtype: string
@returns: a nicely formatted time string.
:rtype: string
:returns: a nicely formatted time string.
"""
chunks = []
if seconds < 0:
chunks.append(('-'))
chunks.append('-')
seconds = -seconds
hour = 60 * 60
@@ -207,11 +207,11 @@ def getRealPath(refPath, filePath):
Does Windows path translation.
Will look for the given file name, but with .flac and .wav as extensions.
@param refPath: path to the file from which the track is referenced;
:param refPath: path to the file from which the track is referenced;
for example, path to the .cue file in the same directory
@type refPath: unicode
:type refPath: unicode
@type filePath: unicode
:type filePath: unicode
"""
assert isinstance(filePath, unicode), "%r is not unicode" % filePath
@@ -271,13 +271,12 @@ def getRelativePath(targetPath, collectionPath):
if targetDir == collectionDir:
logger.debug('getRelativePath: target and collection in same dir')
return os.path.basename(targetPath)
else:
rel = os.path.relpath(
targetDir + os.path.sep,
collectionDir + os.path.sep)
logger.debug('getRelativePath: target and collection '
'in different dir, %r', rel)
return os.path.join(rel, os.path.basename(targetPath))
rel = os.path.relpath(
targetDir + os.path.sep,
collectionDir + os.path.sep)
logger.debug('getRelativePath: target and collection '
'in different dir, %r', rel)
return os.path.join(rel, os.path.basename(targetPath))
def validate_template(template, kind):
@@ -285,9 +284,9 @@ def validate_template(template, kind):
Raise exception if disc/track template includes invalid variables
"""
if kind == 'disc':
matches = re.findall(r'%[^A,R,S,X,d,r,x,y]', template)
matches = re.findall(r'%[^ARSXdrxy]', template)
elif kind == 'track':
matches = re.findall(r'%[^A,R,S,X,a,d,n,r,s,t,x,y]', template)
matches = re.findall(r'%[^ARSXadnrstxy]', template)
if '%' in template and matches:
raise ValueError(kind + ' template string contains invalid '
'variable(s): {}'.format(', '.join(matches)))
@@ -301,11 +300,11 @@ class VersionGetter(object):
def __init__(self, dependency, args, regexp, expander):
"""
@param dependency: name of the dependency providing the program
@param args: the arguments to invoke to show the version
@type args: list of str
@param regexp: the regular expression to get the version
@param expander: the expansion string for the version using the
:param dependency: name of the dependency providing the program
:param args: the arguments to invoke to show the version
:type args: list of str
:param regexp: the regular expression to get the version
:param expander: the expansion string for the version using the
regexp group dict
"""

View File

@@ -156,7 +156,6 @@ class Config:
section = 'drive:' + urllib.quote('%s:%s:%s' % (
vendor, model, release))
self._parser.add_section(section)
__pychecker__ = 'no-local'
for key in ['vendor', 'model', 'release']:
self._parser.set(section, key, locals()[key].strip())

View File

@@ -66,6 +66,6 @@ def getDeviceInfo(path):
except ImportError:
return None
device = cdio.Device(path)
ok, vendor, model, release = device.get_hwinfo()
_, vendor, model, release = device.get_hwinfo()
return (vendor, model, release)
return vendor, model, release

View File

@@ -60,7 +60,7 @@ class FlacEncodeTask(task.Task):
self.schedule(0.0, self._flac_encode)
def _flac_encode(self):
self.new_path = flac.encode(self.track_path, self.track_out_path)
flac.encode(self.track_path, self.track_out_path)
self.stop()

View File

@@ -52,17 +52,19 @@ class TrackMetadata(object):
mbid = None
sortName = None
mbidArtist = None
mbidRecording = None
mbidWorks = []
class DiscMetadata(object):
"""
@param artist: artist(s) name
@param sortName: album artist sort name
@param release: earliest release date, in YYYY-MM-DD
@type release: unicode
@param title: title of the disc (with disambiguation)
@param releaseTitle: title of the release (without disambiguation)
@type tracks: C{list} of L{TrackMetadata}
:param artist: artist(s) name
:param sortName: release artist sort name
:param release: earliest release date, in YYYY-MM-DD
:type release: unicode
:param title: title of the disc (with disambiguation)
:param releaseTitle: title of the release (without disambiguation)
:type tracks: list of :any:`TrackMetadata`
"""
artist = None
sortName = None
@@ -75,6 +77,7 @@ class DiscMetadata(object):
releaseType = None
mbid = None
mbidReleaseGroup = None
mbidArtist = None
url = None
@@ -140,17 +143,31 @@ class _Credit(list):
i.get('artist').get('name', None)))
def getIds(self):
# split()'s the joined string so we get a proper list of MBIDs
return self.joiner(lambda i: i.get('artist').get('id', None),
joinString=";")
joinString=";").split(';')
def _getMetadata(releaseShort, release, discid, country=None):
def _getWorks(recording):
"""Get "performance of" works out of a recording."""
works = []
valid_work_rel_types = [
u'a3005666-a872-32c3-ad06-98af558e99b0', # "Performance"
]
if 'work-relation-list' in recording:
for work in recording['work-relation-list']:
if work['type-id'] in valid_work_rel_types:
works.append(work['work']['id'])
return works
def _getMetadata(release, discid, country=None):
"""
@type release: C{dict}
@param release: a release dict as returned in the value for key release
:type release: dict
:param release: a release dict as returned in the value for key release
from get_release_by_id
@rtype: L{DiscMetadata} or None
:rtype: DiscMetadata or None
"""
logger.debug('getMetadata for release id %r', release['id'])
if not release['id']:
@@ -165,7 +182,8 @@ def _getMetadata(releaseShort, release, discid, country=None):
discMD = DiscMetadata()
discMD.releaseType = releaseShort.get('release-group', {}).get('type')
if 'type' in release['release-group']:
discMD.releaseType = release['release-group']['type']
discCredit = _Credit(release['artist-credit'])
# FIXME: is there a better way to check for VA ?
@@ -176,10 +194,10 @@ def _getMetadata(releaseShort, release, discid, country=None):
if len(discCredit) > 1:
logger.debug('artist-credit more than 1: %r', discCredit)
albumArtistName = discCredit.getName()
releaseArtistName = discCredit.getName()
# getUniqueName gets disambiguating names like Muse (UK rock band)
discMD.artist = albumArtistName
discMD.artist = releaseArtistName
discMD.sortName = discCredit.getSortName()
if 'date' not in release:
logger.warning("release with ID '%s' (%s - %s) does not have a date",
@@ -188,6 +206,7 @@ def _getMetadata(releaseShort, release, discid, country=None):
discMD.release = release['date']
discMD.mbid = release['id']
discMD.mbidReleaseGroup = release['release-group']['id']
discMD.mbidArtist = discCredit.getIds()
discMD.url = 'https://musicbrainz.org/release/' + release['id']
@@ -229,7 +248,9 @@ def _getMetadata(releaseShort, release, discid, country=None):
track.mbidArtist = trackCredit.getIds()
track.title = t['recording']['title']
track.mbid = t['recording']['id']
track.mbid = t['id']
track.mbidRecording = t['recording']['id']
track.mbidWorks = _getWorks(t['recording'])
# FIXME: unit of duration ?
track.duration = int(t['recording'].get('length', 0))
@@ -261,13 +282,14 @@ def musicbrainz(discid, country=None, record=False):
Example disc id: Mj48G109whzEmAbPBoGvd4KyCS4-
@type discid: str
:type discid: str
@rtype: list of L{DiscMetadata}
:rtype: list of :any:`DiscMetadata`
"""
logger.debug('looking up results for discid %r', discid)
import musicbrainzngs
logging.getLogger("musicbrainzngs").setLevel(logging.WARNING)
musicbrainzngs.set_useragent("whipper", whipper.__version__,
"https://github.com/whipper-team/whipper")
ret = []
@@ -303,13 +325,15 @@ def musicbrainz(discid, country=None, record=False):
res = musicbrainzngs.get_release_by_id(
release['id'], includes=["artists", "artist-credits",
"recordings", "discids", "labels"])
"recordings", "discids", "labels",
"recording-level-rels", "work-rels",
"release-groups"])
_record(record, 'release', release['id'], res)
releaseDetail = res['release']
formatted = json.dumps(releaseDetail, sort_keys=False, indent=4)
logger.debug('release %s', formatted)
md = _getMetadata(release, releaseDetail, discid, country)
md = _getMetadata(releaseDetail, discid, country)
if md:
logger.debug('duration %r', md.duration)
ret.append(md)
@@ -317,6 +341,4 @@ def musicbrainz(discid, country=None, record=False):
return ret
elif result.get('cdstub'):
logger.debug('query returned cdstub: ignored')
return None
else:
return None
return None

View File

@@ -28,10 +28,10 @@ class PathFilter(object):
def __init__(self, slashes=True, quotes=True, fat=True, special=False):
"""
@param slashes: whether to convert slashes to dashes
@param quotes: whether to normalize quotes
@param fat: whether to strip characters illegal on FAT filesystems
@param special: whether to strip special characters
:param slashes: whether to convert slashes to dashes
:param quotes: whether to normalize quotes
:param fat: whether to strip characters illegal on FAT filesystems
:param special: whether to strip special characters
"""
self._slashes = slashes
self._quotes = quotes
@@ -45,7 +45,7 @@ class PathFilter(object):
def separators(path):
# replace separators with a space-hyphen or hyphen
path = re.sub(r'[:]', ' -', path, re.UNICODE)
path = re.sub(r'[\|]', '-', path, re.UNICODE)
path = re.sub(r'[|]', '-', path, re.UNICODE)
return path
# change all fancy single/double quotes to normal quotes
@@ -56,12 +56,12 @@ class PathFilter(object):
if self._special:
path = separators(path)
path = re.sub(r'[\*\?&!\'\"\$\(\)`{}\[\]<>]',
path = re.sub(r'[*?&!\'\"$()`{}\[\]<>]',
'_', path, re.UNICODE)
if self._fat:
path = separators(path)
# : and | already gone, but leave them here for reference
path = re.sub(r'[:\*\?"<>|"]', '_', path, re.UNICODE)
path = re.sub(r'[:*?"<>|]', '_', path, re.UNICODE)
return path

View File

@@ -44,12 +44,11 @@ class Program:
"""
I maintain program state and functionality.
@ivar metadata:
@type metadata: L{mbngs.DiscMetadata}
@ivar result: the rip's result
@type result: L{result.RipResult}
@type outdir: unicode
@type config: L{whipper.common.config.Config}
:vartype metadata: mbngs.DiscMetadata
:cvar result: the rip's result
:vartype result: result.RipResult
:vartype outdir: unicode
:vartype config: whipper.common.config.Config
"""
cuePath = None
@@ -60,7 +59,7 @@ class Program:
def __init__(self, config, record=False):
"""
@param record: whether to record results of API calls for playback.
:param record: whether to record results of API calls for playback.
"""
self._record = record
self._cache = cache.ResultCache()
@@ -81,7 +80,8 @@ class Program:
self._filter = path.PathFilter(**d)
def setWorkingDirectory(self, workingDirectory):
@staticmethod
def setWorkingDirectory(workingDirectory):
if workingDirectory:
logger.info('changing to working directory %s', workingDirectory)
os.chdir(workingDirectory)
@@ -91,20 +91,24 @@ class Program:
Also warn about buggy cdrdao versions.
"""
from pkg_resources import parse_version as V
version = cdrdao.getCDRDAOVersion()
version = cdrdao.version()
if V(version) < V('1.2.3rc2'):
logger.warning('cdrdao older than 1.2.3 has a pre-gap length bug.'
' See http://sourceforge.net/tracker/?func=detail&aid=604751&group_id=2171&atid=102171') # noqa: E501
toc = cdrdao.ReadTOCTask(device).table
t = cdrdao.ReadTOCTask(device, fast_toc=True)
runner.run(t)
toc = t.toc.table
assert toc.hasTOC()
return toc
def getTable(self, runner, cddbdiscid, mbdiscid, device, offset,
out_path):
toc_path):
"""
Retrieve the Table either from the cache or the drive.
@rtype: L{table.Table}
:rtype: table.Table
"""
tcache = cache.TableCache()
ptable = tcache.get(cddbdiscid, mbdiscid)
@@ -122,8 +126,10 @@ class Program:
logger.debug('getTable: cddbdiscid %s, mbdiscid %s not in cache '
'for offset %s, reading table', cddbdiscid, mbdiscid,
offset)
t = cdrdao.ReadTableTask(device, out_path)
itable = t.table
t = cdrdao.ReadTOCTask(device, toc_path=toc_path)
t.description = "Reading table"
runner.run(t)
itable = t.toc.table
tdict[offset] = itable
ptable.persist(tdict)
logger.debug('getTable: read table %r', itable)
@@ -145,7 +151,7 @@ class Program:
Retrieve the persistable RipResult either from our cache (from a
previous, possibly aborted rip), or return a new one.
@rtype: L{result.RipResult}
:rtype: result.RipResult
"""
assert self.result is None
@@ -157,8 +163,9 @@ class Program:
def saveRipResult(self):
self._presult.persist()
def addDisambiguation(self, template_part, metadata):
"Add disambiguation to template path part string."
@staticmethod
def addDisambiguation(template_part, metadata):
"""Add disambiguation to template path part string."""
if metadata.catalogNumber:
template_part += ' (%s)' % metadata.catalogNumber
elif metadata.barcode:
@@ -181,12 +188,12 @@ class Program:
Disc files (.cue, .log, .m3u) are named according to the disc
template, filling in the variables and adding the file
extension. Variables for both disc and track template are:
- %A: album artist
- %S: album artist sort name
- %A: release artist
- %S: release artist sort name
- %d: disc title
- %y: release year
- %r: release type, lowercase
- %R: Release type, normal case
- %R: release type, normal case
- %x: audio extension, lowercase
- %X: audio extension, uppercase
"""
@@ -235,11 +242,12 @@ class Program:
template = re.sub(r'%(\w)', r'%(\1)s', template)
return os.path.join(outdir, template % v)
def getCDDB(self, cddbdiscid):
@staticmethod
def getCDDB(cddbdiscid):
"""
@param cddbdiscid: list of id, tracks, offsets, seconds
:param cddbdiscid: list of id, tracks, offsets, seconds
@rtype: str
:rtype: str
"""
# FIXME: convert to nonblocking?
try:
@@ -262,7 +270,7 @@ class Program:
def getMusicBrainz(self, ittoc, mbdiscid, release=None, country=None,
prompt=False):
"""
@type ittoc: L{whipper.image.table.Table}
:type ittoc: whipper.image.table.Table
"""
# look up disc on MusicBrainz
print('Disc duration: %s, %d audio tracks' % (
@@ -270,10 +278,8 @@ class Program:
ittoc.getAudioTracks()))
logger.debug('MusicBrainz submit url: %r',
ittoc.getMusicBrainzSubmitURL())
ret = None
metadatas = None
e = None
for _ in range(0, 4):
try:
@@ -310,6 +316,7 @@ class Program:
print('Type : %s' % metadata.releaseType)
if metadata.barcode:
print("Barcode : %s" % metadata.barcode)
# TODO: Add test for non ASCII catalog numbers: see issue #215
if metadata.catalogNumber:
print("Cat no : %s" %
metadata.catalogNumber.encode('utf-8'))
@@ -344,7 +351,7 @@ class Program:
elif not metadatas:
logger.warning("requested release id '%s', but none of "
"the found releases match", release)
return
return None
else:
if lowest:
metadatas = deltas[lowest]
@@ -363,7 +370,7 @@ class Program:
"not the same", releaseTitle, i,
metadata.releaseTitle)
if (not release and len(list(deltas)) > 1):
if not release and len(list(deltas)) > 1:
logger.warning('picked closest match in duration. '
'Others may be wrong in MusicBrainz, '
'please correct')
@@ -383,30 +390,33 @@ class Program:
"""
Based on the metadata, get a dict of tags for the given track.
@param number: track number (0 for HTOA)
@type number: int
:param number: track number (0 for HTOA)
:type number: int
@rtype: dict
:rtype: dict
"""
trackArtist = u'Unknown Artist'
albumArtist = u'Unknown Artist'
releaseArtist = u'Unknown Artist'
disc = u'Unknown Disc'
title = u'Unknown Track'
if self.metadata:
trackArtist = self.metadata.artist
albumArtist = self.metadata.artist
releaseArtist = self.metadata.artist
disc = self.metadata.title
mbidAlbum = self.metadata.mbid
mbidTrackAlbum = self.metadata.mbidArtist
mbidRelease = self.metadata.mbid
mbidReleaseGroup = self.metadata.mbidReleaseGroup
mbidReleaseArtist = self.metadata.mbidArtist
if number > 0:
try:
track = self.metadata.tracks[number - 1]
trackArtist = track.artist
title = track.title
mbidRecording = track.mbidRecording
mbidTrack = track.mbid
mbidTrackArtist = track.mbidArtist
mbidWorks = track.mbidWorks
except IndexError as e:
logger.error('no track %d found, %r', number, e)
raise
@@ -420,7 +430,7 @@ class Program:
tags['MUSICBRAINZ_DISCID'] = mbdiscid
if self.metadata and not self.metadata.various:
tags['ALBUMARTIST'] = albumArtist
tags['ALBUMARTIST'] = releaseArtist
tags['ARTIST'] = trackArtist
tags['TITLE'] = title
tags['ALBUM'] = disc
@@ -432,10 +442,14 @@ class Program:
tags['DATE'] = self.metadata.release
if number > 0:
tags['MUSICBRAINZ_TRACKID'] = mbidTrack
tags['MUSICBRAINZ_RELEASETRACKID'] = mbidTrack
tags['MUSICBRAINZ_TRACKID'] = mbidRecording
tags['MUSICBRAINZ_ARTISTID'] = mbidTrackArtist
tags['MUSICBRAINZ_ALBUMID'] = mbidAlbum
tags['MUSICBRAINZ_ALBUMARTISTID'] = mbidTrackAlbum
tags['MUSICBRAINZ_ALBUMID'] = mbidRelease
tags['MUSICBRAINZ_RELEASEGROUPID'] = mbidReleaseGroup
tags['MUSICBRAINZ_ALBUMARTISTID'] = mbidReleaseArtist
if len(mbidWorks) > 0:
tags['MUSICBRAINZ_WORKID'] = mbidWorks
# TODO/FIXME: ISRC tag
@@ -445,7 +459,7 @@ class Program:
"""
Check if we have hidden track one audio.
@returns: tuple of (start, stop), or None
:returns: tuple of (start, stop), or None
"""
track = self.result.table.tracks[0]
try:
@@ -455,9 +469,10 @@ class Program:
start = index.absolute
stop = track.getIndex(1).absolute - 1
return (start, stop)
return start, stop
def verifyTrack(self, runner, trackResult):
@staticmethod
def verifyTrack(runner, trackResult):
is_wave = not trackResult.filename.endswith('.flac')
t = checksum.CRC32Task(trackResult.filename, is_wave=is_wave)
@@ -481,8 +496,8 @@ class Program:
Ripping the track may change the track's filename as stored in
trackResult.
@param trackResult: the object to store information in.
@type trackResult: L{result.TrackResult}
:param trackResult: the object to store information in.
:type trackResult: result.TrackResult
"""
if trackResult.number == 0:
start, stop = self.getHTOA()
@@ -589,10 +604,10 @@ class Program:
return cuePath
def writeLog(self, discName, logger):
def writeLog(self, discName, txt_logger):
logPath = common.truncate_filename(discName + '.log')
handle = open(logPath, 'w')
log = logger.log(self.result)
log = txt_logger.log(self.result)
handle.write(log.encode('utf-8'))
handle.close()

View File

@@ -21,9 +21,7 @@
import os
import tempfile
"""
Rename files on file system and inside metafiles in a resumable way.
"""
"""Rename files on file system and inside metafiles in a resumable way."""
class Operator(object):
@@ -111,10 +109,10 @@ class FileRenamer(Operator):
"""
Add a rename operation.
@param source: source filename
@type source: str
@param destination: destination filename
@type destination: str
:param source: source filename
:type source: str
:param destination: destination filename
:type destination: str
"""
@@ -144,16 +142,16 @@ class Operation(object):
def serialize(self):
"""
Serialize the operation.
The return value should bu usable with L{deserialize}
The return value should bu usable with :any:`deserialize`
@rtype: str
:rtype: str
"""
def deserialize(cls, data):
"""
Deserialize the operation with the given operation data.
@type data: str
:type data: str
"""
raise NotImplementedError
deserialize = classmethod(deserialize)

View File

@@ -87,6 +87,7 @@ class PopenTask(task.Task):
return
self._done()
# FIXME: catching too general exception (Exception)
except Exception as e:
logger.debug('exception during _read(): %s', e)
self.setException(e)
@@ -115,13 +116,13 @@ class PopenTask(task.Task):
os.kill(self._popen.pid, signal.SIGTERM)
# self.stop()
def readbytesout(self, bytes):
def readbytesout(self, bytes_stdout):
"""
Called when bytes have been read from stdout.
"""
pass
def readbyteserr(self, bytes):
def readbyteserr(self, bytes_stderr):
"""
Called when bytes have been read from stderr.
"""

View File

@@ -28,8 +28,8 @@ class Popen(subprocess.Popen):
def recv_err(self, maxsize=None):
return self._recv('stderr', maxsize)
def send_recv(self, input='', maxsize=None):
return self.send(input), self.recv(maxsize), self.recv_err(maxsize)
def send_recv(self, in_put='', maxsize=None):
return self.send(in_put), self.recv(maxsize), self.recv_err(maxsize)
def get_conn_maxsize(self, which, maxsize):
if maxsize is None:
@@ -44,16 +44,16 @@ class Popen(subprocess.Popen):
if subprocess.mswindows:
def send(self, input):
def send(self, in_put):
if not self.stdin:
return None
try:
x = msvcrt.get_osfhandle(self.stdin.fileno())
(errCode, written) = WriteFile(x, input)
(errCode, written) = WriteFile(x, in_put)
except ValueError:
return self._close('stdin')
except (subprocess.pywintypes.error, Exception), why:
except (subprocess.pywintypes.error, Exception) as why:
if why.args[0] in (109, errno.ESHUTDOWN):
return self._close('stdin')
raise
@@ -74,7 +74,7 @@ class Popen(subprocess.Popen):
(errCode, read) = ReadFile(x, nAvail, None)
except ValueError:
return self._close(which)
except (subprocess.pywintypes.error, Exception), why:
except (subprocess.pywintypes.error, Exception) as why:
if why.args[0] in (109, errno.ESHUTDOWN):
return self._close(which)
raise
@@ -85,7 +85,7 @@ class Popen(subprocess.Popen):
else:
def send(self, input):
def send(self, in_put):
if not self.stdin:
return None
@@ -93,8 +93,8 @@ class Popen(subprocess.Popen):
return 0
try:
written = os.write(self.stdin.fileno(), input)
except OSError, why:
written = os.write(self.stdin.fileno(), in_put)
except OSError as why:
if why.args[0] == errno.EPIPE: # broken pipe
return self._close('stdin')
raise
@@ -153,7 +153,7 @@ def recv_some(p, t=.1, e=1, tr=5, stderr=0):
def send_all(p, data):
while len(data):
while data:
sent = p.send(data)
if sent is None:
raise Exception(message)

View File

@@ -134,7 +134,8 @@ def perform_lookup(disc_id, freedb_server, freedb_port):
if len(matches) > 0:
# for each result, query FreeDB for XMCD file data
for (category, disc_id, title) in matches:
# XXX: Pylint, redefining argument with the local name 'disc_id'
for (category, disc_id, _) in matches:
sleep(1) # add a slight delay to keep the server happy
query = freedb_command(freedb_server,
@@ -145,7 +146,7 @@ def perform_lookup(disc_id, freedb_server, freedb_port):
response = RESPONSE.match(next(query))
if response is not None:
# FIXME - check response code here
# FIXME: check response code here
freedb = {}
line = next(query)
while not line.startswith(u"."):

View File

@@ -22,10 +22,7 @@ from __future__ import print_function
import logging
import sys
try:
from gi.repository import GLib as gobject
except ImportError:
import gobject
from gi.repository import GLib as GLib
logger = logging.getLogger(__name__)
@@ -77,13 +74,16 @@ class LogStub(object):
I am a stub for a log interface.
"""
def log(self, message, *args):
@staticmethod
def log(message, *args):
logger.info(message, *args)
def debug(self, message, *args):
@staticmethod
def debug(message, *args):
logger.debug(message, *args)
def warning(self, message, *args):
@staticmethod
def warning(message, *args):
logger.warning(message, *args)
@@ -97,8 +97,8 @@ class Task(LogStub):
stopping myself from running.
The listener can then handle the Task.exception.
@ivar description: what am I doing
@ivar exception: set if an exception happened during the task
:cvar description: what am I doing
:cvar exception: set if an exception happened during the task
execution. Will be raised through run() at the end.
"""
logCategory = 'Task'
@@ -191,8 +191,8 @@ class Task(LogStub):
# for now
if str(exception):
msg = ": %s" % str(exception)
line = "exception %(exc)s at %(filename)s:%(line)s: "
"%(func)s()%(msg)s" % locals()
line = ("exception %(exc)s at %(filename)s:%(line)s: "
"%(func)s()%(msg)s" % locals())
self.exception = exception
self.exceptionMessage = line
@@ -213,13 +213,13 @@ class Task(LogStub):
self.debug('set exception, %r, %r' % (
exception, self.exceptionMessage))
def schedule(self, delta, callable, *args, **kwargs):
def schedule(self, delta, callable_task, *args, **kwargs):
if not self.runner:
print("ERROR: scheduling on a task that's altready stopped")
import traceback
traceback.print_stack()
return
self.runner.schedule(self, delta, callable, *args, **kwargs)
self.runner.schedule(self, delta, callable_task, *args, **kwargs)
def addListener(self, listener):
"""
@@ -238,6 +238,7 @@ class Task(LogStub):
method = getattr(l, methodName)
try:
method(self, *args, **kwargs)
# FIXME: catching too general exception (Exception)
except Exception as e:
self.setException(e)
@@ -253,16 +254,16 @@ class ITaskListener(object):
"""
Implement me to be informed about progress.
@type value: float
@param value: progress, from 0.0 to 1.0
:type value: float
:param value: progress, from 0.0 to 1.0
"""
def described(self, task, description):
"""
Implement me to be informed about description changes.
@type description: str
@param description: description
:type description: str
:param description: description
"""
def started(self, task):
@@ -297,8 +298,8 @@ class BaseMultiTask(Task, ITaskListener):
"""
I perform multiple tasks.
@ivar tasks: the tasks to run
@type tasks: list of L{Task}
:ivar tasks: the tasks to run
:type tasks: list of :any:`Task`
"""
description = 'Doing various tasks'
@@ -312,7 +313,7 @@ class BaseMultiTask(Task, ITaskListener):
"""
Add a task.
@type task: L{Task}
:type task: Task
"""
if self.tasks is None:
self.tasks = []
@@ -350,6 +351,7 @@ class BaseMultiTask(Task, ITaskListener):
task.start(self.runner)
self.debug('BaseMultiTask.next(): started task %d of %d: %r',
self._task, len(self.tasks), task)
# FIXME: catching too general exception (Exception)
except Exception as e:
self.setException(e)
self.debug('Got exception during next: %r', self.exceptionMessage)
@@ -444,26 +446,26 @@ class TaskRunner(LogStub):
"""
Run the given task.
@type task: Task
:type task: Task
"""
raise NotImplementedError
# methods for tasks to call
def schedule(self, delta, callable, *args, **kwargs):
def schedule(self, delta, callable_task, *args, **kwargs):
"""
Schedule a single future call.
Subclasses should implement this.
@type delta: float
@param delta: time in the future to schedule call for, in seconds.
:type delta: float
:param delta: time in the future to schedule call for, in seconds.
"""
raise NotImplementedError
class SyncRunner(TaskRunner, ITaskListener):
"""
I run the task synchronously in a gobject MainLoop.
I run the task synchronously in a GObject MainLoop.
"""
def __init__(self, verbose=True):
@@ -478,11 +480,11 @@ class SyncRunner(TaskRunner, ITaskListener):
self._verboseRun = verbose
self._skip = skip
self._loop = gobject.MainLoop()
self._loop = GLib.MainLoop()
self._task.addListener(self)
# only start the task after going into the mainloop,
# otherwise the task might complete before we are in it
gobject.timeout_add(0L, self._startWrap, self._task)
GLib.timeout_add(0L, self._startWrap, self._task)
self.debug('run loop')
self._loop.run()
@@ -503,6 +505,7 @@ class SyncRunner(TaskRunner, ITaskListener):
try:
self.debug('start task %r' % task)
task.start(self)
# FIXME: catching too general exception (Exception)
except Exception as e:
# getExceptionMessage uses global exception state that doesn't
# hang around, so store the message
@@ -510,23 +513,19 @@ class SyncRunner(TaskRunner, ITaskListener):
self.debug('exception during start: %r', task.exceptionMessage)
self.stopped(task)
def schedule(self, task, delta, callable, *args, **kwargs):
def schedule(self, task, delta, callable_task, *args, **kwargs):
def c():
try:
self.debug('schedule: calling %r(*args=%r, **kwargs=%r)',
callable, args, kwargs)
callable(*args, **kwargs)
callable_task(*args, **kwargs)
return False
except Exception as e:
self.debug('exception when calling scheduled callable %r',
callable)
callable_task)
task.setException(e)
self.stopped(task)
raise
self.debug('schedule: scheduling %r(*args=%r, **kwargs=%r)',
callable, args, kwargs)
gobject.timeout_add(int(delta * 1000L), c)
GLib.timeout_add(int(delta * 1000L), c)
# ITaskListener methods
def progressed(self, task, value):

View File

@@ -62,14 +62,14 @@ class CueFile(object):
"""
I represent a .cue file as an object.
@type table: L{table.Table}
@ivar table: the index table.
:vartype table: table.Table
:ivar table: the index table.
"""
logCategory = 'CueFile'
def __init__(self, path):
"""
@type path: unicode
:type path: unicode
"""
assert isinstance(path, unicode), "%r is not unicode" % path
@@ -154,7 +154,7 @@ class CueFile(object):
"""
Add a message about a given line in the cue file.
@param number: line number, counting from 0.
:param number: line number, counting from 0.
"""
self._messages.append((number + 1, message))
@@ -182,7 +182,7 @@ class CueFile(object):
"""
Translate the .cue's FILE to an existing path.
@type path: unicode
:type path: unicode
"""
return common.getRealPath(self._path, path)
@@ -192,14 +192,14 @@ class File:
I represent a FILE line in a cue file.
"""
def __init__(self, path, format):
def __init__(self, path, file_format):
"""
@type path: unicode
:type path: unicode
"""
assert isinstance(path, unicode), "%r is not unicode" % path
self.path = path
self.format = format
self.format = file_format
def __repr__(self):
return '<File %r of format %s>' % (self.path, self.format)

View File

@@ -36,15 +36,15 @@ logger = logging.getLogger(__name__)
class Image(object):
"""
@ivar table: The Table of Contents for this image.
@type table: L{table.Table}
:ivar table: The Table of Contents for this image.
:vartype table: table.Table
"""
logCategory = 'Image'
def __init__(self, path):
"""
@type path: unicode
@param path: .cue path
:type path: unicode
:param path: .cue path
"""
assert isinstance(path, unicode), "%r is not unicode" % path
@@ -60,7 +60,7 @@ class Image(object):
"""
Translate the .cue's FILE to an existing path.
@param path: .cue path
:param path: .cue path
"""
assert isinstance(path, unicode), "%r is not unicode" % path
@@ -121,6 +121,7 @@ class ImageVerifyTask(task.MultiSeparateTask):
task.MultiSeparateTask.__init__(self)
self._image = image
# XXX: Pylint, redefining name 'cue' from outer scope (import)
cue = image.cue
self._tasks = []
self.lengths = {}
@@ -183,6 +184,7 @@ class ImageEncodeTask(task.MultiSeparateTask):
task.MultiSeparateTask.__init__(self)
self._image = image
# XXX: Pylint, redefining name 'cue' from outer scope (import)
cue = image.cue
self._tasks = []
self.lengths = {}
@@ -192,7 +194,7 @@ class ImageEncodeTask(task.MultiSeparateTask):
path = image.getRealPath(index.path)
assert isinstance(path, unicode), "%r is not unicode" % path
logger.debug('schedule encode of %r', path)
root, ext = os.path.splitext(os.path.basename(path))
root, _ = os.path.splitext(os.path.basename(path))
outpath = os.path.join(outdir, root + '.' + 'flac')
logger.debug('schedule encode to %r', outpath)
taskk = encode.FlacEncodeTask(
@@ -205,7 +207,6 @@ class ImageEncodeTask(task.MultiSeparateTask):
add(htoa)
except (KeyError, IndexError):
logger.debug('no HTOA track')
pass
for trackIndex, track in enumerate(cue.table.tracks):
logger.debug('encoding track %d', trackIndex + 1)

View File

@@ -57,17 +57,18 @@ class Track:
"""
I represent a track entry in an Table.
@ivar number: track number (1-based)
@type number: int
@ivar audio: whether the track is audio
@type audio: bool
@type indexes: dict of number -> L{Index}
@ivar isrc: ISRC code (12 alphanumeric characters)
@type isrc: str
@ivar cdtext: dictionary of CD Text information; see L{CDTEXT_KEYS}.
@type cdtext: str -> unicode
@ivar pre_emphasis: whether track is pre-emphasised
@type pre_emphasis: bool
:cvar number: track number (1-based)
:vartype number: int
:cvar audio: whether the track is audio
:vartype audio: bool
:vartype indexes: dict of number -> :any:`Index`
:cvar isrc: ISRC code (12 alphanumeric characters)
:vartype isrc: str
:cvar cdtext: dictionary of CD Text information;
:any:`see CDTEXT_KEYS`
:vartype cdtext: str -> unicode
:cvar pre_emphasis: whether track is pre-emphasised
:vartype pre_emphasis: bool
"""
number = None
@@ -90,7 +91,7 @@ class Track:
def index(self, number, absolute=None, path=None, relative=None,
counter=None):
"""
@type path: unicode or None
:type path: unicode or None
"""
if path is not None:
assert isinstance(path, unicode), "%r is not unicode" % path
@@ -130,9 +131,9 @@ class Track:
class Index:
"""
@ivar counter: counter for the index source; distinguishes between
:cvar counter: counter for the index source; distinguishes between
the matching FILE lines in .cue files for example
@type path: unicode or None
:vartype path: unicode or None
"""
number = None
absolute = None
@@ -161,11 +162,11 @@ class Table(object):
"""
I represent a table of indexes on a CD.
@ivar tracks: tracks on this CD
@type tracks: list of L{Track}
@ivar catalog: catalog number
@type catalog: str
@type cdtext: dict of str -> str
:cvar tracks: tracks on this CD
:vartype tracks: list of :any:`Track`
:cvar catalog: catalog number
:vartype catalog: str
:vartype cdtext: dict of str -> str
"""
tracks = None # list of Track
@@ -193,22 +194,22 @@ class Table(object):
def getTrackStart(self, number):
"""
@param number: the track number, 1-based
@type number: int
:param number: the track number, 1-based
:type number: int
@returns: the start of the given track number's index 1, in CD frames
@rtype: int
:returns: the start of the given track number's index 1, in CD frames
:rtype: int
"""
track = self.tracks[number - 1]
return track.getIndex(1).absolute
def getTrackEnd(self, number):
"""
@param number: the track number, 1-based
@type number: int
:param number: the track number, 1-based
:type number: int
@returns: the end of the given track number (ie index 1 of next track)
@rtype: int
:returns: the end of the given track number (ie index 1 of next track)
:rtype: int
"""
# default to end of disc
end = self.leadout - 1
@@ -228,28 +229,29 @@ class Table(object):
def getTrackLength(self, number):
"""
@param number: the track number, 1-based
@type number: int
:param number: the track number, 1-based
:type number: int
@returns: the length of the given track number, in CD frames
@rtype: int
:returns: the length of the given track number, in CD frames
:rtype: int
"""
return self.getTrackEnd(number) - self.getTrackStart(number) + 1
def getAudioTracks(self):
"""
@returns: the number of audio tracks on the CD
@rtype: int
:returns: the number of audio tracks on the CD
:rtype: int
"""
return len([t for t in self.tracks if t.audio])
def hasDataTracks(self):
"""
@returns: whether this disc contains data tracks
:returns: whether this disc contains data tracks
"""
return len([t for t in self.tracks if not t.audio]) > 0
def _cddbSum(self, i):
@staticmethod
def _cddbSum(i):
ret = 0
while i > 0:
ret += (i % 10)
@@ -267,7 +269,7 @@ class Table(object):
- offset of index 1 of each track
- length of disc in seconds (including data track)
@rtype: list of int
:rtype: list of int
"""
offsets = []
@@ -319,8 +321,8 @@ class Table(object):
"""
Calculate the CDDB disc ID.
@rtype: str
@returns: the 8-character hexadecimal disc ID
:rtype: str
:returns: the 8-character hexadecimal disc ID
"""
values = self.getCDDBValues()
return "%08x" % int(values)
@@ -329,8 +331,8 @@ class Table(object):
"""
Calculate the MusicBrainz disc ID.
@rtype: str
@returns: the 28-character base64-encoded disc ID
:rtype: str
:returns: the 28-character base64-encoded disc ID
"""
if self.mbdiscid:
logger.debug('getMusicBrainzDiscId: returning cached %r',
@@ -339,13 +341,9 @@ class Table(object):
values = self._getMusicBrainzValues()
# MusicBrainz disc id does not take into account data tracks
# P2.3
try:
import hashlib
sha1 = hashlib.sha1
except ImportError:
from sha import sha as sha1
import base64
import hashlib
sha1 = hashlib.sha1
sha = sha1()
@@ -404,7 +402,7 @@ class Table(object):
"""
Get the length in frames (excluding HTOA)
@param data: whether to include the data tracks in the length
:param data: whether to include the data tracks in the length
"""
# the 'real' leadout, not offset by 150 frames
if data:
@@ -434,7 +432,7 @@ class Table(object):
- leadout of disc
- offset of index 1 of each track
@rtype: list of int
:rtype: list of int
"""
# MusicBrainz disc id does not take into account data tracks
@@ -473,13 +471,13 @@ class Table(object):
def cue(self, cuePath='', program='whipper'):
"""
@param cuePath: path to the cue file to be written. If empty,
:param cuePath: path to the cue file to be written. If empty,
will treat paths as if in current directory.
Dump our internal representation to a .cue file content.
@rtype: C{unicode}
:rtype: unicode
"""
logger.debug('generating .cue for cuePath %r', cuePath)
@@ -636,8 +634,8 @@ class Table(object):
Assumes all indexes have an absolute offset and will raise if not.
@type track: C{int}
@type index: C{int}
:type track: int
:type index: int
"""
logger.debug('setFile: track %d, index %d, path %r, length %r, '
'counter %r', track, index, path, length, counter)
@@ -707,7 +705,7 @@ class Table(object):
The other table is assumed to be from an additional session,
@type other: L{Table}
:type other: Table
"""
gap = self._getSessionGap(session)
@@ -732,7 +730,8 @@ class Table(object):
self.leadout += other.leadout + gap # FIXME
logger.debug('fixing leadout, now %d', self.leadout)
def _getSessionGap(self, session):
@staticmethod
def _getSessionGap(session):
# From cdrecord multi-session info:
# For the first additional session this is 11250 sectors
# lead-out/lead-in overhead + 150 sectors for the pre-gap of the first
@@ -753,11 +752,11 @@ class Table(object):
"""
Return the next track and index.
@param track: track number, 1-based
:param track: track number, 1-based
@raises IndexError: on last index
:raises IndexError: on last index
@rtype: tuple of (int, int)
:rtype: tuple of (int, int)
"""
t = self.tracks[track - 1]
indexes = list(t.indexes)
@@ -824,7 +823,7 @@ class Table(object):
discId1 &= 0xffffffff
discId2 &= 0xffffffff
return ("%08x" % discId1, "%08x" % discId2)
return "%08x" % discId1, "%08x" % discId2
def accuraterip_path(self):
discId1, discId2 = self.accuraterip_ids()

View File

@@ -104,10 +104,10 @@ class Sources:
def append(self, counter, offset, source):
"""
@param counter: the source counter; updates for each different
:param counter: the source counter; updates for each different
data source (silence or different file path)
@type counter: int
@param offset: the absolute disc offset where this source starts
:type counter: int
:param offset: the absolute disc offset where this source starts
"""
logger.debug('appending source, counter %d, abs offset %d, '
'source %r', counter, offset, source)
@@ -117,7 +117,7 @@ class Sources:
"""
Retrieve the source used at the given offset.
"""
for i, (c, o, s) in enumerate(self._sources):
for i, (_, o, _) in enumerate(self._sources):
if offset < o:
return self._sources[i - 1]
@@ -127,7 +127,7 @@ class Sources:
"""
Retrieve the absolute offset of the first source for this counter
"""
for i, (c, o, s) in enumerate(self._sources):
for i, (c, _, _) in enumerate(self._sources):
if c == counter:
return self._sources[i][1]
@@ -138,7 +138,7 @@ class TocFile(object):
def __init__(self, path):
"""
@type path: unicode
:type path: unicode
"""
assert isinstance(path, unicode), "%r is not unicode" % path
self._path = path
@@ -151,7 +151,7 @@ class TocFile(object):
def _index(self, currentTrack, i, absoluteOffset, trackOffset):
absolute = absoluteOffset + trackOffset
# this may be in a new source, so calculate relative
c, o, s = self._sources.get(absolute)
c, _, s = self._sources.get(absolute)
logger.debug('at abs offset %d, we are in source %r',
absolute, s)
counterStart = self._sources.getCounterStart(c)
@@ -341,7 +341,7 @@ class TocFile(object):
continue
length = common.msfToFrames(m.group('length'))
c, o, s = self._sources.get(absoluteOffset)
c, _, s = self._sources.get(absoluteOffset)
logger.debug('at abs offset %d, we are in source %r',
absoluteOffset, s)
counterStart = self._sources.getCounterStart(c)
@@ -380,7 +380,7 @@ class TocFile(object):
"""
Add a message about a given line in the cue file.
@param number: line number, counting from 0.
:param number: line number, counting from 0.
"""
self._messages.append((number + 1, message))
@@ -412,7 +412,7 @@ class TocFile(object):
"""
Translate the .toc's FILE to an existing path.
@type path: unicode
:type path: unicode
"""
return common.getRealPath(self._path, path)
@@ -424,10 +424,10 @@ class File:
def __init__(self, path, start, length):
"""
@type path: C{unicode}
@type start: C{int}
@param start: starting point for the track in this file, in frames
@param length: length for the track in this file, in frames
:type path: unicode
:type start: int
:param start: starting point for the track in this file, in frames
:param length: length for the track in this file, in frames
"""
assert isinstance(path, unicode), "%r is not unicode" % path

View File

@@ -1,54 +1,5 @@
from subprocess import Popen, PIPE
import logging
logger = logging.getLogger(__name__)
ARB = 'accuraterip-checksum'
FLAC = 'flac'
import accuraterip
def _execute(cmd, **redirects):
logger.debug('executing %r', cmd)
return Popen(cmd, **redirects)
def accuraterip_checksum(f, track_number, total_tracks, wave=False, v2=False):
v = '--accuraterip-v1'
if v2:
v = '--accuraterip-v2'
track_number, total_tracks = str(track_number), str(total_tracks)
if wave:
cmd = [ARB, v, f, track_number, total_tracks]
redirects = dict(stdout=PIPE, stderr=PIPE)
else:
flac = _execute([FLAC, '-cds', f], stdout=PIPE)
cmd = [ARB, v, '/dev/stdin', track_number, total_tracks]
redirects = dict(stdin=flac.stdout, stdout=PIPE, stderr=PIPE)
arc = _execute(cmd, **redirects)
if not wave:
flac.stdout.close()
out, err = arc.communicate()
if not wave:
flac.wait()
if flac.returncode != 0:
logger.warning('ARC calculation failed: flac '
'return code is non zero: %r', flac.returncode)
return None
if arc.returncode != 0:
logger.warning('ARC calculation failed: '
'arc return code is non zero: %r', arc.returncode)
return None
try:
checksum = int('0x%s' % out.strip(), base=16)
logger.debug('returned %r', checksum)
return checksum
except ValueError:
logger.warning('ARC output is not usable')
return None
def accuraterip_checksum(f, track_number, total_tracks):
return accuraterip.compute(f.encode('utf-8'), track_number, total_tracks)

View File

@@ -88,10 +88,10 @@ class ProgressParser:
def __init__(self, start, stop):
"""
@param start: first frame to rip
@type start: int
@param stop: last frame to rip (inclusive)
@type stop: int
:param start: first frame to rip
:type start: int
:param stop: last frame to rip (inclusive)
:type stop: int
"""
self.start = start
self.stop = stop
@@ -159,11 +159,10 @@ class ProgressParser:
markEnd = frameOffset
# FIXME: doing this is way too slow even for a testcase, so disable
if False:
for frame in range(markStart, markEnd):
if frame not in list(self._reads.keys()):
self._reads[frame] = 0
self._reads[frame] += 1
# for frame in range(markStart, markEnd):
# if frame not in list(self._reads.keys()):
# self._reads[frame] = 0
# self._reads[frame] += 1
# cdparanoia reads quite a bit beyond the current track before it
# goes back to verify; don't count those
@@ -206,8 +205,6 @@ class ProgressParser:
class ReadTrackTask(task.Task):
"""
I am a task that reads a track using cdparanoia.
@ivar reads: how many reads were done to rip the track
"""
description = "Reading track"
@@ -222,22 +219,22 @@ class ReadTrackTask(task.Task):
"""
Read the given track.
@param path: where to store the ripped track
@type path: unicode
@param table: table of contents of CD
@type table: L{table.Table}
@param start: first frame to rip
@type start: int
@param stop: last frame to rip (inclusive); >= start
@type stop: int
@param offset: read offset, in samples
@type offset: int
@param device: the device to rip from
@type device: str
@param action: a string representing the action; e.g. Read/Verify
@type action: str
@param what: a string representing what's being read; e.g. Track
@type what: str
:param path: where to store the ripped track
:type path: unicode
:param table: table of contents of CD
:type table: table.Table
:param start: first frame to rip
:type start: int
:param stop: last frame to rip (inclusive); >= start
:type stop: int
:param offset: read offset, in samples
:type offset: int
:param device: the device to rip from
:type device: str
:param action: a string representing the action; e.g. Read/Verify
:type action: str
:param what: a string representing what's being read; e.g. Track
:type what: str
"""
assert isinstance(path, unicode), "%r is not unicode" % path
@@ -264,7 +261,7 @@ class ReadTrackTask(task.Task):
stopTrack = 0
stopOffset = self._stop
for i, t in enumerate(self._table.tracks):
for i, _ in enumerate(self._table.tracks):
if self._table.getTrackStart(i + 1) <= self._start:
startTrack = i + 1
startOffset = self._start - self._table.getTrackStart(i + 1)
@@ -300,7 +297,6 @@ class ReadTrackTask(task.Task):
stderr=subprocess.PIPE,
close_fds=True)
except OSError as e:
import errno
if e.errno == errno.ENOENT:
raise common.MissingDependencyException('cd-paranoia')
@@ -405,17 +401,16 @@ class ReadVerifyTrackTask(task.MultiSeparateTask):
The path where the file is stored can be changed if necessary, for
example if the file name is too long.
@ivar path: the path where the file is to be stored.
@ivar checksum: the checksum of the track; set if they match.
@ivar testchecksum: the test checksum of the track.
@ivar copychecksum: the copy checksum of the track.
@ivar testspeed: the test speed of the track, as a multiple of
:cvar checksum: the checksum of the track; set if they match.
:cvar testchecksum: the test checksum of the track.
:cvar copychecksum: the copy checksum of the track.
:cvar testspeed: the test speed of the track, as a multiple of
track duration.
@ivar copyspeed: the copy speed of the track, as a multiple of
:cvar copyspeed: the copy speed of the track, as a multiple of
track duration.
@ivar testduration: the test duration of the track, in seconds.
@ivar copyduration: the copy duration of the track, in seconds.
@ivar peak: the peak level of the track
:cvar testduration: the test duration of the track, in seconds.
:cvar copyduration: the copy duration of the track, in seconds.
:cvar peak: the peak level of the track
"""
checksum = None
@@ -434,20 +429,20 @@ class ReadVerifyTrackTask(task.MultiSeparateTask):
def __init__(self, path, table, start, stop, overread, offset=0,
device=None, taglist=None, what="track"):
"""
@param path: where to store the ripped track
@type path: str
@param table: table of contents of CD
@type table: L{table.Table}
@param start: first frame to rip
@type start: int
@param stop: last frame to rip (inclusive)
@type stop: int
@param offset: read offset, in samples
@type offset: int
@param device: the device to rip from
@type device: str
@param taglist: a dict of tags
@type taglist: dict
:param path: where to store the ripped track
:type path: str
:param table: table of contents of CD
:type table: table.Table
:param start: first frame to rip
:type start: int
:param stop: last frame to rip (inclusive)
:type stop: int
:param offset: read offset, in samples
:type offset: int
:param device: the device to rip from
:type device: str
:param taglist: a dict of tags
:type taglist: dict
"""
task.MultiSeparateTask.__init__(self)
@@ -458,6 +453,7 @@ class ReadVerifyTrackTask(task.MultiSeparateTask):
# FIXME: choose a dir on the same disk/dir as the final path
fd, tmppath = tempfile.mkstemp(suffix='.whipper.wav')
tmppath = unicode(tmppath)
os.fchmod(fd, 0644)
os.close(fd)
self._tmpwavpath = tmppath
@@ -540,6 +536,7 @@ class ReadVerifyTrackTask(task.MultiSeparateTask):
try:
logger.debug('moving to final path %r', self.path)
os.rename(self._tmppath, self.path)
# FIXME: catching too general exception (Exception)
except Exception as e:
logger.debug('exception while moving to final '
'path %r: %s', self.path, e)
@@ -548,6 +545,7 @@ class ReadVerifyTrackTask(task.MultiSeparateTask):
os.unlink(self._tmppath)
else:
logger.debug('stop: exception %r', self.exception)
# FIXME: catching too general exception (Exception)
except Exception as e:
print('WARNING: unhandled exception %r' % (e, ))
@@ -569,6 +567,7 @@ def getCdParanoiaVersion():
_OK_RE = re.compile(r'Drive tests OK with Paranoia.')
_WARNING_RE = re.compile(r'WARNING! PARANOIA MAY NOT BE')
_ABORTING_RE = re.compile(r'aborting test\.')
class AnalyzeTask(ctask.PopenTask):
@@ -592,25 +591,22 @@ class AnalyzeTask(ctask.PopenTask):
def commandMissing(self):
raise common.MissingDependencyException('cd-paranoia')
def readbyteserr(self, bytes):
self._output.append(bytes)
def readbyteserr(self, bytes_stderr):
self._output.append(bytes_stderr)
def done(self):
if self.cwd:
shutil.rmtree(self.cwd)
output = "".join(self._output)
m = _OK_RE.search(output)
if m:
self.defeatsCache = True
else:
self.defeatsCache = False
self.defeatsCache = bool(m)
def failed(self):
# cdparanoia exits with return code 1 if it can't determine
# whether it can defeat the audio cache
output = "".join(self._output)
m = _WARNING_RE.search(output)
if m:
if m or _ABORTING_RE.search(output):
self.defeatsCache = False
if self.cwd:
shutil.rmtree(self.cwd)

View File

@@ -2,58 +2,163 @@ import os
import re
import shutil
import tempfile
import subprocess
from subprocess import Popen, PIPE
from whipper.common.common import EjectError, truncate_filename
from whipper.common.common import truncate_filename
from whipper.image.toc import TocFile
from whipper.extern.task import task
from whipper.extern import asyncsub
import logging
logger = logging.getLogger(__name__)
CDRDAO = 'cdrdao'
_TRACK_RE = re.compile(r"^Analyzing track (?P<track>[0-9]*) \(AUDIO\): start (?P<start>[0-9]*:[0-9]*:[0-9]*), length (?P<length>[0-9]*:[0-9]*:[0-9]*)") # noqa: E501
_CRC_RE = re.compile(
r"Found (?P<channels>[0-9]*) Q sub-channels with CRC errors")
_BEGIN_CDRDAO_RE = re.compile(r"-" * 60)
_LAST_TRACK_RE = re.compile(r"^[ ]?(?P<track>[0-9]*)")
_LEADOUT_RE = re.compile(
r"^Leadout AUDIO\s*[0-9]\s*[0-9]*:[0-9]*:[0-9]*\([0-9]*\)")
def read_toc(device, fast_toc=False, toc_path=None):
class ProgressParser:
tracks = 0
currentTrack = 0
oldline = '' # for leadout/final track number detection
def parse(self, line):
cdrdao_m = _BEGIN_CDRDAO_RE.match(line)
if cdrdao_m:
logger.debug("RE: Begin cdrdao toc-read")
leadout_m = _LEADOUT_RE.match(line)
if leadout_m:
logger.debug("RE: Reached leadout")
last_track_m = _LAST_TRACK_RE.match(self.oldline)
if last_track_m:
self.tracks = last_track_m.group('track')
track_s = _TRACK_RE.search(line)
if track_s:
logger.debug("RE: Began reading track: %d",
int(track_s.group('track')))
self.currentTrack = int(track_s.group('track'))
crc_s = _CRC_RE.search(line)
if crc_s:
print("Track %d finished, "
"found %d Q sub-channels with CRC errors" %
(self.currentTrack, int(crc_s.group('channels'))))
self.oldline = line
class ReadTOCTask(task.Task):
"""
Return cdrdao-generated table of contents for 'device'.
Task that reads the TOC of the disc using cdrdao
"""
# cdrdao MUST be passed a non-existing filename as its last argument
# to write the TOC to; it does not support writing to stdout or
# overwriting an existing file, nor does linux seem to support
# locking a non-existant file. Thus, this race-condition introducing
# hack is carried from morituri to whipper and will be removed when
# cdrdao is fixed.
fd, tocfile = tempfile.mkstemp(suffix=u'.cdrdao.read-toc.whipper')
os.close(fd)
os.unlink(tocfile)
description = "Reading TOC"
toc = None
cmd = [CDRDAO, 'read-toc'] + (['--fast-toc'] if fast_toc else []) + [
'--device', device, tocfile]
# PIPE is the closest to >/dev/null we can get
logger.debug("executing %r", cmd)
p = Popen(cmd, stdout=PIPE, stderr=PIPE)
_, stderr = p.communicate()
if p.returncode != 0:
msg = 'cdrdao read-toc failed: return code is non-zero: ' + \
str(p.returncode)
logger.critical(msg)
# Gracefully handle missing disc
if "ERROR: Unit not ready, giving up." in stderr:
raise EjectError(device, "no disc detected")
raise IOError(msg)
def __init__(self, device, fast_toc=False, toc_path=None):
"""
Read the TOC for 'device'.
toc = TocFile(tocfile)
toc.parse()
if toc_path is not None:
t_comp = os.path.abspath(toc_path).split(os.sep)
t_dirn = os.sep.join(t_comp[:-1])
# If the output path doesn't exist, make it recursively
if not os.path.isdir(t_dirn):
os.makedirs(t_dirn)
t_dst = truncate_filename(os.path.join(t_dirn, t_comp[-1] + '.toc'))
shutil.copy(tocfile, os.path.join(t_dirn, t_dst))
os.unlink(tocfile)
return toc
:param device: block device to read TOC from
:type device: str
:param fast_toc: If to use fast-toc cdrdao mode
:type fast_toc: bool
:param toc_path: Where to save TOC if wanted.
:type toc_path: str
"""
self.device = device
self.fast_toc = fast_toc
self.toc_path = toc_path
self._buffer = "" # accumulate characters
self._parser = ProgressParser()
self.fd, self.tocfile = tempfile.mkstemp(
suffix=u'.cdrdao.read-toc.whipper.task')
def start(self, runner):
task.Task.start(self, runner)
os.close(self.fd)
os.unlink(self.tocfile)
cmd = ([CDRDAO, 'read-toc']
+ (['--fast-toc'] if self.fast_toc else [])
+ ['--device', self.device, self.tocfile])
self._popen = asyncsub.Popen(cmd,
bufsize=1024,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
close_fds=True)
self.schedule(0.01, self._read, runner)
def _read(self, runner):
ret = self._popen.recv_err()
if not ret:
if self._popen.poll() is not None:
self._done()
return
self.schedule(0.01, self._read, runner)
return
self._buffer += ret
# parse buffer into lines if possible, and parse them
if "\n" in self._buffer:
lines = self._buffer.split('\n')
if lines[-1] != "\n":
# last line didn't end yet
self._buffer = lines[-1]
del lines[-1]
else:
self._buffer = ""
for line in lines:
self._parser.parse(line)
if (self._parser.currentTrack != 0 and
self._parser.tracks != 0):
progress = (float('%d' % self._parser.currentTrack) /
float(self._parser.tracks))
if progress < 1.0:
self.setProgress(progress)
# 0 does not give us output before we complete, 1.0 gives us output
# too late
self.schedule(0.01, self._read, runner)
def _poll(self, runner):
if self._popen.poll() is None:
self.schedule(1.0, self._poll, runner)
return
self._done()
def _done(self):
self.setProgress(1.0)
self.toc = TocFile(self.tocfile)
self.toc.parse()
if self.toc_path is not None:
t_comp = os.path.abspath(self.toc_path).split(os.sep)
t_dirn = os.sep.join(t_comp[:-1])
# If the output path doesn't exist, make it recursively
if not os.path.isdir(t_dirn):
os.makedirs(t_dirn)
t_dst = truncate_filename(
os.path.join(t_dirn, t_comp[-1] + '.toc'))
shutil.copy(self.tocfile, os.path.join(t_dirn, t_dst))
os.unlink(self.tocfile)
self.stop()
return
def DetectCdr(device):
@@ -63,10 +168,7 @@ def DetectCdr(device):
cmd = [CDRDAO, 'disk-info', '-v1', '--device', device]
logger.debug("executing %r", cmd)
p = Popen(cmd, stdout=PIPE, stderr=PIPE)
if 'CD-R medium : n/a' in p.stdout.read():
return False
else:
return True
return 'CD-R medium : n/a' not in p.stdout.read()
def version():
@@ -74,7 +176,7 @@ def version():
Return cdrdao version as a string.
"""
cdrdao = Popen(CDRDAO, stderr=PIPE)
out, err = cdrdao.communicate()
_, err = cdrdao.communicate()
if cdrdao.returncode != 1:
logger.warning("cdrdao version detection failed: "
"return code is %s", cdrdao.returncode)
@@ -86,24 +188,3 @@ def version():
"could not find version")
return None
return m.group('version')
def ReadTOCTask(device):
"""
stopgap morituri-insanity compatibility layer
"""
return read_toc(device, fast_toc=True)
def ReadTableTask(device, toc_path=None):
"""
stopgap morituri-insanity compatibility layer
"""
return read_toc(device, toc_path=toc_path)
def getCDRDAOVersion():
"""
stopgap morituri-insanity compatibility layer
"""
return version()

View File

@@ -18,7 +18,7 @@ def peak_level(track_path):
logger.warning("SoX peak detection failed: file not found")
return None
sox = Popen([SOX, track_path, "-n", "stats", "-b", "16"], stderr=PIPE)
out, err = sox.communicate()
_, err = sox.communicate()
if sox.returncode:
logger.warning("SoX peak detection failed: %s", sox.returncode)
return None

View File

@@ -13,7 +13,7 @@ class AudioLengthTask(ctask.PopenTask):
"""
I calculate the length of a track in audio samples.
@ivar length: length of the decoded audio file, in audio samples.
:cvar length: length of the decoded audio file, in audio samples.
"""
logCategory = 'AudioLengthTask'
description = 'Getting length of audio track'
@@ -21,7 +21,7 @@ class AudioLengthTask(ctask.PopenTask):
def __init__(self, path):
"""
@type path: unicode
:type path: unicode
"""
assert isinstance(path, unicode), "%r is not unicode" % path
@@ -35,11 +35,11 @@ class AudioLengthTask(ctask.PopenTask):
def commandMissing(self):
raise common.MissingDependencyException('soxi')
def readbytesout(self, bytes):
self._output.append(bytes)
def readbytesout(self, bytes_stdout):
self._output.append(bytes_stdout)
def readbyteserr(self, bytes):
self._error.append(bytes)
def readbyteserr(self, bytes_stderr):
self._error.append(bytes_stderr)
def failed(self):
self.setException(Exception("soxi failed: %s" % "".join(self._error)))

View File

@@ -1,4 +1,5 @@
import os
import subprocess
import logging
logger = logging.getLogger(__name__)
@@ -9,7 +10,12 @@ def eject_device(device):
Eject the given device.
"""
logger.debug("ejecting device %s", device)
os.system('eject %s' % device)
try:
# `eject device` prints nothing to stdout
subprocess.check_output(['eject', device], stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
logger.warning("command '%s' returned with exit code '%d' (%s)",
' '.join(e.cmd), e.returncode, e.output.rstrip())
def load_device(device):
@@ -17,7 +23,13 @@ def load_device(device):
Load the given device.
"""
logger.debug("loading (eject -t) device %s", device)
os.system('eject -t %s' % device)
try:
# `eject -t device` prints nothing to stdout
subprocess.check_output(['eject', '-t', device],
stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
logger.warning("command '%s' returned with exit code '%d' (%s)",
' '.join(e.cmd), e.returncode, e.output.rstrip())
def unmount_device(device):
@@ -28,7 +40,7 @@ def unmount_device(device):
If the given device is a symlink, the target will be checked.
"""
device = os.path.realpath(device)
logger.debug('possibly unmount real path %r' % device)
logger.debug('possibly unmount real path %r', device)
proc = open('/proc/mounts').read()
if device in proc:
print('Device %s is mounted, unmounting' % device)

View File

@@ -1,5 +1,8 @@
import time
import hashlib
import re
import ruamel.yaml as yaml
from ruamel.yaml.comments import CommentedMap as OrderedDict
import whipper
@@ -16,65 +19,57 @@ class WhipperLogger(result.Logger):
def log(self, ripResult, epoch=time.time()):
"""Returns big str: logfile joined text lines"""
lines = self.logRip(ripResult, epoch=epoch)
return "\n".join(lines)
return self.logRip(ripResult, epoch)
def logRip(self, ripResult, epoch):
"""Returns logfile lines list"""
lines = []
riplog = OrderedDict()
# Ripper version
lines.append("Log created by: whipper %s (internal logger)" %
whipper.__version__)
riplog["Log created by"] = "whipper %s (internal logger)" % (
whipper.__version__)
# Rip date
date = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(epoch)).strip()
lines.append("Log creation date: %s" % date)
lines.append("")
riplog["Log creation date"] = date
# Rip technical settings
lines.append("Ripping phase information:")
lines.append(" Drive: %s%s (revision %s)" % (
ripResult.vendor, ripResult.model, ripResult.release))
lines.append(" Extraction engine: cdparanoia %s" %
ripResult.cdparanoiaVersion)
if ripResult.cdparanoiaDefeatsCache is None:
defeat = "Unknown"
elif ripResult.cdparanoiaDefeatsCache:
defeat = "Yes"
else:
defeat = "No"
lines.append(" Defeat audio cache: %s" % defeat)
lines.append(" Read offset correction: %+d" % ripResult.offset)
data = OrderedDict()
data["Drive"] = "%s%s (revision %s)" % (
ripResult.vendor, ripResult.model, ripResult.release)
data["Extraction engine"] = "cdparanoia %s" % (
ripResult.cdparanoiaVersion)
data["Defeat audio cache"] = ripResult.cdparanoiaDefeatsCache
data["Read offset correction"] = ripResult.offset
# Currently unsupported by the official cdparanoia package
over = "No"
# Only implemented in whipper (ripResult.overread)
if ripResult.overread:
over = "Yes"
lines.append(" Overread into lead-out: %s" % over)
data["Overread into lead-out"] = True if ripResult.overread else False
# Next one fully works only using the patched cdparanoia package
# lines.append("Fill up missing offset samples with silence: Yes")
lines.append(" Gap detection: cdrdao %s" % ripResult.cdrdaoVersion)
if ripResult.isCdr:
isCdr = "Yes"
else:
isCdr = "No"
lines.append(" CD-R detected: %s" % isCdr)
lines.append("")
# lines.append("Fill up missing offset samples with silence: true")
data["Gap detection"] = "cdrdao %s" % ripResult.cdrdaoVersion
data["CD-R detected"] = ripResult.isCdr
riplog["Ripping phase information"] = data
# CD metadata
lines.append("CD metadata:")
lines.append(" Album: %s - %s" % (ripResult.artist, ripResult.title))
lines.append(" CDDB Disc ID: %s" % ripResult. table.getCDDBDiscId())
lines.append(" MusicBrainz Disc ID: %s" %
ripResult. table.getMusicBrainzDiscId())
lines.append(" MusicBrainz lookup url: %s" %
ripResult. table.getMusicBrainzSubmitURL())
lines.append("")
release = OrderedDict()
release["Artist"] = ripResult.artist
release["Title"] = ripResult.title
data = OrderedDict()
data["Release"] = release
data["CDDB Disc ID"] = ripResult.table.getCDDBDiscId()
data["MusicBrainz Disc ID"] = ripResult.table.getMusicBrainzDiscId()
data["MusicBrainz lookup URL"] = (
ripResult.table.getMusicBrainzSubmitURL())
if ripResult.metadata:
data["MusicBrainz Release URL"] = ripResult.metadata.url
riplog["CD metadata"] = data
# TOC section
lines.append("TOC:")
data = OrderedDict()
table = ripResult.table
# Test for HTOA presence
@@ -89,154 +84,171 @@ class WhipperLogger(result.Logger):
htoastart = htoa.absolute
htoaend = table.getTrackEnd(0)
htoalength = table.tracks[0].getIndex(1).absolute - htoastart
lines.append(" 0:")
lines.append(" Start: %s" % common.framesToMSF(htoastart))
lines.append(" Length: %s" % common.framesToMSF(htoalength))
lines.append(" Start sector: %d" % htoastart)
lines.append(" End sector: %d" % htoaend)
lines.append("")
track = OrderedDict()
track["Start"] = common.framesToMSF(htoastart)
track["Length"] = common.framesToMSF(htoalength)
track["Start sector"] = htoastart
track["End sector"] = htoaend
data[0] = track
# For every track include information in the TOC
for t in table.tracks:
# FIXME: what happens to a track start over 60 minutes ?
# Answer: tested empirically, everything seems OK
start = t.getIndex(1).absolute
length = table.getTrackLength(t.number)
end = table.getTrackEnd(t.number)
lines.append(" %d:" % t.number)
lines.append(" Start: %s" % common.framesToMSF(start))
lines.append(" Length: %s" % common.framesToMSF(length))
lines.append(" Start sector: %d" % start)
lines.append(" End sector: %d" % end)
lines.append("")
track = OrderedDict()
track["Start"] = common.framesToMSF(start)
track["Length"] = common.framesToMSF(length)
track["Start sector"] = start
track["End sector"] = end
data[t.number] = track
riplog["TOC"] = data
# Tracks section
lines.append("Tracks:")
data = OrderedDict()
duration = 0.0
for t in ripResult.tracks:
if not t.filename:
continue
track_lines, ARDB_entry, ARDB_match = self.trackLog(t)
track_dict, ARDB_entry, ARDB_match = self.trackLog(t)
self._inARDatabase += int(ARDB_entry)
self._accuratelyRipped += int(ARDB_match)
lines.extend(track_lines)
lines.append("")
data[t.number] = track_dict
duration += t.testduration + t.copyduration
riplog["Tracks"] = data
# Status report
lines.append("Conclusive status report:")
arHeading = " AccurateRip summary:"
data = OrderedDict()
if self._inARDatabase == 0:
lines.append("%s None of the tracks are present in the "
"AccurateRip database" % arHeading)
message = ("None of the tracks are present in the "
"AccurateRip database")
else:
nonHTOA = len(ripResult.tracks)
if ripResult.tracks[0].number == 0:
nonHTOA -= 1
if self._accuratelyRipped == 0:
lines.append("%s No tracks could be verified as accurate "
"(you may have a different pressing from the "
"one(s) in the database)" % arHeading)
message = ("No tracks could be verified as accurate "
"(you may have a different pressing from the "
"one(s) in the database)")
elif self._accuratelyRipped < nonHTOA:
accurateTracks = nonHTOA - self._accuratelyRipped
lines.append("%s Some tracks could not be verified as "
"accurate (%d/%d got no match)" % (
arHeading, accurateTracks, nonHTOA))
message = ("Some tracks could not be verified as "
"accurate (%d/%d got no match)") % (
accurateTracks, nonHTOA)
else:
lines.append("%s All tracks accurately ripped" % arHeading)
message = "All tracks accurately ripped"
data["AccurateRip summary"] = message
hsHeading = " Health status:"
if self._errors:
lines.append("%s There were errors" % hsHeading)
message = "There were errors"
else:
lines.append("%s No errors occurred" % hsHeading)
lines.append(" EOF: End of status report")
lines.append("")
message = "No errors occurred"
data["Health Status"] = message
data["EOF"] = "End of status report"
riplog["Conclusive status report"] = data
riplog = yaml.dump(
riplog,
default_flow_style=False,
width=4000,
Dumper=yaml.RoundTripDumper
)
# Add a newline after the "Log creation date" line
riplog = re.sub(
r'^(Log creation date: .*)$',
"\\1\n",
riplog,
flags=re.MULTILINE
)
# Add a newline after a dictionary ends and returns to top-level
riplog = re.sub(
r"^(\s{2})([^\n]*)\n([A-Z][^\n]+)",
"\\1\\2\n\n\\3",
riplog,
flags=re.MULTILINE
)
# Add a newline after a track closes
riplog = re.sub(
r"^(\s{4}[^\n]*)\n(\s{2}[0-9]+)",
"\\1\n\n\\2",
riplog,
flags=re.MULTILINE
)
# Remove single quotes around the "Log creation date" value
riplog = re.sub(
r"^(Log creation date: )'(.*)'",
"\\1\\2",
riplog,
flags=re.MULTILINE
)
# Log hash
hasher = hashlib.sha256()
hasher.update("\n".join(lines).encode("utf-8"))
lines.append("SHA-256 hash: %s" % hasher.hexdigest().upper())
lines.append("")
return lines
hasher.update(riplog.encode("utf-8"))
riplog += "\nSHA-256 hash: %s\n" % hasher.hexdigest().upper()
return riplog
def trackLog(self, trackResult):
"""Returns Tracks section lines: data picked from trackResult"""
lines = []
# Track number
lines.append(" %d:" % trackResult.number)
track = OrderedDict()
# Filename (including path) of ripped track
lines.append(" Filename: %s" % trackResult.filename)
track["Filename"] = trackResult.filename
# Pre-gap length
pregap = trackResult.pregap
if pregap:
lines.append(" Pre-gap length: %s" % common.framesToMSF(pregap))
track["Pre-gap length"] = common.framesToMSF(pregap)
# Peak level
peak = trackResult.peak / 32768.0
lines.append(" Peak level: %.6f" % peak)
track["Peak level"] = float("%.6f" % peak)
# Pre-emphasis status
# Only implemented in whipper (trackResult.pre_emphasis)
if trackResult.pre_emphasis:
preEmph = "Yes"
else:
preEmph = "No"
lines.append(" Pre-emphasis: %s" % preEmph)
track["Pre-emphasis"] = trackResult.pre_emphasis
# Extraction speed
if trackResult.copyspeed:
lines.append(" Extraction speed: %.1f X" % (
trackResult.copyspeed))
track["Extraction speed"] = "%.1f X" % trackResult.copyspeed
# Extraction quality
if trackResult.quality and trackResult.quality > 0.001:
lines.append(" Extraction quality: %.2f %%" %
(trackResult.quality * 100.0, ))
track["Extraction quality"] = "%.2f %%" % (
trackResult.quality * 100.0, )
# Ripper Test CRC
if trackResult.testcrc is not None:
lines.append(" Test CRC: %08X" % trackResult.testcrc)
track["Test CRC"] = "%08X" % trackResult.testcrc
# Ripper Copy CRC
if trackResult.copycrc is not None:
lines.append(" Copy CRC: %08X" % trackResult.copycrc)
track["Copy CRC"] = "%08X" % trackResult.copycrc
# AccurateRip track status
ARDB_entry = 0
ARDB_match = 0
for v in ("v1", "v2"):
data = OrderedDict()
if trackResult.AR[v]["DBCRC"]:
lines.append(" AccurateRip %s:" % v)
ARDB_entry += 1
if trackResult.AR[v]["CRC"] == trackResult.AR[v]["DBCRC"]:
lines.append(" Result: Found, exact match")
data["Result"] = "Found, exact match"
ARDB_match += 1
else:
lines.append(" Result: Found, NO exact match")
lines.append(
" Confidence: %d" % trackResult.AR[v]["DBConfidence"]
)
lines.append(
" Local CRC: %s" % trackResult.AR[v]["CRC"].upper()
)
lines.append(
" Remote CRC: %s" % trackResult.AR[v]["DBCRC"].upper()
)
data["Result"] = "Found, NO exact match"
data["Confidence"] = trackResult.AR[v]["DBConfidence"]
data["Local CRC"] = trackResult.AR[v]["CRC"].upper()
data["Remote CRC"] = trackResult.AR[v]["DBCRC"].upper()
elif trackResult.number != 0:
lines.append(" AccurateRip %s:" % v)
lines.append(
" Result: Track not present in AccurateRip database"
)
data["Result"] = "Track not present in AccurateRip database"
track["AccurateRip %s" % v] = data
# Check if Test & Copy CRCs are equal
if trackResult.testcrc == trackResult.copycrc:
lines.append(" Status: Copy OK")
track["Status"] = "Copy OK"
else:
self._errors = True
lines.append(" Status: Error, CRC mismatch")
return lines, bool(ARDB_entry), bool(ARDB_match)
track["Status"] = "Error, CRC mismatch"
return track, bool(ARDB_entry), bool(ARDB_match)

View File

@@ -69,16 +69,18 @@ class RipResult:
I hold information about the result for rips.
I can be used to write log files.
@ivar offset: sample read offset
@ivar table: the full index table
@type table: L{whipper.image.table.Table}
:cvar offset: sample read offset
:cvar table: the full index table
:vartype table: whipper.image.table.Table
:cvar metadata: disc metadata from MusicBrainz (if available)
:vartype metadata: whipper.common.mbngs.DiscMetadata
@ivar vendor: vendor of the CD drive
@ivar model: model of the CD drive
@ivar release: release of the CD drive
:cvar vendor: vendor of the CD drive
:cvar model: model of the CD drive
:cvar release: release of the CD drive
@ivar cdrdaoVersion: version of cdrdao used for the rip
@ivar cdparanoiaVersion: version of cdparanoia used for the rip
:cvar cdrdaoVersion: version of cdrdao used for the rip
:cvar cdparanoiaVersion: version of cdparanoia used for the rip
"""
offset = 0
@@ -88,6 +90,7 @@ class RipResult:
table = None
artist = None
title = None
metadata = None
vendor = None
model = None
@@ -104,10 +107,10 @@ class RipResult:
def getTrackResult(self, number):
"""
@param number: the track number (0 for HTOA)
:param number: the track number (0 for HTOA)
@type number: int
@rtype: L{TrackResult}
:type number: int
:rtype: TrackResult
"""
for t in self.tracks:
if t.number == number:
@@ -125,11 +128,11 @@ class Logger(object):
"""
Create a log from the given ripresult.
@param epoch: when the log file gets generated
@type epoch: float
@type ripResult: L{RipResult}
:param epoch: when the log file gets generated
:type epoch: float
:type ripResult: RipResult
@rtype: str
:rtype: str
"""
raise NotImplementedError
@@ -140,7 +143,8 @@ class Logger(object):
class EntryPoint(object):
name = 'whipper'
def load(self):
@staticmethod
def load():
from whipper.result import logger
return logger.WhipperLogger
@@ -149,7 +153,7 @@ def getLoggers():
"""
Get all logger plugins with entry point 'whipper.logger'.
@rtype: dict of C{str} -> C{Logger}
:rtype: dict of :class:`str` -> :any:`Logger`
"""
d = {}

View File

@@ -46,6 +46,7 @@ class TestCase(unittest.TestCase):
# and we'd like to check for the actual exception under TaskException,
# so override the way twisted.trial.unittest does, without failure
# XXX: Pylint, method could be a function (no-self-use)
def failUnlessRaises(self, exception, f, *args, **kwargs):
try:
result = f(*args, **kwargs)
@@ -53,7 +54,7 @@ class TestCase(unittest.TestCase):
return inst
except exception as e:
raise Exception('%s raised instead of %s:\n %s' %
(sys.exec_info()[0], exception.__name__, str(e))
(sys.exc_info()[0], exception.__name__, str(e))
)
else:
raise Exception('%s not raised (%r returned)' %
@@ -62,7 +63,8 @@ class TestCase(unittest.TestCase):
assertRaises = failUnlessRaises
def readCue(self, name):
@staticmethod
def readCue(name):
"""
Read a .cue file, and replace the version comment with the current
version so we can use it in comparisons.
@@ -71,7 +73,7 @@ class TestCase(unittest.TestCase):
ret = open(cuefile).read().decode('utf-8')
ret = re.sub(
'REM COMMENT "whipper.*',
'REM COMMENT "whipper %s"' % (whipper.__version__),
'REM COMMENT "whipper %s"' % whipper.__version__,
ret, re.MULTILINE)
return ret

View File

@@ -0,0 +1,91 @@
CD_DA
CATALOG "0075596150125"
// Track 1
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
FILE "data.wav" 0 03:05:62
// Track 2
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
FILE "data.wav" 03:05:62 03:53:53
// Track 3
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
FILE "data.wav" 06:59:40 03:36:70
// Track 4
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
FILE "data.wav" 10:36:35 04:14:42
// Track 5
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
FILE "data.wav" 14:51:02 05:48:05
// Track 6
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
FILE "data.wav" 20:39:07 04:21:23
// Track 7
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
FILE "data.wav" 25:00:30 03:30:50
// Track 8
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
FILE "data.wav" 28:31:05 05:46:00
// Track 9
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
FILE "data.wav" 34:17:05 04:10:22
// Track 10
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
FILE "data.wav" 38:27:27 04:51:65
// Track 11
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
FILE "data.wav" 43:19:17 05:40:03

View File

@@ -0,0 +1,30 @@
# vi:si:et:sw=4:sts=4:ts=4:set fileencoding=utf-8
u"""Tests for whipper.command.mblookup"""
import os
import pickle
import unittest
from whipper.command import mblookup
class MBLookupTestCase(unittest.TestCase):
u"""Test cases for whipper.command.mblookup.MBLookup"""
@staticmethod
def _mock_musicbrainz(discid, country=None, record=False):
u"""Mock function for whipper.common.mbngs.musicbrainz function."""
filename = u"whipper.discid.{}.pickle".format(discid)
path = os.path.join(os.path.dirname(__file__), filename)
with open(path) as p:
return pickle.load(p)
def testMissingReleaseType(self):
u"""Test that lookup for release without a type set doesn't fail."""
# Using: Gustafsson, Österberg & Cowle - What's Up? 8 (disc 4)
# https://musicbrainz.org/release/d8e6153a-2c47-4804-9d73-0aac1081c3b1
mblookup.musicbrainz = self._mock_musicbrainz
discid = u"xu338_M8WukSRi0J.KTlDoflB8Y-"
# https://musicbrainz.org/cdtoc/xu338_M8WukSRi0J.KTlDoflB8Y-
lookup = mblookup.MBLookup([discid], u'whipper mblookup', None)
lookup.do()

View File

@@ -78,8 +78,8 @@ class TestAccurateRipResponse(TestCase):
self.assertEqual(responses[1].discId1, '0000f21c')
self.assertEqual(responses[1].discId2, '00027ef8')
self.assertEqual(responses[1].cddbDiscId, '05021002')
self.assertEqual(responses[1].confidences[0], 5)
self.assertEqual(responses[1].confidences[1], 5)
self.assertEqual(responses[1].confidences[0], 6)
self.assertEqual(responses[1].confidences[1], 6)
self.assertEqual(responses[1].checksums[0], 'dc77f9ab')
self.assertEqual(responses[1].checksums[1], 'dd97d2c3')
@@ -203,7 +203,7 @@ class TestVerifyResult(TestCase):
'v2': {
'CRC': 'dc77f9ab',
'DBCRC': 'dc77f9ab',
'DBConfidence': 5,
'DBConfidence': 6
},
'DBMaxConfidence': 12,
'DBMaxConfidenceCRC': '284fc705',
@@ -217,7 +217,7 @@ class TestVerifyResult(TestCase):
'v2': {
'CRC': 'dd97d2c3',
'DBCRC': 'dd97d2c3',
'DBConfidence': 5,
'DBConfidence': 6,
},
'DBMaxConfidence': 20,
'DBMaxConfidenceCRC': '9cc1f32e',

View File

@@ -1,5 +1,5 @@
# -*- Mode: Python; test-case-name: whipper.test.test_common_mbngs -*-
# vi:si:et:sw=4:sts=4:ts=4
# vi:si:et:sw=4:sts=4:ts=4:set fileencoding=utf-8
import os
import json
@@ -12,15 +12,17 @@ from whipper.common import mbngs
class MetadataTestCase(unittest.TestCase):
# Generated with rip -R cd info
def testJeffEverybodySingle(self):
filename = 'whipper.release.3451f29c-9bb8-4cc5-bfcc-bd50104b94f8.json'
def testMissingReleaseDate(self):
# Using: The KLF - Space & Chill Out
# https://musicbrainz.org/release/c56ff16e-1d81-47de-926f-ba22891bd2bd
filename = 'whipper.release.c56ff16e-1d81-47de-926f-ba22891bd2bd.json'
path = os.path.join(os.path.dirname(__file__), filename)
handle = open(path, "rb")
response = json.loads(handle.read())
handle.close()
discid = "wbjbST2jUHRZaB1inCyxxsL7Eqc-"
discid = "b.yqPuCBdsV5hrzDvYrw52iK_jE-"
metadata = mbngs._getMetadata({}, response['release'], discid)
metadata = mbngs._getMetadata(response['release'], discid)
self.assertFalse(metadata.release)
@@ -33,21 +35,22 @@ class MetadataTestCase(unittest.TestCase):
handle.close()
discid = "f7XO36a7n1LCCskkCiulReWbwZA-"
metadata = mbngs._getMetadata({}, response['release'], discid)
metadata = mbngs._getMetadata(response['release'], discid)
self.assertEqual(metadata.artist, u'Various Artists')
self.assertEqual(metadata.release, u'2001-10-15')
self.assertEqual(metadata.mbidArtist,
u'89ad4ac3-39f7-470e-963a-56509c546377')
[u'89ad4ac3-39f7-470e-963a-56509c546377'])
self.assertEqual(len(metadata.tracks), 18)
track16 = metadata.tracks[15]
self.assertEqual(track16.artist, 'Tom Jones & Stereophonics')
self.assertEqual(track16.mbidArtist,
u'57c6f649-6cde-48a7-8114-2a200247601a'
';0bfba3d3-6a04-4779-bb0a-df07df5b0558')
self.assertEqual(track16.mbidArtist, [
u'57c6f649-6cde-48a7-8114-2a200247601a',
u'0bfba3d3-6a04-4779-bb0a-df07df5b0558',
])
self.assertEqual(track16.sortName,
u'Jones, Tom & Stereophonics')
@@ -60,15 +63,16 @@ class MetadataTestCase(unittest.TestCase):
handle.close()
discid = "xAq8L4ELMW14.6wI6tt7QAcxiDI-"
metadata = mbngs._getMetadata({}, response['release'], discid)
metadata = mbngs._getMetadata(response['release'], discid)
self.assertEqual(metadata.artist, u'Isobel Campbell & Mark Lanegan')
self.assertEqual(metadata.sortName,
u'Campbell, Isobel & Lanegan, Mark')
self.assertEqual(metadata.release, u'2006-01-30')
self.assertEqual(metadata.mbidArtist,
u'd51f3a15-12a2-41a0-acfa-33b5eae71164;'
'a9126556-f555-4920-9617-6e013f8228a7')
self.assertEqual(metadata.mbidArtist, [
u'd51f3a15-12a2-41a0-acfa-33b5eae71164',
u'a9126556-f555-4920-9617-6e013f8228a7',
])
self.assertEqual(len(metadata.tracks), 12)
@@ -78,9 +82,10 @@ class MetadataTestCase(unittest.TestCase):
self.assertEqual(track12.sortName,
u'Campbell, Isobel'
' & Lanegan, Mark')
self.assertEqual(track12.mbidArtist,
u'd51f3a15-12a2-41a0-acfa-33b5eae71164;'
'a9126556-f555-4920-9617-6e013f8228a7')
self.assertEqual(track12.mbidArtist, [
u'd51f3a15-12a2-41a0-acfa-33b5eae71164',
u'a9126556-f555-4920-9617-6e013f8228a7',
])
def testMalaInCuba(self):
# single artist disc, but with multiple artists tracks
@@ -92,13 +97,13 @@ class MetadataTestCase(unittest.TestCase):
handle.close()
discid = "u0aKVpO.59JBy6eQRX2vYcoqQZ0-"
metadata = mbngs._getMetadata({}, response['release'], discid)
metadata = mbngs._getMetadata(response['release'], discid)
self.assertEqual(metadata.artist, u'Mala')
self.assertEqual(metadata.sortName, u'Mala')
self.assertEqual(metadata.release, u'2012-09-17')
self.assertEqual(metadata.mbidArtist,
u'09f221eb-c97e-4da5-ac22-d7ab7c555bbb')
[u'09f221eb-c97e-4da5-ac22-d7ab7c555bbb'])
self.assertEqual(len(metadata.tracks), 14)
@@ -107,49 +112,52 @@ class MetadataTestCase(unittest.TestCase):
self.assertEqual(track6.artist, u'Mala feat. Dreiser & Sexto Sentido')
self.assertEqual(track6.sortName,
u'Mala feat. Dreiser & Sexto Sentido')
self.assertEqual(track6.mbidArtist,
u'09f221eb-c97e-4da5-ac22-d7ab7c555bbb'
';ec07a209-55ff-4084-bc41-9d4d1764e075'
';f626b92e-07b1-4a19-ad13-c09d690db66c')
self.assertEqual(track6.mbidArtist, [
u'09f221eb-c97e-4da5-ac22-d7ab7c555bbb',
u'ec07a209-55ff-4084-bc41-9d4d1764e075',
u'f626b92e-07b1-4a19-ad13-c09d690db66c',
])
def testNorthernGateway(self):
def testUnknownArtist(self):
"""
check the received metadata for artists tagged with [unknown]
and artists tagged with an alias in MusicBrainz
see https://github.com/whipper-team/whipper/issues/155
"""
filename = 'whipper.release.38b05c7d-65fe-4dc0-9c10-33a391b86703.json'
# Using: CunninLynguists - Sloppy Seconds, Volume 1
# https://musicbrainz.org/release/8478d4da-0cda-4e46-ae8c-1eeacfa5cf37
filename = 'whipper.release.8478d4da-0cda-4e46-ae8c-1eeacfa5cf37.json'
path = os.path.join(os.path.dirname(__file__), filename)
handle = open(path, "rb")
response = json.loads(handle.read())
handle.close()
discid = "rzGHHqfPWIq1GsOLhhlBcZuqo.I-"
discid = "RhrwgVb0hZNkabQCw1dZIhdbMFg-"
metadata = mbngs._getMetadata({}, response['release'], discid)
self.assertEqual(metadata.artist, u'Various Artists')
self.assertEqual(metadata.release, u'2010')
metadata = mbngs._getMetadata(response['release'], discid)
self.assertEqual(metadata.artist, u'CunninLynguists')
self.assertEqual(metadata.release, u'2003')
self.assertEqual(metadata.mbidArtist,
u'89ad4ac3-39f7-470e-963a-56509c546377')
[u'69c4cc43-8163-41c5-ac81-30946d27bb69'])
self.assertEqual(len(metadata.tracks), 10)
self.assertEqual(len(metadata.tracks), 30)
track2 = metadata.tracks[1]
track8 = metadata.tracks[7]
self.assertEqual(track2.artist, u'Twisted Reaction feat. Danielle')
self.assertEqual(track2.sortName,
u'Twisted Reaction feat. [unknown]')
self.assertEqual(track2.mbidArtist,
u'4f69f624-73ea-4a16-b822-bd2ca58032bf'
';125ec42a-7229-4250-afc5-e057484327fe')
self.assertEqual(track8.artist, u'???')
self.assertEqual(track8.sortName, u'[unknown]')
self.assertEqual(track8.mbidArtist,
[u'125ec42a-7229-4250-afc5-e057484327fe'])
track4 = metadata.tracks[3]
track9 = metadata.tracks[8]
self.assertEqual(track4.artist, u'BioGenesis')
self.assertEqual(track4.sortName,
u'Bio Genesis')
self.assertEqual(track4.mbidArtist,
u'dd61b86c-c015-43e1-9a28-58fceb0975c8')
self.assertEqual(track9.artist, u'CunninLynguists feat. Tonedeff')
self.assertEqual(track9.sortName,
u'CunninLynguists feat. Tonedeff')
self.assertEqual(track9.mbidArtist, [
u'69c4cc43-8163-41c5-ac81-30946d27bb69',
u'b3869d83-9fb5-4eac-b5ca-2d155fcbee12'
])
def testNenaAndKimWildSingle(self):
"""
@@ -163,12 +171,13 @@ class MetadataTestCase(unittest.TestCase):
handle.close()
discid = "X2c2IQ5vUy5x6Jh7Xi_DGHtA1X8-"
metadata = mbngs._getMetadata({}, response['release'], discid)
metadata = mbngs._getMetadata(response['release'], discid)
self.assertEqual(metadata.artist, u'Nena & Kim Wilde')
self.assertEqual(metadata.release, u'2003-05-19')
self.assertEqual(metadata.mbidArtist,
u'38bfaa7f-ee98-48cb-acd0-946d7aeecd76'
';4b462375-c508-432a-8c88-ceeec38b16ae')
self.assertEqual(metadata.mbidArtist, [
u'38bfaa7f-ee98-48cb-acd0-946d7aeecd76',
u'4b462375-c508-432a-8c88-ceeec38b16ae',
])
self.assertEqual(len(metadata.tracks), 4)
@@ -176,14 +185,85 @@ class MetadataTestCase(unittest.TestCase):
self.assertEqual(track1.artist, u'Nena & Kim Wilde')
self.assertEqual(track1.sortName, u'Nena & Wilde, Kim')
self.assertEqual(track1.mbidArtist,
u'38bfaa7f-ee98-48cb-acd0-946d7aeecd76'
';4b462375-c508-432a-8c88-ceeec38b16ae')
self.assertEqual(track1.mbidArtist, [
u'38bfaa7f-ee98-48cb-acd0-946d7aeecd76',
u'4b462375-c508-432a-8c88-ceeec38b16ae',
])
self.assertEqual(track1.mbid,
u'1cc96e78-28ed-3820-b0b6-614c35b121ac')
self.assertEqual(track1.mbidRecording,
u'fde5622c-ce23-4ebb-975d-51d4a926f901')
track2 = metadata.tracks[1]
self.assertEqual(track2.artist, u'Nena & Kim Wilde')
self.assertEqual(track2.sortName, u'Nena & Wilde, Kim')
self.assertEqual(track2.mbidArtist,
u'38bfaa7f-ee98-48cb-acd0-946d7aeecd76'
';4b462375-c508-432a-8c88-ceeec38b16ae')
self.assertEqual(track2.mbidArtist, [
u'38bfaa7f-ee98-48cb-acd0-946d7aeecd76',
u'4b462375-c508-432a-8c88-ceeec38b16ae',
])
self.assertEqual(track2.mbid,
u'f16db4bf-9a34-3d5a-a975-c9375ab7a2ca')
self.assertEqual(track2.mbidRecording,
u'5f19758e-7421-4c71-a599-9a9575d8e1b0')
def testMissingReleaseGroupType(self):
"""Check that whipper doesn't break if there's no type."""
# Using: Gustafsson, Österberg & Cowle - What's Up? 8 (disc 4)
# https://musicbrainz.org/release/d8e6153a-2c47-4804-9d73-0aac1081c3b1
filename = 'whipper.release.d8e6153a-2c47-4804-9d73-0aac1081c3b1.json'
path = os.path.join(os.path.dirname(__file__), filename)
handle = open(path, "rb")
response = json.loads(handle.read())
handle.close()
discid = "xu338_M8WukSRi0J.KTlDoflB8Y-" # disc 4
metadata = mbngs._getMetadata(response['release'], discid)
self.assertEqual(metadata.releaseType, None)
def testAllAvailableMetadata(self):
"""Check that all possible metadata gets assigned."""
# Using: David Rovics - The Other Side
# https://musicbrainz.org/release/6109ceed-7e21-490b-b5ad-3a66b4e4cfbb
filename = 'whipper.release.6109ceed-7e21-490b-b5ad-3a66b4e4cfbb.json'
path = os.path.join(os.path.dirname(__file__), filename)
handle = open(path, "rb")
response = json.loads(handle.read())
handle.close()
discid = "cHW1Uutl_kyWNaLJsLmTGTe4rnE-"
metadata = mbngs._getMetadata(response['release'], discid)
self.assertEqual(metadata.artist, u'David Rovics')
self.assertEqual(metadata.sortName, u'Rovics, David')
self.assertFalse(metadata.various)
self.assertIsInstance(metadata.tracks, list)
self.assertEqual(metadata.release, u'2015')
self.assertEqual(metadata.releaseTitle, u'The Other Side')
self.assertEqual(metadata.releaseType, u'Album')
self.assertEqual(metadata.mbid,
u'6109ceed-7e21-490b-b5ad-3a66b4e4cfbb')
self.assertEqual(metadata.mbidReleaseGroup,
u'99850b41-a06e-4fb8-992c-75c191a77803')
self.assertEqual(metadata.mbidArtist,
[u'4d56eb9f-13b3-4f05-9db7-50195378d49f'])
self.assertEqual(metadata.url,
u'https://musicbrainz.org/release'
'/6109ceed-7e21-490b-b5ad-3a66b4e4cfbb')
self.assertEqual(metadata.catalogNumber, u'[none]')
self.assertEqual(metadata.barcode, u'700261430249')
self.assertEqual(len(metadata.tracks), 16)
track1 = metadata.tracks[0]
self.assertEqual(track1.artist, u'David Rovics')
self.assertEqual(track1.title, u'Waiting for the Hurricane')
self.assertEqual(track1.duration, 176320)
self.assertEqual(track1.mbid,
u'4116eea3-b9c2-452a-8d63-92f1e585b225')
self.assertEqual(track1.sortName, u'Rovics, David')
self.assertEqual(track1.mbidArtist,
[u'4d56eb9f-13b3-4f05-9db7-50195378d49f'])
self.assertEqual(track1.mbidRecording,
u'b191794d-b7c6-4d6f-971e-0a543959b5ad')
self.assertEqual(track1.mbidWorks,
[u'90d5be68-0b29-45a3-ba01-c27ad78e3625'])

View File

@@ -15,9 +15,8 @@ class PathTestCase(unittest.TestCase):
path = prog.getPath(u'/tmp', DEFAULT_DISC_TEMPLATE,
'mbdiscid', None)
self.assertEqual(path,
unicode('/tmp/unknown/Unknown Artist - mbdiscid/'
'Unknown Artist - mbdiscid'))
self.assertEqual(path, (u'/tmp/unknown/Unknown Artist - mbdiscid/'
u'Unknown Artist - mbdiscid'))
def testStandardTemplateFilled(self):
prog = program.Program(config.Config())
@@ -27,9 +26,8 @@ class PathTestCase(unittest.TestCase):
path = prog.getPath(u'/tmp', DEFAULT_DISC_TEMPLATE,
'mbdiscid', md, 0)
self.assertEqual(path,
unicode('/tmp/unknown/Jeff Buckley - Grace/'
'Jeff Buckley - Grace'))
self.assertEqual(path, (u'/tmp/unknown/Jeff Buckley - Grace/'
u'Jeff Buckley - Grace'))
def testIssue66TemplateFilled(self):
prog = program.Program(config.Config())

View File

@@ -59,7 +59,8 @@ class KanyeMixedTestCase(unittest.TestCase):
class WriteCueFileTestCase(unittest.TestCase):
def testWrite(self):
@staticmethod
def testWrite():
fd, path = tempfile.mkstemp(suffix=u'.whipper.test.cue')
os.close(fd)

View File

@@ -320,6 +320,20 @@ class TOTBLTestCase(common.TestCase):
self.assertEqual(self.toc.table.getCDDBDiscId(), '810b7b0b')
class GentlemenTestCase(common.TestCase):
def setUp(self):
self.path = os.path.join(os.path.dirname(__file__),
u'gentlemen.fast.toc')
self.toc = toc.TocFile(self.path)
self.toc.parse()
self.assertEquals(len(self.toc.table.tracks), 11)
def testCDDBId(self):
self.toc.table.absolutize()
self.assertEquals(self.toc.table.getCDDBDiscId(), '810b7b0b')
# The Strokes - Someday has a 1 frame SILENCE marked as such in toc
@@ -353,7 +367,8 @@ class StrokesTestCase(common.TestCase):
'strokes-someday.eac.cue')).read()).decode('utf-8')
common.diffStrings(ref, cue)
def _filterCue(self, output):
@staticmethod
def _filterCue(output):
# helper to be able to compare our generated .cue with the
# EAC-extracted one
discard = ['TITLE', 'PERFORMER', 'FLAGS', 'REM']

View File

@@ -75,8 +75,8 @@ class AnalyzeFileTask(cdparanoia.AnalyzeTask):
def __init__(self, path):
self.command = ['cat', path]
def readbytesout(self, bytes):
self.readbyteserr(bytes)
def readbytesout(self, bytes_stdout):
self.readbyteserr(bytes_stdout)
class CacheTestCase(common.TestCase):

View File

@@ -9,7 +9,7 @@ from whipper.test import common
class VersionTestCase(common.TestCase):
def testGetVersion(self):
v = cdrdao.getCDRDAOVersion()
v = cdrdao.version()
self.assertTrue(v)
# make sure it starts with a digit
self.assertTrue(int(v[0]))

View File

@@ -0,0 +1,80 @@
Log created by: whipper 0.7.4.dev87+gb71ec9f.d20191026 (internal logger)
Log creation date: 2019-10-26T14:25:02Z
Ripping phase information:
Drive: HL-DT-STBD-RE WH14NS40 (revision 1.03)
Extraction engine: cdparanoia cdparanoia III 10.2 libcdio 2.0.0 x86_64-pc-linux-gnu
Defeat audio cache: true
Read offset correction: 6
Overread into lead-out: false
Gap detection: cdrdao 1.2.4
CD-R detected: false
CD metadata:
Release:
Artist: Example - Symbol - Artist
Title: 'Album With: - Dashes'
CDDB Disc ID: c30bde0d
MusicBrainz Disc ID: eyjySLXGdKigAjY3_C0nbBmNUHc-
MusicBrainz lookup URL: https://musicbrainz.org/cdtoc/attach?toc=1+13+228039+150+16414+33638+51378+69369+88891+104871+121645+138672+160748+178096+194680+212628&tracks=13&id=eyjySLXGdKigAjY3_C0nbBmNUHc-
TOC:
1:
Start: 00:00:00
Length: 03:36:64
Start sector: 0
End sector: 16263
2:
Start: 03:36:64
Length: 03:49:49
Start sector: 16264
End sector: 33487
Tracks:
1:
Filename: ./soundtrack/Various Artists - Shark Tale - Motion Picture Soundtrack/01. Sean Paul & Ziggy Marley - Three Little Birds.flac
Peak level: 0.90036
Pre-emphasis:
Extraction speed: 7.0 X
Extraction quality: 100.00 %
Test CRC: 0025D726
Copy CRC: 0025D726
AccurateRip v1:
Result: Found, exact match
Confidence: 14
Local CRC: 95E6A189
Remote CRC: 95E6A189
AccurateRip v2:
Result: Found, exact match
Confidence: 11
Local CRC: 113FA733
Remote CRC: 113FA733
Status: Copy OK
2:
Filename: ./soundtrack/Various Artists - Shark Tale - Motion Picture Soundtrack/02. Christina Aguilera feat. Missy Elliott - Car Wash (Shark Tale mix).flac
Peak level: 0.972351
Pre-emphasis:
Extraction speed: 7.7 X
Extraction quality: 100.00 %
Test CRC: F77C14CB
Copy CRC: F77C14CB
AccurateRip v1:
Result: Found, exact match
Confidence: 14
Local CRC: 0B3316DB
Remote CRC: 0B3316DB
AccurateRip v2:
Result: Found, exact match
Confidence: 10
Local CRC: A0AE0E57
Remote CRC: A0AE0E57
Status: Copy OK
Conclusive status report:
AccurateRip summary: All tracks accurately ripped
Health Status: No errors occurred
EOF: End of status report
SHA-256 hash: 2B176D8C722989B25459160E335E5CC0C1A6813C9DA69F869B625FBF737C475E

View File

@@ -0,0 +1,169 @@
from __future__ import print_function
import hashlib
import os
import re
import unittest
import ruamel.yaml
from whipper.result.result import TrackResult, RipResult
from whipper.result.logger import WhipperLogger
class MockImageTrack:
def __init__(self, number, start, end):
self.number = number
self.absolute = self.start = start
self.end = end
def getIndex(self, num):
if num == 0:
raise KeyError
else:
return self
class MockImageTable:
"""Mock of whipper.image.table.Table, with fake information."""
def __init__(self):
self.tracks = [
MockImageTrack(1, 0, 16263),
MockImageTrack(2, 16264, 33487)
]
def getCDDBDiscId(self):
return "c30bde0d"
def getMusicBrainzDiscId(self):
return "eyjySLXGdKigAjY3_C0nbBmNUHc-"
def getMusicBrainzSubmitURL(self):
return (
"https://musicbrainz.org/cdtoc/attach?toc=1+13+228039+150+16414+"
"33638+51378+69369+88891+104871+121645+138672+160748+178096+194680"
"+212628&tracks=13&id=eyjySLXGdKigAjY3_C0nbBmNUHc-"
)
def getTrackLength(self, number):
return self.tracks[number-1].end - self.tracks[number-1].start + 1
def getTrackEnd(self, number):
return self.tracks[number-1].end
class LoggerTestCase(unittest.TestCase):
def setUp(self):
self.path = os.path.join(os.path.dirname(__file__))
def testLogger(self):
ripResult = RipResult()
ripResult.offset = 6
ripResult.overread = False
ripResult.isCdr = False
ripResult.table = MockImageTable()
ripResult.artist = "Example - Symbol - Artist"
ripResult.title = "Album With: - Dashes"
ripResult.vendor = "HL-DT-STBD-RE "
ripResult.model = "WH14NS40"
ripResult.release = "1.03"
ripResult.cdrdaoVersion = "1.2.4"
ripResult.cdparanoiaVersion = (
"cdparanoia III 10.2 "
"libcdio 2.0.0 x86_64-pc-linux-gnu"
)
ripResult.cdparanoiaDefeatsCache = True
trackResult = TrackResult()
trackResult.number = 1
trackResult.filename = (
"./soundtrack/Various Artists - Shark Tale - Motion Picture "
"Soundtrack/01. Sean Paul & Ziggy Marley - Three Little Birds.flac"
)
trackResult.pregap = 0
trackResult.peak = 29503
trackResult.quality = 1
trackResult.copyspeed = 7
trackResult.testduration = 10
trackResult.copyduration = 10
trackResult.testcrc = 0x0025D726
trackResult.copycrc = 0x0025D726
trackResult.AR = {
"v1": {
"DBConfidence": 14,
"DBCRC": "95E6A189",
"CRC": "95E6A189"
},
"v2": {
"DBConfidence": 11,
"DBCRC": "113FA733",
"CRC": "113FA733"
}
}
ripResult.tracks.append(trackResult)
trackResult = TrackResult()
trackResult.number = 2
trackResult.filename = (
"./soundtrack/Various Artists - Shark Tale - Motion Picture "
"Soundtrack/02. Christina Aguilera feat. Missy Elliott - Car "
"Wash (Shark Tale mix).flac"
)
trackResult.pregap = 0
trackResult.peak = 31862
trackResult.quality = 1
trackResult.copyspeed = 7.7
trackResult.testduration = 10
trackResult.copyduration = 10
trackResult.testcrc = 0xF77C14CB
trackResult.copycrc = 0xF77C14CB
trackResult.AR = {
"v1": {
"DBConfidence": 14,
"DBCRC": "0B3316DB",
"CRC": "0B3316DB"
},
"v2": {
"DBConfidence": 10,
"DBCRC": "A0AE0E57",
"CRC": "A0AE0E57"
}
}
ripResult.tracks.append(trackResult)
logger = WhipperLogger()
actual = logger.log(ripResult)
actualLines = actual.splitlines()
expectedLines = open(
os.path.join(self.path, 'test_result_logger.log'), 'r'
).read().splitlines()
# do not test on version line, date line, or SHA-256 hash line
self.assertListEqual(actualLines[2:-1], expectedLines[2:-1])
self.assertRegexpMatches(
actualLines[0],
re.compile((
r'Log created by: whipper '
r'[\d]+\.[\d]+.[\d]+\.dev[\w\.\+]+ \(internal logger\)'
))
)
self.assertRegexpMatches(
actualLines[1],
re.compile((
r'Log creation date: '
r'20[\d]{2}\-[\d]{2}\-[\d]{2}T[\d]{2}:[\d]{2}:[\d]{2}Z'
))
)
yaml = ruamel.yaml.YAML()
parsedLog = yaml.load(actual)
self.assertEqual(
actual,
ruamel.yaml.dump(
parsedLog,
default_flow_style=False,
width=4000,
Dumper=ruamel.yaml.RoundTripDumper
)
)
self.assertEqual(
parsedLog['SHA-256 hash'],
hashlib.sha256("\n".join(actualLines[:-1])).hexdigest().upper()
)

View File

@@ -0,0 +1,538 @@
(lp0
ccopy_reg
_reconstructor
p1
(cwhipper.common.mbngs
DiscMetadata
p2
c__builtin__
object
p3
Ntp4
Rp5
(dp6
S'sortName'
p7
VGustafsson, J<>rgen, <20>sterberg, Eva & Cowle, Andy
p8
sS'artist'
p9
VJ<EFBFBD>rgen Gustafsson, Eva <20>sterberg & Andy Cowle
p10
sS'url'
p11
S'https://musicbrainz.org/release/d8e6153a-2c47-4804-9d73-0aac1081c3b1'
p12
sS'barcode'
p13
S'9789162267957'
p14
sS'tracks'
p15
(lp16
g1
(cwhipper.common.mbngs
TrackMetadata
p17
g3
Ntp18
Rp19
(dp20
g7
VGustafsson, J<>rgen, <20>sterberg, Eva & Cowle, Andy
p21
sS'mbidRecording'
p22
S'2ed20192-08ae-4852-a05a-b59b9c27f8c0'
p23
sS'title'
p24
S'Best friends (sid. 96)'
p25
sg9
VJ<EFBFBD>rgen Gustafsson, Eva <20>sterberg & Andy Cowle
p26
sS'mbidWorks'
p27
(lp28
sS'mbid'
p29
S'f9f6ae81-7d3b-475f-bb66-1dbd09705ca2'
p30
sS'duration'
p31
I86666
sS'mbidArtist'
p32
(lp33
S'0508d601-375d-419e-b581-8d5f0b43e573'
p34
aS'56309f78-8e31-4362-b875-5bdd4ac2b81f'
p35
aS'591599ca-8598-4407-8aa8-bbe7aedd9d24'
p36
asbag1
(g17
g3
Ntp37
Rp38
(dp39
g7
VGustafsson, J<>rgen, <20>sterberg, Eva & Cowle, Andy
p40
sg22
S'314126fa-75a4-4e1c-9bae-4b00253a47e3'
p41
sg24
S'Best friends (sid. 97)'
p42
sg9
VJ<EFBFBD>rgen Gustafsson, Eva <20>sterberg & Andy Cowle
p43
sg27
(lp44
sg29
S'e41896d4-5ab9-41b4-861a-8021370885ce'
p45
sg31
I139800
sg32
(lp46
S'0508d601-375d-419e-b581-8d5f0b43e573'
p47
aS'56309f78-8e31-4362-b875-5bdd4ac2b81f'
p48
aS'591599ca-8598-4407-8aa8-bbe7aedd9d24'
p49
asbag1
(g17
g3
Ntp50
Rp51
(dp52
g7
VGustafsson, J<>rgen, <20>sterberg, Eva & Cowle, Andy
p53
sg22
S'81a572b6-fc21-4f47-b63c-ff42c5b2aa50'
p54
sg24
VWorking life (sid. 98\u201399)
p55
sg9
VJ<EFBFBD>rgen Gustafsson, Eva <20>sterberg & Andy Cowle
p56
sg27
(lp57
sg29
S'a58c92f9-84ef-492e-be75-a2482ff5b0cd'
p58
sg31
I171293
sg32
(lp59
S'0508d601-375d-419e-b581-8d5f0b43e573'
p60
aS'56309f78-8e31-4362-b875-5bdd4ac2b81f'
p61
aS'591599ca-8598-4407-8aa8-bbe7aedd9d24'
p62
asbag1
(g17
g3
Ntp63
Rp64
(dp65
g7
VGustafsson, J<>rgen, <20>sterberg, Eva & Cowle, Andy
p66
sg22
S'03497930-6681-47a2-b8ba-98b04a51682f'
p67
sg24
S'Pioneers in sport (sid. 100)'
p68
sg9
VJ<EFBFBD>rgen Gustafsson, Eva <20>sterberg & Andy Cowle
p69
sg27
(lp70
sg29
S'7701ef4a-2682-4183-990f-2724a7445bb6'
p71
sg31
I97813
sg32
(lp72
S'0508d601-375d-419e-b581-8d5f0b43e573'
p73
aS'56309f78-8e31-4362-b875-5bdd4ac2b81f'
p74
aS'591599ca-8598-4407-8aa8-bbe7aedd9d24'
p75
asbag1
(g17
g3
Ntp76
Rp77
(dp78
g7
VGustafsson, J<>rgen, <20>sterberg, Eva & Cowle, Andy
p79
sg22
S'a0bb30e0-87a8-4598-82ee-ef47ce46b9c5'
p80
sg24
S'Pioneers in sport (sid. 101)'
p81
sg9
VJ<EFBFBD>rgen Gustafsson, Eva <20>sterberg & Andy Cowle
p82
sg27
(lp83
sg29
S'f5c49a53-71d3-487c-96dc-108fcd3a5243'
p84
sg31
I93546
sg32
(lp85
S'0508d601-375d-419e-b581-8d5f0b43e573'
p86
aS'56309f78-8e31-4362-b875-5bdd4ac2b81f'
p87
aS'591599ca-8598-4407-8aa8-bbe7aedd9d24'
p88
asbag1
(g17
g3
Ntp89
Rp90
(dp91
g7
VGustafsson, J<>rgen, <20>sterberg, Eva & Cowle, Andy
p92
sg22
S'54ef6217-6954-4b42-9c1b-8b30d11f4204'
p93
sg24
S'Weird, but true! (sid. 102)'
p94
sg9
VJ<EFBFBD>rgen Gustafsson, Eva <20>sterberg & Andy Cowle
p95
sg27
(lp96
sg29
S'def7ea86-07d2-41c0-9ae8-562377484188'
p97
sg31
I124186
sg32
(lp98
S'0508d601-375d-419e-b581-8d5f0b43e573'
p99
aS'56309f78-8e31-4362-b875-5bdd4ac2b81f'
p100
aS'591599ca-8598-4407-8aa8-bbe7aedd9d24'
p101
asbag1
(g17
g3
Ntp102
Rp103
(dp104
g7
VGustafsson, J<>rgen, <20>sterberg, Eva & Cowle, Andy
p105
sg22
S'e9d8e7bc-8d8e-4d0f-87b0-429066b3bb1a'
p106
sg24
S'Weird, but true! (sid. 103)'
p107
sg9
VJ<EFBFBD>rgen Gustafsson, Eva <20>sterberg & Andy Cowle
p108
sg27
(lp109
sg29
S'a7c5c2f8-a9fc-4ebd-8fd4-dc6d200ff794'
p110
sg31
I98000
sg32
(lp111
S'0508d601-375d-419e-b581-8d5f0b43e573'
p112
aS'56309f78-8e31-4362-b875-5bdd4ac2b81f'
p113
aS'591599ca-8598-4407-8aa8-bbe7aedd9d24'
p114
asbag1
(g17
g3
Ntp115
Rp116
(dp117
g7
VGustafsson, J<>rgen, <20>sterberg, Eva & Cowle, Andy
p118
sg22
S'c330f9eb-f63d-4f1d-b858-73c3912f5100'
p119
sg24
S'Useful inventions (sid. 104)'
p120
sg9
VJ<EFBFBD>rgen Gustafsson, Eva <20>sterberg & Andy Cowle
p121
sg27
(lp122
sg29
S'2c9b71c1-7987-4e01-9d3d-d9eae9ea7d04'
p123
sg31
I111933
sg32
(lp124
S'0508d601-375d-419e-b581-8d5f0b43e573'
p125
aS'56309f78-8e31-4362-b875-5bdd4ac2b81f'
p126
aS'591599ca-8598-4407-8aa8-bbe7aedd9d24'
p127
asbag1
(g17
g3
Ntp128
Rp129
(dp130
g7
VGustafsson, J<>rgen, <20>sterberg, Eva & Cowle, Andy
p131
sg22
S'2fd621a4-575f-4ad3-8beb-4f435f81714e'
p132
sg24
S'Useful inventions (sid. 105)'
p133
sg9
VJ<EFBFBD>rgen Gustafsson, Eva <20>sterberg & Andy Cowle
p134
sg27
(lp135
sg29
S'09f7c0fe-165f-4e3d-8eab-0379e3330191'
p136
sg31
I111186
sg32
(lp137
S'0508d601-375d-419e-b581-8d5f0b43e573'
p138
aS'56309f78-8e31-4362-b875-5bdd4ac2b81f'
p139
aS'591599ca-8598-4407-8aa8-bbe7aedd9d24'
p140
asbag1
(g17
g3
Ntp141
Rp142
(dp143
g7
VGustafsson, J<>rgen, <20>sterberg, Eva & Cowle, Andy
p144
sg22
S'85300031-cf37-4fee-aac5-2dd4f3302d5d'
p145
sg24
S'Friends or not friends'
p146
sg9
VJ<EFBFBD>rgen Gustafsson, Eva <20>sterberg & Andy Cowle
p147
sg27
(lp148
sg29
S'80fc7cd1-576d-4e97-b20d-7af146209771'
p149
sg31
I252000
sg32
(lp150
S'0508d601-375d-419e-b581-8d5f0b43e573'
p151
aS'56309f78-8e31-4362-b875-5bdd4ac2b81f'
p152
aS'591599ca-8598-4407-8aa8-bbe7aedd9d24'
p153
asbag1
(g17
g3
Ntp154
Rp155
(dp156
g7
VGustafsson, J<>rgen, <20>sterberg, Eva & Cowle, Andy
p157
sg22
S'56510ac8-444c-48f4-8b35-86ba3d79a93f'
p158
sg24
S'People at work'
p159
sg9
VJ<EFBFBD>rgen Gustafsson, Eva <20>sterberg & Andy Cowle
p160
sg27
(lp161
sg29
S'47dc69cf-dfe4-4084-a22e-62bfd0cbfc11'
p162
sg31
I137520
sg32
(lp163
S'0508d601-375d-419e-b581-8d5f0b43e573'
p164
aS'56309f78-8e31-4362-b875-5bdd4ac2b81f'
p165
aS'591599ca-8598-4407-8aa8-bbe7aedd9d24'
p166
asbag1
(g17
g3
Ntp167
Rp168
(dp169
g7
VGustafsson, J<>rgen, <20>sterberg, Eva & Cowle, Andy
p170
sg22
S'51bd9b54-4145-4ab0-8334-ba7bf40160b0'
p171
sg24
S'Sports'
p172
sg9
VJ<EFBFBD>rgen Gustafsson, Eva <20>sterberg & Andy Cowle
p173
sg27
(lp174
sg29
S'e30daeed-c043-4140-bdf1-182baf9eab16'
p175
sg31
I192213
sg32
(lp176
S'0508d601-375d-419e-b581-8d5f0b43e573'
p177
aS'56309f78-8e31-4362-b875-5bdd4ac2b81f'
p178
aS'591599ca-8598-4407-8aa8-bbe7aedd9d24'
p179
asbag1
(g17
g3
Ntp180
Rp181
(dp182
g7
VGustafsson, J<>rgen, <20>sterberg, Eva & Cowle, Andy
p183
sg22
S'ebc81f6c-9c8e-498a-b622-dc27f450a89a'
p184
sg24
S'Stories to tell'
p185
sg9
VJ<EFBFBD>rgen Gustafsson, Eva <20>sterberg & Andy Cowle
p186
sg27
(lp187
sg29
S'c7e2f93a-7f68-4a1f-9458-a4399803a1b6'
p188
sg31
I209640
sg32
(lp189
S'0508d601-375d-419e-b581-8d5f0b43e573'
p190
aS'56309f78-8e31-4362-b875-5bdd4ac2b81f'
p191
aS'591599ca-8598-4407-8aa8-bbe7aedd9d24'
p192
asbag1
(g17
g3
Ntp193
Rp194
(dp195
g7
VGustafsson, J<>rgen, <20>sterberg, Eva & Cowle, Andy
p196
sg22
S'e36f3f9d-6bf6-44db-938b-64372b772da5'
p197
sg24
S'Inventions'
p198
sg9
VJ<EFBFBD>rgen Gustafsson, Eva <20>sterberg & Andy Cowle
p199
sg27
(lp200
sg29
S'9871987a-ab2c-4a2f-b4e0-61dafb058540'
p201
sg31
I141186
sg32
(lp202
S'0508d601-375d-419e-b581-8d5f0b43e573'
p203
aS'56309f78-8e31-4362-b875-5bdd4ac2b81f'
p204
aS'591599ca-8598-4407-8aa8-bbe7aedd9d24'
p205
asbasg31
I1966982
sg29
S'd8e6153a-2c47-4804-9d73-0aac1081c3b1'
p206
sS'various'
p207
I00
sS'catalogNumber'
p208
S'6795-7'
p209
sS'release'
p210
S'2008'
p211
sg24
VWhat\u2019s Up? 8 (Disc 4 of 4)
p212
sg32
(lp213
S'0508d601-375d-419e-b581-8d5f0b43e573'
p214
aS'56309f78-8e31-4362-b875-5bdd4ac2b81f'
p215
aS'591599ca-8598-4407-8aa8-bbe7aedd9d24'
p216
asS'mbidReleaseGroup'
p217
S'6aa93fc6-6389-414d-a3ed-7ece36bc4931'
p218
sS'releaseTitle'
p219
VWhat\u2019s Up? 8
p220
sba.

View File

@@ -1 +0,0 @@
{"release": {"status": "Official", "artist-credit": [{"artist": {"sort-name": "Buckley, Jeff", "id": "e6e879c0-3d56-4f12-b3c5-3ce459661a8e", "name": "Jeff Buckley"}}], "text-representation": {"language": "eng", "script": "Latn"}, "title": "Everybody Here Wants You", "artist-credit-phrase": "Jeff Buckley", "quality": "normal", "id": "3451f29c-9bb8-4cc5-bfcc-bd50104b94f8", "medium-list": [{"disc-list": [{"id": "C6N7.QADBQ968Qr8OOjxfQlGtA8-", "sectors": "122983"}, {"id": "wbjbST2jUHRZaB1inCyxxsL7Eqc-", "sectors": "122833"}], "position": "1", "track-list": [{"recording": {"artist-credit": [{"artist": {"sort-name": "Buckley, Jeff", "id": "e6e879c0-3d56-4f12-b3c5-3ce459661a8e", "name": "Jeff Buckley"}}], "length": "286920", "artist-credit-phrase": "Jeff Buckley", "id": "8f8c284b-6818-4a66-a517-37dc8c04a881", "title": "Everybody Here Wants You"}, "position": "1"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Buckley, Jeff", "id": "e6e879c0-3d56-4f12-b3c5-3ce459661a8e", "name": "Jeff Buckley"}}], "length": "204746", "artist-credit-phrase": "Jeff Buckley", "id": "7d939d14-06a2-478e-b279-ebe20fae8b2f", "title": "Thousand Fold"}, "position": "2"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Buckley, Jeff", "id": "e6e879c0-3d56-4f12-b3c5-3ce459661a8e", "name": "Jeff Buckley"}}], "length": "288466", "artist-credit-phrase": "Jeff Buckley", "id": "54323c4c-e0f6-4a81-8b80-e1c0b822a3f7", "title": "Eternal Life (road version)"}, "position": "3"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Buckley, Jeff", "id": "e6e879c0-3d56-4f12-b3c5-3ce459661a8e", "name": "Jeff Buckley"}}], "length": "574026", "artist-credit-phrase": "Jeff Buckley", "id": "4dda67d1-8123-4545-9a78-7b4232089e96", "title": "Hallelujah (live)"}, "position": "4"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Buckley, Jeff", "id": "e6e879c0-3d56-4f12-b3c5-3ce459661a8e", "name": "Jeff Buckley"}}], "length": "284000", "artist-credit-phrase": "Jeff Buckley", "id": "5db42013-aa5c-4eb4-a549-46ca721990cf", "title": "Last Goodbye (live from Sydney)"}, "position": "5"}], "format": "CD"}]}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long