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 sudo: required
language: bash language: python
python:
- "2.7"
virtualenv:
system_site_packages: false
cache: pip
env: env:
- FLAKE8=false - FLAKE8=false
@@ -9,21 +16,18 @@ env:
install: install:
# Dependencies # Dependencies
- sudo apt-get -qq update - sudo apt-get -qq update
- sudo pip install --upgrade -qq pip - 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 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
- sudo pip install pycdio==0.21 requests # 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 # Testing dependencies
- sudo apt-get -qq install python-twisted-core - pip install twisted flake8
- sudo pip install flake8
# Build bundled C utils
- cd src
- sudo make install
- cd ..
# Installing # Installing
- sudo python setup.py install - python setup.py install
script: script:
- if [ ! "$FLAKE8" = true ]; then python -m unittest discover; fi - if [ ! "$FLAKE8" = true ]; then python -m unittest discover; fi

View File

@@ -2,7 +2,66 @@
## [Unreleased](https://github.com/whipper-team/whipper/tree/HEAD) ## [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) ## [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) [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) - Possible HTOA error [\#281](https://github.com/whipper-team/whipper/issues/281)
- Disc template KeyError [\#279](https://github.com/whipper-team/whipper/issues/279) - 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) - 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) - Unicode issues [\#215](https://github.com/whipper-team/whipper/issues/215)
- whipper offset find exception [\#208](https://github.com/whipper-team/whipper/issues/208) - 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) - 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) - 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) - 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) - 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) - 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) - 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:** **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)) - 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)) - 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)) - 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) ## [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) [Full Changelog](https://github.com/whipper-team/whipper/compare/v0.5.0...v0.5.1)
@@ -271,6 +330,7 @@
**Merged pull requests:** **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)) - 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)) - 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)) - 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 run --branch --omit='whipper/test/*' --source=whipper -m unittest discover
$ coverage report -m $ coverage report -m
Name Stmts Miss Branch BrPart Cover Missing 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/__main__.py 7 7 2 0 0% 4-14
whipper/command/__init__.py 0 0 0 0 100% whipper/command/__init__.py 0 0 0 0 100%
whipper/command/accurip.py 43 43 18 0 0% 21-92 whipper/command/accurip.py 41 41 18 0 0% 21-90
whipper/command/basecommand.py 69 53 30 0 16% 56-114, 121-130, 133, 136, 139, 142-145 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 224 186 58 0 13% 71-79, 84-193, 196, 208, 231-284, 291-318, 321-491 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/drive.py 57 57 10 0 0% 21-107
whipper/command/image.py 38 38 6 0 0% 21-76 whipper/command/image.py 38 38 6 0 0% 21-76
whipper/command/main.py 68 68 22 0 0% 4-115 whipper/command/main.py 68 68 22 0 0% 4-116
whipper/command/mblookup.py 28 28 8 0 0% 1-41 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-221 whipper/command/offset.py 110 110 32 0 0% 21-219
whipper/common/__init__.py 0 0 0 0 100% 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/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 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/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/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/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 92 8 18 4 89% 105-106, 124-125, 131, 141, 143, 145, 130->131, 140->141, 142->143, 144->145 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/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/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/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/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/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% 135, 158, 60->68 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, 90-93, 101, 114-115, 122, 128, 134, 140, 146, 84->86, 98->101 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/__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/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/__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/__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/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/image.py 116 93 18 0 17% 49-57, 65-67, 74-107, 121-154, 157-173, 184-214
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/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/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/__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/arc.py 3 0 0 0 100%
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/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 59 36 14 2 34% 26-56, 63-69, 79-81, 85-87, 95, 102, 78->79, 84->85 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/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/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/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/__init__.py 0 0 0 0 100%
whipper/result/logger.py 148 148 48 0 0% 1-242 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 56 13 6 0 69% 112-116, 134, 144-145, 154-161 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 FROM debian:buster
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y cdrdao python-gobject-2 python-musicbrainzngs python-mutagen python-setuptools \ && apt-get install -y cdrdao git python-gobject-2 python-musicbrainzngs python-mutagen \
python-cddb python-requests libsndfile1-dev flac sox \ python-setuptools python-requests libsndfile1-dev flac sox \
libiso9660-dev python-pip swig make pkgconf \ libiso9660-dev python-pip swig make pkgconf \
eject locales \ eject locales \
autoconf libtool curl \ 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 # libcdio-paranoia / libcdio-utils are wrongfully packaged in Debian, thus built manually
# see https://github.com/whipper-team/whipper/pull/237#issuecomment-367985625 # 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 - \ RUN curl -o - 'https://ftp.gnu.org/gnu/libcdio/libcdio-2.1.0.tar.bz2' | tar jxf - \
&& cd libcdio-2.0.0 \ && cd libcdio-2.1.0 \
&& autoreconf -fi \ && autoreconf -fi \
&& ./configure --disable-dependency-tracking --disable-cxx --disable-example-progs --disable-static \ && ./configure --disable-dependency-tracking --disable-cxx --disable-example-progs --disable-static \
&& make install \ && make install \
&& cd .. \ && cd .. \
&& rm -rf libcdio-2.0.0 && rm -rf libcdio-2.1.0
# Install cd-paranoia from tarball # 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 - \ 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+0.94+2 \ && cd libcdio-paranoia-10.2+2.0.0 \
&& autoreconf -fi \ && autoreconf -fi \
&& ./configure --disable-dependency-tracking --disable-example-progs --disable-static \ && ./configure --disable-dependency-tracking --disable-example-progs --disable-static \
&& make install \ && make install \
&& cd .. \ && cd .. \
&& rm -rf libcdio-paranoia-10.2+0.94+2 && rm -rf libcdio-paranoia-10.2+2.0.0
RUN ldconfig RUN ldconfig
@@ -45,8 +45,7 @@ RUN echo "LC_ALL=en_US.UTF-8" >> /etc/environment \
# install whipper # install whipper
RUN mkdir /whipper RUN mkdir /whipper
COPY . /whipper/ COPY . /whipper/
RUN cd /whipper/src && make && make install \ RUN cd /whipper && python2 setup.py install \
&& cd /whipper && python2 setup.py install \
&& rm -rf /whipper \ && rm -rf /whipper \
&& whipper -v && 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 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) [![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. 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). 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 ## Table of content
- [Rationale](#rationale) - [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) - [Logger plugins](#logger-plugins)
- [License](#license) - [License](#license)
- [Contributing](#contributing) - [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) - [Bug reports & feature requests](#bug-reports--feature-requests)
- [Credits](#credits) - [Credits](#credits)
- [Links](#links) - [Links](#links)
@@ -51,7 +55,8 @@ https://web.archive.org/web/20160528213242/https://thomas.apestaart.org/thomas/t
- Performs Test & Copy rips - Performs Test & Copy rips
- Verifies rip accuracy using the [AccurateRip database](http://www.accuraterip.com/) - Verifies rip accuracy using the [AccurateRip database](http://www.accuraterip.com/)
- Uses [MusicBrainz](https://musicbrainz.org/doc/About) for metadata lookup - 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) - Detects and rips _non digitally silent_ [Hidden Track One Audio](http://wiki.hydrogenaud.io/index.php?title=HTOA) (HTOA)
- Provides batch ripping capabilities - Provides batch ripping capabilities
- Provides templates for file and directory naming - 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: 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): 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: 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 \ alias whipper="docker run -ti --rm --device=/dev/cdrom \
-v ~/.config/whipper:/home/worker/.config/whipper \ -v ~/.config/whipper:/home/worker/.config/whipper \
-v ${PWD}/output:/output \ -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 \ …`). 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) [![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. 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 ## 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: 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 - [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** - 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: 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). - 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 - [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` - [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` - [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-setuptools](https://pypi.python.org/pypi/setuptools), for installation, plugins support
- [python-requests](https://pypi.python.org/pypi/requests), for retrieving AccurateRip database entries - [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). - [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 - [libsndfile](http://www.mega-nerd.com/libsndfile/), for reading wav files
- [flac](https://xiph.org/flac/), for reading flac files - [flac](https://xiph.org/flac/), for reading flac files
- [sox](http://sox.sourceforge.net/), for track peak detection - [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/) - [libsndfile](http://www.mega-nerd.com/libsndfile/)
- [flac](https://xiph.org/flac/) - [flac](https://xiph.org/flac/)
- [sox](http://sox.sourceforge.net/) - [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: 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 ```Text
Copyright (C) 2009 Thomas Vander Stichele Copyright (C) 2009 Thomas Vander Stichele
Copyright (C) 2016-2018 The Whipper Team: JoeLametta, Frederik Olesen, Copyright (C) 2016-2019 The Whipper Team: JoeLametta, Samantha Baldwin,
Samantha Baldwin, Merlijn Wajer, et al. Merlijn Wajer, Frederik “Freso” S. Olesen, et al.
This program is free software; you can redistribute it and/or modify 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 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 please include tests for new or changed functionality. You can run tests
with `python -m unittest discover` from your source checkout. 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 ### 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. 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) - [Thomas Vander Stichele](https://github.com/thomasvs)
- [Joe Lametta](https://github.com/JoeLametta) - [Joe Lametta](https://github.com/JoeLametta)
- [Merlijn Wajer](https://github.com/MerlijnWajer)
- [Samantha Baldwin](https://github.com/RecursiveForest) - [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). 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) - [Redacted thread (official)](https://redacted.ch/forums.php?action=viewthread&threadid=150)
Other relevant links: Other relevant links:
- [Whipper - Hydrogenaudio Knowledgebase](https://wiki.hydrogenaud.io/index.php?title=Whipper)
- [Repology: versions for whipper](https://repology.org/metapackage/whipper/versions) - [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) - [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 - write xbmc/plex plugin
SPECIFIC ALBUMS ISSUES SPECIFIC RELEASES ISSUES
- on ana, Goldfrapp tells me I have offset 0! - on ana, Goldfrapp tells me I have offset 0!
@@ -67,7 +67,7 @@ SPECIFIC ALBUMS ISSUES
NO DECISION YET 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 - check if cdda2wav or icedax analyze pregaps correctly

View File

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

View File

@@ -3,3 +3,5 @@ mutagen
pycdio>0.20 pycdio>0.20
PyGObject PyGObject
requests 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 setuptools import setup, find_packages, Extension
from whipper import __version__ as whipper_version
setup( setup(
name="whipper", name="whipper",
version=whipper_version, use_scm_version=True,
description="a secure cd ripper preferring accuracy over speed", description="a secure cd ripper preferring accuracy over speed",
author=['Thomas Vander Stichele', 'The Whipper Team'], author=['Thomas Vander Stichele', 'The Whipper Team'],
maintainer=['The Whipper Team'], maintainer=['The Whipper Team'],
url='https://github.com/whipper-team/whipper', url='https://github.com/whipper-team/whipper',
license='GPL3', license='GPL3',
python_requires='>=2.7,<3',
packages=find_packages(), packages=find_packages(),
setup_requires=['setuptools_scm'],
ext_modules=[
Extension('accuraterip',
libraries=['sndfile'],
sources=['src/accuraterip-checksum.c'])
],
entry_points={ entry_points={
'console_scripts': [ 'console_scripts': [
'whipper = whipper.command.main:main' 'whipper = whipper.command.main:main'
@@ -18,4 +24,7 @@ setup(
data_files=[ data_files=[
('share/metainfo', ['com.github.whipper_team.Whipper.metainfo.xml']), ('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: ## Description
A C99 commandline program to compute the AccurateRip checksum of singletrack WAV files. 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.
Implemented according to 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: ## Usage
accuraterip-checksum [--version / --accuraterip-v1 / --accuraterip-v2 (default)] filename track_number total_tracks 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: accuraterip-checksum TRACK_FILE TRACK TOTAL_TRACKS
By default, the V2 (AccurateRip version 2) checksum will be printed.
You can also obtain the V1 checksum with the "--accuraterip-v1" parameter.
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. 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 libsndfile1-dev
The configuration files of an Eclipse project are included. ## Author
You can use "EGit" (Eclipse git) to import the whole repository.
It should as well ask you to import the project configuration then.
# Author:
Leo Bogert (http://leo.bogert.de) Leo Bogert (http://leo.bogert.de)
# Version: ## Version
1.4 1.5
# Donations: ## Donations
bitcoin:14kPd2QWsri3y2irVFX6wC33vv7FqTaEBh bitcoin:14kPd2QWsri3y2irVFX6wC33vv7FqTaEBh
# License: ## License
GPLv3 GPLv3

View File

@@ -1,12 +1,10 @@
/* /*
============================================================================ ============================================================================
Name : accuraterip-checksum.c Name : accuraterip-checksum.c
Author : Leo Bogert (http://leo.bogert.de) Authors : Leo Bogert (http://leo.bogert.de), Andreas Oberritter
Git : http://leo.bogert.de/accuraterip-checksum License : GPLv3
Version : See global variable "version" Description : A Python C extension to compute the AccurateRip checksum of WAV or FLAC tracks.
Copyright : GPL Implemented according to http://www.hydrogenaudio.org/forums/index.php?showtopic=97603
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
============================================================================ ============================================================================
*/ */
@@ -17,10 +15,10 @@
#include <string.h> #include <string.h>
#include <stdint.h> #include <stdint.h>
#include <sndfile.h> #include <sndfile.h>
#include <Python.h>
const char *const version = "1.4"; static bool check_fileformat(const SF_INFO *sfinfo)
{
bool check_fileformat(const SF_INFO* sfinfo) {
#ifdef DEBUG #ifdef DEBUG
printf("Channels: %i\n", sfinfo->channels); printf("Channels: %i\n", sfinfo->channels);
printf("Format: %X\n", sfinfo->format); printf("Format: %X\n", sfinfo->format);
@@ -30,27 +28,25 @@ bool check_fileformat(const SF_INFO* sfinfo) {
printf("Seekable: %i\n", sfinfo->seekable); printf("Seekable: %i\n", sfinfo->seekable);
#endif #endif
if(sfinfo->channels != 2) return false; switch (sfinfo->format & SF_FORMAT_TYPEMASK) {
if((sfinfo->format & SF_FORMAT_TYPEMASK & SF_FORMAT_WAV) != SF_FORMAT_WAV) return false; case SF_FORMAT_WAV:
if((sfinfo->format & SF_FORMAT_SUBMASK & SF_FORMAT_PCM_16) != SF_FORMAT_PCM_16) return false; case SF_FORMAT_FLAC:
//if((sfinfo->format & SF_FORMAT_ENDMASK & SF_ENDIAN_LITTLE) != SF_ENDIAN_LITTLE) return false; return (sfinfo->channels == 2) &&
if(sfinfo->samplerate != 44100) return false; (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) { static void *load_full_audiodata(SNDFILE *sndfile, const SF_INFO *sfinfo, size_t size)
// 16bit = samplesize, 8 bit = bitcount in byte {
return sfinfo->frames * sfinfo->channels * (16 / 8); void *data = malloc(size);
}
uint32_t* load_full_audiodata(SNDFILE* sndfile, const SF_INFO* sfinfo) {
uint32_t* data = (uint32_t*)malloc(get_full_audiodata_size(sfinfo));
if(data == NULL) if(data == NULL)
return 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); free(data);
return NULL; return NULL;
} }
@@ -58,170 +54,100 @@ uint32_t* load_full_audiodata(SNDFILE* sndfile, const SF_INFO* sfinfo) {
return data; 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) { 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)
#define DWORD uint32_t {
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 if (track_number == 1) // first?
int DataSize = audio_data_size; // size of the data AR_CRCPosCheckFrom += ((SectorBytes * 5) / sizeof(uint32_t));
int TrackNumber = track_number; // actual track number on disc, note that for the first & last track the first and last 5 sectors are skipped if (track_number == total_tracks) // last?
int AudioTrackCount = total_tracks; // CD track count AR_CRCPosCheckTo -= ((SectorBytes * 5) / sizeof(uint32_t));
//---------AccurateRip CRC checks------------ for (i = 0; i < Datauint32_tSize; i++) {
DWORD AR_CRC = 0, AR_CRCPosMulti = 1; if (MulBy >= AR_CRCPosCheckFrom && MulBy <= AR_CRCPosCheckTo) {
DWORD AR_CRCPosCheckFrom = 0; uint64_t product = (uint64_t)audio_data[i] * (uint64_t)MulBy;
DWORD AR_CRCPosCheckTo = DataSize / sizeof(DWORD); csum_hi += (uint32_t)(product >> 32);
#define SectorBytes 2352 // each sector csum_lo += (uint32_t)(product);
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;
} }
MulBy++; MulBy++;
} }
return AC_CRCNEW; *v1 = csum_lo;
*v2 = csum_lo + csum_hi;
} }
void print_syntax_to_stderr() { static PyObject *accuraterip_compute(PyObject *self, PyObject *args)
fprintf(stderr, "Syntax: accuraterip-checksum [--version / --accuraterip-v1 / --accuraterip-v2 (default)] filename track_number total_tracks\n"); {
} 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) { if (!PyArg_ParseTuple(args, "sII", &filename, &track_number, &total_tracks))
int arg_offset; goto err;
bool use_v1;
switch(argc) { if (track_number < 1 || track_number > total_tracks) {
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) {
fprintf(stderr, "Invalid track_number!\n"); 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"); fprintf(stderr, "Invalid total_tracks!\n");
return EXIT_FAILURE; goto err;
} }
#ifdef DEBUG #ifdef DEBUG
printf("Reading %s\n", filename); printf("Reading %s\n", filename);
#endif #endif
SF_INFO sfinfo; memset(&sfinfo, 0, sizeof(sfinfo));
sfinfo.channels = 0; sndfile = sf_open(filename, SFM_READ, &sfinfo);
sfinfo.format = 0; if (sndfile == NULL) {
sfinfo.frames = 0;
sfinfo.samplerate = 0;
sfinfo.sections = 0;
sfinfo.seekable = 0;
SNDFILE* sndfile = sf_open(filename, SFM_READ, &sfinfo);
if(sndfile == NULL) {
fprintf(stderr, "sf_open failed! sf_error==%i\n", sf_error(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"); fprintf(stderr, "check_fileformat failed!\n");
sf_close(sndfile); goto err;
return EXIT_FAILURE;
} }
uint32_t* audio_data = load_full_audiodata(sndfile, &sfinfo); size = sfinfo.frames * sfinfo.channels * sizeof(uint16_t);
if(audio_data == NULL) { audio_data = load_full_audiodata(sndfile, &sfinfo, size);
if (audio_data == NULL) {
fprintf(stderr, "load_full_audiodata failed!\n"); fprintf(stderr, "load_full_audiodata failed!\n");
sf_close(sndfile); goto err;
return EXIT_FAILURE;
} }
const int checksum = use_v1 ? compute_checksums(audio_data, size, track_number, total_tracks, &v1, &v2);
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);
free(audio_data); 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 os
import sys 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 level = logging.INFO
if 'WHIPPER_DEBUG' in os.environ: 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.checksums) == r.num_tracks
assert len(r.confidences) == r.num_tracks assert len(r.confidences) == r.num_tracks
entry = {} entry = {"confidence": r.confidences[track], "response": i + 1}
entry["confidence"] = r.confidences[track]
entry["response"] = i + 1
checksum = r.checksums[track] checksum = r.checksums[track]
if checksum in checksums: if checksum in checksums:
checksums[checksum].append(entry) checksums[checksum].append(entry)

View File

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

View File

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

View File

@@ -29,16 +29,18 @@ Example disc id: KnpGsLhvH.lPrNc1PBL21lb9Bg4-"""
print('- Release %d:' % (i + 1, )) print('- Release %d:' % (i + 1, ))
print(' Artist: %s' % md.artist.encode('utf-8')) print(' Artist: %s' % md.artist.encode('utf-8'))
print(' Title: %s' % md.title.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(' URL: %s' % md.url)
print(' Tracks: %d' % len(md.tracks)) print(' Tracks: %d' % len(md.tracks))
if md.catalogNumber: if md.catalogNumber:
print(' Cat no: %s' % md.catalogNumber) print(' Cat no: %s' % md.catalogNumber)
if md.barcode: if md.barcode:
print(' Barcode: %s' % md.barcode) print(' Barcode: %s' % md.barcode)
for j, track in enumerate(md.tracks): for j, track in enumerate(md.tracks):
print(' Track %2d: %s - %s' % ( print(' Track %2d: %s - %s' % (
j + 1, track.artist.encode('utf-8'), j + 1, track.artist.encode('utf-8'),
track.title.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 # first get the Table Of Contents of the CD
t = cdrdao.ReadTOCTask(device) t = cdrdao.ReadTOCTask(device)
table = t.table runner.run(t)
table = t.toc.table
logger.debug("CDDB disc id: %r", table.getCDDBDiscId()) logger.debug("CDDB disc id: %r", table.getCDDBDiscId())
responses = None
try: try:
responses = accurip.get_db_entry(table.accuraterip_path()) responses = accurip.get_db_entry(table.accuraterip_path())
except accurip.EntryNotFound: except accurip.EntryNotFound:
logger.warning("AccurateRip entry not found: drive offset " logger.warning("AccurateRip entry not found: drive offset "
"can't be determined, try again with another disc") "can't be determined, try again with another disc")
return return None
if responses: if responses:
logger.debug('%d AccurateRip responses found.', len(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) logger.warning('cannot rip with offset %d...', offset)
continue continue
logger.debug('AR checksums calculated: %s %s', archecksums) logger.debug('AR checksums calculated: %s', archecksums)
c, i = match(archecksums, 1, responses) c, i = match(archecksums, 1, responses)
if c: if c:
@@ -170,6 +170,8 @@ CD in the AccurateRip database."""
logger.error('no matching offset found. ' logger.error('no matching offset found. '
'Consider trying again with a different disc') 'Consider trying again with a different disc')
return None
def _arcs(self, runner, table, track, offset): def _arcs(self, runner, table, track, offset):
# rips the track with the given offset, return the arcs checksums # rips the track with the given offset, return the arcs checksums
logger.debug('ripping track %r with offset %d...', track, offset) logger.debug('ripping track %r with offset %d...', track, offset)
@@ -188,17 +190,13 @@ CD in the AccurateRip database."""
track, offset) track, offset)
runner.run(t) runner.run(t)
v1 = arc.accuraterip_checksum( v1, v2 = arc.accuraterip_checksum(path, track, len(table.tracks))
path, track, len(table.tracks), wave=True, v2=False
)
v2 = arc.accuraterip_checksum(
path, track, len(table.tracks), wave=True, v2=True
)
os.unlink(path) 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) print('\nRead offset of device is: %d.' % offset)
info = drive.getDeviceInfo(device) info = drive.getDeviceInfo(device)

View File

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

View File

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

View File

@@ -47,7 +47,7 @@ class CRC32Task(etask.Task):
def _crc32(self): def _crc32(self):
if not self.is_wave: if not self.is_wave:
fd, tmpf = tempfile.mkstemp() _, tmpf = tempfile.mkstemp()
try: try:
subprocess.check_call(['flac', '-d', self.path, '-fo', tmpf]) 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. Converts a string value in MM:SS:FF to frames.
@param msf: the MM:SS:FF value to convert :param msf: the MM:SS:FF value to convert
@type msf: str :type msf: str
@rtype: int :rtype: int
@returns: number of frames :returns: number of frames
""" """
if ':' not in msf: if ':' not in msf:
return int(msf) return int(msf)
@@ -76,7 +76,7 @@ def framesToMSF(frames, frameDelimiter=':'):
f = frames % FRAMES_PER_SECOND f = frames % FRAMES_PER_SECOND
frames -= f frames -= f
s = (frames / FRAMES_PER_SECOND) % 60 s = (frames / FRAMES_PER_SECOND) % 60
frames -= s * 60 frames -= s * FRAMES_PER_SECOND
m = frames / FRAMES_PER_SECOND / 60 m = frames / FRAMES_PER_SECOND / 60
return "%02d:%02d%s%02d" % (m, s, frameDelimiter, f) 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. 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. As a side consequence, there is no way to show seconds without fractions.
@param seconds: the time in seconds to format. :param seconds: the time in seconds to format.
@type seconds: int or float :type seconds: int or float
@param fractional: how many digits to show for the fractional part of :param fractional: how many digits to show for the fractional part of
seconds. seconds.
@type fractional: int :type fractional: int
@rtype: string :rtype: string
@returns: a nicely formatted time string. :returns: a nicely formatted time string.
""" """
chunks = [] chunks = []
if seconds < 0: if seconds < 0:
chunks.append(('-')) chunks.append('-')
seconds = -seconds seconds = -seconds
hour = 60 * 60 hour = 60 * 60
@@ -207,11 +207,11 @@ def getRealPath(refPath, filePath):
Does Windows path translation. Does Windows path translation.
Will look for the given file name, but with .flac and .wav as extensions. 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 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 assert isinstance(filePath, unicode), "%r is not unicode" % filePath
@@ -271,13 +271,12 @@ def getRelativePath(targetPath, collectionPath):
if targetDir == collectionDir: if targetDir == collectionDir:
logger.debug('getRelativePath: target and collection in same dir') logger.debug('getRelativePath: target and collection in same dir')
return os.path.basename(targetPath) return os.path.basename(targetPath)
else: rel = os.path.relpath(
rel = os.path.relpath( targetDir + os.path.sep,
targetDir + os.path.sep, collectionDir + os.path.sep)
collectionDir + os.path.sep) logger.debug('getRelativePath: target and collection '
logger.debug('getRelativePath: target and collection ' 'in different dir, %r', rel)
'in different dir, %r', rel) return os.path.join(rel, os.path.basename(targetPath))
return os.path.join(rel, os.path.basename(targetPath))
def validate_template(template, kind): def validate_template(template, kind):
@@ -285,9 +284,9 @@ def validate_template(template, kind):
Raise exception if disc/track template includes invalid variables Raise exception if disc/track template includes invalid variables
""" """
if kind == 'disc': 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': 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: if '%' in template and matches:
raise ValueError(kind + ' template string contains invalid ' raise ValueError(kind + ' template string contains invalid '
'variable(s): {}'.format(', '.join(matches))) 'variable(s): {}'.format(', '.join(matches)))
@@ -301,11 +300,11 @@ class VersionGetter(object):
def __init__(self, dependency, args, regexp, expander): def __init__(self, dependency, args, regexp, expander):
""" """
@param dependency: name of the dependency providing the program :param dependency: name of the dependency providing the program
@param args: the arguments to invoke to show the version :param args: the arguments to invoke to show the version
@type args: list of str :type args: list of str
@param regexp: the regular expression to get the version :param regexp: the regular expression to get the version
@param expander: the expansion string for the version using the :param expander: the expansion string for the version using the
regexp group dict regexp group dict
""" """

View File

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

View File

@@ -66,6 +66,6 @@ def getDeviceInfo(path):
except ImportError: except ImportError:
return None return None
device = cdio.Device(path) 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) self.schedule(0.0, self._flac_encode)
def _flac_encode(self): 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() self.stop()

View File

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

View File

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

View File

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

View File

@@ -21,9 +21,7 @@
import os import os
import tempfile 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): class Operator(object):
@@ -111,10 +109,10 @@ class FileRenamer(Operator):
""" """
Add a rename operation. Add a rename operation.
@param source: source filename :param source: source filename
@type source: str :type source: str
@param destination: destination filename :param destination: destination filename
@type destination: str :type destination: str
""" """
@@ -144,16 +142,16 @@ class Operation(object):
def serialize(self): def serialize(self):
""" """
Serialize the operation. 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): def deserialize(cls, data):
""" """
Deserialize the operation with the given operation data. Deserialize the operation with the given operation data.
@type data: str :type data: str
""" """
raise NotImplementedError raise NotImplementedError
deserialize = classmethod(deserialize) deserialize = classmethod(deserialize)

View File

@@ -87,6 +87,7 @@ class PopenTask(task.Task):
return return
self._done() self._done()
# FIXME: catching too general exception (Exception)
except Exception as e: except Exception as e:
logger.debug('exception during _read(): %s', e) logger.debug('exception during _read(): %s', e)
self.setException(e) self.setException(e)
@@ -115,13 +116,13 @@ class PopenTask(task.Task):
os.kill(self._popen.pid, signal.SIGTERM) os.kill(self._popen.pid, signal.SIGTERM)
# self.stop() # self.stop()
def readbytesout(self, bytes): def readbytesout(self, bytes_stdout):
""" """
Called when bytes have been read from stdout. Called when bytes have been read from stdout.
""" """
pass pass
def readbyteserr(self, bytes): def readbyteserr(self, bytes_stderr):
""" """
Called when bytes have been read from 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): def recv_err(self, maxsize=None):
return self._recv('stderr', maxsize) return self._recv('stderr', maxsize)
def send_recv(self, input='', maxsize=None): def send_recv(self, in_put='', maxsize=None):
return self.send(input), self.recv(maxsize), self.recv_err(maxsize) return self.send(in_put), self.recv(maxsize), self.recv_err(maxsize)
def get_conn_maxsize(self, which, maxsize): def get_conn_maxsize(self, which, maxsize):
if maxsize is None: if maxsize is None:
@@ -44,16 +44,16 @@ class Popen(subprocess.Popen):
if subprocess.mswindows: if subprocess.mswindows:
def send(self, input): def send(self, in_put):
if not self.stdin: if not self.stdin:
return None return None
try: try:
x = msvcrt.get_osfhandle(self.stdin.fileno()) x = msvcrt.get_osfhandle(self.stdin.fileno())
(errCode, written) = WriteFile(x, input) (errCode, written) = WriteFile(x, in_put)
except ValueError: except ValueError:
return self._close('stdin') 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): if why.args[0] in (109, errno.ESHUTDOWN):
return self._close('stdin') return self._close('stdin')
raise raise
@@ -74,7 +74,7 @@ class Popen(subprocess.Popen):
(errCode, read) = ReadFile(x, nAvail, None) (errCode, read) = ReadFile(x, nAvail, None)
except ValueError: except ValueError:
return self._close(which) 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): if why.args[0] in (109, errno.ESHUTDOWN):
return self._close(which) return self._close(which)
raise raise
@@ -85,7 +85,7 @@ class Popen(subprocess.Popen):
else: else:
def send(self, input): def send(self, in_put):
if not self.stdin: if not self.stdin:
return None return None
@@ -93,8 +93,8 @@ class Popen(subprocess.Popen):
return 0 return 0
try: try:
written = os.write(self.stdin.fileno(), input) written = os.write(self.stdin.fileno(), in_put)
except OSError, why: except OSError as why:
if why.args[0] == errno.EPIPE: # broken pipe if why.args[0] == errno.EPIPE: # broken pipe
return self._close('stdin') return self._close('stdin')
raise raise
@@ -153,7 +153,7 @@ def recv_some(p, t=.1, e=1, tr=5, stderr=0):
def send_all(p, data): def send_all(p, data):
while len(data): while data:
sent = p.send(data) sent = p.send(data)
if sent is None: if sent is None:
raise Exception(message) raise Exception(message)

View File

@@ -134,7 +134,8 @@ def perform_lookup(disc_id, freedb_server, freedb_port):
if len(matches) > 0: if len(matches) > 0:
# for each result, query FreeDB for XMCD file data # 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 sleep(1) # add a slight delay to keep the server happy
query = freedb_command(freedb_server, query = freedb_command(freedb_server,
@@ -145,7 +146,7 @@ def perform_lookup(disc_id, freedb_server, freedb_port):
response = RESPONSE.match(next(query)) response = RESPONSE.match(next(query))
if response is not None: if response is not None:
# FIXME - check response code here # FIXME: check response code here
freedb = {} freedb = {}
line = next(query) line = next(query)
while not line.startswith(u"."): while not line.startswith(u"."):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,54 +1,5 @@
from subprocess import Popen, PIPE import accuraterip
import logging
logger = logging.getLogger(__name__)
ARB = 'accuraterip-checksum'
FLAC = 'flac'
def _execute(cmd, **redirects): def accuraterip_checksum(f, track_number, total_tracks):
logger.debug('executing %r', cmd) return accuraterip.compute(f.encode('utf-8'), track_number, total_tracks)
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

View File

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

View File

@@ -2,58 +2,163 @@ import os
import re import re
import shutil import shutil
import tempfile import tempfile
import subprocess
from subprocess import Popen, PIPE 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.image.toc import TocFile
from whipper.extern.task import task
from whipper.extern import asyncsub
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
CDRDAO = 'cdrdao' 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 description = "Reading TOC"
# to write the TOC to; it does not support writing to stdout or toc = None
# 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)
cmd = [CDRDAO, 'read-toc'] + (['--fast-toc'] if fast_toc else []) + [ def __init__(self, device, fast_toc=False, toc_path=None):
'--device', device, tocfile] """
# PIPE is the closest to >/dev/null we can get Read the TOC for 'device'.
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)
toc = TocFile(tocfile) :param device: block device to read TOC from
toc.parse() :type device: str
if toc_path is not None: :param fast_toc: If to use fast-toc cdrdao mode
t_comp = os.path.abspath(toc_path).split(os.sep) :type fast_toc: bool
t_dirn = os.sep.join(t_comp[:-1]) :param toc_path: Where to save TOC if wanted.
# If the output path doesn't exist, make it recursively :type toc_path: str
if not os.path.isdir(t_dirn): """
os.makedirs(t_dirn)
t_dst = truncate_filename(os.path.join(t_dirn, t_comp[-1] + '.toc')) self.device = device
shutil.copy(tocfile, os.path.join(t_dirn, t_dst)) self.fast_toc = fast_toc
os.unlink(tocfile) self.toc_path = toc_path
return toc 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): def DetectCdr(device):
@@ -63,10 +168,7 @@ def DetectCdr(device):
cmd = [CDRDAO, 'disk-info', '-v1', '--device', device] cmd = [CDRDAO, 'disk-info', '-v1', '--device', device]
logger.debug("executing %r", cmd) logger.debug("executing %r", cmd)
p = Popen(cmd, stdout=PIPE, stderr=PIPE) p = Popen(cmd, stdout=PIPE, stderr=PIPE)
if 'CD-R medium : n/a' in p.stdout.read(): return 'CD-R medium : n/a' not in p.stdout.read()
return False
else:
return True
def version(): def version():
@@ -74,7 +176,7 @@ def version():
Return cdrdao version as a string. Return cdrdao version as a string.
""" """
cdrdao = Popen(CDRDAO, stderr=PIPE) cdrdao = Popen(CDRDAO, stderr=PIPE)
out, err = cdrdao.communicate() _, err = cdrdao.communicate()
if cdrdao.returncode != 1: if cdrdao.returncode != 1:
logger.warning("cdrdao version detection failed: " logger.warning("cdrdao version detection failed: "
"return code is %s", cdrdao.returncode) "return code is %s", cdrdao.returncode)
@@ -86,24 +188,3 @@ def version():
"could not find version") "could not find version")
return None return None
return m.group('version') 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") logger.warning("SoX peak detection failed: file not found")
return None return None
sox = Popen([SOX, track_path, "-n", "stats", "-b", "16"], stderr=PIPE) sox = Popen([SOX, track_path, "-n", "stats", "-b", "16"], stderr=PIPE)
out, err = sox.communicate() _, err = sox.communicate()
if sox.returncode: if sox.returncode:
logger.warning("SoX peak detection failed: %s", sox.returncode) logger.warning("SoX peak detection failed: %s", sox.returncode)
return None return None

View File

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

View File

@@ -1,4 +1,5 @@
import os import os
import subprocess
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -9,7 +10,12 @@ def eject_device(device):
Eject the given device. Eject the given device.
""" """
logger.debug("ejecting device %s", 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): def load_device(device):
@@ -17,7 +23,13 @@ def load_device(device):
Load the given device. Load the given device.
""" """
logger.debug("loading (eject -t) device %s", 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): def unmount_device(device):
@@ -28,7 +40,7 @@ def unmount_device(device):
If the given device is a symlink, the target will be checked. If the given device is a symlink, the target will be checked.
""" """
device = os.path.realpath(device) 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() proc = open('/proc/mounts').read()
if device in proc: if device in proc:
print('Device %s is mounted, unmounting' % device) print('Device %s is mounted, unmounting' % device)

View File

@@ -1,5 +1,8 @@
import time import time
import hashlib import hashlib
import re
import ruamel.yaml as yaml
from ruamel.yaml.comments import CommentedMap as OrderedDict
import whipper import whipper
@@ -16,65 +19,57 @@ class WhipperLogger(result.Logger):
def log(self, ripResult, epoch=time.time()): def log(self, ripResult, epoch=time.time()):
"""Returns big str: logfile joined text lines""" """Returns big str: logfile joined text lines"""
lines = self.logRip(ripResult, epoch=epoch) return self.logRip(ripResult, epoch)
return "\n".join(lines)
def logRip(self, ripResult, epoch): def logRip(self, ripResult, epoch):
"""Returns logfile lines list""" """Returns logfile lines list"""
lines = [] riplog = OrderedDict()
# Ripper version # Ripper version
lines.append("Log created by: whipper %s (internal logger)" % riplog["Log created by"] = "whipper %s (internal logger)" % (
whipper.__version__) whipper.__version__)
# Rip date # Rip date
date = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(epoch)).strip() date = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(epoch)).strip()
lines.append("Log creation date: %s" % date) riplog["Log creation date"] = date
lines.append("")
# Rip technical settings # Rip technical settings
lines.append("Ripping phase information:") data = OrderedDict()
lines.append(" Drive: %s%s (revision %s)" % (
ripResult.vendor, ripResult.model, ripResult.release)) data["Drive"] = "%s%s (revision %s)" % (
lines.append(" Extraction engine: cdparanoia %s" % ripResult.vendor, ripResult.model, ripResult.release)
ripResult.cdparanoiaVersion) data["Extraction engine"] = "cdparanoia %s" % (
if ripResult.cdparanoiaDefeatsCache is None: ripResult.cdparanoiaVersion)
defeat = "Unknown" data["Defeat audio cache"] = ripResult.cdparanoiaDefeatsCache
elif ripResult.cdparanoiaDefeatsCache: data["Read offset correction"] = ripResult.offset
defeat = "Yes"
else:
defeat = "No"
lines.append(" Defeat audio cache: %s" % defeat)
lines.append(" Read offset correction: %+d" % ripResult.offset)
# Currently unsupported by the official cdparanoia package # Currently unsupported by the official cdparanoia package
over = "No"
# Only implemented in whipper (ripResult.overread) # Only implemented in whipper (ripResult.overread)
if ripResult.overread: data["Overread into lead-out"] = True if ripResult.overread else False
over = "Yes"
lines.append(" Overread into lead-out: %s" % over)
# Next one fully works only using the patched cdparanoia package # Next one fully works only using the patched cdparanoia package
# lines.append("Fill up missing offset samples with silence: Yes") # lines.append("Fill up missing offset samples with silence: true")
lines.append(" Gap detection: cdrdao %s" % ripResult.cdrdaoVersion) data["Gap detection"] = "cdrdao %s" % ripResult.cdrdaoVersion
if ripResult.isCdr:
isCdr = "Yes" data["CD-R detected"] = ripResult.isCdr
else: riplog["Ripping phase information"] = data
isCdr = "No"
lines.append(" CD-R detected: %s" % isCdr)
lines.append("")
# CD metadata # CD metadata
lines.append("CD metadata:") release = OrderedDict()
lines.append(" Album: %s - %s" % (ripResult.artist, ripResult.title)) release["Artist"] = ripResult.artist
lines.append(" CDDB Disc ID: %s" % ripResult. table.getCDDBDiscId()) release["Title"] = ripResult.title
lines.append(" MusicBrainz Disc ID: %s" % data = OrderedDict()
ripResult. table.getMusicBrainzDiscId()) data["Release"] = release
lines.append(" MusicBrainz lookup url: %s" % data["CDDB Disc ID"] = ripResult.table.getCDDBDiscId()
ripResult. table.getMusicBrainzSubmitURL()) data["MusicBrainz Disc ID"] = ripResult.table.getMusicBrainzDiscId()
lines.append("") data["MusicBrainz lookup URL"] = (
ripResult.table.getMusicBrainzSubmitURL())
if ripResult.metadata:
data["MusicBrainz Release URL"] = ripResult.metadata.url
riplog["CD metadata"] = data
# TOC section # TOC section
lines.append("TOC:") data = OrderedDict()
table = ripResult.table table = ripResult.table
# Test for HTOA presence # Test for HTOA presence
@@ -89,154 +84,171 @@ class WhipperLogger(result.Logger):
htoastart = htoa.absolute htoastart = htoa.absolute
htoaend = table.getTrackEnd(0) htoaend = table.getTrackEnd(0)
htoalength = table.tracks[0].getIndex(1).absolute - htoastart htoalength = table.tracks[0].getIndex(1).absolute - htoastart
lines.append(" 0:") track = OrderedDict()
lines.append(" Start: %s" % common.framesToMSF(htoastart)) track["Start"] = common.framesToMSF(htoastart)
lines.append(" Length: %s" % common.framesToMSF(htoalength)) track["Length"] = common.framesToMSF(htoalength)
lines.append(" Start sector: %d" % htoastart) track["Start sector"] = htoastart
lines.append(" End sector: %d" % htoaend) track["End sector"] = htoaend
lines.append("") data[0] = track
# For every track include information in the TOC # For every track include information in the TOC
for t in table.tracks: 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 start = t.getIndex(1).absolute
length = table.getTrackLength(t.number) length = table.getTrackLength(t.number)
end = table.getTrackEnd(t.number) end = table.getTrackEnd(t.number)
lines.append(" %d:" % t.number) track = OrderedDict()
lines.append(" Start: %s" % common.framesToMSF(start)) track["Start"] = common.framesToMSF(start)
lines.append(" Length: %s" % common.framesToMSF(length)) track["Length"] = common.framesToMSF(length)
lines.append(" Start sector: %d" % start) track["Start sector"] = start
lines.append(" End sector: %d" % end) track["End sector"] = end
lines.append("") data[t.number] = track
riplog["TOC"] = data
# Tracks section # Tracks section
lines.append("Tracks:") data = OrderedDict()
duration = 0.0 duration = 0.0
for t in ripResult.tracks: for t in ripResult.tracks:
if not t.filename: if not t.filename:
continue 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._inARDatabase += int(ARDB_entry)
self._accuratelyRipped += int(ARDB_match) self._accuratelyRipped += int(ARDB_match)
lines.extend(track_lines) data[t.number] = track_dict
lines.append("")
duration += t.testduration + t.copyduration duration += t.testduration + t.copyduration
riplog["Tracks"] = data
# Status report # Status report
lines.append("Conclusive status report:") data = OrderedDict()
arHeading = " AccurateRip summary:"
if self._inARDatabase == 0: if self._inARDatabase == 0:
lines.append("%s None of the tracks are present in the " message = ("None of the tracks are present in the "
"AccurateRip database" % arHeading) "AccurateRip database")
else: else:
nonHTOA = len(ripResult.tracks) nonHTOA = len(ripResult.tracks)
if ripResult.tracks[0].number == 0: if ripResult.tracks[0].number == 0:
nonHTOA -= 1 nonHTOA -= 1
if self._accuratelyRipped == 0: if self._accuratelyRipped == 0:
lines.append("%s No tracks could be verified as accurate " message = ("No tracks could be verified as accurate "
"(you may have a different pressing from the " "(you may have a different pressing from the "
"one(s) in the database)" % arHeading) "one(s) in the database)")
elif self._accuratelyRipped < nonHTOA: elif self._accuratelyRipped < nonHTOA:
accurateTracks = nonHTOA - self._accuratelyRipped accurateTracks = nonHTOA - self._accuratelyRipped
lines.append("%s Some tracks could not be verified as " message = ("Some tracks could not be verified as "
"accurate (%d/%d got no match)" % ( "accurate (%d/%d got no match)") % (
arHeading, accurateTracks, nonHTOA)) accurateTracks, nonHTOA)
else: else:
lines.append("%s All tracks accurately ripped" % arHeading) message = "All tracks accurately ripped"
data["AccurateRip summary"] = message
hsHeading = " Health status:"
if self._errors: if self._errors:
lines.append("%s There were errors" % hsHeading) message = "There were errors"
else: else:
lines.append("%s No errors occurred" % hsHeading) message = "No errors occurred"
lines.append(" EOF: End of status report") data["Health Status"] = message
lines.append("") 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 # Log hash
hasher = hashlib.sha256() hasher = hashlib.sha256()
hasher.update("\n".join(lines).encode("utf-8")) hasher.update(riplog.encode("utf-8"))
lines.append("SHA-256 hash: %s" % hasher.hexdigest().upper()) riplog += "\nSHA-256 hash: %s\n" % hasher.hexdigest().upper()
lines.append("") return riplog
return lines
def trackLog(self, trackResult): def trackLog(self, trackResult):
"""Returns Tracks section lines: data picked from trackResult""" """Returns Tracks section lines: data picked from trackResult"""
lines = [] track = OrderedDict()
# Track number
lines.append(" %d:" % trackResult.number)
# Filename (including path) of ripped track # Filename (including path) of ripped track
lines.append(" Filename: %s" % trackResult.filename) track["Filename"] = trackResult.filename
# Pre-gap length # Pre-gap length
pregap = trackResult.pregap pregap = trackResult.pregap
if pregap: if pregap:
lines.append(" Pre-gap length: %s" % common.framesToMSF(pregap)) track["Pre-gap length"] = common.framesToMSF(pregap)
# Peak level # Peak level
peak = trackResult.peak / 32768.0 peak = trackResult.peak / 32768.0
lines.append(" Peak level: %.6f" % peak) track["Peak level"] = float("%.6f" % peak)
# Pre-emphasis status # Pre-emphasis status
# Only implemented in whipper (trackResult.pre_emphasis) # Only implemented in whipper (trackResult.pre_emphasis)
if trackResult.pre_emphasis: track["Pre-emphasis"] = trackResult.pre_emphasis
preEmph = "Yes"
else:
preEmph = "No"
lines.append(" Pre-emphasis: %s" % preEmph)
# Extraction speed # Extraction speed
if trackResult.copyspeed: if trackResult.copyspeed:
lines.append(" Extraction speed: %.1f X" % ( track["Extraction speed"] = "%.1f X" % trackResult.copyspeed
trackResult.copyspeed))
# Extraction quality # Extraction quality
if trackResult.quality and trackResult.quality > 0.001: if trackResult.quality and trackResult.quality > 0.001:
lines.append(" Extraction quality: %.2f %%" % track["Extraction quality"] = "%.2f %%" % (
(trackResult.quality * 100.0, )) trackResult.quality * 100.0, )
# Ripper Test CRC # Ripper Test CRC
if trackResult.testcrc is not None: if trackResult.testcrc is not None:
lines.append(" Test CRC: %08X" % trackResult.testcrc) track["Test CRC"] = "%08X" % trackResult.testcrc
# Ripper Copy CRC # Ripper Copy CRC
if trackResult.copycrc is not None: if trackResult.copycrc is not None:
lines.append(" Copy CRC: %08X" % trackResult.copycrc) track["Copy CRC"] = "%08X" % trackResult.copycrc
# AccurateRip track status # AccurateRip track status
ARDB_entry = 0 ARDB_entry = 0
ARDB_match = 0 ARDB_match = 0
for v in ("v1", "v2"): for v in ("v1", "v2"):
data = OrderedDict()
if trackResult.AR[v]["DBCRC"]: if trackResult.AR[v]["DBCRC"]:
lines.append(" AccurateRip %s:" % v)
ARDB_entry += 1 ARDB_entry += 1
if trackResult.AR[v]["CRC"] == trackResult.AR[v]["DBCRC"]: if trackResult.AR[v]["CRC"] == trackResult.AR[v]["DBCRC"]:
lines.append(" Result: Found, exact match") data["Result"] = "Found, exact match"
ARDB_match += 1 ARDB_match += 1
else: else:
lines.append(" Result: Found, NO exact match") data["Result"] = "Found, NO exact match"
lines.append( data["Confidence"] = trackResult.AR[v]["DBConfidence"]
" Confidence: %d" % trackResult.AR[v]["DBConfidence"] data["Local CRC"] = trackResult.AR[v]["CRC"].upper()
) data["Remote CRC"] = trackResult.AR[v]["DBCRC"].upper()
lines.append(
" Local CRC: %s" % trackResult.AR[v]["CRC"].upper()
)
lines.append(
" Remote CRC: %s" % trackResult.AR[v]["DBCRC"].upper()
)
elif trackResult.number != 0: elif trackResult.number != 0:
lines.append(" AccurateRip %s:" % v) data["Result"] = "Track not present in AccurateRip database"
lines.append( track["AccurateRip %s" % v] = data
" Result: Track not present in AccurateRip database"
)
# Check if Test & Copy CRCs are equal # Check if Test & Copy CRCs are equal
if trackResult.testcrc == trackResult.copycrc: if trackResult.testcrc == trackResult.copycrc:
lines.append(" Status: Copy OK") track["Status"] = "Copy OK"
else: else:
self._errors = True self._errors = True
lines.append(" Status: Error, CRC mismatch") track["Status"] = "Error, CRC mismatch"
return lines, bool(ARDB_entry), bool(ARDB_match) 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 hold information about the result for rips.
I can be used to write log files. I can be used to write log files.
@ivar offset: sample read offset :cvar offset: sample read offset
@ivar table: the full index table :cvar table: the full index table
@type table: L{whipper.image.table.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 :cvar vendor: vendor of the CD drive
@ivar model: model of the CD drive :cvar model: model of the CD drive
@ivar release: release of the CD drive :cvar release: release of the CD drive
@ivar cdrdaoVersion: version of cdrdao used for the rip :cvar cdrdaoVersion: version of cdrdao used for the rip
@ivar cdparanoiaVersion: version of cdparanoia used for the rip :cvar cdparanoiaVersion: version of cdparanoia used for the rip
""" """
offset = 0 offset = 0
@@ -88,6 +90,7 @@ class RipResult:
table = None table = None
artist = None artist = None
title = None title = None
metadata = None
vendor = None vendor = None
model = None model = None
@@ -104,10 +107,10 @@ class RipResult:
def getTrackResult(self, number): def getTrackResult(self, number):
""" """
@param number: the track number (0 for HTOA) :param number: the track number (0 for HTOA)
@type number: int :type number: int
@rtype: L{TrackResult} :rtype: TrackResult
""" """
for t in self.tracks: for t in self.tracks:
if t.number == number: if t.number == number:
@@ -125,11 +128,11 @@ class Logger(object):
""" """
Create a log from the given ripresult. Create a log from the given ripresult.
@param epoch: when the log file gets generated :param epoch: when the log file gets generated
@type epoch: float :type epoch: float
@type ripResult: L{RipResult} :type ripResult: RipResult
@rtype: str :rtype: str
""" """
raise NotImplementedError raise NotImplementedError
@@ -140,7 +143,8 @@ class Logger(object):
class EntryPoint(object): class EntryPoint(object):
name = 'whipper' name = 'whipper'
def load(self): @staticmethod
def load():
from whipper.result import logger from whipper.result import logger
return logger.WhipperLogger return logger.WhipperLogger
@@ -149,7 +153,7 @@ def getLoggers():
""" """
Get all logger plugins with entry point 'whipper.logger'. Get all logger plugins with entry point 'whipper.logger'.
@rtype: dict of C{str} -> C{Logger} :rtype: dict of :class:`str` -> :any:`Logger`
""" """
d = {} d = {}

View File

@@ -46,6 +46,7 @@ class TestCase(unittest.TestCase):
# and we'd like to check for the actual exception under TaskException, # and we'd like to check for the actual exception under TaskException,
# so override the way twisted.trial.unittest does, without failure # 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): def failUnlessRaises(self, exception, f, *args, **kwargs):
try: try:
result = f(*args, **kwargs) result = f(*args, **kwargs)
@@ -53,7 +54,7 @@ class TestCase(unittest.TestCase):
return inst return inst
except exception as e: except exception as e:
raise Exception('%s raised instead of %s:\n %s' % 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: else:
raise Exception('%s not raised (%r returned)' % raise Exception('%s not raised (%r returned)' %
@@ -62,7 +63,8 @@ class TestCase(unittest.TestCase):
assertRaises = failUnlessRaises assertRaises = failUnlessRaises
def readCue(self, name): @staticmethod
def readCue(name):
""" """
Read a .cue file, and replace the version comment with the current Read a .cue file, and replace the version comment with the current
version so we can use it in comparisons. version so we can use it in comparisons.
@@ -71,7 +73,7 @@ class TestCase(unittest.TestCase):
ret = open(cuefile).read().decode('utf-8') ret = open(cuefile).read().decode('utf-8')
ret = re.sub( ret = re.sub(
'REM COMMENT "whipper.*', 'REM COMMENT "whipper.*',
'REM COMMENT "whipper %s"' % (whipper.__version__), 'REM COMMENT "whipper %s"' % whipper.__version__,
ret, re.MULTILINE) ret, re.MULTILINE)
return ret 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].discId1, '0000f21c')
self.assertEqual(responses[1].discId2, '00027ef8') self.assertEqual(responses[1].discId2, '00027ef8')
self.assertEqual(responses[1].cddbDiscId, '05021002') self.assertEqual(responses[1].cddbDiscId, '05021002')
self.assertEqual(responses[1].confidences[0], 5) self.assertEqual(responses[1].confidences[0], 6)
self.assertEqual(responses[1].confidences[1], 5) self.assertEqual(responses[1].confidences[1], 6)
self.assertEqual(responses[1].checksums[0], 'dc77f9ab') self.assertEqual(responses[1].checksums[0], 'dc77f9ab')
self.assertEqual(responses[1].checksums[1], 'dd97d2c3') self.assertEqual(responses[1].checksums[1], 'dd97d2c3')
@@ -203,7 +203,7 @@ class TestVerifyResult(TestCase):
'v2': { 'v2': {
'CRC': 'dc77f9ab', 'CRC': 'dc77f9ab',
'DBCRC': 'dc77f9ab', 'DBCRC': 'dc77f9ab',
'DBConfidence': 5, 'DBConfidence': 6
}, },
'DBMaxConfidence': 12, 'DBMaxConfidence': 12,
'DBMaxConfidenceCRC': '284fc705', 'DBMaxConfidenceCRC': '284fc705',
@@ -217,7 +217,7 @@ class TestVerifyResult(TestCase):
'v2': { 'v2': {
'CRC': 'dd97d2c3', 'CRC': 'dd97d2c3',
'DBCRC': 'dd97d2c3', 'DBCRC': 'dd97d2c3',
'DBConfidence': 5, 'DBConfidence': 6,
}, },
'DBMaxConfidence': 20, 'DBMaxConfidence': 20,
'DBMaxConfidenceCRC': '9cc1f32e', 'DBMaxConfidenceCRC': '9cc1f32e',

View File

@@ -1,5 +1,5 @@
# -*- Mode: Python; test-case-name: whipper.test.test_common_mbngs -*- # -*- 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 os
import json import json
@@ -12,15 +12,17 @@ from whipper.common import mbngs
class MetadataTestCase(unittest.TestCase): class MetadataTestCase(unittest.TestCase):
# Generated with rip -R cd info # Generated with rip -R cd info
def testJeffEverybodySingle(self): def testMissingReleaseDate(self):
filename = 'whipper.release.3451f29c-9bb8-4cc5-bfcc-bd50104b94f8.json' # 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) path = os.path.join(os.path.dirname(__file__), filename)
handle = open(path, "rb") handle = open(path, "rb")
response = json.loads(handle.read()) response = json.loads(handle.read())
handle.close() handle.close()
discid = "wbjbST2jUHRZaB1inCyxxsL7Eqc-" discid = "b.yqPuCBdsV5hrzDvYrw52iK_jE-"
metadata = mbngs._getMetadata({}, response['release'], discid) metadata = mbngs._getMetadata(response['release'], discid)
self.assertFalse(metadata.release) self.assertFalse(metadata.release)
@@ -33,21 +35,22 @@ class MetadataTestCase(unittest.TestCase):
handle.close() handle.close()
discid = "f7XO36a7n1LCCskkCiulReWbwZA-" discid = "f7XO36a7n1LCCskkCiulReWbwZA-"
metadata = mbngs._getMetadata({}, response['release'], discid) metadata = mbngs._getMetadata(response['release'], discid)
self.assertEqual(metadata.artist, u'Various Artists') self.assertEqual(metadata.artist, u'Various Artists')
self.assertEqual(metadata.release, u'2001-10-15') self.assertEqual(metadata.release, u'2001-10-15')
self.assertEqual(metadata.mbidArtist, self.assertEqual(metadata.mbidArtist,
u'89ad4ac3-39f7-470e-963a-56509c546377') [u'89ad4ac3-39f7-470e-963a-56509c546377'])
self.assertEqual(len(metadata.tracks), 18) self.assertEqual(len(metadata.tracks), 18)
track16 = metadata.tracks[15] track16 = metadata.tracks[15]
self.assertEqual(track16.artist, 'Tom Jones & Stereophonics') self.assertEqual(track16.artist, 'Tom Jones & Stereophonics')
self.assertEqual(track16.mbidArtist, self.assertEqual(track16.mbidArtist, [
u'57c6f649-6cde-48a7-8114-2a200247601a' u'57c6f649-6cde-48a7-8114-2a200247601a',
';0bfba3d3-6a04-4779-bb0a-df07df5b0558') u'0bfba3d3-6a04-4779-bb0a-df07df5b0558',
])
self.assertEqual(track16.sortName, self.assertEqual(track16.sortName,
u'Jones, Tom & Stereophonics') u'Jones, Tom & Stereophonics')
@@ -60,15 +63,16 @@ class MetadataTestCase(unittest.TestCase):
handle.close() handle.close()
discid = "xAq8L4ELMW14.6wI6tt7QAcxiDI-" 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.artist, u'Isobel Campbell & Mark Lanegan')
self.assertEqual(metadata.sortName, self.assertEqual(metadata.sortName,
u'Campbell, Isobel & Lanegan, Mark') u'Campbell, Isobel & Lanegan, Mark')
self.assertEqual(metadata.release, u'2006-01-30') self.assertEqual(metadata.release, u'2006-01-30')
self.assertEqual(metadata.mbidArtist, self.assertEqual(metadata.mbidArtist, [
u'd51f3a15-12a2-41a0-acfa-33b5eae71164;' u'd51f3a15-12a2-41a0-acfa-33b5eae71164',
'a9126556-f555-4920-9617-6e013f8228a7') u'a9126556-f555-4920-9617-6e013f8228a7',
])
self.assertEqual(len(metadata.tracks), 12) self.assertEqual(len(metadata.tracks), 12)
@@ -78,9 +82,10 @@ class MetadataTestCase(unittest.TestCase):
self.assertEqual(track12.sortName, self.assertEqual(track12.sortName,
u'Campbell, Isobel' u'Campbell, Isobel'
' & Lanegan, Mark') ' & Lanegan, Mark')
self.assertEqual(track12.mbidArtist, self.assertEqual(track12.mbidArtist, [
u'd51f3a15-12a2-41a0-acfa-33b5eae71164;' u'd51f3a15-12a2-41a0-acfa-33b5eae71164',
'a9126556-f555-4920-9617-6e013f8228a7') u'a9126556-f555-4920-9617-6e013f8228a7',
])
def testMalaInCuba(self): def testMalaInCuba(self):
# single artist disc, but with multiple artists tracks # single artist disc, but with multiple artists tracks
@@ -92,13 +97,13 @@ class MetadataTestCase(unittest.TestCase):
handle.close() handle.close()
discid = "u0aKVpO.59JBy6eQRX2vYcoqQZ0-" discid = "u0aKVpO.59JBy6eQRX2vYcoqQZ0-"
metadata = mbngs._getMetadata({}, response['release'], discid) metadata = mbngs._getMetadata(response['release'], discid)
self.assertEqual(metadata.artist, u'Mala') self.assertEqual(metadata.artist, u'Mala')
self.assertEqual(metadata.sortName, u'Mala') self.assertEqual(metadata.sortName, u'Mala')
self.assertEqual(metadata.release, u'2012-09-17') self.assertEqual(metadata.release, u'2012-09-17')
self.assertEqual(metadata.mbidArtist, self.assertEqual(metadata.mbidArtist,
u'09f221eb-c97e-4da5-ac22-d7ab7c555bbb') [u'09f221eb-c97e-4da5-ac22-d7ab7c555bbb'])
self.assertEqual(len(metadata.tracks), 14) 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.artist, u'Mala feat. Dreiser & Sexto Sentido')
self.assertEqual(track6.sortName, self.assertEqual(track6.sortName,
u'Mala feat. Dreiser & Sexto Sentido') u'Mala feat. Dreiser & Sexto Sentido')
self.assertEqual(track6.mbidArtist, self.assertEqual(track6.mbidArtist, [
u'09f221eb-c97e-4da5-ac22-d7ab7c555bbb' u'09f221eb-c97e-4da5-ac22-d7ab7c555bbb',
';ec07a209-55ff-4084-bc41-9d4d1764e075' u'ec07a209-55ff-4084-bc41-9d4d1764e075',
';f626b92e-07b1-4a19-ad13-c09d690db66c') u'f626b92e-07b1-4a19-ad13-c09d690db66c',
])
def testNorthernGateway(self): def testUnknownArtist(self):
""" """
check the received metadata for artists tagged with [unknown] check the received metadata for artists tagged with [unknown]
and artists tagged with an alias in MusicBrainz and artists tagged with an alias in MusicBrainz
see https://github.com/whipper-team/whipper/issues/155 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) path = os.path.join(os.path.dirname(__file__), filename)
handle = open(path, "rb") handle = open(path, "rb")
response = json.loads(handle.read()) response = json.loads(handle.read())
handle.close() handle.close()
discid = "rzGHHqfPWIq1GsOLhhlBcZuqo.I-" discid = "RhrwgVb0hZNkabQCw1dZIhdbMFg-"
metadata = mbngs._getMetadata({}, response['release'], discid) metadata = mbngs._getMetadata(response['release'], discid)
self.assertEqual(metadata.artist, u'Various Artists') self.assertEqual(metadata.artist, u'CunninLynguists')
self.assertEqual(metadata.release, u'2010') self.assertEqual(metadata.release, u'2003')
self.assertEqual(metadata.mbidArtist, 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(track8.artist, u'???')
self.assertEqual(track2.sortName, self.assertEqual(track8.sortName, u'[unknown]')
u'Twisted Reaction feat. [unknown]') self.assertEqual(track8.mbidArtist,
self.assertEqual(track2.mbidArtist, [u'125ec42a-7229-4250-afc5-e057484327fe'])
u'4f69f624-73ea-4a16-b822-bd2ca58032bf'
';125ec42a-7229-4250-afc5-e057484327fe')
track4 = metadata.tracks[3] track9 = metadata.tracks[8]
self.assertEqual(track4.artist, u'BioGenesis') self.assertEqual(track9.artist, u'CunninLynguists feat. Tonedeff')
self.assertEqual(track4.sortName, self.assertEqual(track9.sortName,
u'Bio Genesis') u'CunninLynguists feat. Tonedeff')
self.assertEqual(track4.mbidArtist, self.assertEqual(track9.mbidArtist, [
u'dd61b86c-c015-43e1-9a28-58fceb0975c8') u'69c4cc43-8163-41c5-ac81-30946d27bb69',
u'b3869d83-9fb5-4eac-b5ca-2d155fcbee12'
])
def testNenaAndKimWildSingle(self): def testNenaAndKimWildSingle(self):
""" """
@@ -163,12 +171,13 @@ class MetadataTestCase(unittest.TestCase):
handle.close() handle.close()
discid = "X2c2IQ5vUy5x6Jh7Xi_DGHtA1X8-" 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.artist, u'Nena & Kim Wilde')
self.assertEqual(metadata.release, u'2003-05-19') self.assertEqual(metadata.release, u'2003-05-19')
self.assertEqual(metadata.mbidArtist, self.assertEqual(metadata.mbidArtist, [
u'38bfaa7f-ee98-48cb-acd0-946d7aeecd76' u'38bfaa7f-ee98-48cb-acd0-946d7aeecd76',
';4b462375-c508-432a-8c88-ceeec38b16ae') u'4b462375-c508-432a-8c88-ceeec38b16ae',
])
self.assertEqual(len(metadata.tracks), 4) 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.artist, u'Nena & Kim Wilde')
self.assertEqual(track1.sortName, u'Nena & Wilde, Kim') self.assertEqual(track1.sortName, u'Nena & Wilde, Kim')
self.assertEqual(track1.mbidArtist, self.assertEqual(track1.mbidArtist, [
u'38bfaa7f-ee98-48cb-acd0-946d7aeecd76' u'38bfaa7f-ee98-48cb-acd0-946d7aeecd76',
';4b462375-c508-432a-8c88-ceeec38b16ae') 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] track2 = metadata.tracks[1]
self.assertEqual(track2.artist, u'Nena & Kim Wilde') self.assertEqual(track2.artist, u'Nena & Kim Wilde')
self.assertEqual(track2.sortName, u'Nena & Wilde, Kim') self.assertEqual(track2.sortName, u'Nena & Wilde, Kim')
self.assertEqual(track2.mbidArtist, self.assertEqual(track2.mbidArtist, [
u'38bfaa7f-ee98-48cb-acd0-946d7aeecd76' u'38bfaa7f-ee98-48cb-acd0-946d7aeecd76',
';4b462375-c508-432a-8c88-ceeec38b16ae') 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, path = prog.getPath(u'/tmp', DEFAULT_DISC_TEMPLATE,
'mbdiscid', None) 'mbdiscid', None)
self.assertEqual(path, self.assertEqual(path, (u'/tmp/unknown/Unknown Artist - mbdiscid/'
unicode('/tmp/unknown/Unknown Artist - mbdiscid/' u'Unknown Artist - mbdiscid'))
'Unknown Artist - mbdiscid'))
def testStandardTemplateFilled(self): def testStandardTemplateFilled(self):
prog = program.Program(config.Config()) prog = program.Program(config.Config())
@@ -27,9 +26,8 @@ class PathTestCase(unittest.TestCase):
path = prog.getPath(u'/tmp', DEFAULT_DISC_TEMPLATE, path = prog.getPath(u'/tmp', DEFAULT_DISC_TEMPLATE,
'mbdiscid', md, 0) 'mbdiscid', md, 0)
self.assertEqual(path, self.assertEqual(path, (u'/tmp/unknown/Jeff Buckley - Grace/'
unicode('/tmp/unknown/Jeff Buckley - Grace/' u'Jeff Buckley - Grace'))
'Jeff Buckley - Grace'))
def testIssue66TemplateFilled(self): def testIssue66TemplateFilled(self):
prog = program.Program(config.Config()) prog = program.Program(config.Config())

View File

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

View File

@@ -320,6 +320,20 @@ class TOTBLTestCase(common.TestCase):
self.assertEqual(self.toc.table.getCDDBDiscId(), '810b7b0b') 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 # 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') 'strokes-someday.eac.cue')).read()).decode('utf-8')
common.diffStrings(ref, cue) common.diffStrings(ref, cue)
def _filterCue(self, output): @staticmethod
def _filterCue(output):
# helper to be able to compare our generated .cue with the # helper to be able to compare our generated .cue with the
# EAC-extracted one # EAC-extracted one
discard = ['TITLE', 'PERFORMER', 'FLAGS', 'REM'] discard = ['TITLE', 'PERFORMER', 'FLAGS', 'REM']

View File

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

View File

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