Merge branch 'develop'
This commit is contained in:
28
.travis.yml
28
.travis.yml
@@ -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
|
||||||
|
|||||||
66
CHANGELOG.md
66
CHANGELOG.md
@@ -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 doesn’t 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))
|
||||||
|
|||||||
54
COVERAGE
54
COVERAGE
@@ -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%
|
||||||
|
|||||||
21
Dockerfile
21
Dockerfile
@@ -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
|
||||||
|
|
||||||
|
|||||||
93
README.md
93
README.md
@@ -8,12 +8,14 @@
|
|||||||
[](https://github.com/whipper-team/whipper/issues)
|
[](https://github.com/whipper-team/whipper/issues)
|
||||||
[](https://github.com/whipper-team/whipper/graphs/contributors)
|
[](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
|
|||||||
|
|
||||||
[](https://repology.org/metapackage/whipper)
|
[](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
4
TODO
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -3,3 +3,5 @@ mutagen
|
|||||||
pycdio>0.20
|
pycdio>0.20
|
||||||
PyGObject
|
PyGObject
|
||||||
requests
|
requests
|
||||||
|
ruamel.yaml
|
||||||
|
setuptools_scm
|
||||||
|
|||||||
35
scripts/accuraterip-checksum
Normal file
35
scripts/accuraterip-checksum
Normal 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)
|
||||||
15
setup.py
15
setup.py
@@ -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
1
src/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
accuraterip-checksum
|
|
||||||
47
src/Makefile
47
src/Makefile
@@ -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
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
VERSION = 1.4
|
|
||||||
|
|
||||||
# paths
|
|
||||||
PREFIX = /usr/local
|
|
||||||
|
|
||||||
# flags
|
|
||||||
CFLAGS = -std=c99
|
|
||||||
LDFLAGS = -lsndfile
|
|
||||||
|
|
||||||
# compiler and linker
|
|
||||||
CC = cc
|
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -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
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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.
|
||||||
"""
|
"""
|
||||||
|
|||||||
20
whipper/extern/asyncsub.py
vendored
20
whipper/extern/asyncsub.py
vendored
@@ -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)
|
||||||
|
|||||||
5
whipper/extern/freedb.py
vendored
5
whipper/extern/freedb.py
vendored
@@ -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"."):
|
||||||
|
|||||||
69
whipper/extern/task/task.py
vendored
69
whipper/extern/task/task.py
vendored
@@ -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):
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)))
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 = {}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
91
whipper/test/gentlemen.fast.toc
Normal file
91
whipper/test/gentlemen.fast.toc
Normal 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
|
||||||
|
|
||||||
30
whipper/test/test_command_mblookup.py
Normal file
30
whipper/test/test_command_mblookup.py
Normal 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()
|
||||||
@@ -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',
|
||||||
|
|||||||
@@ -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'])
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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']
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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]))
|
||||||
|
|||||||
80
whipper/test/test_result_logger.log
Normal file
80
whipper/test/test_result_logger.log
Normal 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
|
||||||
169
whipper/test/test_result_logger.py
Normal file
169
whipper/test/test_result_logger.py
Normal 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()
|
||||||
|
)
|
||||||
538
whipper/test/whipper.discid.xu338_M8WukSRi0J.KTlDoflB8Y-.pickle
Normal file
538
whipper/test/whipper.discid.xu338_M8WukSRi0J.KTlDoflB8Y-.pickle
Normal 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.
|
||||||
@@ -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
Reference in New Issue
Block a user