Merge branch 'develop'
This commit is contained in:
28
.travis.yml
28
.travis.yml
@@ -1,6 +1,13 @@
|
||||
dist: xenial
|
||||
sudo: required
|
||||
|
||||
language: bash
|
||||
language: python
|
||||
python:
|
||||
- "2.7"
|
||||
virtualenv:
|
||||
system_site_packages: false
|
||||
|
||||
cache: pip
|
||||
|
||||
env:
|
||||
- FLAKE8=false
|
||||
@@ -9,21 +16,18 @@ env:
|
||||
install:
|
||||
# Dependencies
|
||||
- sudo apt-get -qq update
|
||||
- sudo pip install --upgrade -qq pip
|
||||
- sudo apt-get -qq install cdparanoia cdrdao flac gir1.2-glib-2.0 libcdio-dev libiso9660-dev libsndfile1-dev python-cddb python-gi python-musicbrainzngs python-mutagen python-setuptools sox swig libcdio-utils
|
||||
- sudo pip install pycdio==0.21 requests
|
||||
- pip install --upgrade -qq pip
|
||||
- sudo apt-get -qq install cdparanoia cdrdao flac gir1.2-glib-2.0 libcdio-dev libgirepository1.0-dev libiso9660-dev libsndfile1-dev sox swig libcdio-utils
|
||||
# newer version of pydcio requires newer version of libcdio than travis has
|
||||
- pip install pycdio==0.21
|
||||
# install rest of dependencies
|
||||
- pip install -r requirements.txt
|
||||
|
||||
# Testing dependencies
|
||||
- sudo apt-get -qq install python-twisted-core
|
||||
- sudo pip install flake8
|
||||
|
||||
# Build bundled C utils
|
||||
- cd src
|
||||
- sudo make install
|
||||
- cd ..
|
||||
- pip install twisted flake8
|
||||
|
||||
# Installing
|
||||
- sudo python setup.py install
|
||||
- python setup.py install
|
||||
|
||||
script:
|
||||
- if [ ! "$FLAKE8" = true ]; then python -m unittest discover; fi
|
||||
|
||||
66
CHANGELOG.md
66
CHANGELOG.md
@@ -2,7 +2,66 @@
|
||||
|
||||
## [Unreleased](https://github.com/whipper-team/whipper/tree/HEAD)
|
||||
|
||||
[Full Changelog](https://github.com/whipper-team/whipper/compare/v0.7.3...HEAD)
|
||||
[Full Changelog](https://github.com/whipper-team/whipper/compare/v0.8.0...HEAD)
|
||||
|
||||
## [v0.8.0](https://github.com/whipper-team/whipper/tree/v0.8.0) (2019-10-27)
|
||||
[Full Changelog](https://github.com/whipper-team/whipper/compare/v0.7.3...v0.8.0)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- whipper bails out if MusicBrainz release group 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)
|
||||
[Full Changelog](https://github.com/whipper-team/whipper/compare/v0.7.2...v0.7.3)
|
||||
@@ -15,7 +74,6 @@
|
||||
- Possible HTOA error [\#281](https://github.com/whipper-team/whipper/issues/281)
|
||||
- Disc template KeyError [\#279](https://github.com/whipper-team/whipper/issues/279)
|
||||
- Enhanced CD causes computer to freeze. [\#256](https://github.com/whipper-team/whipper/issues/256)
|
||||
- pycdio & libcdio issues [\#238](https://github.com/whipper-team/whipper/issues/238)
|
||||
- Unicode issues [\#215](https://github.com/whipper-team/whipper/issues/215)
|
||||
- whipper offset find exception [\#208](https://github.com/whipper-team/whipper/issues/208)
|
||||
- ZeroDivisionError: float division by zero [\#202](https://github.com/whipper-team/whipper/issues/202)
|
||||
@@ -26,7 +84,9 @@
|
||||
- On Ubuntu 18.10 cd-paranoia binary is called cdparanoia [\#347](https://github.com/whipper-team/whipper/issues/347)
|
||||
- WARNING:whipper.common.program:network error: NetworkError\(\) [\#338](https://github.com/whipper-team/whipper/issues/338)
|
||||
- Can not install [\#314](https://github.com/whipper-team/whipper/issues/314)
|
||||
- use standard logging [\#303](https://github.com/whipper-team/whipper/issues/303)
|
||||
- Write musicbrainz\_discid tag when disc is unknown [\#280](https://github.com/whipper-team/whipper/issues/280)
|
||||
- pycdio & libcdio issues [\#238](https://github.com/whipper-team/whipper/issues/238)
|
||||
- Write .toc files in addition to .cue files to support cdrdao and non-compliant .cue sheets [\#214](https://github.com/whipper-team/whipper/issues/214)
|
||||
|
||||
**Merged pull requests:**
|
||||
@@ -198,7 +258,6 @@
|
||||
- Add flake8 testing to CI [\#151](https://github.com/whipper-team/whipper/pull/151) ([Freso](https://github.com/Freso))
|
||||
- Clean up files in misc/ [\#150](https://github.com/whipper-team/whipper/pull/150) ([Freso](https://github.com/Freso))
|
||||
- Update .gitignore [\#148](https://github.com/whipper-team/whipper/pull/148) ([Freso](https://github.com/Freso))
|
||||
- Fix references to morituri. [\#109](https://github.com/whipper-team/whipper/pull/109) ([Freso](https://github.com/Freso))
|
||||
|
||||
## [v0.5.1](https://github.com/whipper-team/whipper/tree/v0.5.1) (2017-04-24)
|
||||
[Full Changelog](https://github.com/whipper-team/whipper/compare/v0.5.0...v0.5.1)
|
||||
@@ -271,6 +330,7 @@
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Fix references to morituri. [\#109](https://github.com/whipper-team/whipper/pull/109) ([Freso](https://github.com/Freso))
|
||||
- Small cleanups of setup.py [\#102](https://github.com/whipper-team/whipper/pull/102) ([Freso](https://github.com/Freso))
|
||||
- Persist False value for defeats\_cache correctly [\#98](https://github.com/whipper-team/whipper/pull/98) ([ribbons](https://github.com/ribbons))
|
||||
- Update suggested commands given by `drive list` [\#97](https://github.com/whipper-team/whipper/pull/97) ([ribbons](https://github.com/ribbons))
|
||||
|
||||
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 report -m
|
||||
|
||||
Name Stmts Miss Branch BrPart Cover Missing
|
||||
-----------------------------------------------------------------------------
|
||||
whipper/__init__.py 10 2 4 2 71% 9, 11, 8->9, 10->11
|
||||
whipper/__init__.py 15 5 4 2 63% 9-12, 16, 18, 15->16, 17->18
|
||||
whipper/__main__.py 7 7 2 0 0% 4-14
|
||||
whipper/command/__init__.py 0 0 0 0 100%
|
||||
whipper/command/accurip.py 43 43 18 0 0% 21-92
|
||||
whipper/command/basecommand.py 69 53 30 0 16% 56-114, 121-130, 133, 136, 139, 142-145
|
||||
whipper/command/cd.py 224 186 58 0 13% 71-79, 84-193, 196, 208, 231-284, 291-318, 321-491
|
||||
whipper/command/accurip.py 41 41 18 0 0% 21-90
|
||||
whipper/command/basecommand.py 69 29 30 8 53% 70, 72, 76, 82-88, 98-102, 107-114, 127, 129, 133, 139, 142-145, 68->70, 71->72, 75->76, 80->82, 96->98, 106->107, 126->127, 128->129
|
||||
whipper/command/cd.py 227 189 60 0 13% 72-80, 85-196, 199, 212, 236-288, 295-322, 325-496
|
||||
whipper/command/drive.py 57 57 10 0 0% 21-107
|
||||
whipper/command/image.py 38 38 6 0 0% 21-76
|
||||
whipper/command/main.py 68 68 22 0 0% 4-115
|
||||
whipper/command/mblookup.py 28 28 8 0 0% 1-41
|
||||
whipper/command/offset.py 110 110 32 0 0% 21-221
|
||||
whipper/command/main.py 68 68 22 0 0% 4-116
|
||||
whipper/command/mblookup.py 29 3 8 2 86% 21-23, 35->37, 37->28
|
||||
whipper/command/offset.py 110 110 32 0 0% 21-219
|
||||
whipper/common/__init__.py 0 0 0 0 100%
|
||||
whipper/common/accurip.py 133 5 54 5 95% 121, 130, 139-141, 116->121, 125->130, 155->158, 246->252, 255->261
|
||||
whipper/common/cache.py 105 50 34 6 44% 66-90, 96, 99, 107-112, 115-116, 132, 144-148, 171-178, 202-207, 212-228, 95->96, 98->99, 131->132, 142->152, 143->144, 170->171
|
||||
whipper/common/accurip.py 132 5 54 5 95% 119, 125, 134-136, 114->119, 120->125, 150->153, 241->247, 250->256
|
||||
whipper/common/cache.py 104 49 34 6 44% 66-90, 96, 99, 108-111, 114-115, 131, 143-147, 170-177, 201-206, 211-227, 95->96, 98->99, 130->131, 141->151, 142->143, 169->170
|
||||
whipper/common/checksum.py 26 14 2 0 43% 41-42, 45-46, 49-64
|
||||
whipper/common/common.py 150 28 38 6 78% 51-52, 119-120, 143-144, 162-169, 181, 275-280, 287-292, 329-333, 118->119, 131->134, 180->181, 190->197, 271->275, 327->335
|
||||
whipper/common/config.py 92 8 18 4 89% 105-106, 124-125, 131, 141, 143, 145, 130->131, 140->141, 142->143, 144->145
|
||||
whipper/common/common.py 150 28 38 6 78% 51-52, 119-120, 143-144, 162-169, 181, 274-279, 286-291, 328-332, 118->119, 131->134, 180->181, 190->197, 271->274, 326->334
|
||||
whipper/common/config.py 91 8 18 4 89% 105-106, 124-125, 131, 141, 143, 145, 130->131, 140->141, 142->143, 144->145
|
||||
whipper/common/directory.py 21 8 10 2 55% 29, 39, 44-51, 28->29, 38->39
|
||||
whipper/common/drive.py 31 20 6 0 35% 35-40, 44-50, 54-60, 64-71
|
||||
whipper/common/encode.py 44 23 2 0 46% 37-38, 41-42, 45-46, 53-56, 59-60, 63-64, 76-77, 80-81, 84-91
|
||||
whipper/common/mbngs.py 159 53 58 7 66% 38-39, 45, 90-96, 157-158, 163-164, 208, 211, 214, 237-239, 248, 268-322, 156->157, 162->163, 207->208, 210->211, 213->214, 236->237, 245->248
|
||||
whipper/common/mbngs.py 174 52 66 7 70% 38-39, 45, 93-99, 174-175, 180-181, 227, 233, 258-260, 269, 289-344, 159->158, 173->174, 179->180, 226->227, 232->233, 257->258, 266->269
|
||||
whipper/common/path.py 24 0 8 3 91% 42->45, 52->57, 62->67
|
||||
whipper/common/program.py 337 259 110 5 20% 85-87, 93-100, 109-141, 150-155, 158, 162-166, 211, 222-223, 225-229, 245-260, 268-380, 391-442, 450-458, 461-476, 487-527, 539-556, 559-577, 580-590, 593-601, 77->80, 208->211, 221->222, 224->225, 231->235
|
||||
whipper/common/renamer.py 102 2 16 1 97% 135, 158, 60->68
|
||||
whipper/common/task.py 77 19 14 2 75% 47-52, 86-87, 90-93, 101, 114-115, 122, 128, 134, 140, 146, 84->86, 98->101
|
||||
whipper/common/program.py 346 268 112 5 19% 85-87, 93-104, 113-147, 156-161, 164, 169-173, 218, 229-230, 232-236, 253-268, 276-387, 398-456, 464-472, 476-491, 502-542, 554-571, 574-592, 595-605, 608-616, 76->79, 215->218, 228->229, 231->232, 238->242
|
||||
whipper/common/renamer.py 102 2 16 1 97% 133, 156, 58->66
|
||||
whipper/common/task.py 77 19 14 2 75% 47-52, 86-87, 91-94, 102, 115-116, 123, 129, 135, 141, 147, 84->86, 99->102
|
||||
whipper/extern/__init__.py 0 0 0 0 100%
|
||||
whipper/extern/asyncsub.py 130 71 66 12 40% 15-17, 32, 37-38, 47-84, 89-102, 115, 122, 134, 145, 151, 156-160, 164-176, 14->15, 35->37, 45->47, 110->113, 114->115, 121->122, 133->134, 139->141, 141->152, 144->145, 148->151, 163->164
|
||||
whipper/extern/freedb.py 104 83 38 1 17% 49, 57-58, 61, 64, 84-162, 170-222, 56->57
|
||||
whipper/extern/freedb.py 104 83 38 1 17% 49, 57-58, 61, 64, 84-163, 171-223, 56->57
|
||||
whipper/extern/task/__init__.py 0 0 0 0 100%
|
||||
whipper/extern/task/task.py 277 116 54 11 54% 57, 61, 81, 87, 153-155, 174-176, 184-200, 218-221, 241-242, 283-284, 287-293, 308-309, 317-319, 328-335, 341-357, 361, 364, 371-388, 399-400, 403-406, 410, 413, 428, 431-433, 449, 461, 506-511, 520-525, 536-544, 547-555, 558-559, 567, 572-574, 56->57, 60->61, 69->71, 152->153, 166->exit, 217->218, 231->233, 236->exit, 496->498, 533->536, 571->572
|
||||
whipper/extern/task/task.py 271 115 54 11 53% 54, 58, 79, 87, 153-155, 174-176, 184-200, 218-221, 242-243, 284-285, 288-294, 309-310, 318-320, 329-336, 342-359, 363, 366, 373-390, 401-402, 405-408, 412, 415, 430, 433-435, 451, 463, 509-514, 521-526, 535-543, 546-554, 557-558, 566, 571-573, 53->54, 57->58, 66->68, 152->153, 166->exit, 217->218, 231->233, 236->exit, 498->500, 532->535, 570->571
|
||||
whipper/image/__init__.py 0 0 0 0 100%
|
||||
whipper/image/cue.py 91 9 20 3 89% 99, 116-117, 132-134, 159, 187, 205, 98->99, 115->116, 131->132
|
||||
whipper/image/image.py 117 94 18 0 17% 49-57, 65-67, 74-107, 121-153, 156-172, 183-213
|
||||
whipper/image/table.py 398 22 114 16 93% 237, 346-347, 499, 578, 663-664, 684-685, 694-697, 701-702, 747, 793-794, 796-797, 841-842, 847-849, 180->183, 498->499, 532->536, 555->558, 577->578, 585->592, 683->684, 692->698, 693->694, 722->726, 726->721, 746->747, 792->793, 795->796, 840->841, 846->847
|
||||
whipper/image/image.py 116 93 18 0 17% 49-57, 65-67, 74-107, 121-154, 157-173, 184-214
|
||||
whipper/image/table.py 395 18 114 16 93% 238, 497, 576, 661-662, 682-683, 692-695, 746, 792-793, 795-796, 840-841, 846-848, 181->184, 496->497, 530->534, 553->556, 575->576, 583->590, 681->682, 690->696, 691->692, 720->724, 724->719, 745->746, 791->792, 794->795, 839->840, 845->846
|
||||
whipper/image/toc.py 203 16 60 10 90% 134, 261-262, 278-281, 339-341, 363-365, 385, 409, 439, 130->134, 212->220, 260->261, 277->278, 287->292, 323->330, 338->339, 362->363, 372->376, 404->409
|
||||
whipper/program/__init__.py 0 0 0 0 100%
|
||||
whipper/program/arc.py 38 15 12 4 58% 26-28, 32, 37-41, 48-54, 22->26, 31->32, 36->37, 43->48
|
||||
whipper/program/cdparanoia.py 315 185 86 3 39% 48-50, 59-60, 124-126, 163-166, 199-200, 242-256, 259-310, 313-351, 354-358, 361-397, 452-504, 509-554, 587-590, 593, 600, 606, 611-616, 123->124, 599->600, 603->606
|
||||
whipper/program/cdrdao.py 59 36 14 2 34% 26-56, 63-69, 79-81, 85-87, 95, 102, 78->79, 84->85
|
||||
whipper/program/arc.py 3 0 0 0 100%
|
||||
whipper/program/cdparanoia.py 309 180 78 2 39% 48-50, 59-60, 124-126, 198-199, 239-253, 256-306, 309-347, 350-354, 357-393, 447-500, 505-552, 586-589, 592, 599, 607-612, 123->124, 598->599
|
||||
whipper/program/cdrdao.py 114 75 34 2 28% 33-58, 80-86, 90-105, 108-137, 140-144, 147-161, 168-171, 181-183, 187-189, 180->181, 186->187
|
||||
whipper/program/flac.py 9 5 0 0 44% 12-19
|
||||
whipper/program/sox.py 17 4 4 2 71% 18-19, 23-24, 17->18, 22->23
|
||||
whipper/program/soxi.py 28 2 2 1 90% 36, 49, 48->49
|
||||
whipper/program/utils.py 16 10 2 0 33% 11-12, 19-20, 30-35
|
||||
whipper/program/utils.py 23 16 2 0 28% 12-17, 25-31, 42-47
|
||||
whipper/result/__init__.py 0 0 0 0 100%
|
||||
whipper/result/logger.py 148 148 48 0 0% 1-242
|
||||
whipper/result/result.py 56 13 6 0 69% 112-116, 134, 144-145, 154-161
|
||||
whipper/result/logger.py 144 23 40 16 78% 68, 84-92, 112, 123, 128, 130, 134-135, 143, 202, 240, 244-245, 252-253, 67->68, 83->84, 111->112, 122->123, 127->128, 129->130, 133->134, 142->143, 201->202, 213->217, 217->222, 222->226, 226->230, 234->244, 236->240, 249->252
|
||||
whipper/result/result.py 57 13 6 0 70% 115-119, 137, 148-149, 158-165
|
||||
-----------------------------------------------------------------------------
|
||||
TOTAL 3961 1910 1104 108 49%
|
||||
TOTAL 3997 1766 1108 129 53%
|
||||
|
||||
21
Dockerfile
21
Dockerfile
@@ -1,31 +1,31 @@
|
||||
FROM debian:buster
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y cdrdao python-gobject-2 python-musicbrainzngs python-mutagen python-setuptools \
|
||||
python-cddb python-requests libsndfile1-dev flac sox \
|
||||
&& apt-get install -y cdrdao git python-gobject-2 python-musicbrainzngs python-mutagen \
|
||||
python-setuptools python-requests libsndfile1-dev flac sox \
|
||||
libiso9660-dev python-pip swig make pkgconf \
|
||||
eject locales \
|
||||
autoconf libtool curl \
|
||||
&& pip install pycdio==2.0.0
|
||||
&& pip install pycdio==2.1.0
|
||||
|
||||
# libcdio-paranoia / libcdio-utils are wrongfully packaged in Debian, thus built manually
|
||||
# see https://github.com/whipper-team/whipper/pull/237#issuecomment-367985625
|
||||
RUN curl -o - 'https://ftp.gnu.org/gnu/libcdio/libcdio-2.0.0.tar.gz' | tar zxf - \
|
||||
&& cd libcdio-2.0.0 \
|
||||
RUN curl -o - 'https://ftp.gnu.org/gnu/libcdio/libcdio-2.1.0.tar.bz2' | tar jxf - \
|
||||
&& cd libcdio-2.1.0 \
|
||||
&& autoreconf -fi \
|
||||
&& ./configure --disable-dependency-tracking --disable-cxx --disable-example-progs --disable-static \
|
||||
&& make install \
|
||||
&& cd .. \
|
||||
&& rm -rf libcdio-2.0.0
|
||||
&& rm -rf libcdio-2.1.0
|
||||
|
||||
# Install cd-paranoia from tarball
|
||||
RUN curl -o - 'https://ftp.gnu.org/gnu/libcdio/libcdio-paranoia-10.2+0.94+2.tar.gz' | tar zxf - \
|
||||
&& cd libcdio-paranoia-10.2+0.94+2 \
|
||||
RUN curl -o - 'https://ftp.gnu.org/gnu/libcdio/libcdio-paranoia-10.2+2.0.0.tar.bz2' | tar jxf - \
|
||||
&& cd libcdio-paranoia-10.2+2.0.0 \
|
||||
&& autoreconf -fi \
|
||||
&& ./configure --disable-dependency-tracking --disable-example-progs --disable-static \
|
||||
&& make install \
|
||||
&& cd .. \
|
||||
&& rm -rf libcdio-paranoia-10.2+0.94+2
|
||||
&& rm -rf libcdio-paranoia-10.2+2.0.0
|
||||
|
||||
RUN ldconfig
|
||||
|
||||
@@ -45,8 +45,7 @@ RUN echo "LC_ALL=en_US.UTF-8" >> /etc/environment \
|
||||
# install whipper
|
||||
RUN mkdir /whipper
|
||||
COPY . /whipper/
|
||||
RUN cd /whipper/src && make && make install \
|
||||
&& cd /whipper && python2 setup.py install \
|
||||
RUN cd /whipper && python2 setup.py install \
|
||||
&& rm -rf /whipper \
|
||||
&& whipper -v
|
||||
|
||||
|
||||
93
README.md
93
README.md
@@ -8,12 +8,14 @@
|
||||
[](https://github.com/whipper-team/whipper/issues)
|
||||
[](https://github.com/whipper-team/whipper/graphs/contributors)
|
||||
|
||||
Whipper is a Python 2.7 CD-DA ripper based on the [morituri project](https://github.com/thomasvs/morituri) (_CDDA ripper for *nix systems aiming for accuracy over speed_). It enhances morituri which development seems to have halted merging old ignored pull requests, improving it with bugfixes and new features.
|
||||
Whipper is a Python 2.7 CD-DA ripper based on the [morituri project](https://github.com/thomasvs/morituri) (_CDDA ripper for *nix systems aiming for accuracy over speed_). It started just as a fork of morituri - which development seems to have halted - merging old ignored pull requests, improving it with bugfixes and new features. Nowadays whipper's codebase diverges significantly from morituri's one.
|
||||
|
||||
Whipper is currently developed and tested _only_ on Linux distributions but _may_ work fine on other *nix OSes too.
|
||||
|
||||
In order to track whipper's latest changes it's advised to check its commit history (README and [CHANGELOG](#changelog) files may not be comprehensive).
|
||||
|
||||
We've nearly completed porting the codebase to Python 3 (Python 2 won't be supported anymore in future releases). If you would like to follow the progress of the port e/o help us with it, please check [pull request #411](https://github.com/whipper-team/whipper/pull/411).
|
||||
|
||||
## Table of content
|
||||
|
||||
- [Rationale](#rationale)
|
||||
@@ -34,6 +36,8 @@ In order to track whipper's latest changes it's advised to check its commit hist
|
||||
- [Logger plugins](#logger-plugins)
|
||||
- [License](#license)
|
||||
- [Contributing](#contributing)
|
||||
- [Developer Certificate of Origin (DCO)](#developer-certificate-of-origin-dco)
|
||||
- [DCO Sign-Off Methods](#dco-sign-off-methods)
|
||||
- [Bug reports & feature requests](#bug-reports--feature-requests)
|
||||
- [Credits](#credits)
|
||||
- [Links](#links)
|
||||
@@ -51,7 +55,8 @@ https://web.archive.org/web/20160528213242/https://thomas.apestaart.org/thomas/t
|
||||
- Performs Test & Copy rips
|
||||
- Verifies rip accuracy using the [AccurateRip database](http://www.accuraterip.com/)
|
||||
- Uses [MusicBrainz](https://musicbrainz.org/doc/About) for metadata lookup
|
||||
- Supports reading the [pre-emphasis](http://wiki.hydrogenaud.io/index.php?title=Pre-emphasis) flag embedded into some CDs (and correctly tags the resulting rip). _Currently whipper only reports the pre-emphasis flag value stored in the TOC._
|
||||
- Supports reading the [pre-emphasis](http://wiki.hydrogenaud.io/index.php?title=Pre-emphasis) flag embedded into some CDs (and correctly tags the resulting rip)
|
||||
- _Currently whipper only reports the pre-emphasis flag value stored in the TOC_
|
||||
- Detects and rips _non digitally silent_ [Hidden Track One Audio](http://wiki.hydrogenaud.io/index.php?title=HTOA) (HTOA)
|
||||
- Provides batch ripping capabilities
|
||||
- Provides templates for file and directory naming
|
||||
@@ -72,11 +77,11 @@ Whipper still isn't available as an official package in every Linux distribution
|
||||
|
||||
You can easily install whipper without needing to care about the required dependencies by making use of the automatically built images hosted on Docker Hub:
|
||||
|
||||
`docker pull joelametta/whipper`
|
||||
`docker pull whipperteam/whipper`
|
||||
|
||||
Alternatively, in case you prefer building Docker images locally, just issue the following command (it relies on the [Dockerfile](https://github.com/whipper-team/whipper/blob/master/Dockerfile) included in whipper's repository):
|
||||
|
||||
`docker build -t whipper/whipper`
|
||||
`docker build -t whipperteam/whipper`
|
||||
|
||||
It's recommended to create an alias for a convenient usage:
|
||||
|
||||
@@ -84,7 +89,7 @@ It's recommended to create an alias for a convenient usage:
|
||||
alias whipper="docker run -ti --rm --device=/dev/cdrom \
|
||||
-v ~/.config/whipper:/home/worker/.config/whipper \
|
||||
-v ${PWD}/output:/output \
|
||||
whipper/whipper"
|
||||
whipperteam/whipper"
|
||||
```
|
||||
|
||||
You should put this e.g. into your `.bash_aliases`. Also keep in mind to substitute the path definitions to something that fits to your needs (e.g. replace `… -v ${PWD}/output:/output …` with `… -v ${HOME}/ripped:/output \ …`).
|
||||
@@ -106,6 +111,8 @@ This is a noncomprehensive summary which shows whipper's packaging status (unoff
|
||||
|
||||
[](https://repology.org/metapackage/whipper)
|
||||
|
||||
Someone also packaged whipper as snap: [unofficial snap on snapcraft](https://snapcraft.io/whipper).
|
||||
|
||||
In case you decide to install whipper using an unofficial repository just keep in mind it is your responsibility to verify that the provided content is safe to use.
|
||||
|
||||
## Building
|
||||
@@ -117,8 +124,8 @@ If you are building from a source tarball or checkout, you can choose to use whi
|
||||
Whipper relies on the following packages in order to run correctly and provide all the supported features:
|
||||
|
||||
- [cd-paranoia](https://www.gnu.org/software/libcdio/), for the actual ripping
|
||||
- To avoid bugs it's advised to use `cd-paranoia` version **10.2+0.94+2-2**
|
||||
- The package named `libcdio-utils`, available on Debian and Ubuntu, is affected by a bug: it doesn't include the `cd-paranoia` binary (needed by whipper). For more details see: [#888053 (Debian)](https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=888053), [#1750264 (Ubuntu)](https://bugs.launchpad.net/ubuntu/+source/libcdio/+bug/1750264).
|
||||
- To avoid bugs it's advised to use `cd-paranoia` versions ≥ **10.2+0.94+2-2**
|
||||
- The package named `libcdio-utils`, available on Debian and Ubuntu, is affected by a bug (except for Debian testing/sid): it doesn't include the `cd-paranoia` binary (needed by whipper). For more details see: [#888053 (Debian)](https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=888053), [#889803 (Debian)](https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=889803) and [#1750264 (Ubuntu)](https://bugs.launchpad.net/ubuntu/+source/libcdio/+bug/1750264).
|
||||
- [cdrdao](http://cdrdao.sourceforge.net/), for session, TOC, pre-gap, and ISRC extraction
|
||||
- [GObject Introspection](https://wiki.gnome.org/Projects/GObjectIntrospection), to provide GLib-2.0 methods used by `task.py`
|
||||
- [PyGObject](https://pypi.org/project/PyGObject/), required by `task.py`
|
||||
@@ -127,7 +134,8 @@ Whipper relies on the following packages in order to run correctly and provide a
|
||||
- [python-setuptools](https://pypi.python.org/pypi/setuptools), for installation, plugins support
|
||||
- [python-requests](https://pypi.python.org/pypi/requests), for retrieving AccurateRip database entries
|
||||
- [pycdio](https://pypi.python.org/pypi/pycdio/), for drive identification (required for drive offset and caching behavior to be stored in the configuration file).
|
||||
- To avoid bugs it's advised to use `pycdio` **0.20** or **0.21** with `libcdio` ≥ **0.90** ≤ **0.94* or `pycdio` **2.0.0** with `libcdio` **2.0.0**. All other combinations won't probably work.
|
||||
- To avoid bugs it's advised to use the most recent `pycdio` version with the corresponding `libcdio` release or, if stuck to old pycdio versions, **0.20**/**0.21** with `libcdio` ≥ **0.90** ≤ **0.94**. All other combinations won't probably work.
|
||||
- [ruamel.yaml](https://pypi.org/project/ruamel.yaml/), for generating well formed YAML report logfiles
|
||||
- [libsndfile](http://www.mega-nerd.com/libsndfile/), for reading wav files
|
||||
- [flac](https://xiph.org/flac/), for reading flac files
|
||||
- [sox](http://sox.sourceforge.net/), for track peak detection
|
||||
@@ -140,6 +148,8 @@ Some dependencies aren't available in the PyPI. They can be probably installed u
|
||||
- [libsndfile](http://www.mega-nerd.com/libsndfile/)
|
||||
- [flac](https://xiph.org/flac/)
|
||||
- [sox](http://sox.sourceforge.net/)
|
||||
- [git](https://git-scm.com/) or [mercurial](https://www.mercurial-scm.org/)
|
||||
- Required either when running whipper without installing it or when building it from its source code (code cloned from a git/mercurial repository).
|
||||
|
||||
PyPI installable dependencies are listed in the [requirements.txt](https://github.com/whipper-team/whipper/blob/master/requirements.txt) file and can be installed issuing the following command:
|
||||
|
||||
@@ -303,8 +313,8 @@ Licensed under the [GNU GPLv3 license](http://www.gnu.org/licenses/gpl-3.0).
|
||||
|
||||
```Text
|
||||
Copyright (C) 2009 Thomas Vander Stichele
|
||||
Copyright (C) 2016-2018 The Whipper Team: JoeLametta, Frederik Olesen,
|
||||
Samantha Baldwin, Merlijn Wajer, et al.
|
||||
Copyright (C) 2016-2019 The Whipper Team: JoeLametta, Samantha Baldwin,
|
||||
Merlijn Wajer, Frederik “Freso” S. Olesen, et al.
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -328,6 +338,65 @@ repository](https://github.com/whipper-team/whipper). Where possible,
|
||||
please include tests for new or changed functionality. You can run tests
|
||||
with `python -m unittest discover` from your source checkout.
|
||||
|
||||
### Developer Certificate of Origin (DCO)
|
||||
|
||||
To make a good faith effort to ensure licensing criteria are met, this project requires the Developer Certificate of Origin (DCO) process to be followed.
|
||||
|
||||
The Developer Certificate of Origin (DCO) is a document that certifies you own and/or have the right to contribute the work and license it appropriately. The DCO is used instead of a _much more annoying_
|
||||
[CLA (Contributor License Agreement)](https://en.wikipedia.org/wiki/Contributor_License_Agreement). With the DCO, you retain copyright of your own work :). The DCO originated in the Linux community, and is used by other projects like Git and Docker.
|
||||
|
||||
The DCO agreement is shown below and it's also available online: [HERE](https://developercertificate.org/).
|
||||
|
||||
```
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||
1 Letterman Drive
|
||||
Suite D4700
|
||||
San Francisco, CA, 94129
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
```
|
||||
|
||||
#### DCO Sign-Off Methods
|
||||
|
||||
The DCO requires a sign-off message in the following format appear on each commit in the pull request:
|
||||
|
||||
```
|
||||
Signed-off-by: Full Name <email>
|
||||
```
|
||||
|
||||
The DCO text can either be manually added to your commit body, or you can add either `-s` or `--signoff` to your usual Git commit commands. If you forget to add the sign-off you can also amend a previous commit with the sign-off by running `git commit --amend -s`.
|
||||
|
||||
### Bug reports & feature requests
|
||||
|
||||
Please use the [issue tracker](https://github.com/whipper-team/whipper/issues) to report any bugs or to file feature requests.
|
||||
@@ -349,8 +418,9 @@ Thanks to:
|
||||
|
||||
- [Thomas Vander Stichele](https://github.com/thomasvs)
|
||||
- [Joe Lametta](https://github.com/JoeLametta)
|
||||
- [Merlijn Wajer](https://github.com/MerlijnWajer)
|
||||
- [Samantha Baldwin](https://github.com/RecursiveForest)
|
||||
- [Frederik “Freso” S. Olesen](https://github.com/Freso)
|
||||
- [Merlijn Wajer](https://github.com/MerlijnWajer)
|
||||
|
||||
And to all the [contributors](https://github.com/whipper-team/whipper/graphs/contributors).
|
||||
|
||||
@@ -365,5 +435,6 @@ You can find us and talk about the project on:
|
||||
- [Redacted thread (official)](https://redacted.ch/forums.php?action=viewthread&threadid=150)
|
||||
|
||||
Other relevant links:
|
||||
- [Whipper - Hydrogenaudio Knowledgebase](https://wiki.hydrogenaud.io/index.php?title=Whipper)
|
||||
- [Repology: versions for whipper](https://repology.org/metapackage/whipper/versions)
|
||||
- [Unattended ripping using whipper (by Thomas McWork)](https://github.com/thomas-mc-work/most-possible-unattended-rip)
|
||||
|
||||
4
TODO
4
TODO
@@ -56,7 +56,7 @@ HARD
|
||||
|
||||
- write xbmc/plex plugin
|
||||
|
||||
SPECIFIC ALBUMS ISSUES
|
||||
SPECIFIC RELEASES ISSUES
|
||||
|
||||
- on ana, Goldfrapp tells me I have offset 0!
|
||||
|
||||
@@ -67,7 +67,7 @@ SPECIFIC ALBUMS ISSUES
|
||||
|
||||
NO DECISION YET
|
||||
|
||||
- possibly figure out how to name albums with credited artist; look at gorky and spiritualized electric mainline
|
||||
- possibly figure out how to name releases with credited artist; look at gorky and spiritualized electric mainline
|
||||
|
||||
- check if cdda2wav or icedax analyze pregaps correctly
|
||||
|
||||
|
||||
@@ -64,4 +64,4 @@ if len(line) > 11:
|
||||
line = line[:-2] + '"'
|
||||
lines.append(line)
|
||||
|
||||
print "\n".join(lines)
|
||||
print("\n".join(lines))
|
||||
|
||||
@@ -3,3 +3,5 @@ mutagen
|
||||
pycdio>0.20
|
||||
PyGObject
|
||||
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 whipper import __version__ as whipper_version
|
||||
from setuptools import setup, find_packages, Extension
|
||||
|
||||
setup(
|
||||
name="whipper",
|
||||
version=whipper_version,
|
||||
use_scm_version=True,
|
||||
description="a secure cd ripper preferring accuracy over speed",
|
||||
author=['Thomas Vander Stichele', 'The Whipper Team'],
|
||||
maintainer=['The Whipper Team'],
|
||||
url='https://github.com/whipper-team/whipper',
|
||||
license='GPL3',
|
||||
python_requires='>=2.7,<3',
|
||||
packages=find_packages(),
|
||||
setup_requires=['setuptools_scm'],
|
||||
ext_modules=[
|
||||
Extension('accuraterip',
|
||||
libraries=['sndfile'],
|
||||
sources=['src/accuraterip-checksum.c'])
|
||||
],
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'whipper = whipper.command.main:main'
|
||||
@@ -18,4 +24,7 @@ setup(
|
||||
data_files=[
|
||||
('share/metainfo', ['com.github.whipper_team.Whipper.metainfo.xml']),
|
||||
],
|
||||
scripts=[
|
||||
'scripts/accuraterip-checksum',
|
||||
],
|
||||
)
|
||||
|
||||
1
src/.gitignore
vendored
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:
|
||||
A C99 commandline program to compute the AccurateRip checksum of singletrack WAV files.
|
||||
Implemented according to
|
||||
## Description
|
||||
A C99 command line program to compute the [AccurateRip](http://accuraterip.com/) checksum of single track WAV files, i.e. WAV files which contain only a single track of an audio CD.
|
||||
Such files can for example be generated by [Exact Audio Copy](http://exactaudiocopy.de/) and various other CD ripping programs, as listed e.g. [here](http://accuraterip.com/software.htm) and [here](https://wiki.hydrogenaud.io/index.php?title=AccurateRip).
|
||||
|
||||
http://www.hydrogenaudio.org/forums/index.php?showtopic=97603
|
||||
Implemented according to [this thread on HydrogenAudio](http://www.hydrogenaudio.org/forums/index.php?showtopic=97603).
|
||||
|
||||
# Syntax:
|
||||
accuraterip-checksum [--version / --accuraterip-v1 / --accuraterip-v2 (default)] filename track_number total_tracks
|
||||
## Usage
|
||||
Calculate AccurateRip v2 checksum of track number ```TRACK``` which is contained in WAV file ```TRACK_FILE```, and which was ripped from a disc with a total track count of ```TOTAL_TRACKS```:
|
||||
|
||||
# Output:
|
||||
By default, the V2 (AccurateRip version 2) checksum will be printed.
|
||||
You can also obtain the V1 checksum with the "--accuraterip-v1" parameter.
|
||||
accuraterip-checksum TRACK_FILE TRACK TOTAL_TRACKS
|
||||
|
||||
You can obtain the version of accuraterip-checksum using the "--version" parameter. This is not to be confused with the AccurateRip version!
|
||||
Explicitly choose AccurateRip checksum version, where ```VERSION``` is 1 or 2:
|
||||
|
||||
The version of accuraterip-checksum should be added to audio files which are tagged using the output of accuraterip-checksum. If any severe bugs are ever found in accuraterip-checksum, this will allow you to identify files which were tagged using affected version.
|
||||
accuraterip-checksum --accuraterip-vVERSION TRACK_FILE TRACK TOTAL_TRACKS
|
||||
|
||||
Show accuraterip-checksum program version (this is **not** the AccurateRip checksum version!):
|
||||
|
||||
# Compiling:
|
||||
accuraterip-checksum --version
|
||||
|
||||
The version of accuraterip-checksum should be added to the tags of audio files which were processed using the output of accuraterip-checksum:
|
||||
If any severe bugs are ever found in accuraterip-checksum this will allow you to identify files which were tagged using affected version.
|
||||
|
||||
## Dependencies
|
||||
libsndfile is used for reading the WAV files.
|
||||
Therefore, on Ubuntu 12.04, make sure you have the following packages installed:
|
||||
Therefore, on Ubuntu, make sure you have the following packages installed:
|
||||
|
||||
libsndfile1
|
||||
|
||||
For compiling you need:
|
||||
|
||||
libsndfile1 (should be installed by default)
|
||||
libsndfile1-dev
|
||||
|
||||
The configuration files of an Eclipse project are included.
|
||||
You can use "EGit" (Eclipse git) to import the whole repository.
|
||||
It should as well ask you to import the project configuration then.
|
||||
|
||||
# Author:
|
||||
## Author
|
||||
Leo Bogert (http://leo.bogert.de)
|
||||
|
||||
# Version:
|
||||
1.4
|
||||
## Version
|
||||
1.5
|
||||
|
||||
# Donations:
|
||||
## Donations
|
||||
bitcoin:14kPd2QWsri3y2irVFX6wC33vv7FqTaEBh
|
||||
|
||||
# License:
|
||||
## License
|
||||
GPLv3
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
/*
|
||||
============================================================================
|
||||
Name : accuraterip-checksum.c
|
||||
Author : Leo Bogert (http://leo.bogert.de)
|
||||
Git : http://leo.bogert.de/accuraterip-checksum
|
||||
Version : See global variable "version"
|
||||
Copyright : GPL
|
||||
Description : A C99 commandline program to compute the AccurateRip checksum of singletrack WAV files.
|
||||
Implemented according to http://www.hydrogenaudio.org/forums/index.php?showtopic=97603
|
||||
Authors : Leo Bogert (http://leo.bogert.de), Andreas Oberritter
|
||||
License : GPLv3
|
||||
Description : A Python C extension to compute the AccurateRip checksum of WAV or FLAC tracks.
|
||||
Implemented according to http://www.hydrogenaudio.org/forums/index.php?showtopic=97603
|
||||
============================================================================
|
||||
*/
|
||||
|
||||
@@ -17,10 +15,10 @@
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <sndfile.h>
|
||||
#include <Python.h>
|
||||
|
||||
const char *const version = "1.4";
|
||||
|
||||
bool check_fileformat(const SF_INFO* sfinfo) {
|
||||
static bool check_fileformat(const SF_INFO *sfinfo)
|
||||
{
|
||||
#ifdef DEBUG
|
||||
printf("Channels: %i\n", sfinfo->channels);
|
||||
printf("Format: %X\n", sfinfo->format);
|
||||
@@ -30,27 +28,25 @@ bool check_fileformat(const SF_INFO* sfinfo) {
|
||||
printf("Seekable: %i\n", sfinfo->seekable);
|
||||
#endif
|
||||
|
||||
if(sfinfo->channels != 2) return false;
|
||||
if((sfinfo->format & SF_FORMAT_TYPEMASK & SF_FORMAT_WAV) != SF_FORMAT_WAV) return false;
|
||||
if((sfinfo->format & SF_FORMAT_SUBMASK & SF_FORMAT_PCM_16) != SF_FORMAT_PCM_16) return false;
|
||||
//if((sfinfo->format & SF_FORMAT_ENDMASK & SF_ENDIAN_LITTLE) != SF_ENDIAN_LITTLE) return false;
|
||||
if(sfinfo->samplerate != 44100) return false;
|
||||
switch (sfinfo->format & SF_FORMAT_TYPEMASK) {
|
||||
case SF_FORMAT_WAV:
|
||||
case SF_FORMAT_FLAC:
|
||||
return (sfinfo->channels == 2) &&
|
||||
(sfinfo->samplerate == 44100) &&
|
||||
((sfinfo->format & SF_FORMAT_SUBMASK) == SF_FORMAT_PCM_16);
|
||||
}
|
||||
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t get_full_audiodata_size(const SF_INFO* sfinfo) {
|
||||
// 16bit = samplesize, 8 bit = bitcount in byte
|
||||
return sfinfo->frames * sfinfo->channels * (16 / 8);
|
||||
}
|
||||
|
||||
uint32_t* load_full_audiodata(SNDFILE* sndfile, const SF_INFO* sfinfo) {
|
||||
uint32_t* data = (uint32_t*)malloc(get_full_audiodata_size(sfinfo));
|
||||
static void *load_full_audiodata(SNDFILE *sndfile, const SF_INFO *sfinfo, size_t size)
|
||||
{
|
||||
void *data = malloc(size);
|
||||
|
||||
if(data == NULL)
|
||||
return NULL;
|
||||
|
||||
if(sf_readf_short(sndfile, (short*)data, sfinfo->frames) != sfinfo->frames) {
|
||||
if(sf_readf_short(sndfile, data, sfinfo->frames) != sfinfo->frames) {
|
||||
free(data);
|
||||
return NULL;
|
||||
}
|
||||
@@ -58,170 +54,100 @@ uint32_t* load_full_audiodata(SNDFILE* sndfile, const SF_INFO* sfinfo) {
|
||||
return data;
|
||||
}
|
||||
|
||||
uint32_t compute_v1_checksum(const uint32_t* audio_data, const size_t audio_data_size, const int track_number, const int total_tracks) {
|
||||
#define DWORD uint32_t
|
||||
static void compute_checksums(const uint32_t *audio_data, size_t audio_data_size, size_t track_number, size_t total_tracks, uint32_t *v1, uint32_t *v2)
|
||||
{
|
||||
uint32_t csum_hi = 0;
|
||||
uint32_t csum_lo = 0;
|
||||
uint32_t AR_CRCPosCheckFrom = 0;
|
||||
size_t Datauint32_tSize = audio_data_size / sizeof(uint32_t);
|
||||
uint32_t AR_CRCPosCheckTo = Datauint32_tSize;
|
||||
const size_t SectorBytes = 2352; // each sector
|
||||
uint32_t MulBy = 1;
|
||||
size_t i;
|
||||
|
||||
const DWORD *pAudioData = audio_data; // this should point entire track audio data
|
||||
int DataSize = audio_data_size; // size of the data
|
||||
int TrackNumber = track_number; // actual track number on disc, note that for the first & last track the first and last 5 sectors are skipped
|
||||
int AudioTrackCount = total_tracks; // CD track count
|
||||
if (track_number == 1) // first?
|
||||
AR_CRCPosCheckFrom += ((SectorBytes * 5) / sizeof(uint32_t));
|
||||
if (track_number == total_tracks) // last?
|
||||
AR_CRCPosCheckTo -= ((SectorBytes * 5) / sizeof(uint32_t));
|
||||
|
||||
//---------AccurateRip CRC checks------------
|
||||
DWORD AR_CRC = 0, AR_CRCPosMulti = 1;
|
||||
DWORD AR_CRCPosCheckFrom = 0;
|
||||
DWORD AR_CRCPosCheckTo = DataSize / sizeof(DWORD);
|
||||
#define SectorBytes 2352 // each sector
|
||||
if (TrackNumber == 1) // first?
|
||||
AR_CRCPosCheckFrom+= ((SectorBytes * 5) / sizeof(DWORD));
|
||||
if (TrackNumber == AudioTrackCount) // last?
|
||||
AR_CRCPosCheckTo-=((SectorBytes * 5) / sizeof(DWORD));
|
||||
|
||||
|
||||
int DataDWORDSize = DataSize / sizeof(DWORD);
|
||||
for (int i = 0; i < DataDWORDSize; i++)
|
||||
{
|
||||
if (AR_CRCPosMulti >= AR_CRCPosCheckFrom && AR_CRCPosMulti <= AR_CRCPosCheckTo)
|
||||
AR_CRC+=(AR_CRCPosMulti * pAudioData[i]);
|
||||
|
||||
AR_CRCPosMulti++;
|
||||
}
|
||||
|
||||
return AR_CRC;
|
||||
}
|
||||
|
||||
uint32_t compute_v2_checksum(const uint32_t* audio_data, const size_t audio_data_size, const int track_number, const int total_tracks) {
|
||||
#define DWORD uint32_t
|
||||
#define __int64 uint64_t
|
||||
|
||||
const DWORD *pAudioData = audio_data; // this should point entire track audio data
|
||||
int DataSize = audio_data_size; // size of the data
|
||||
int TrackNumber = track_number; // actual track number on disc, note that for the first & last track the first and last 5 sectors are skipped
|
||||
int AudioTrackCount = total_tracks; // CD track count
|
||||
|
||||
//---------AccurateRip CRC checks------------
|
||||
DWORD AR_CRCPosCheckFrom = 0;
|
||||
DWORD AR_CRCPosCheckTo = DataSize / sizeof(DWORD);
|
||||
#define SectorBytes 2352 // each sector
|
||||
if (TrackNumber == 1) // first?
|
||||
AR_CRCPosCheckFrom+= ((SectorBytes * 5) / sizeof(DWORD));
|
||||
if (TrackNumber == AudioTrackCount) // last?
|
||||
AR_CRCPosCheckTo-=((SectorBytes * 5) / sizeof(DWORD));
|
||||
|
||||
int DataDWORDSize = DataSize / sizeof(DWORD);
|
||||
|
||||
DWORD AC_CRCNEW = 0;
|
||||
DWORD MulBy = 1;
|
||||
for (int i = 0; i < DataDWORDSize; i++)
|
||||
{
|
||||
if (MulBy >= AR_CRCPosCheckFrom && MulBy <= AR_CRCPosCheckTo)
|
||||
{
|
||||
DWORD Value = pAudioData[i];
|
||||
|
||||
uint64_t CalcCRCNEW = (uint64_t)Value * (uint64_t)MulBy;
|
||||
DWORD LOCalcCRCNEW = (DWORD)(CalcCRCNEW & (uint64_t)0xFFFFFFFF);
|
||||
DWORD HICalcCRCNEW = (DWORD)(CalcCRCNEW / (uint64_t)0x100000000);
|
||||
AC_CRCNEW+=HICalcCRCNEW;
|
||||
AC_CRCNEW+=LOCalcCRCNEW;
|
||||
for (i = 0; i < Datauint32_tSize; i++) {
|
||||
if (MulBy >= AR_CRCPosCheckFrom && MulBy <= AR_CRCPosCheckTo) {
|
||||
uint64_t product = (uint64_t)audio_data[i] * (uint64_t)MulBy;
|
||||
csum_hi += (uint32_t)(product >> 32);
|
||||
csum_lo += (uint32_t)(product);
|
||||
}
|
||||
MulBy++;
|
||||
MulBy++;
|
||||
}
|
||||
|
||||
return AC_CRCNEW;
|
||||
*v1 = csum_lo;
|
||||
*v2 = csum_lo + csum_hi;
|
||||
}
|
||||
|
||||
void print_syntax_to_stderr() {
|
||||
fprintf(stderr, "Syntax: accuraterip-checksum [--version / --accuraterip-v1 / --accuraterip-v2 (default)] filename track_number total_tracks\n");
|
||||
}
|
||||
static PyObject *accuraterip_compute(PyObject *self, PyObject *args)
|
||||
{
|
||||
const char *filename;
|
||||
unsigned int track_number;
|
||||
unsigned int total_tracks;
|
||||
uint32_t v1, v2;
|
||||
void *audio_data;
|
||||
size_t size;
|
||||
SF_INFO sfinfo;
|
||||
SNDFILE *sndfile = NULL;
|
||||
|
||||
int main(int argc, const char** argv) {
|
||||
int arg_offset;
|
||||
bool use_v1;
|
||||
if (!PyArg_ParseTuple(args, "sII", &filename, &track_number, &total_tracks))
|
||||
goto err;
|
||||
|
||||
switch(argc) {
|
||||
case 2:
|
||||
if(strcmp(argv[1], "--version") != 0) {
|
||||
print_syntax_to_stderr();
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
printf("accuraterip-checksum version %s\n", version);
|
||||
return EXIT_SUCCESS;
|
||||
case 4:
|
||||
arg_offset = 0;
|
||||
use_v1 = false;
|
||||
break;
|
||||
case 5:
|
||||
arg_offset = 1;
|
||||
if(!strcmp(argv[1], "--accuraterip-v1")) {
|
||||
use_v1 = true;
|
||||
} else if(!strcmp(argv[1], "--accuraterip-v2")) {
|
||||
use_v1 = false;
|
||||
} else {
|
||||
print_syntax_to_stderr();
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
print_syntax_to_stderr();
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
const char* filename = argv[1 + arg_offset];
|
||||
const char* track_number_string = argv[2 + arg_offset];
|
||||
const char* total_tracks_string = argv[3 + arg_offset];
|
||||
|
||||
const int track_number = atoi(track_number_string);
|
||||
const int total_tracks = atoi(total_tracks_string);
|
||||
|
||||
if(track_number < 1 || track_number > total_tracks) {
|
||||
if (track_number < 1 || track_number > total_tracks) {
|
||||
fprintf(stderr, "Invalid track_number!\n");
|
||||
return EXIT_FAILURE;
|
||||
goto err;
|
||||
}
|
||||
|
||||
if(total_tracks < 1 || total_tracks > 99) {
|
||||
if (total_tracks < 1 || total_tracks > 99) {
|
||||
fprintf(stderr, "Invalid total_tracks!\n");
|
||||
return EXIT_FAILURE;
|
||||
goto err;
|
||||
}
|
||||
|
||||
#ifdef DEBUG
|
||||
printf("Reading %s\n", filename);
|
||||
#endif
|
||||
|
||||
SF_INFO sfinfo;
|
||||
sfinfo.channels = 0;
|
||||
sfinfo.format = 0;
|
||||
sfinfo.frames = 0;
|
||||
sfinfo.samplerate = 0;
|
||||
sfinfo.sections = 0;
|
||||
sfinfo.seekable = 0;
|
||||
|
||||
SNDFILE* sndfile = sf_open(filename, SFM_READ, &sfinfo);
|
||||
|
||||
if(sndfile == NULL) {
|
||||
memset(&sfinfo, 0, sizeof(sfinfo));
|
||||
sndfile = sf_open(filename, SFM_READ, &sfinfo);
|
||||
if (sndfile == NULL) {
|
||||
fprintf(stderr, "sf_open failed! sf_error==%i\n", sf_error(NULL));
|
||||
return EXIT_FAILURE;
|
||||
goto err;
|
||||
}
|
||||
|
||||
if(!check_fileformat(&sfinfo)) {
|
||||
if (!check_fileformat(&sfinfo)) {
|
||||
fprintf(stderr, "check_fileformat failed!\n");
|
||||
sf_close(sndfile);
|
||||
return EXIT_FAILURE;
|
||||
goto err;
|
||||
}
|
||||
|
||||
uint32_t* audio_data = load_full_audiodata(sndfile, &sfinfo);
|
||||
if(audio_data == NULL) {
|
||||
size = sfinfo.frames * sfinfo.channels * sizeof(uint16_t);
|
||||
audio_data = load_full_audiodata(sndfile, &sfinfo, size);
|
||||
if (audio_data == NULL) {
|
||||
fprintf(stderr, "load_full_audiodata failed!\n");
|
||||
sf_close(sndfile);
|
||||
return EXIT_FAILURE;
|
||||
goto err;
|
||||
}
|
||||
|
||||
const int checksum = use_v1 ?
|
||||
compute_v1_checksum(audio_data, get_full_audiodata_size(&sfinfo), track_number, total_tracks)
|
||||
: compute_v2_checksum(audio_data, get_full_audiodata_size(&sfinfo), track_number, total_tracks);
|
||||
|
||||
printf("%08X\n", checksum);
|
||||
|
||||
sf_close(sndfile);
|
||||
compute_checksums(audio_data, size, track_number, total_tracks, &v1, &v2);
|
||||
free(audio_data);
|
||||
sf_close(sndfile);
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
return Py_BuildValue("II", v1, v2);
|
||||
|
||||
err:
|
||||
if (sndfile)
|
||||
sf_close(sndfile);
|
||||
return Py_BuildValue("OO", Py_None, Py_None);
|
||||
}
|
||||
|
||||
static PyMethodDef accuraterip_methods[] = {
|
||||
{ "compute", accuraterip_compute, METH_VARARGS, "Compute AccurateRip v1 and v2 checksums" },
|
||||
{ NULL, NULL, 0, NULL },
|
||||
};
|
||||
|
||||
PyMODINIT_FUNC initaccuraterip(void)
|
||||
{
|
||||
Py_InitModule("accuraterip", accuraterip_methods);
|
||||
}
|
||||
|
||||
@@ -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 sys
|
||||
|
||||
__version__ = '0.7.3'
|
||||
from pkg_resources import (get_distribution,
|
||||
DistributionNotFound, RequirementParseError)
|
||||
try:
|
||||
__version__ = get_distribution(__name__).version
|
||||
except (DistributionNotFound, RequirementParseError):
|
||||
# not installed as package or is being run from source/git checkout
|
||||
from setuptools_scm import get_version
|
||||
__version__ = get_version()
|
||||
|
||||
level = logging.INFO
|
||||
if 'WHIPPER_DEBUG' in os.environ:
|
||||
|
||||
@@ -59,9 +59,7 @@ retrieves and display accuraterip data from the given URL
|
||||
assert len(r.checksums) == r.num_tracks
|
||||
assert len(r.confidences) == r.num_tracks
|
||||
|
||||
entry = {}
|
||||
entry["confidence"] = r.confidences[track]
|
||||
entry["response"] = i + 1
|
||||
entry = {"confidence": r.confidences[track], "response": i + 1}
|
||||
checksum = r.checksums[track]
|
||||
if checksum in checksums:
|
||||
checksums[checksum].append(entry)
|
||||
|
||||
@@ -25,7 +25,7 @@ logger = logging.getLogger(__name__)
|
||||
# options) to the child command.
|
||||
|
||||
|
||||
class BaseCommand():
|
||||
class BaseCommand:
|
||||
"""
|
||||
Register and handle whipper command arguments with ArgumentParser.
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ DEFAULT_DISC_TEMPLATE = u'%r/%A - %d/%A - %d'
|
||||
|
||||
TEMPLATE_DESCRIPTION = '''
|
||||
Tracks are named according to the track template, filling in the variables
|
||||
and adding the file extension. Variables exclusive to the track template are:
|
||||
and adding the file extension. Variables exclusive to the track template are:
|
||||
- %t: track number
|
||||
- %a: track artist
|
||||
- %n: track title
|
||||
@@ -51,12 +51,12 @@ and adding the file extension. Variables exclusive to the track template are:
|
||||
Disc files (.cue, .log, .m3u) are named according to the disc template,
|
||||
filling in the variables and adding the file extension. Variables for both
|
||||
disc and track template are:
|
||||
- %A: album artist
|
||||
- %S: album sort name
|
||||
- %A: release artist
|
||||
- %S: release sort name
|
||||
- %d: disc title
|
||||
- %y: release year
|
||||
- %r: release type, lowercase
|
||||
- %R: Release type, normal case
|
||||
- %R: release type, normal case
|
||||
- %x: audio extension, lowercase
|
||||
- %X: audio extension, uppercase
|
||||
|
||||
@@ -66,6 +66,7 @@ disc and track template are:
|
||||
class _CD(BaseCommand):
|
||||
eject = True
|
||||
|
||||
# XXX: Pylint, parameters differ from overridden 'add_arguments' method
|
||||
@staticmethod
|
||||
def add_arguments(parser):
|
||||
parser.add_argument('-R', '--release-id',
|
||||
@@ -94,7 +95,6 @@ class _CD(BaseCommand):
|
||||
utils.unmount_device(self.device)
|
||||
|
||||
# first, read the normal TOC, which is fast
|
||||
logger.info("reading TOC...")
|
||||
self.ittoc = self.program.getFastToc(self.runner, self.device)
|
||||
|
||||
# already show us some info based on this
|
||||
@@ -134,20 +134,23 @@ class _CD(BaseCommand):
|
||||
return -1
|
||||
|
||||
# Change working directory before cdrdao's task
|
||||
if self.options.working_directory is not None:
|
||||
if getattr(self.options, 'working_directory', False):
|
||||
os.chdir(os.path.expanduser(self.options.working_directory))
|
||||
out_bpath = self.options.output_directory.decode('utf-8')
|
||||
# Needed to preserve cdrdao's tocfile
|
||||
out_fpath = self.program.getPath(out_bpath,
|
||||
self.options.disc_template,
|
||||
self.mbdiscid,
|
||||
self.program.metadata)
|
||||
if hasattr(self.options, 'output_directory'):
|
||||
out_bpath = self.options.output_directory.decode('utf-8')
|
||||
# Needed to preserve cdrdao's tocfile
|
||||
out_fpath = self.program.getPath(out_bpath,
|
||||
self.options.disc_template,
|
||||
self.mbdiscid,
|
||||
self.program.metadata)
|
||||
else:
|
||||
out_fpath = None
|
||||
# now, read the complete index table, which is slower
|
||||
offset = getattr(self.options, 'offset', 0)
|
||||
self.itable = self.program.getTable(self.runner,
|
||||
self.ittoc.getCDDBDiscId(),
|
||||
self.ittoc.getMusicBrainzDiscId(),
|
||||
self.device, self.options.offset,
|
||||
out_fpath)
|
||||
self.device, offset, out_fpath)
|
||||
|
||||
assert self.itable.getCDDBDiscId() == self.ittoc.getCDDBDiscId(), \
|
||||
"full table's id %s differs from toc id %s" % (
|
||||
@@ -157,17 +160,13 @@ class _CD(BaseCommand):
|
||||
"full table's mb id %s differs from toc id mb %s" % (
|
||||
self.itable.getMusicBrainzDiscId(),
|
||||
self.ittoc.getMusicBrainzDiscId())
|
||||
assert self.itable.accuraterip_path() == \
|
||||
self.ittoc.accuraterip_path(), \
|
||||
"full table's AR URL %s differs from toc AR URL %s" % (
|
||||
self.itable.accuraterip_url(), self.ittoc.accuraterip_url())
|
||||
|
||||
if self.program.metadata:
|
||||
self.program.metadata.discid = self.ittoc.getMusicBrainzDiscId()
|
||||
|
||||
# result
|
||||
|
||||
self.program.result.cdrdaoVersion = cdrdao.getCDRDAOVersion()
|
||||
self.program.result.cdrdaoVersion = cdrdao.version()
|
||||
self.program.result.cdparanoiaVersion = \
|
||||
cdparanoia.getCdParanoiaVersion()
|
||||
info = drive.getDeviceInfo(self.device)
|
||||
@@ -186,24 +185,29 @@ class _CD(BaseCommand):
|
||||
_, self.program.result.vendor, self.program.result.model, \
|
||||
self.program.result.release = \
|
||||
cdio.Device(self.device).get_hwinfo()
|
||||
self.program.result.metadata = self.program.metadata
|
||||
|
||||
self.doCommand()
|
||||
|
||||
if self.options.eject in ('success', 'always'):
|
||||
if (self.options.eject == 'success' and self.eject or
|
||||
self.options.eject == 'always'):
|
||||
utils.eject_device(self.device)
|
||||
|
||||
return None
|
||||
|
||||
def doCommand(self):
|
||||
pass
|
||||
|
||||
|
||||
class Info(_CD):
|
||||
summary = "retrieve information about the currently inserted CD"
|
||||
description = ("Display MusicBrainz, CDDB/FreeDB, and AccurateRip"
|
||||
description = ("Display MusicBrainz, CDDB/FreeDB, and AccurateRip "
|
||||
"information for the currently inserted CD.")
|
||||
eject = False
|
||||
|
||||
# Requires opts.device
|
||||
|
||||
# XXX: Pylint, parameters differ from overridden 'add_arguments' method
|
||||
def add_arguments(self):
|
||||
_CD.add_arguments(self.parser)
|
||||
|
||||
@@ -227,6 +231,7 @@ Log files will log the path to tracks relative to this directory.
|
||||
# Requires opts.record
|
||||
# Requires opts.device
|
||||
|
||||
# XXX: Pylint, parameters differ from overridden 'add_arguments' method
|
||||
def add_arguments(self):
|
||||
loggers = list(result.getLoggers())
|
||||
default_offset = None
|
||||
@@ -245,7 +250,6 @@ Log files will log the path to tracks relative to this directory.
|
||||
default='whipper',
|
||||
help=("logger to use (choose from: '%s" %
|
||||
"', '".join(loggers) + "')"))
|
||||
# FIXME: get from config
|
||||
self.parser.add_argument('-o', '--offset',
|
||||
action="store", dest="offset",
|
||||
default=default_offset,
|
||||
@@ -414,6 +418,7 @@ Log files will log the path to tracks relative to this directory.
|
||||
len(self.itable.tracks),
|
||||
extra))
|
||||
break
|
||||
# FIXME: catching too general exception (Exception)
|
||||
except Exception as e:
|
||||
logger.debug('got exception %r on try %d', e, tries)
|
||||
|
||||
@@ -441,17 +446,17 @@ Log files will log the path to tracks relative to this directory.
|
||||
logger.debug('HTOA peak %r is equal to the SILENT '
|
||||
'threshold, disregarding', trackResult.peak)
|
||||
self.itable.setFile(1, 0, None,
|
||||
self.ittoc.getTrackStart(1), number)
|
||||
self.itable.getTrackStart(1), number)
|
||||
logger.debug('unlinking %r', trackResult.filename)
|
||||
os.unlink(trackResult.filename)
|
||||
trackResult.filename = None
|
||||
logger.info('HTOA discarded, contains digital silence')
|
||||
else:
|
||||
self.itable.setFile(1, 0, trackResult.filename,
|
||||
self.ittoc.getTrackStart(1), number)
|
||||
self.itable.getTrackStart(1), number)
|
||||
else:
|
||||
self.itable.setFile(number, 1, trackResult.filename,
|
||||
self.ittoc.getTrackLength(number), number)
|
||||
self.itable.getTrackLength(number), number)
|
||||
|
||||
self.program.saveRipResult()
|
||||
|
||||
@@ -480,7 +485,7 @@ Log files will log the path to tracks relative to this directory.
|
||||
self.program.write_m3u(discName)
|
||||
|
||||
try:
|
||||
self.program.verifyImage(self.runner, self.ittoc)
|
||||
self.program.verifyImage(self.runner, self.itable)
|
||||
except accurip.EntryNotFound:
|
||||
logger.warning('AccurateRip entry not found')
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ Verifies the image from the given .cue files against the AccurateRip database.
|
||||
class Image(BaseCommand):
|
||||
summary = "handle images"
|
||||
description = """
|
||||
Handle disc images. Disc images are described by a .cue file.
|
||||
Handle disc images. Disc images are described by a .cue file.
|
||||
Disc images can be verified.
|
||||
"""
|
||||
subcommands = {
|
||||
|
||||
@@ -45,6 +45,7 @@ def main():
|
||||
logger.critical("SystemError: %s", e)
|
||||
if (isinstance(e, common.EjectError) and
|
||||
cmd.options.eject in ('failure', 'always')):
|
||||
# XXX: Pylint, instance of 'SystemError' has no 'device' member
|
||||
eject_device(e.device)
|
||||
return 255
|
||||
except RuntimeError as e:
|
||||
@@ -52,7 +53,7 @@ def main():
|
||||
return 1
|
||||
except KeyboardInterrupt:
|
||||
return 2
|
||||
except ImportError as e:
|
||||
except ImportError:
|
||||
raise
|
||||
except task.TaskException as e:
|
||||
if isinstance(e.exception, ImportError):
|
||||
@@ -74,11 +75,11 @@ def main():
|
||||
|
||||
|
||||
class Whipper(BaseCommand):
|
||||
description = """whipper is a CD ripping utility focusing on accuracy over speed.
|
||||
|
||||
whipper gives you a tree of subcommands to work with.
|
||||
You can get help on subcommands by using the -h option to the subcommand.
|
||||
"""
|
||||
description = (
|
||||
"whipper is a CD ripping utility focusing on accuracy over speed.\n\n"
|
||||
"whipper gives you a tree of subcommands to work with.\n"
|
||||
"You can get help on subcommands by using the -h option "
|
||||
"to the subcommand.\n")
|
||||
no_add_help = True
|
||||
subcommands = {
|
||||
'accurip': accurip.AccuRip,
|
||||
@@ -101,10 +102,10 @@ You can get help on subcommands by using the -h option to the subcommand.
|
||||
help="show this help message and exit")
|
||||
self.parser.add_argument('-e', '--eject',
|
||||
action="store", dest="eject",
|
||||
default="always",
|
||||
default="success",
|
||||
choices=('never', 'failure',
|
||||
'success', 'always'),
|
||||
help="when to eject disc (default: always)")
|
||||
help="when to eject disc (default: success)")
|
||||
|
||||
def handle_arguments(self):
|
||||
if self.options.help:
|
||||
|
||||
@@ -29,16 +29,18 @@ Example disc id: KnpGsLhvH.lPrNc1PBL21lb9Bg4-"""
|
||||
print('- Release %d:' % (i + 1, ))
|
||||
print(' Artist: %s' % md.artist.encode('utf-8'))
|
||||
print(' Title: %s' % md.title.encode('utf-8'))
|
||||
print(' Type: %s' % md.releaseType.encode('utf-8')) # noqa: E501
|
||||
print(' Type: %s' % unicode(md.releaseType).encode('utf-8')) # noqa: E501
|
||||
print(' URL: %s' % md.url)
|
||||
print(' Tracks: %d' % len(md.tracks))
|
||||
if md.catalogNumber:
|
||||
print(' Cat no: %s' % md.catalogNumber)
|
||||
if md.barcode:
|
||||
print(' Barcode: %s' % md.barcode)
|
||||
print(' Barcode: %s' % md.barcode)
|
||||
|
||||
for j, track in enumerate(md.tracks):
|
||||
print(' Track %2d: %s - %s' % (
|
||||
j + 1, track.artist.encode('utf-8'),
|
||||
track.title.encode('utf-8')
|
||||
))
|
||||
|
||||
return None
|
||||
|
||||
@@ -85,16 +85,16 @@ CD in the AccurateRip database."""
|
||||
|
||||
# first get the Table Of Contents of the CD
|
||||
t = cdrdao.ReadTOCTask(device)
|
||||
table = t.table
|
||||
runner.run(t)
|
||||
table = t.toc.table
|
||||
|
||||
logger.debug("CDDB disc id: %r", table.getCDDBDiscId())
|
||||
responses = None
|
||||
try:
|
||||
responses = accurip.get_db_entry(table.accuraterip_path())
|
||||
except accurip.EntryNotFound:
|
||||
logger.warning("AccurateRip entry not found: drive offset "
|
||||
"can't be determined, try again with another disc")
|
||||
return
|
||||
return None
|
||||
|
||||
if responses:
|
||||
logger.debug('%d AccurateRip responses found.', len(responses))
|
||||
@@ -133,7 +133,7 @@ CD in the AccurateRip database."""
|
||||
logger.warning('cannot rip with offset %d...', offset)
|
||||
continue
|
||||
|
||||
logger.debug('AR checksums calculated: %s %s', archecksums)
|
||||
logger.debug('AR checksums calculated: %s', archecksums)
|
||||
|
||||
c, i = match(archecksums, 1, responses)
|
||||
if c:
|
||||
@@ -170,6 +170,8 @@ CD in the AccurateRip database."""
|
||||
logger.error('no matching offset found. '
|
||||
'Consider trying again with a different disc')
|
||||
|
||||
return None
|
||||
|
||||
def _arcs(self, runner, table, track, offset):
|
||||
# rips the track with the given offset, return the arcs checksums
|
||||
logger.debug('ripping track %r with offset %d...', track, offset)
|
||||
@@ -188,17 +190,13 @@ CD in the AccurateRip database."""
|
||||
track, offset)
|
||||
runner.run(t)
|
||||
|
||||
v1 = arc.accuraterip_checksum(
|
||||
path, track, len(table.tracks), wave=True, v2=False
|
||||
)
|
||||
v2 = arc.accuraterip_checksum(
|
||||
path, track, len(table.tracks), wave=True, v2=True
|
||||
)
|
||||
v1, v2 = arc.accuraterip_checksum(path, track, len(table.tracks))
|
||||
|
||||
os.unlink(path)
|
||||
return ("%08x" % v1, "%08x" % v2)
|
||||
return "%08x" % v1, "%08x" % v2
|
||||
|
||||
def _foundOffset(self, device, offset):
|
||||
@staticmethod
|
||||
def _foundOffset(device, offset):
|
||||
print('\nRead offset of device is: %d.' % offset)
|
||||
|
||||
info = drive.getDeviceInfo(device)
|
||||
|
||||
@@ -110,19 +110,14 @@ def calculate_checksums(track_paths):
|
||||
logger.debug('checksumming %d tracks', track_count)
|
||||
# This is done sequentially because it is very fast.
|
||||
for i, path in enumerate(track_paths):
|
||||
v1_sum = accuraterip_checksum(
|
||||
path, i+1, track_count, wave=True, v2=False
|
||||
)
|
||||
if not v1_sum:
|
||||
v1_sum, v2_sum = accuraterip_checksum(path, i+1, track_count)
|
||||
if v1_sum is None:
|
||||
logger.error('could not calculate AccurateRip v1 checksum '
|
||||
'for track %d %r', i + 1, path)
|
||||
v1_checksums.append(None)
|
||||
else:
|
||||
v1_checksums.append("%08x" % v1_sum)
|
||||
v2_sum = accuraterip_checksum(
|
||||
path, i+1, track_count, wave=True, v2=True
|
||||
)
|
||||
if not v2_sum:
|
||||
if v2_sum is None:
|
||||
logger.error('could not calculate AccurateRip v2 checksum '
|
||||
'for track %d %r', i + 1, path)
|
||||
v2_checksums.append(None)
|
||||
@@ -236,7 +231,7 @@ def print_report(result):
|
||||
"""
|
||||
Print AccurateRip verification results.
|
||||
"""
|
||||
for i, track in enumerate(result.tracks):
|
||||
for _, track in enumerate(result.tracks):
|
||||
status = 'rip NOT accurate'
|
||||
conf = '(not found)'
|
||||
db = 'notfound'
|
||||
|
||||
@@ -39,7 +39,7 @@ class Persister:
|
||||
Call persist to store the object to disk; it will get stored if it
|
||||
changed from the on-disk object.
|
||||
|
||||
@ivar object: the persistent object
|
||||
:ivar object: the persistent object
|
||||
"""
|
||||
|
||||
def __init__(self, path=None, default=None):
|
||||
@@ -93,10 +93,10 @@ class Persister:
|
||||
self.object = default
|
||||
|
||||
if not self._path:
|
||||
return None
|
||||
return
|
||||
|
||||
if not os.path.exists(self._path):
|
||||
return None
|
||||
return
|
||||
|
||||
handle = open(self._path)
|
||||
import pickle
|
||||
@@ -104,12 +104,11 @@ class Persister:
|
||||
try:
|
||||
self.object = pickle.load(handle)
|
||||
logger.debug('loaded persisted object from %r', self._path)
|
||||
# FIXME: catching too general exception (Exception)
|
||||
except Exception as e:
|
||||
# TODO: restrict kind of caught exceptions?
|
||||
# can fail for various reasons; in that case, pretend we didn't
|
||||
# load it
|
||||
logger.debug(e)
|
||||
pass
|
||||
|
||||
def delete(self):
|
||||
self.object = None
|
||||
@@ -128,7 +127,7 @@ class PersistedCache:
|
||||
try:
|
||||
os.makedirs(self.path)
|
||||
except OSError as e:
|
||||
if e.errno != 17: # FIXME
|
||||
if e.errno != os.errno.EEXIST: # FIXME: errno 17 is 'File Exists'
|
||||
raise
|
||||
|
||||
def _getPath(self, key):
|
||||
@@ -163,7 +162,7 @@ class ResultCache:
|
||||
Retrieve the persistable RipResult either from our cache (from a
|
||||
previous, possibly aborted rip), or return a new one.
|
||||
|
||||
@rtype: L{Persistable} for L{result.RipResult}
|
||||
:rtype: :any:`Persistable` for :any:`result.RipResult`
|
||||
"""
|
||||
presult = self._pcache.get(cddbdiscid)
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ class CRC32Task(etask.Task):
|
||||
|
||||
def _crc32(self):
|
||||
if not self.is_wave:
|
||||
fd, tmpf = tempfile.mkstemp()
|
||||
_, tmpf = tempfile.mkstemp()
|
||||
|
||||
try:
|
||||
subprocess.check_call(['flac', '-d', self.path, '-fo', tmpf])
|
||||
|
||||
@@ -56,11 +56,11 @@ def msfToFrames(msf):
|
||||
"""
|
||||
Converts a string value in MM:SS:FF to frames.
|
||||
|
||||
@param msf: the MM:SS:FF value to convert
|
||||
@type msf: str
|
||||
:param msf: the MM:SS:FF value to convert
|
||||
:type msf: str
|
||||
|
||||
@rtype: int
|
||||
@returns: number of frames
|
||||
:rtype: int
|
||||
:returns: number of frames
|
||||
"""
|
||||
if ':' not in msf:
|
||||
return int(msf)
|
||||
@@ -76,7 +76,7 @@ def framesToMSF(frames, frameDelimiter=':'):
|
||||
f = frames % FRAMES_PER_SECOND
|
||||
frames -= f
|
||||
s = (frames / FRAMES_PER_SECOND) % 60
|
||||
frames -= s * 60
|
||||
frames -= s * FRAMES_PER_SECOND
|
||||
m = frames / FRAMES_PER_SECOND / 60
|
||||
|
||||
return "%02d:%02d%s%02d" % (m, s, frameDelimiter, f)
|
||||
@@ -104,19 +104,19 @@ def formatTime(seconds, fractional=3):
|
||||
If it is greater than 0, we will show seconds and fractions of seconds.
|
||||
As a side consequence, there is no way to show seconds without fractions.
|
||||
|
||||
@param seconds: the time in seconds to format.
|
||||
@type seconds: int or float
|
||||
@param fractional: how many digits to show for the fractional part of
|
||||
:param seconds: the time in seconds to format.
|
||||
:type seconds: int or float
|
||||
:param fractional: how many digits to show for the fractional part of
|
||||
seconds.
|
||||
@type fractional: int
|
||||
:type fractional: int
|
||||
|
||||
@rtype: string
|
||||
@returns: a nicely formatted time string.
|
||||
:rtype: string
|
||||
:returns: a nicely formatted time string.
|
||||
"""
|
||||
chunks = []
|
||||
|
||||
if seconds < 0:
|
||||
chunks.append(('-'))
|
||||
chunks.append('-')
|
||||
seconds = -seconds
|
||||
|
||||
hour = 60 * 60
|
||||
@@ -207,11 +207,11 @@ def getRealPath(refPath, filePath):
|
||||
Does Windows path translation.
|
||||
Will look for the given file name, but with .flac and .wav as extensions.
|
||||
|
||||
@param refPath: path to the file from which the track is referenced;
|
||||
:param refPath: path to the file from which the track is referenced;
|
||||
for example, path to the .cue file in the same directory
|
||||
@type refPath: unicode
|
||||
:type refPath: unicode
|
||||
|
||||
@type filePath: unicode
|
||||
:type filePath: unicode
|
||||
"""
|
||||
assert isinstance(filePath, unicode), "%r is not unicode" % filePath
|
||||
|
||||
@@ -271,13 +271,12 @@ def getRelativePath(targetPath, collectionPath):
|
||||
if targetDir == collectionDir:
|
||||
logger.debug('getRelativePath: target and collection in same dir')
|
||||
return os.path.basename(targetPath)
|
||||
else:
|
||||
rel = os.path.relpath(
|
||||
targetDir + os.path.sep,
|
||||
collectionDir + os.path.sep)
|
||||
logger.debug('getRelativePath: target and collection '
|
||||
'in different dir, %r', rel)
|
||||
return os.path.join(rel, os.path.basename(targetPath))
|
||||
rel = os.path.relpath(
|
||||
targetDir + os.path.sep,
|
||||
collectionDir + os.path.sep)
|
||||
logger.debug('getRelativePath: target and collection '
|
||||
'in different dir, %r', rel)
|
||||
return os.path.join(rel, os.path.basename(targetPath))
|
||||
|
||||
|
||||
def validate_template(template, kind):
|
||||
@@ -285,9 +284,9 @@ def validate_template(template, kind):
|
||||
Raise exception if disc/track template includes invalid variables
|
||||
"""
|
||||
if kind == 'disc':
|
||||
matches = re.findall(r'%[^A,R,S,X,d,r,x,y]', template)
|
||||
matches = re.findall(r'%[^ARSXdrxy]', template)
|
||||
elif kind == 'track':
|
||||
matches = re.findall(r'%[^A,R,S,X,a,d,n,r,s,t,x,y]', template)
|
||||
matches = re.findall(r'%[^ARSXadnrstxy]', template)
|
||||
if '%' in template and matches:
|
||||
raise ValueError(kind + ' template string contains invalid '
|
||||
'variable(s): {}'.format(', '.join(matches)))
|
||||
@@ -301,11 +300,11 @@ class VersionGetter(object):
|
||||
|
||||
def __init__(self, dependency, args, regexp, expander):
|
||||
"""
|
||||
@param dependency: name of the dependency providing the program
|
||||
@param args: the arguments to invoke to show the version
|
||||
@type args: list of str
|
||||
@param regexp: the regular expression to get the version
|
||||
@param expander: the expansion string for the version using the
|
||||
:param dependency: name of the dependency providing the program
|
||||
:param args: the arguments to invoke to show the version
|
||||
:type args: list of str
|
||||
:param regexp: the regular expression to get the version
|
||||
:param expander: the expansion string for the version using the
|
||||
regexp group dict
|
||||
"""
|
||||
|
||||
|
||||
@@ -156,7 +156,6 @@ class Config:
|
||||
section = 'drive:' + urllib.quote('%s:%s:%s' % (
|
||||
vendor, model, release))
|
||||
self._parser.add_section(section)
|
||||
__pychecker__ = 'no-local'
|
||||
for key in ['vendor', 'model', 'release']:
|
||||
self._parser.set(section, key, locals()[key].strip())
|
||||
|
||||
|
||||
@@ -66,6 +66,6 @@ def getDeviceInfo(path):
|
||||
except ImportError:
|
||||
return None
|
||||
device = cdio.Device(path)
|
||||
ok, vendor, model, release = device.get_hwinfo()
|
||||
_, vendor, model, release = device.get_hwinfo()
|
||||
|
||||
return (vendor, model, release)
|
||||
return vendor, model, release
|
||||
|
||||
@@ -60,7 +60,7 @@ class FlacEncodeTask(task.Task):
|
||||
self.schedule(0.0, self._flac_encode)
|
||||
|
||||
def _flac_encode(self):
|
||||
self.new_path = flac.encode(self.track_path, self.track_out_path)
|
||||
flac.encode(self.track_path, self.track_out_path)
|
||||
self.stop()
|
||||
|
||||
|
||||
|
||||
@@ -52,17 +52,19 @@ class TrackMetadata(object):
|
||||
mbid = None
|
||||
sortName = None
|
||||
mbidArtist = None
|
||||
mbidRecording = None
|
||||
mbidWorks = []
|
||||
|
||||
|
||||
class DiscMetadata(object):
|
||||
"""
|
||||
@param artist: artist(s) name
|
||||
@param sortName: album artist sort name
|
||||
@param release: earliest release date, in YYYY-MM-DD
|
||||
@type release: unicode
|
||||
@param title: title of the disc (with disambiguation)
|
||||
@param releaseTitle: title of the release (without disambiguation)
|
||||
@type tracks: C{list} of L{TrackMetadata}
|
||||
:param artist: artist(s) name
|
||||
:param sortName: release artist sort name
|
||||
:param release: earliest release date, in YYYY-MM-DD
|
||||
:type release: unicode
|
||||
:param title: title of the disc (with disambiguation)
|
||||
:param releaseTitle: title of the release (without disambiguation)
|
||||
:type tracks: list of :any:`TrackMetadata`
|
||||
"""
|
||||
artist = None
|
||||
sortName = None
|
||||
@@ -75,6 +77,7 @@ class DiscMetadata(object):
|
||||
releaseType = None
|
||||
|
||||
mbid = None
|
||||
mbidReleaseGroup = None
|
||||
mbidArtist = None
|
||||
url = None
|
||||
|
||||
@@ -140,17 +143,31 @@ class _Credit(list):
|
||||
i.get('artist').get('name', None)))
|
||||
|
||||
def getIds(self):
|
||||
# split()'s the joined string so we get a proper list of MBIDs
|
||||
return self.joiner(lambda i: i.get('artist').get('id', None),
|
||||
joinString=";")
|
||||
joinString=";").split(';')
|
||||
|
||||
|
||||
def _getMetadata(releaseShort, release, discid, country=None):
|
||||
def _getWorks(recording):
|
||||
"""Get "performance of" works out of a recording."""
|
||||
works = []
|
||||
valid_work_rel_types = [
|
||||
u'a3005666-a872-32c3-ad06-98af558e99b0', # "Performance"
|
||||
]
|
||||
if 'work-relation-list' in recording:
|
||||
for work in recording['work-relation-list']:
|
||||
if work['type-id'] in valid_work_rel_types:
|
||||
works.append(work['work']['id'])
|
||||
return works
|
||||
|
||||
|
||||
def _getMetadata(release, discid, country=None):
|
||||
"""
|
||||
@type release: C{dict}
|
||||
@param release: a release dict as returned in the value for key release
|
||||
:type release: dict
|
||||
:param release: a release dict as returned in the value for key release
|
||||
from get_release_by_id
|
||||
|
||||
@rtype: L{DiscMetadata} or None
|
||||
:rtype: DiscMetadata or None
|
||||
"""
|
||||
logger.debug('getMetadata for release id %r', release['id'])
|
||||
if not release['id']:
|
||||
@@ -165,7 +182,8 @@ def _getMetadata(releaseShort, release, discid, country=None):
|
||||
|
||||
discMD = DiscMetadata()
|
||||
|
||||
discMD.releaseType = releaseShort.get('release-group', {}).get('type')
|
||||
if 'type' in release['release-group']:
|
||||
discMD.releaseType = release['release-group']['type']
|
||||
discCredit = _Credit(release['artist-credit'])
|
||||
|
||||
# FIXME: is there a better way to check for VA ?
|
||||
@@ -176,10 +194,10 @@ def _getMetadata(releaseShort, release, discid, country=None):
|
||||
if len(discCredit) > 1:
|
||||
logger.debug('artist-credit more than 1: %r', discCredit)
|
||||
|
||||
albumArtistName = discCredit.getName()
|
||||
releaseArtistName = discCredit.getName()
|
||||
|
||||
# getUniqueName gets disambiguating names like Muse (UK rock band)
|
||||
discMD.artist = albumArtistName
|
||||
discMD.artist = releaseArtistName
|
||||
discMD.sortName = discCredit.getSortName()
|
||||
if 'date' not in release:
|
||||
logger.warning("release with ID '%s' (%s - %s) does not have a date",
|
||||
@@ -188,6 +206,7 @@ def _getMetadata(releaseShort, release, discid, country=None):
|
||||
discMD.release = release['date']
|
||||
|
||||
discMD.mbid = release['id']
|
||||
discMD.mbidReleaseGroup = release['release-group']['id']
|
||||
discMD.mbidArtist = discCredit.getIds()
|
||||
discMD.url = 'https://musicbrainz.org/release/' + release['id']
|
||||
|
||||
@@ -229,7 +248,9 @@ def _getMetadata(releaseShort, release, discid, country=None):
|
||||
track.mbidArtist = trackCredit.getIds()
|
||||
|
||||
track.title = t['recording']['title']
|
||||
track.mbid = t['recording']['id']
|
||||
track.mbid = t['id']
|
||||
track.mbidRecording = t['recording']['id']
|
||||
track.mbidWorks = _getWorks(t['recording'])
|
||||
|
||||
# FIXME: unit of duration ?
|
||||
track.duration = int(t['recording'].get('length', 0))
|
||||
@@ -261,13 +282,14 @@ def musicbrainz(discid, country=None, record=False):
|
||||
|
||||
Example disc id: Mj48G109whzEmAbPBoGvd4KyCS4-
|
||||
|
||||
@type discid: str
|
||||
:type discid: str
|
||||
|
||||
@rtype: list of L{DiscMetadata}
|
||||
:rtype: list of :any:`DiscMetadata`
|
||||
"""
|
||||
logger.debug('looking up results for discid %r', discid)
|
||||
import musicbrainzngs
|
||||
|
||||
logging.getLogger("musicbrainzngs").setLevel(logging.WARNING)
|
||||
musicbrainzngs.set_useragent("whipper", whipper.__version__,
|
||||
"https://github.com/whipper-team/whipper")
|
||||
ret = []
|
||||
@@ -303,13 +325,15 @@ def musicbrainz(discid, country=None, record=False):
|
||||
|
||||
res = musicbrainzngs.get_release_by_id(
|
||||
release['id'], includes=["artists", "artist-credits",
|
||||
"recordings", "discids", "labels"])
|
||||
"recordings", "discids", "labels",
|
||||
"recording-level-rels", "work-rels",
|
||||
"release-groups"])
|
||||
_record(record, 'release', release['id'], res)
|
||||
releaseDetail = res['release']
|
||||
formatted = json.dumps(releaseDetail, sort_keys=False, indent=4)
|
||||
logger.debug('release %s', formatted)
|
||||
|
||||
md = _getMetadata(release, releaseDetail, discid, country)
|
||||
md = _getMetadata(releaseDetail, discid, country)
|
||||
if md:
|
||||
logger.debug('duration %r', md.duration)
|
||||
ret.append(md)
|
||||
@@ -317,6 +341,4 @@ def musicbrainz(discid, country=None, record=False):
|
||||
return ret
|
||||
elif result.get('cdstub'):
|
||||
logger.debug('query returned cdstub: ignored')
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@@ -28,10 +28,10 @@ class PathFilter(object):
|
||||
|
||||
def __init__(self, slashes=True, quotes=True, fat=True, special=False):
|
||||
"""
|
||||
@param slashes: whether to convert slashes to dashes
|
||||
@param quotes: whether to normalize quotes
|
||||
@param fat: whether to strip characters illegal on FAT filesystems
|
||||
@param special: whether to strip special characters
|
||||
:param slashes: whether to convert slashes to dashes
|
||||
:param quotes: whether to normalize quotes
|
||||
:param fat: whether to strip characters illegal on FAT filesystems
|
||||
:param special: whether to strip special characters
|
||||
"""
|
||||
self._slashes = slashes
|
||||
self._quotes = quotes
|
||||
@@ -45,7 +45,7 @@ class PathFilter(object):
|
||||
def separators(path):
|
||||
# replace separators with a space-hyphen or hyphen
|
||||
path = re.sub(r'[:]', ' -', path, re.UNICODE)
|
||||
path = re.sub(r'[\|]', '-', path, re.UNICODE)
|
||||
path = re.sub(r'[|]', '-', path, re.UNICODE)
|
||||
return path
|
||||
|
||||
# change all fancy single/double quotes to normal quotes
|
||||
@@ -56,12 +56,12 @@ class PathFilter(object):
|
||||
|
||||
if self._special:
|
||||
path = separators(path)
|
||||
path = re.sub(r'[\*\?&!\'\"\$\(\)`{}\[\]<>]',
|
||||
path = re.sub(r'[*?&!\'\"$()`{}\[\]<>]',
|
||||
'_', path, re.UNICODE)
|
||||
|
||||
if self._fat:
|
||||
path = separators(path)
|
||||
# : and | already gone, but leave them here for reference
|
||||
path = re.sub(r'[:\*\?"<>|"]', '_', path, re.UNICODE)
|
||||
path = re.sub(r'[:*?"<>|]', '_', path, re.UNICODE)
|
||||
|
||||
return path
|
||||
|
||||
@@ -44,12 +44,11 @@ class Program:
|
||||
"""
|
||||
I maintain program state and functionality.
|
||||
|
||||
@ivar metadata:
|
||||
@type metadata: L{mbngs.DiscMetadata}
|
||||
@ivar result: the rip's result
|
||||
@type result: L{result.RipResult}
|
||||
@type outdir: unicode
|
||||
@type config: L{whipper.common.config.Config}
|
||||
:vartype metadata: mbngs.DiscMetadata
|
||||
:cvar result: the rip's result
|
||||
:vartype result: result.RipResult
|
||||
:vartype outdir: unicode
|
||||
:vartype config: whipper.common.config.Config
|
||||
"""
|
||||
|
||||
cuePath = None
|
||||
@@ -60,7 +59,7 @@ class Program:
|
||||
|
||||
def __init__(self, config, record=False):
|
||||
"""
|
||||
@param record: whether to record results of API calls for playback.
|
||||
:param record: whether to record results of API calls for playback.
|
||||
"""
|
||||
self._record = record
|
||||
self._cache = cache.ResultCache()
|
||||
@@ -81,7 +80,8 @@ class Program:
|
||||
|
||||
self._filter = path.PathFilter(**d)
|
||||
|
||||
def setWorkingDirectory(self, workingDirectory):
|
||||
@staticmethod
|
||||
def setWorkingDirectory(workingDirectory):
|
||||
if workingDirectory:
|
||||
logger.info('changing to working directory %s', workingDirectory)
|
||||
os.chdir(workingDirectory)
|
||||
@@ -91,20 +91,24 @@ class Program:
|
||||
Also warn about buggy cdrdao versions.
|
||||
"""
|
||||
from pkg_resources import parse_version as V
|
||||
version = cdrdao.getCDRDAOVersion()
|
||||
version = cdrdao.version()
|
||||
if V(version) < V('1.2.3rc2'):
|
||||
logger.warning('cdrdao older than 1.2.3 has a pre-gap length bug.'
|
||||
' See http://sourceforge.net/tracker/?func=detail&aid=604751&group_id=2171&atid=102171') # noqa: E501
|
||||
toc = cdrdao.ReadTOCTask(device).table
|
||||
|
||||
t = cdrdao.ReadTOCTask(device, fast_toc=True)
|
||||
runner.run(t)
|
||||
toc = t.toc.table
|
||||
|
||||
assert toc.hasTOC()
|
||||
return toc
|
||||
|
||||
def getTable(self, runner, cddbdiscid, mbdiscid, device, offset,
|
||||
out_path):
|
||||
toc_path):
|
||||
"""
|
||||
Retrieve the Table either from the cache or the drive.
|
||||
|
||||
@rtype: L{table.Table}
|
||||
:rtype: table.Table
|
||||
"""
|
||||
tcache = cache.TableCache()
|
||||
ptable = tcache.get(cddbdiscid, mbdiscid)
|
||||
@@ -122,8 +126,10 @@ class Program:
|
||||
logger.debug('getTable: cddbdiscid %s, mbdiscid %s not in cache '
|
||||
'for offset %s, reading table', cddbdiscid, mbdiscid,
|
||||
offset)
|
||||
t = cdrdao.ReadTableTask(device, out_path)
|
||||
itable = t.table
|
||||
t = cdrdao.ReadTOCTask(device, toc_path=toc_path)
|
||||
t.description = "Reading table"
|
||||
runner.run(t)
|
||||
itable = t.toc.table
|
||||
tdict[offset] = itable
|
||||
ptable.persist(tdict)
|
||||
logger.debug('getTable: read table %r', itable)
|
||||
@@ -145,7 +151,7 @@ class Program:
|
||||
Retrieve the persistable RipResult either from our cache (from a
|
||||
previous, possibly aborted rip), or return a new one.
|
||||
|
||||
@rtype: L{result.RipResult}
|
||||
:rtype: result.RipResult
|
||||
"""
|
||||
assert self.result is None
|
||||
|
||||
@@ -157,8 +163,9 @@ class Program:
|
||||
def saveRipResult(self):
|
||||
self._presult.persist()
|
||||
|
||||
def addDisambiguation(self, template_part, metadata):
|
||||
"Add disambiguation to template path part string."
|
||||
@staticmethod
|
||||
def addDisambiguation(template_part, metadata):
|
||||
"""Add disambiguation to template path part string."""
|
||||
if metadata.catalogNumber:
|
||||
template_part += ' (%s)' % metadata.catalogNumber
|
||||
elif metadata.barcode:
|
||||
@@ -181,12 +188,12 @@ class Program:
|
||||
Disc files (.cue, .log, .m3u) are named according to the disc
|
||||
template, filling in the variables and adding the file
|
||||
extension. Variables for both disc and track template are:
|
||||
- %A: album artist
|
||||
- %S: album artist sort name
|
||||
- %A: release artist
|
||||
- %S: release artist sort name
|
||||
- %d: disc title
|
||||
- %y: release year
|
||||
- %r: release type, lowercase
|
||||
- %R: Release type, normal case
|
||||
- %R: release type, normal case
|
||||
- %x: audio extension, lowercase
|
||||
- %X: audio extension, uppercase
|
||||
"""
|
||||
@@ -235,11 +242,12 @@ class Program:
|
||||
template = re.sub(r'%(\w)', r'%(\1)s', template)
|
||||
return os.path.join(outdir, template % v)
|
||||
|
||||
def getCDDB(self, cddbdiscid):
|
||||
@staticmethod
|
||||
def getCDDB(cddbdiscid):
|
||||
"""
|
||||
@param cddbdiscid: list of id, tracks, offsets, seconds
|
||||
:param cddbdiscid: list of id, tracks, offsets, seconds
|
||||
|
||||
@rtype: str
|
||||
:rtype: str
|
||||
"""
|
||||
# FIXME: convert to nonblocking?
|
||||
try:
|
||||
@@ -262,7 +270,7 @@ class Program:
|
||||
def getMusicBrainz(self, ittoc, mbdiscid, release=None, country=None,
|
||||
prompt=False):
|
||||
"""
|
||||
@type ittoc: L{whipper.image.table.Table}
|
||||
:type ittoc: whipper.image.table.Table
|
||||
"""
|
||||
# look up disc on MusicBrainz
|
||||
print('Disc duration: %s, %d audio tracks' % (
|
||||
@@ -270,10 +278,8 @@ class Program:
|
||||
ittoc.getAudioTracks()))
|
||||
logger.debug('MusicBrainz submit url: %r',
|
||||
ittoc.getMusicBrainzSubmitURL())
|
||||
ret = None
|
||||
|
||||
metadatas = None
|
||||
e = None
|
||||
|
||||
for _ in range(0, 4):
|
||||
try:
|
||||
@@ -310,6 +316,7 @@ class Program:
|
||||
print('Type : %s' % metadata.releaseType)
|
||||
if metadata.barcode:
|
||||
print("Barcode : %s" % metadata.barcode)
|
||||
# TODO: Add test for non ASCII catalog numbers: see issue #215
|
||||
if metadata.catalogNumber:
|
||||
print("Cat no : %s" %
|
||||
metadata.catalogNumber.encode('utf-8'))
|
||||
@@ -344,7 +351,7 @@ class Program:
|
||||
elif not metadatas:
|
||||
logger.warning("requested release id '%s', but none of "
|
||||
"the found releases match", release)
|
||||
return
|
||||
return None
|
||||
else:
|
||||
if lowest:
|
||||
metadatas = deltas[lowest]
|
||||
@@ -363,7 +370,7 @@ class Program:
|
||||
"not the same", releaseTitle, i,
|
||||
metadata.releaseTitle)
|
||||
|
||||
if (not release and len(list(deltas)) > 1):
|
||||
if not release and len(list(deltas)) > 1:
|
||||
logger.warning('picked closest match in duration. '
|
||||
'Others may be wrong in MusicBrainz, '
|
||||
'please correct')
|
||||
@@ -383,30 +390,33 @@ class Program:
|
||||
"""
|
||||
Based on the metadata, get a dict of tags for the given track.
|
||||
|
||||
@param number: track number (0 for HTOA)
|
||||
@type number: int
|
||||
:param number: track number (0 for HTOA)
|
||||
:type number: int
|
||||
|
||||
@rtype: dict
|
||||
:rtype: dict
|
||||
"""
|
||||
trackArtist = u'Unknown Artist'
|
||||
albumArtist = u'Unknown Artist'
|
||||
releaseArtist = u'Unknown Artist'
|
||||
disc = u'Unknown Disc'
|
||||
title = u'Unknown Track'
|
||||
|
||||
if self.metadata:
|
||||
trackArtist = self.metadata.artist
|
||||
albumArtist = self.metadata.artist
|
||||
releaseArtist = self.metadata.artist
|
||||
disc = self.metadata.title
|
||||
mbidAlbum = self.metadata.mbid
|
||||
mbidTrackAlbum = self.metadata.mbidArtist
|
||||
mbidRelease = self.metadata.mbid
|
||||
mbidReleaseGroup = self.metadata.mbidReleaseGroup
|
||||
mbidReleaseArtist = self.metadata.mbidArtist
|
||||
|
||||
if number > 0:
|
||||
try:
|
||||
track = self.metadata.tracks[number - 1]
|
||||
trackArtist = track.artist
|
||||
title = track.title
|
||||
mbidRecording = track.mbidRecording
|
||||
mbidTrack = track.mbid
|
||||
mbidTrackArtist = track.mbidArtist
|
||||
mbidWorks = track.mbidWorks
|
||||
except IndexError as e:
|
||||
logger.error('no track %d found, %r', number, e)
|
||||
raise
|
||||
@@ -420,7 +430,7 @@ class Program:
|
||||
tags['MUSICBRAINZ_DISCID'] = mbdiscid
|
||||
|
||||
if self.metadata and not self.metadata.various:
|
||||
tags['ALBUMARTIST'] = albumArtist
|
||||
tags['ALBUMARTIST'] = releaseArtist
|
||||
tags['ARTIST'] = trackArtist
|
||||
tags['TITLE'] = title
|
||||
tags['ALBUM'] = disc
|
||||
@@ -432,10 +442,14 @@ class Program:
|
||||
tags['DATE'] = self.metadata.release
|
||||
|
||||
if number > 0:
|
||||
tags['MUSICBRAINZ_TRACKID'] = mbidTrack
|
||||
tags['MUSICBRAINZ_RELEASETRACKID'] = mbidTrack
|
||||
tags['MUSICBRAINZ_TRACKID'] = mbidRecording
|
||||
tags['MUSICBRAINZ_ARTISTID'] = mbidTrackArtist
|
||||
tags['MUSICBRAINZ_ALBUMID'] = mbidAlbum
|
||||
tags['MUSICBRAINZ_ALBUMARTISTID'] = mbidTrackAlbum
|
||||
tags['MUSICBRAINZ_ALBUMID'] = mbidRelease
|
||||
tags['MUSICBRAINZ_RELEASEGROUPID'] = mbidReleaseGroup
|
||||
tags['MUSICBRAINZ_ALBUMARTISTID'] = mbidReleaseArtist
|
||||
if len(mbidWorks) > 0:
|
||||
tags['MUSICBRAINZ_WORKID'] = mbidWorks
|
||||
|
||||
# TODO/FIXME: ISRC tag
|
||||
|
||||
@@ -445,7 +459,7 @@ class Program:
|
||||
"""
|
||||
Check if we have hidden track one audio.
|
||||
|
||||
@returns: tuple of (start, stop), or None
|
||||
:returns: tuple of (start, stop), or None
|
||||
"""
|
||||
track = self.result.table.tracks[0]
|
||||
try:
|
||||
@@ -455,9 +469,10 @@ class Program:
|
||||
|
||||
start = index.absolute
|
||||
stop = track.getIndex(1).absolute - 1
|
||||
return (start, stop)
|
||||
return start, stop
|
||||
|
||||
def verifyTrack(self, runner, trackResult):
|
||||
@staticmethod
|
||||
def verifyTrack(runner, trackResult):
|
||||
is_wave = not trackResult.filename.endswith('.flac')
|
||||
t = checksum.CRC32Task(trackResult.filename, is_wave=is_wave)
|
||||
|
||||
@@ -481,8 +496,8 @@ class Program:
|
||||
Ripping the track may change the track's filename as stored in
|
||||
trackResult.
|
||||
|
||||
@param trackResult: the object to store information in.
|
||||
@type trackResult: L{result.TrackResult}
|
||||
:param trackResult: the object to store information in.
|
||||
:type trackResult: result.TrackResult
|
||||
"""
|
||||
if trackResult.number == 0:
|
||||
start, stop = self.getHTOA()
|
||||
@@ -589,10 +604,10 @@ class Program:
|
||||
|
||||
return cuePath
|
||||
|
||||
def writeLog(self, discName, logger):
|
||||
def writeLog(self, discName, txt_logger):
|
||||
logPath = common.truncate_filename(discName + '.log')
|
||||
handle = open(logPath, 'w')
|
||||
log = logger.log(self.result)
|
||||
log = txt_logger.log(self.result)
|
||||
handle.write(log.encode('utf-8'))
|
||||
handle.close()
|
||||
|
||||
|
||||
@@ -21,9 +21,7 @@
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
"""
|
||||
Rename files on file system and inside metafiles in a resumable way.
|
||||
"""
|
||||
"""Rename files on file system and inside metafiles in a resumable way."""
|
||||
|
||||
|
||||
class Operator(object):
|
||||
@@ -111,10 +109,10 @@ class FileRenamer(Operator):
|
||||
"""
|
||||
Add a rename operation.
|
||||
|
||||
@param source: source filename
|
||||
@type source: str
|
||||
@param destination: destination filename
|
||||
@type destination: str
|
||||
:param source: source filename
|
||||
:type source: str
|
||||
:param destination: destination filename
|
||||
:type destination: str
|
||||
"""
|
||||
|
||||
|
||||
@@ -144,16 +142,16 @@ class Operation(object):
|
||||
def serialize(self):
|
||||
"""
|
||||
Serialize the operation.
|
||||
The return value should bu usable with L{deserialize}
|
||||
The return value should bu usable with :any:`deserialize`
|
||||
|
||||
@rtype: str
|
||||
:rtype: str
|
||||
"""
|
||||
|
||||
def deserialize(cls, data):
|
||||
"""
|
||||
Deserialize the operation with the given operation data.
|
||||
|
||||
@type data: str
|
||||
:type data: str
|
||||
"""
|
||||
raise NotImplementedError
|
||||
deserialize = classmethod(deserialize)
|
||||
|
||||
@@ -87,6 +87,7 @@ class PopenTask(task.Task):
|
||||
return
|
||||
|
||||
self._done()
|
||||
# FIXME: catching too general exception (Exception)
|
||||
except Exception as e:
|
||||
logger.debug('exception during _read(): %s', e)
|
||||
self.setException(e)
|
||||
@@ -115,13 +116,13 @@ class PopenTask(task.Task):
|
||||
os.kill(self._popen.pid, signal.SIGTERM)
|
||||
# self.stop()
|
||||
|
||||
def readbytesout(self, bytes):
|
||||
def readbytesout(self, bytes_stdout):
|
||||
"""
|
||||
Called when bytes have been read from stdout.
|
||||
"""
|
||||
pass
|
||||
|
||||
def readbyteserr(self, bytes):
|
||||
def readbyteserr(self, bytes_stderr):
|
||||
"""
|
||||
Called when bytes have been read from stderr.
|
||||
"""
|
||||
|
||||
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):
|
||||
return self._recv('stderr', maxsize)
|
||||
|
||||
def send_recv(self, input='', maxsize=None):
|
||||
return self.send(input), self.recv(maxsize), self.recv_err(maxsize)
|
||||
def send_recv(self, in_put='', maxsize=None):
|
||||
return self.send(in_put), self.recv(maxsize), self.recv_err(maxsize)
|
||||
|
||||
def get_conn_maxsize(self, which, maxsize):
|
||||
if maxsize is None:
|
||||
@@ -44,16 +44,16 @@ class Popen(subprocess.Popen):
|
||||
|
||||
if subprocess.mswindows:
|
||||
|
||||
def send(self, input):
|
||||
def send(self, in_put):
|
||||
if not self.stdin:
|
||||
return None
|
||||
|
||||
try:
|
||||
x = msvcrt.get_osfhandle(self.stdin.fileno())
|
||||
(errCode, written) = WriteFile(x, input)
|
||||
(errCode, written) = WriteFile(x, in_put)
|
||||
except ValueError:
|
||||
return self._close('stdin')
|
||||
except (subprocess.pywintypes.error, Exception), why:
|
||||
except (subprocess.pywintypes.error, Exception) as why:
|
||||
if why.args[0] in (109, errno.ESHUTDOWN):
|
||||
return self._close('stdin')
|
||||
raise
|
||||
@@ -74,7 +74,7 @@ class Popen(subprocess.Popen):
|
||||
(errCode, read) = ReadFile(x, nAvail, None)
|
||||
except ValueError:
|
||||
return self._close(which)
|
||||
except (subprocess.pywintypes.error, Exception), why:
|
||||
except (subprocess.pywintypes.error, Exception) as why:
|
||||
if why.args[0] in (109, errno.ESHUTDOWN):
|
||||
return self._close(which)
|
||||
raise
|
||||
@@ -85,7 +85,7 @@ class Popen(subprocess.Popen):
|
||||
|
||||
else:
|
||||
|
||||
def send(self, input):
|
||||
def send(self, in_put):
|
||||
if not self.stdin:
|
||||
return None
|
||||
|
||||
@@ -93,8 +93,8 @@ class Popen(subprocess.Popen):
|
||||
return 0
|
||||
|
||||
try:
|
||||
written = os.write(self.stdin.fileno(), input)
|
||||
except OSError, why:
|
||||
written = os.write(self.stdin.fileno(), in_put)
|
||||
except OSError as why:
|
||||
if why.args[0] == errno.EPIPE: # broken pipe
|
||||
return self._close('stdin')
|
||||
raise
|
||||
@@ -153,7 +153,7 @@ def recv_some(p, t=.1, e=1, tr=5, stderr=0):
|
||||
|
||||
|
||||
def send_all(p, data):
|
||||
while len(data):
|
||||
while data:
|
||||
sent = p.send(data)
|
||||
if sent is None:
|
||||
raise Exception(message)
|
||||
|
||||
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:
|
||||
# for each result, query FreeDB for XMCD file data
|
||||
for (category, disc_id, title) in matches:
|
||||
# XXX: Pylint, redefining argument with the local name 'disc_id'
|
||||
for (category, disc_id, _) in matches:
|
||||
sleep(1) # add a slight delay to keep the server happy
|
||||
|
||||
query = freedb_command(freedb_server,
|
||||
@@ -145,7 +146,7 @@ def perform_lookup(disc_id, freedb_server, freedb_port):
|
||||
|
||||
response = RESPONSE.match(next(query))
|
||||
if response is not None:
|
||||
# FIXME - check response code here
|
||||
# FIXME: check response code here
|
||||
freedb = {}
|
||||
line = next(query)
|
||||
while not line.startswith(u"."):
|
||||
|
||||
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 sys
|
||||
|
||||
try:
|
||||
from gi.repository import GLib as gobject
|
||||
except ImportError:
|
||||
import gobject
|
||||
from gi.repository import GLib as GLib
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -77,13 +74,16 @@ class LogStub(object):
|
||||
I am a stub for a log interface.
|
||||
"""
|
||||
|
||||
def log(self, message, *args):
|
||||
@staticmethod
|
||||
def log(message, *args):
|
||||
logger.info(message, *args)
|
||||
|
||||
def debug(self, message, *args):
|
||||
@staticmethod
|
||||
def debug(message, *args):
|
||||
logger.debug(message, *args)
|
||||
|
||||
def warning(self, message, *args):
|
||||
@staticmethod
|
||||
def warning(message, *args):
|
||||
logger.warning(message, *args)
|
||||
|
||||
|
||||
@@ -97,8 +97,8 @@ class Task(LogStub):
|
||||
stopping myself from running.
|
||||
The listener can then handle the Task.exception.
|
||||
|
||||
@ivar description: what am I doing
|
||||
@ivar exception: set if an exception happened during the task
|
||||
:cvar description: what am I doing
|
||||
:cvar exception: set if an exception happened during the task
|
||||
execution. Will be raised through run() at the end.
|
||||
"""
|
||||
logCategory = 'Task'
|
||||
@@ -191,8 +191,8 @@ class Task(LogStub):
|
||||
# for now
|
||||
if str(exception):
|
||||
msg = ": %s" % str(exception)
|
||||
line = "exception %(exc)s at %(filename)s:%(line)s: "
|
||||
"%(func)s()%(msg)s" % locals()
|
||||
line = ("exception %(exc)s at %(filename)s:%(line)s: "
|
||||
"%(func)s()%(msg)s" % locals())
|
||||
|
||||
self.exception = exception
|
||||
self.exceptionMessage = line
|
||||
@@ -213,13 +213,13 @@ class Task(LogStub):
|
||||
self.debug('set exception, %r, %r' % (
|
||||
exception, self.exceptionMessage))
|
||||
|
||||
def schedule(self, delta, callable, *args, **kwargs):
|
||||
def schedule(self, delta, callable_task, *args, **kwargs):
|
||||
if not self.runner:
|
||||
print("ERROR: scheduling on a task that's altready stopped")
|
||||
import traceback
|
||||
traceback.print_stack()
|
||||
return
|
||||
self.runner.schedule(self, delta, callable, *args, **kwargs)
|
||||
self.runner.schedule(self, delta, callable_task, *args, **kwargs)
|
||||
|
||||
def addListener(self, listener):
|
||||
"""
|
||||
@@ -238,6 +238,7 @@ class Task(LogStub):
|
||||
method = getattr(l, methodName)
|
||||
try:
|
||||
method(self, *args, **kwargs)
|
||||
# FIXME: catching too general exception (Exception)
|
||||
except Exception as e:
|
||||
self.setException(e)
|
||||
|
||||
@@ -253,16 +254,16 @@ class ITaskListener(object):
|
||||
"""
|
||||
Implement me to be informed about progress.
|
||||
|
||||
@type value: float
|
||||
@param value: progress, from 0.0 to 1.0
|
||||
:type value: float
|
||||
:param value: progress, from 0.0 to 1.0
|
||||
"""
|
||||
|
||||
def described(self, task, description):
|
||||
"""
|
||||
Implement me to be informed about description changes.
|
||||
|
||||
@type description: str
|
||||
@param description: description
|
||||
:type description: str
|
||||
:param description: description
|
||||
"""
|
||||
|
||||
def started(self, task):
|
||||
@@ -297,8 +298,8 @@ class BaseMultiTask(Task, ITaskListener):
|
||||
"""
|
||||
I perform multiple tasks.
|
||||
|
||||
@ivar tasks: the tasks to run
|
||||
@type tasks: list of L{Task}
|
||||
:ivar tasks: the tasks to run
|
||||
:type tasks: list of :any:`Task`
|
||||
"""
|
||||
|
||||
description = 'Doing various tasks'
|
||||
@@ -312,7 +313,7 @@ class BaseMultiTask(Task, ITaskListener):
|
||||
"""
|
||||
Add a task.
|
||||
|
||||
@type task: L{Task}
|
||||
:type task: Task
|
||||
"""
|
||||
if self.tasks is None:
|
||||
self.tasks = []
|
||||
@@ -350,6 +351,7 @@ class BaseMultiTask(Task, ITaskListener):
|
||||
task.start(self.runner)
|
||||
self.debug('BaseMultiTask.next(): started task %d of %d: %r',
|
||||
self._task, len(self.tasks), task)
|
||||
# FIXME: catching too general exception (Exception)
|
||||
except Exception as e:
|
||||
self.setException(e)
|
||||
self.debug('Got exception during next: %r', self.exceptionMessage)
|
||||
@@ -444,26 +446,26 @@ class TaskRunner(LogStub):
|
||||
"""
|
||||
Run the given task.
|
||||
|
||||
@type task: Task
|
||||
:type task: Task
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
# methods for tasks to call
|
||||
def schedule(self, delta, callable, *args, **kwargs):
|
||||
def schedule(self, delta, callable_task, *args, **kwargs):
|
||||
"""
|
||||
Schedule a single future call.
|
||||
|
||||
Subclasses should implement this.
|
||||
|
||||
@type delta: float
|
||||
@param delta: time in the future to schedule call for, in seconds.
|
||||
:type delta: float
|
||||
:param delta: time in the future to schedule call for, in seconds.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class SyncRunner(TaskRunner, ITaskListener):
|
||||
"""
|
||||
I run the task synchronously in a gobject MainLoop.
|
||||
I run the task synchronously in a GObject MainLoop.
|
||||
"""
|
||||
|
||||
def __init__(self, verbose=True):
|
||||
@@ -478,11 +480,11 @@ class SyncRunner(TaskRunner, ITaskListener):
|
||||
self._verboseRun = verbose
|
||||
self._skip = skip
|
||||
|
||||
self._loop = gobject.MainLoop()
|
||||
self._loop = GLib.MainLoop()
|
||||
self._task.addListener(self)
|
||||
# only start the task after going into the mainloop,
|
||||
# otherwise the task might complete before we are in it
|
||||
gobject.timeout_add(0L, self._startWrap, self._task)
|
||||
GLib.timeout_add(0L, self._startWrap, self._task)
|
||||
self.debug('run loop')
|
||||
self._loop.run()
|
||||
|
||||
@@ -503,6 +505,7 @@ class SyncRunner(TaskRunner, ITaskListener):
|
||||
try:
|
||||
self.debug('start task %r' % task)
|
||||
task.start(self)
|
||||
# FIXME: catching too general exception (Exception)
|
||||
except Exception as e:
|
||||
# getExceptionMessage uses global exception state that doesn't
|
||||
# hang around, so store the message
|
||||
@@ -510,23 +513,19 @@ class SyncRunner(TaskRunner, ITaskListener):
|
||||
self.debug('exception during start: %r', task.exceptionMessage)
|
||||
self.stopped(task)
|
||||
|
||||
def schedule(self, task, delta, callable, *args, **kwargs):
|
||||
def schedule(self, task, delta, callable_task, *args, **kwargs):
|
||||
def c():
|
||||
try:
|
||||
self.debug('schedule: calling %r(*args=%r, **kwargs=%r)',
|
||||
callable, args, kwargs)
|
||||
callable(*args, **kwargs)
|
||||
callable_task(*args, **kwargs)
|
||||
return False
|
||||
except Exception as e:
|
||||
self.debug('exception when calling scheduled callable %r',
|
||||
callable)
|
||||
callable_task)
|
||||
task.setException(e)
|
||||
self.stopped(task)
|
||||
raise
|
||||
self.debug('schedule: scheduling %r(*args=%r, **kwargs=%r)',
|
||||
callable, args, kwargs)
|
||||
|
||||
gobject.timeout_add(int(delta * 1000L), c)
|
||||
GLib.timeout_add(int(delta * 1000L), c)
|
||||
|
||||
# ITaskListener methods
|
||||
def progressed(self, task, value):
|
||||
|
||||
@@ -62,14 +62,14 @@ class CueFile(object):
|
||||
"""
|
||||
I represent a .cue file as an object.
|
||||
|
||||
@type table: L{table.Table}
|
||||
@ivar table: the index table.
|
||||
:vartype table: table.Table
|
||||
:ivar table: the index table.
|
||||
"""
|
||||
logCategory = 'CueFile'
|
||||
|
||||
def __init__(self, path):
|
||||
"""
|
||||
@type path: unicode
|
||||
:type path: unicode
|
||||
"""
|
||||
assert isinstance(path, unicode), "%r is not unicode" % path
|
||||
|
||||
@@ -154,7 +154,7 @@ class CueFile(object):
|
||||
"""
|
||||
Add a message about a given line in the cue file.
|
||||
|
||||
@param number: line number, counting from 0.
|
||||
:param number: line number, counting from 0.
|
||||
"""
|
||||
self._messages.append((number + 1, message))
|
||||
|
||||
@@ -182,7 +182,7 @@ class CueFile(object):
|
||||
"""
|
||||
Translate the .cue's FILE to an existing path.
|
||||
|
||||
@type path: unicode
|
||||
:type path: unicode
|
||||
"""
|
||||
return common.getRealPath(self._path, path)
|
||||
|
||||
@@ -192,14 +192,14 @@ class File:
|
||||
I represent a FILE line in a cue file.
|
||||
"""
|
||||
|
||||
def __init__(self, path, format):
|
||||
def __init__(self, path, file_format):
|
||||
"""
|
||||
@type path: unicode
|
||||
:type path: unicode
|
||||
"""
|
||||
assert isinstance(path, unicode), "%r is not unicode" % path
|
||||
|
||||
self.path = path
|
||||
self.format = format
|
||||
self.format = file_format
|
||||
|
||||
def __repr__(self):
|
||||
return '<File %r of format %s>' % (self.path, self.format)
|
||||
|
||||
@@ -36,15 +36,15 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class Image(object):
|
||||
"""
|
||||
@ivar table: The Table of Contents for this image.
|
||||
@type table: L{table.Table}
|
||||
:ivar table: The Table of Contents for this image.
|
||||
:vartype table: table.Table
|
||||
"""
|
||||
logCategory = 'Image'
|
||||
|
||||
def __init__(self, path):
|
||||
"""
|
||||
@type path: unicode
|
||||
@param path: .cue path
|
||||
:type path: unicode
|
||||
:param path: .cue path
|
||||
"""
|
||||
assert isinstance(path, unicode), "%r is not unicode" % path
|
||||
|
||||
@@ -60,7 +60,7 @@ class Image(object):
|
||||
"""
|
||||
Translate the .cue's FILE to an existing path.
|
||||
|
||||
@param path: .cue path
|
||||
:param path: .cue path
|
||||
"""
|
||||
assert isinstance(path, unicode), "%r is not unicode" % path
|
||||
|
||||
@@ -121,6 +121,7 @@ class ImageVerifyTask(task.MultiSeparateTask):
|
||||
task.MultiSeparateTask.__init__(self)
|
||||
|
||||
self._image = image
|
||||
# XXX: Pylint, redefining name 'cue' from outer scope (import)
|
||||
cue = image.cue
|
||||
self._tasks = []
|
||||
self.lengths = {}
|
||||
@@ -183,6 +184,7 @@ class ImageEncodeTask(task.MultiSeparateTask):
|
||||
task.MultiSeparateTask.__init__(self)
|
||||
|
||||
self._image = image
|
||||
# XXX: Pylint, redefining name 'cue' from outer scope (import)
|
||||
cue = image.cue
|
||||
self._tasks = []
|
||||
self.lengths = {}
|
||||
@@ -192,7 +194,7 @@ class ImageEncodeTask(task.MultiSeparateTask):
|
||||
path = image.getRealPath(index.path)
|
||||
assert isinstance(path, unicode), "%r is not unicode" % path
|
||||
logger.debug('schedule encode of %r', path)
|
||||
root, ext = os.path.splitext(os.path.basename(path))
|
||||
root, _ = os.path.splitext(os.path.basename(path))
|
||||
outpath = os.path.join(outdir, root + '.' + 'flac')
|
||||
logger.debug('schedule encode to %r', outpath)
|
||||
taskk = encode.FlacEncodeTask(
|
||||
@@ -205,7 +207,6 @@ class ImageEncodeTask(task.MultiSeparateTask):
|
||||
add(htoa)
|
||||
except (KeyError, IndexError):
|
||||
logger.debug('no HTOA track')
|
||||
pass
|
||||
|
||||
for trackIndex, track in enumerate(cue.table.tracks):
|
||||
logger.debug('encoding track %d', trackIndex + 1)
|
||||
|
||||
@@ -57,17 +57,18 @@ class Track:
|
||||
"""
|
||||
I represent a track entry in an Table.
|
||||
|
||||
@ivar number: track number (1-based)
|
||||
@type number: int
|
||||
@ivar audio: whether the track is audio
|
||||
@type audio: bool
|
||||
@type indexes: dict of number -> L{Index}
|
||||
@ivar isrc: ISRC code (12 alphanumeric characters)
|
||||
@type isrc: str
|
||||
@ivar cdtext: dictionary of CD Text information; see L{CDTEXT_KEYS}.
|
||||
@type cdtext: str -> unicode
|
||||
@ivar pre_emphasis: whether track is pre-emphasised
|
||||
@type pre_emphasis: bool
|
||||
:cvar number: track number (1-based)
|
||||
:vartype number: int
|
||||
:cvar audio: whether the track is audio
|
||||
:vartype audio: bool
|
||||
:vartype indexes: dict of number -> :any:`Index`
|
||||
:cvar isrc: ISRC code (12 alphanumeric characters)
|
||||
:vartype isrc: str
|
||||
:cvar cdtext: dictionary of CD Text information;
|
||||
:any:`see CDTEXT_KEYS`
|
||||
:vartype cdtext: str -> unicode
|
||||
:cvar pre_emphasis: whether track is pre-emphasised
|
||||
:vartype pre_emphasis: bool
|
||||
"""
|
||||
|
||||
number = None
|
||||
@@ -90,7 +91,7 @@ class Track:
|
||||
def index(self, number, absolute=None, path=None, relative=None,
|
||||
counter=None):
|
||||
"""
|
||||
@type path: unicode or None
|
||||
:type path: unicode or None
|
||||
"""
|
||||
if path is not None:
|
||||
assert isinstance(path, unicode), "%r is not unicode" % path
|
||||
@@ -130,9 +131,9 @@ class Track:
|
||||
|
||||
class Index:
|
||||
"""
|
||||
@ivar counter: counter for the index source; distinguishes between
|
||||
:cvar counter: counter for the index source; distinguishes between
|
||||
the matching FILE lines in .cue files for example
|
||||
@type path: unicode or None
|
||||
:vartype path: unicode or None
|
||||
"""
|
||||
number = None
|
||||
absolute = None
|
||||
@@ -161,11 +162,11 @@ class Table(object):
|
||||
"""
|
||||
I represent a table of indexes on a CD.
|
||||
|
||||
@ivar tracks: tracks on this CD
|
||||
@type tracks: list of L{Track}
|
||||
@ivar catalog: catalog number
|
||||
@type catalog: str
|
||||
@type cdtext: dict of str -> str
|
||||
:cvar tracks: tracks on this CD
|
||||
:vartype tracks: list of :any:`Track`
|
||||
:cvar catalog: catalog number
|
||||
:vartype catalog: str
|
||||
:vartype cdtext: dict of str -> str
|
||||
"""
|
||||
|
||||
tracks = None # list of Track
|
||||
@@ -193,22 +194,22 @@ class Table(object):
|
||||
|
||||
def getTrackStart(self, number):
|
||||
"""
|
||||
@param number: the track number, 1-based
|
||||
@type number: int
|
||||
:param number: the track number, 1-based
|
||||
:type number: int
|
||||
|
||||
@returns: the start of the given track number's index 1, in CD frames
|
||||
@rtype: int
|
||||
:returns: the start of the given track number's index 1, in CD frames
|
||||
:rtype: int
|
||||
"""
|
||||
track = self.tracks[number - 1]
|
||||
return track.getIndex(1).absolute
|
||||
|
||||
def getTrackEnd(self, number):
|
||||
"""
|
||||
@param number: the track number, 1-based
|
||||
@type number: int
|
||||
:param number: the track number, 1-based
|
||||
:type number: int
|
||||
|
||||
@returns: the end of the given track number (ie index 1 of next track)
|
||||
@rtype: int
|
||||
:returns: the end of the given track number (ie index 1 of next track)
|
||||
:rtype: int
|
||||
"""
|
||||
# default to end of disc
|
||||
end = self.leadout - 1
|
||||
@@ -228,28 +229,29 @@ class Table(object):
|
||||
|
||||
def getTrackLength(self, number):
|
||||
"""
|
||||
@param number: the track number, 1-based
|
||||
@type number: int
|
||||
:param number: the track number, 1-based
|
||||
:type number: int
|
||||
|
||||
@returns: the length of the given track number, in CD frames
|
||||
@rtype: int
|
||||
:returns: the length of the given track number, in CD frames
|
||||
:rtype: int
|
||||
"""
|
||||
return self.getTrackEnd(number) - self.getTrackStart(number) + 1
|
||||
|
||||
def getAudioTracks(self):
|
||||
"""
|
||||
@returns: the number of audio tracks on the CD
|
||||
@rtype: int
|
||||
:returns: the number of audio tracks on the CD
|
||||
:rtype: int
|
||||
"""
|
||||
return len([t for t in self.tracks if t.audio])
|
||||
|
||||
def hasDataTracks(self):
|
||||
"""
|
||||
@returns: whether this disc contains data tracks
|
||||
:returns: whether this disc contains data tracks
|
||||
"""
|
||||
return len([t for t in self.tracks if not t.audio]) > 0
|
||||
|
||||
def _cddbSum(self, i):
|
||||
@staticmethod
|
||||
def _cddbSum(i):
|
||||
ret = 0
|
||||
while i > 0:
|
||||
ret += (i % 10)
|
||||
@@ -267,7 +269,7 @@ class Table(object):
|
||||
- offset of index 1 of each track
|
||||
- length of disc in seconds (including data track)
|
||||
|
||||
@rtype: list of int
|
||||
:rtype: list of int
|
||||
"""
|
||||
offsets = []
|
||||
|
||||
@@ -319,8 +321,8 @@ class Table(object):
|
||||
"""
|
||||
Calculate the CDDB disc ID.
|
||||
|
||||
@rtype: str
|
||||
@returns: the 8-character hexadecimal disc ID
|
||||
:rtype: str
|
||||
:returns: the 8-character hexadecimal disc ID
|
||||
"""
|
||||
values = self.getCDDBValues()
|
||||
return "%08x" % int(values)
|
||||
@@ -329,8 +331,8 @@ class Table(object):
|
||||
"""
|
||||
Calculate the MusicBrainz disc ID.
|
||||
|
||||
@rtype: str
|
||||
@returns: the 28-character base64-encoded disc ID
|
||||
:rtype: str
|
||||
:returns: the 28-character base64-encoded disc ID
|
||||
"""
|
||||
if self.mbdiscid:
|
||||
logger.debug('getMusicBrainzDiscId: returning cached %r',
|
||||
@@ -339,13 +341,9 @@ class Table(object):
|
||||
values = self._getMusicBrainzValues()
|
||||
|
||||
# MusicBrainz disc id does not take into account data tracks
|
||||
# P2.3
|
||||
try:
|
||||
import hashlib
|
||||
sha1 = hashlib.sha1
|
||||
except ImportError:
|
||||
from sha import sha as sha1
|
||||
import base64
|
||||
import hashlib
|
||||
sha1 = hashlib.sha1
|
||||
|
||||
sha = sha1()
|
||||
|
||||
@@ -404,7 +402,7 @@ class Table(object):
|
||||
"""
|
||||
Get the length in frames (excluding HTOA)
|
||||
|
||||
@param data: whether to include the data tracks in the length
|
||||
:param data: whether to include the data tracks in the length
|
||||
"""
|
||||
# the 'real' leadout, not offset by 150 frames
|
||||
if data:
|
||||
@@ -434,7 +432,7 @@ class Table(object):
|
||||
- leadout of disc
|
||||
- offset of index 1 of each track
|
||||
|
||||
@rtype: list of int
|
||||
:rtype: list of int
|
||||
"""
|
||||
# MusicBrainz disc id does not take into account data tracks
|
||||
|
||||
@@ -473,13 +471,13 @@ class Table(object):
|
||||
|
||||
def cue(self, cuePath='', program='whipper'):
|
||||
"""
|
||||
@param cuePath: path to the cue file to be written. If empty,
|
||||
:param cuePath: path to the cue file to be written. If empty,
|
||||
will treat paths as if in current directory.
|
||||
|
||||
|
||||
Dump our internal representation to a .cue file content.
|
||||
|
||||
@rtype: C{unicode}
|
||||
:rtype: unicode
|
||||
"""
|
||||
logger.debug('generating .cue for cuePath %r', cuePath)
|
||||
|
||||
@@ -636,8 +634,8 @@ class Table(object):
|
||||
|
||||
Assumes all indexes have an absolute offset and will raise if not.
|
||||
|
||||
@type track: C{int}
|
||||
@type index: C{int}
|
||||
:type track: int
|
||||
:type index: int
|
||||
"""
|
||||
logger.debug('setFile: track %d, index %d, path %r, length %r, '
|
||||
'counter %r', track, index, path, length, counter)
|
||||
@@ -707,7 +705,7 @@ class Table(object):
|
||||
The other table is assumed to be from an additional session,
|
||||
|
||||
|
||||
@type other: L{Table}
|
||||
:type other: Table
|
||||
"""
|
||||
gap = self._getSessionGap(session)
|
||||
|
||||
@@ -732,7 +730,8 @@ class Table(object):
|
||||
self.leadout += other.leadout + gap # FIXME
|
||||
logger.debug('fixing leadout, now %d', self.leadout)
|
||||
|
||||
def _getSessionGap(self, session):
|
||||
@staticmethod
|
||||
def _getSessionGap(session):
|
||||
# From cdrecord multi-session info:
|
||||
# For the first additional session this is 11250 sectors
|
||||
# lead-out/lead-in overhead + 150 sectors for the pre-gap of the first
|
||||
@@ -753,11 +752,11 @@ class Table(object):
|
||||
"""
|
||||
Return the next track and index.
|
||||
|
||||
@param track: track number, 1-based
|
||||
:param track: track number, 1-based
|
||||
|
||||
@raises IndexError: on last index
|
||||
:raises IndexError: on last index
|
||||
|
||||
@rtype: tuple of (int, int)
|
||||
:rtype: tuple of (int, int)
|
||||
"""
|
||||
t = self.tracks[track - 1]
|
||||
indexes = list(t.indexes)
|
||||
@@ -824,7 +823,7 @@ class Table(object):
|
||||
discId1 &= 0xffffffff
|
||||
discId2 &= 0xffffffff
|
||||
|
||||
return ("%08x" % discId1, "%08x" % discId2)
|
||||
return "%08x" % discId1, "%08x" % discId2
|
||||
|
||||
def accuraterip_path(self):
|
||||
discId1, discId2 = self.accuraterip_ids()
|
||||
|
||||
@@ -104,10 +104,10 @@ class Sources:
|
||||
|
||||
def append(self, counter, offset, source):
|
||||
"""
|
||||
@param counter: the source counter; updates for each different
|
||||
:param counter: the source counter; updates for each different
|
||||
data source (silence or different file path)
|
||||
@type counter: int
|
||||
@param offset: the absolute disc offset where this source starts
|
||||
:type counter: int
|
||||
:param offset: the absolute disc offset where this source starts
|
||||
"""
|
||||
logger.debug('appending source, counter %d, abs offset %d, '
|
||||
'source %r', counter, offset, source)
|
||||
@@ -117,7 +117,7 @@ class Sources:
|
||||
"""
|
||||
Retrieve the source used at the given offset.
|
||||
"""
|
||||
for i, (c, o, s) in enumerate(self._sources):
|
||||
for i, (_, o, _) in enumerate(self._sources):
|
||||
if offset < o:
|
||||
return self._sources[i - 1]
|
||||
|
||||
@@ -127,7 +127,7 @@ class Sources:
|
||||
"""
|
||||
Retrieve the absolute offset of the first source for this counter
|
||||
"""
|
||||
for i, (c, o, s) in enumerate(self._sources):
|
||||
for i, (c, _, _) in enumerate(self._sources):
|
||||
if c == counter:
|
||||
return self._sources[i][1]
|
||||
|
||||
@@ -138,7 +138,7 @@ class TocFile(object):
|
||||
|
||||
def __init__(self, path):
|
||||
"""
|
||||
@type path: unicode
|
||||
:type path: unicode
|
||||
"""
|
||||
assert isinstance(path, unicode), "%r is not unicode" % path
|
||||
self._path = path
|
||||
@@ -151,7 +151,7 @@ class TocFile(object):
|
||||
def _index(self, currentTrack, i, absoluteOffset, trackOffset):
|
||||
absolute = absoluteOffset + trackOffset
|
||||
# this may be in a new source, so calculate relative
|
||||
c, o, s = self._sources.get(absolute)
|
||||
c, _, s = self._sources.get(absolute)
|
||||
logger.debug('at abs offset %d, we are in source %r',
|
||||
absolute, s)
|
||||
counterStart = self._sources.getCounterStart(c)
|
||||
@@ -341,7 +341,7 @@ class TocFile(object):
|
||||
continue
|
||||
|
||||
length = common.msfToFrames(m.group('length'))
|
||||
c, o, s = self._sources.get(absoluteOffset)
|
||||
c, _, s = self._sources.get(absoluteOffset)
|
||||
logger.debug('at abs offset %d, we are in source %r',
|
||||
absoluteOffset, s)
|
||||
counterStart = self._sources.getCounterStart(c)
|
||||
@@ -380,7 +380,7 @@ class TocFile(object):
|
||||
"""
|
||||
Add a message about a given line in the cue file.
|
||||
|
||||
@param number: line number, counting from 0.
|
||||
:param number: line number, counting from 0.
|
||||
"""
|
||||
self._messages.append((number + 1, message))
|
||||
|
||||
@@ -412,7 +412,7 @@ class TocFile(object):
|
||||
"""
|
||||
Translate the .toc's FILE to an existing path.
|
||||
|
||||
@type path: unicode
|
||||
:type path: unicode
|
||||
"""
|
||||
return common.getRealPath(self._path, path)
|
||||
|
||||
@@ -424,10 +424,10 @@ class File:
|
||||
|
||||
def __init__(self, path, start, length):
|
||||
"""
|
||||
@type path: C{unicode}
|
||||
@type start: C{int}
|
||||
@param start: starting point for the track in this file, in frames
|
||||
@param length: length for the track in this file, in frames
|
||||
:type path: unicode
|
||||
:type start: int
|
||||
:param start: starting point for the track in this file, in frames
|
||||
:param length: length for the track in this file, in frames
|
||||
"""
|
||||
assert isinstance(path, unicode), "%r is not unicode" % path
|
||||
|
||||
|
||||
@@ -1,54 +1,5 @@
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ARB = 'accuraterip-checksum'
|
||||
FLAC = 'flac'
|
||||
import accuraterip
|
||||
|
||||
|
||||
def _execute(cmd, **redirects):
|
||||
logger.debug('executing %r', cmd)
|
||||
return Popen(cmd, **redirects)
|
||||
|
||||
|
||||
def accuraterip_checksum(f, track_number, total_tracks, wave=False, v2=False):
|
||||
v = '--accuraterip-v1'
|
||||
if v2:
|
||||
v = '--accuraterip-v2'
|
||||
|
||||
track_number, total_tracks = str(track_number), str(total_tracks)
|
||||
|
||||
if wave:
|
||||
cmd = [ARB, v, f, track_number, total_tracks]
|
||||
redirects = dict(stdout=PIPE, stderr=PIPE)
|
||||
else:
|
||||
flac = _execute([FLAC, '-cds', f], stdout=PIPE)
|
||||
cmd = [ARB, v, '/dev/stdin', track_number, total_tracks]
|
||||
redirects = dict(stdin=flac.stdout, stdout=PIPE, stderr=PIPE)
|
||||
arc = _execute(cmd, **redirects)
|
||||
|
||||
if not wave:
|
||||
flac.stdout.close()
|
||||
|
||||
out, err = arc.communicate()
|
||||
|
||||
if not wave:
|
||||
flac.wait()
|
||||
if flac.returncode != 0:
|
||||
logger.warning('ARC calculation failed: flac '
|
||||
'return code is non zero: %r', flac.returncode)
|
||||
return None
|
||||
|
||||
if arc.returncode != 0:
|
||||
logger.warning('ARC calculation failed: '
|
||||
'arc return code is non zero: %r', arc.returncode)
|
||||
return None
|
||||
|
||||
try:
|
||||
checksum = int('0x%s' % out.strip(), base=16)
|
||||
logger.debug('returned %r', checksum)
|
||||
return checksum
|
||||
except ValueError:
|
||||
logger.warning('ARC output is not usable')
|
||||
return None
|
||||
def accuraterip_checksum(f, track_number, total_tracks):
|
||||
return accuraterip.compute(f.encode('utf-8'), track_number, total_tracks)
|
||||
|
||||
@@ -88,10 +88,10 @@ class ProgressParser:
|
||||
|
||||
def __init__(self, start, stop):
|
||||
"""
|
||||
@param start: first frame to rip
|
||||
@type start: int
|
||||
@param stop: last frame to rip (inclusive)
|
||||
@type stop: int
|
||||
:param start: first frame to rip
|
||||
:type start: int
|
||||
:param stop: last frame to rip (inclusive)
|
||||
:type stop: int
|
||||
"""
|
||||
self.start = start
|
||||
self.stop = stop
|
||||
@@ -159,11 +159,10 @@ class ProgressParser:
|
||||
markEnd = frameOffset
|
||||
|
||||
# FIXME: doing this is way too slow even for a testcase, so disable
|
||||
if False:
|
||||
for frame in range(markStart, markEnd):
|
||||
if frame not in list(self._reads.keys()):
|
||||
self._reads[frame] = 0
|
||||
self._reads[frame] += 1
|
||||
# for frame in range(markStart, markEnd):
|
||||
# if frame not in list(self._reads.keys()):
|
||||
# self._reads[frame] = 0
|
||||
# self._reads[frame] += 1
|
||||
|
||||
# cdparanoia reads quite a bit beyond the current track before it
|
||||
# goes back to verify; don't count those
|
||||
@@ -206,8 +205,6 @@ class ProgressParser:
|
||||
class ReadTrackTask(task.Task):
|
||||
"""
|
||||
I am a task that reads a track using cdparanoia.
|
||||
|
||||
@ivar reads: how many reads were done to rip the track
|
||||
"""
|
||||
|
||||
description = "Reading track"
|
||||
@@ -222,22 +219,22 @@ class ReadTrackTask(task.Task):
|
||||
"""
|
||||
Read the given track.
|
||||
|
||||
@param path: where to store the ripped track
|
||||
@type path: unicode
|
||||
@param table: table of contents of CD
|
||||
@type table: L{table.Table}
|
||||
@param start: first frame to rip
|
||||
@type start: int
|
||||
@param stop: last frame to rip (inclusive); >= start
|
||||
@type stop: int
|
||||
@param offset: read offset, in samples
|
||||
@type offset: int
|
||||
@param device: the device to rip from
|
||||
@type device: str
|
||||
@param action: a string representing the action; e.g. Read/Verify
|
||||
@type action: str
|
||||
@param what: a string representing what's being read; e.g. Track
|
||||
@type what: str
|
||||
:param path: where to store the ripped track
|
||||
:type path: unicode
|
||||
:param table: table of contents of CD
|
||||
:type table: table.Table
|
||||
:param start: first frame to rip
|
||||
:type start: int
|
||||
:param stop: last frame to rip (inclusive); >= start
|
||||
:type stop: int
|
||||
:param offset: read offset, in samples
|
||||
:type offset: int
|
||||
:param device: the device to rip from
|
||||
:type device: str
|
||||
:param action: a string representing the action; e.g. Read/Verify
|
||||
:type action: str
|
||||
:param what: a string representing what's being read; e.g. Track
|
||||
:type what: str
|
||||
"""
|
||||
assert isinstance(path, unicode), "%r is not unicode" % path
|
||||
|
||||
@@ -264,7 +261,7 @@ class ReadTrackTask(task.Task):
|
||||
stopTrack = 0
|
||||
stopOffset = self._stop
|
||||
|
||||
for i, t in enumerate(self._table.tracks):
|
||||
for i, _ in enumerate(self._table.tracks):
|
||||
if self._table.getTrackStart(i + 1) <= self._start:
|
||||
startTrack = i + 1
|
||||
startOffset = self._start - self._table.getTrackStart(i + 1)
|
||||
@@ -300,7 +297,6 @@ class ReadTrackTask(task.Task):
|
||||
stderr=subprocess.PIPE,
|
||||
close_fds=True)
|
||||
except OSError as e:
|
||||
import errno
|
||||
if e.errno == errno.ENOENT:
|
||||
raise common.MissingDependencyException('cd-paranoia')
|
||||
|
||||
@@ -405,17 +401,16 @@ class ReadVerifyTrackTask(task.MultiSeparateTask):
|
||||
The path where the file is stored can be changed if necessary, for
|
||||
example if the file name is too long.
|
||||
|
||||
@ivar path: the path where the file is to be stored.
|
||||
@ivar checksum: the checksum of the track; set if they match.
|
||||
@ivar testchecksum: the test checksum of the track.
|
||||
@ivar copychecksum: the copy checksum of the track.
|
||||
@ivar testspeed: the test speed of the track, as a multiple of
|
||||
:cvar checksum: the checksum of the track; set if they match.
|
||||
:cvar testchecksum: the test checksum of the track.
|
||||
:cvar copychecksum: the copy checksum of the track.
|
||||
:cvar testspeed: the test speed of the track, as a multiple of
|
||||
track duration.
|
||||
@ivar copyspeed: the copy speed of the track, as a multiple of
|
||||
:cvar copyspeed: the copy speed of the track, as a multiple of
|
||||
track duration.
|
||||
@ivar testduration: the test duration of the track, in seconds.
|
||||
@ivar copyduration: the copy duration of the track, in seconds.
|
||||
@ivar peak: the peak level of the track
|
||||
:cvar testduration: the test duration of the track, in seconds.
|
||||
:cvar copyduration: the copy duration of the track, in seconds.
|
||||
:cvar peak: the peak level of the track
|
||||
"""
|
||||
|
||||
checksum = None
|
||||
@@ -434,20 +429,20 @@ class ReadVerifyTrackTask(task.MultiSeparateTask):
|
||||
def __init__(self, path, table, start, stop, overread, offset=0,
|
||||
device=None, taglist=None, what="track"):
|
||||
"""
|
||||
@param path: where to store the ripped track
|
||||
@type path: str
|
||||
@param table: table of contents of CD
|
||||
@type table: L{table.Table}
|
||||
@param start: first frame to rip
|
||||
@type start: int
|
||||
@param stop: last frame to rip (inclusive)
|
||||
@type stop: int
|
||||
@param offset: read offset, in samples
|
||||
@type offset: int
|
||||
@param device: the device to rip from
|
||||
@type device: str
|
||||
@param taglist: a dict of tags
|
||||
@type taglist: dict
|
||||
:param path: where to store the ripped track
|
||||
:type path: str
|
||||
:param table: table of contents of CD
|
||||
:type table: table.Table
|
||||
:param start: first frame to rip
|
||||
:type start: int
|
||||
:param stop: last frame to rip (inclusive)
|
||||
:type stop: int
|
||||
:param offset: read offset, in samples
|
||||
:type offset: int
|
||||
:param device: the device to rip from
|
||||
:type device: str
|
||||
:param taglist: a dict of tags
|
||||
:type taglist: dict
|
||||
"""
|
||||
task.MultiSeparateTask.__init__(self)
|
||||
|
||||
@@ -458,6 +453,7 @@ class ReadVerifyTrackTask(task.MultiSeparateTask):
|
||||
# FIXME: choose a dir on the same disk/dir as the final path
|
||||
fd, tmppath = tempfile.mkstemp(suffix='.whipper.wav')
|
||||
tmppath = unicode(tmppath)
|
||||
os.fchmod(fd, 0644)
|
||||
os.close(fd)
|
||||
self._tmpwavpath = tmppath
|
||||
|
||||
@@ -540,6 +536,7 @@ class ReadVerifyTrackTask(task.MultiSeparateTask):
|
||||
try:
|
||||
logger.debug('moving to final path %r', self.path)
|
||||
os.rename(self._tmppath, self.path)
|
||||
# FIXME: catching too general exception (Exception)
|
||||
except Exception as e:
|
||||
logger.debug('exception while moving to final '
|
||||
'path %r: %s', self.path, e)
|
||||
@@ -548,6 +545,7 @@ class ReadVerifyTrackTask(task.MultiSeparateTask):
|
||||
os.unlink(self._tmppath)
|
||||
else:
|
||||
logger.debug('stop: exception %r', self.exception)
|
||||
# FIXME: catching too general exception (Exception)
|
||||
except Exception as e:
|
||||
print('WARNING: unhandled exception %r' % (e, ))
|
||||
|
||||
@@ -569,6 +567,7 @@ def getCdParanoiaVersion():
|
||||
|
||||
_OK_RE = re.compile(r'Drive tests OK with Paranoia.')
|
||||
_WARNING_RE = re.compile(r'WARNING! PARANOIA MAY NOT BE')
|
||||
_ABORTING_RE = re.compile(r'aborting test\.')
|
||||
|
||||
|
||||
class AnalyzeTask(ctask.PopenTask):
|
||||
@@ -592,25 +591,22 @@ class AnalyzeTask(ctask.PopenTask):
|
||||
def commandMissing(self):
|
||||
raise common.MissingDependencyException('cd-paranoia')
|
||||
|
||||
def readbyteserr(self, bytes):
|
||||
self._output.append(bytes)
|
||||
def readbyteserr(self, bytes_stderr):
|
||||
self._output.append(bytes_stderr)
|
||||
|
||||
def done(self):
|
||||
if self.cwd:
|
||||
shutil.rmtree(self.cwd)
|
||||
output = "".join(self._output)
|
||||
m = _OK_RE.search(output)
|
||||
if m:
|
||||
self.defeatsCache = True
|
||||
else:
|
||||
self.defeatsCache = False
|
||||
self.defeatsCache = bool(m)
|
||||
|
||||
def failed(self):
|
||||
# cdparanoia exits with return code 1 if it can't determine
|
||||
# whether it can defeat the audio cache
|
||||
output = "".join(self._output)
|
||||
m = _WARNING_RE.search(output)
|
||||
if m:
|
||||
if m or _ABORTING_RE.search(output):
|
||||
self.defeatsCache = False
|
||||
if self.cwd:
|
||||
shutil.rmtree(self.cwd)
|
||||
|
||||
@@ -2,58 +2,163 @@ import os
|
||||
import re
|
||||
import shutil
|
||||
import tempfile
|
||||
import subprocess
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
from whipper.common.common import EjectError, truncate_filename
|
||||
from whipper.common.common import truncate_filename
|
||||
from whipper.image.toc import TocFile
|
||||
from whipper.extern.task import task
|
||||
from whipper.extern import asyncsub
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CDRDAO = 'cdrdao'
|
||||
|
||||
_TRACK_RE = re.compile(r"^Analyzing track (?P<track>[0-9]*) \(AUDIO\): start (?P<start>[0-9]*:[0-9]*:[0-9]*), length (?P<length>[0-9]*:[0-9]*:[0-9]*)") # noqa: E501
|
||||
_CRC_RE = re.compile(
|
||||
r"Found (?P<channels>[0-9]*) Q sub-channels with CRC errors")
|
||||
_BEGIN_CDRDAO_RE = re.compile(r"-" * 60)
|
||||
_LAST_TRACK_RE = re.compile(r"^[ ]?(?P<track>[0-9]*)")
|
||||
_LEADOUT_RE = re.compile(
|
||||
r"^Leadout AUDIO\s*[0-9]\s*[0-9]*:[0-9]*:[0-9]*\([0-9]*\)")
|
||||
|
||||
def read_toc(device, fast_toc=False, toc_path=None):
|
||||
|
||||
class ProgressParser:
|
||||
tracks = 0
|
||||
currentTrack = 0
|
||||
oldline = '' # for leadout/final track number detection
|
||||
|
||||
def parse(self, line):
|
||||
cdrdao_m = _BEGIN_CDRDAO_RE.match(line)
|
||||
|
||||
if cdrdao_m:
|
||||
logger.debug("RE: Begin cdrdao toc-read")
|
||||
|
||||
leadout_m = _LEADOUT_RE.match(line)
|
||||
|
||||
if leadout_m:
|
||||
logger.debug("RE: Reached leadout")
|
||||
last_track_m = _LAST_TRACK_RE.match(self.oldline)
|
||||
if last_track_m:
|
||||
self.tracks = last_track_m.group('track')
|
||||
|
||||
track_s = _TRACK_RE.search(line)
|
||||
if track_s:
|
||||
logger.debug("RE: Began reading track: %d",
|
||||
int(track_s.group('track')))
|
||||
self.currentTrack = int(track_s.group('track'))
|
||||
|
||||
crc_s = _CRC_RE.search(line)
|
||||
if crc_s:
|
||||
print("Track %d finished, "
|
||||
"found %d Q sub-channels with CRC errors" %
|
||||
(self.currentTrack, int(crc_s.group('channels'))))
|
||||
|
||||
self.oldline = line
|
||||
|
||||
|
||||
class ReadTOCTask(task.Task):
|
||||
"""
|
||||
Return cdrdao-generated table of contents for 'device'.
|
||||
Task that reads the TOC of the disc using cdrdao
|
||||
"""
|
||||
# cdrdao MUST be passed a non-existing filename as its last argument
|
||||
# to write the TOC to; it does not support writing to stdout or
|
||||
# overwriting an existing file, nor does linux seem to support
|
||||
# locking a non-existant file. Thus, this race-condition introducing
|
||||
# hack is carried from morituri to whipper and will be removed when
|
||||
# cdrdao is fixed.
|
||||
fd, tocfile = tempfile.mkstemp(suffix=u'.cdrdao.read-toc.whipper')
|
||||
os.close(fd)
|
||||
os.unlink(tocfile)
|
||||
description = "Reading TOC"
|
||||
toc = None
|
||||
|
||||
cmd = [CDRDAO, 'read-toc'] + (['--fast-toc'] if fast_toc else []) + [
|
||||
'--device', device, tocfile]
|
||||
# PIPE is the closest to >/dev/null we can get
|
||||
logger.debug("executing %r", cmd)
|
||||
p = Popen(cmd, stdout=PIPE, stderr=PIPE)
|
||||
_, stderr = p.communicate()
|
||||
if p.returncode != 0:
|
||||
msg = 'cdrdao read-toc failed: return code is non-zero: ' + \
|
||||
str(p.returncode)
|
||||
logger.critical(msg)
|
||||
# Gracefully handle missing disc
|
||||
if "ERROR: Unit not ready, giving up." in stderr:
|
||||
raise EjectError(device, "no disc detected")
|
||||
raise IOError(msg)
|
||||
def __init__(self, device, fast_toc=False, toc_path=None):
|
||||
"""
|
||||
Read the TOC for 'device'.
|
||||
|
||||
toc = TocFile(tocfile)
|
||||
toc.parse()
|
||||
if toc_path is not None:
|
||||
t_comp = os.path.abspath(toc_path).split(os.sep)
|
||||
t_dirn = os.sep.join(t_comp[:-1])
|
||||
# If the output path doesn't exist, make it recursively
|
||||
if not os.path.isdir(t_dirn):
|
||||
os.makedirs(t_dirn)
|
||||
t_dst = truncate_filename(os.path.join(t_dirn, t_comp[-1] + '.toc'))
|
||||
shutil.copy(tocfile, os.path.join(t_dirn, t_dst))
|
||||
os.unlink(tocfile)
|
||||
return toc
|
||||
:param device: block device to read TOC from
|
||||
:type device: str
|
||||
:param fast_toc: If to use fast-toc cdrdao mode
|
||||
:type fast_toc: bool
|
||||
:param toc_path: Where to save TOC if wanted.
|
||||
:type toc_path: str
|
||||
"""
|
||||
|
||||
self.device = device
|
||||
self.fast_toc = fast_toc
|
||||
self.toc_path = toc_path
|
||||
self._buffer = "" # accumulate characters
|
||||
self._parser = ProgressParser()
|
||||
|
||||
self.fd, self.tocfile = tempfile.mkstemp(
|
||||
suffix=u'.cdrdao.read-toc.whipper.task')
|
||||
|
||||
def start(self, runner):
|
||||
task.Task.start(self, runner)
|
||||
os.close(self.fd)
|
||||
os.unlink(self.tocfile)
|
||||
|
||||
cmd = ([CDRDAO, 'read-toc']
|
||||
+ (['--fast-toc'] if self.fast_toc else [])
|
||||
+ ['--device', self.device, self.tocfile])
|
||||
|
||||
self._popen = asyncsub.Popen(cmd,
|
||||
bufsize=1024,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
close_fds=True)
|
||||
|
||||
self.schedule(0.01, self._read, runner)
|
||||
|
||||
def _read(self, runner):
|
||||
ret = self._popen.recv_err()
|
||||
if not ret:
|
||||
if self._popen.poll() is not None:
|
||||
self._done()
|
||||
return
|
||||
self.schedule(0.01, self._read, runner)
|
||||
return
|
||||
self._buffer += ret
|
||||
|
||||
# parse buffer into lines if possible, and parse them
|
||||
if "\n" in self._buffer:
|
||||
lines = self._buffer.split('\n')
|
||||
if lines[-1] != "\n":
|
||||
# last line didn't end yet
|
||||
self._buffer = lines[-1]
|
||||
del lines[-1]
|
||||
else:
|
||||
self._buffer = ""
|
||||
for line in lines:
|
||||
self._parser.parse(line)
|
||||
if (self._parser.currentTrack != 0 and
|
||||
self._parser.tracks != 0):
|
||||
progress = (float('%d' % self._parser.currentTrack) /
|
||||
float(self._parser.tracks))
|
||||
if progress < 1.0:
|
||||
self.setProgress(progress)
|
||||
|
||||
# 0 does not give us output before we complete, 1.0 gives us output
|
||||
# too late
|
||||
self.schedule(0.01, self._read, runner)
|
||||
|
||||
def _poll(self, runner):
|
||||
if self._popen.poll() is None:
|
||||
self.schedule(1.0, self._poll, runner)
|
||||
return
|
||||
|
||||
self._done()
|
||||
|
||||
def _done(self):
|
||||
self.setProgress(1.0)
|
||||
self.toc = TocFile(self.tocfile)
|
||||
self.toc.parse()
|
||||
if self.toc_path is not None:
|
||||
t_comp = os.path.abspath(self.toc_path).split(os.sep)
|
||||
t_dirn = os.sep.join(t_comp[:-1])
|
||||
# If the output path doesn't exist, make it recursively
|
||||
if not os.path.isdir(t_dirn):
|
||||
os.makedirs(t_dirn)
|
||||
t_dst = truncate_filename(
|
||||
os.path.join(t_dirn, t_comp[-1] + '.toc'))
|
||||
shutil.copy(self.tocfile, os.path.join(t_dirn, t_dst))
|
||||
os.unlink(self.tocfile)
|
||||
self.stop()
|
||||
return
|
||||
|
||||
|
||||
def DetectCdr(device):
|
||||
@@ -63,10 +168,7 @@ def DetectCdr(device):
|
||||
cmd = [CDRDAO, 'disk-info', '-v1', '--device', device]
|
||||
logger.debug("executing %r", cmd)
|
||||
p = Popen(cmd, stdout=PIPE, stderr=PIPE)
|
||||
if 'CD-R medium : n/a' in p.stdout.read():
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return 'CD-R medium : n/a' not in p.stdout.read()
|
||||
|
||||
|
||||
def version():
|
||||
@@ -74,7 +176,7 @@ def version():
|
||||
Return cdrdao version as a string.
|
||||
"""
|
||||
cdrdao = Popen(CDRDAO, stderr=PIPE)
|
||||
out, err = cdrdao.communicate()
|
||||
_, err = cdrdao.communicate()
|
||||
if cdrdao.returncode != 1:
|
||||
logger.warning("cdrdao version detection failed: "
|
||||
"return code is %s", cdrdao.returncode)
|
||||
@@ -86,24 +188,3 @@ def version():
|
||||
"could not find version")
|
||||
return None
|
||||
return m.group('version')
|
||||
|
||||
|
||||
def ReadTOCTask(device):
|
||||
"""
|
||||
stopgap morituri-insanity compatibility layer
|
||||
"""
|
||||
return read_toc(device, fast_toc=True)
|
||||
|
||||
|
||||
def ReadTableTask(device, toc_path=None):
|
||||
"""
|
||||
stopgap morituri-insanity compatibility layer
|
||||
"""
|
||||
return read_toc(device, toc_path=toc_path)
|
||||
|
||||
|
||||
def getCDRDAOVersion():
|
||||
"""
|
||||
stopgap morituri-insanity compatibility layer
|
||||
"""
|
||||
return version()
|
||||
|
||||
@@ -18,7 +18,7 @@ def peak_level(track_path):
|
||||
logger.warning("SoX peak detection failed: file not found")
|
||||
return None
|
||||
sox = Popen([SOX, track_path, "-n", "stats", "-b", "16"], stderr=PIPE)
|
||||
out, err = sox.communicate()
|
||||
_, err = sox.communicate()
|
||||
if sox.returncode:
|
||||
logger.warning("SoX peak detection failed: %s", sox.returncode)
|
||||
return None
|
||||
|
||||
@@ -13,7 +13,7 @@ class AudioLengthTask(ctask.PopenTask):
|
||||
"""
|
||||
I calculate the length of a track in audio samples.
|
||||
|
||||
@ivar length: length of the decoded audio file, in audio samples.
|
||||
:cvar length: length of the decoded audio file, in audio samples.
|
||||
"""
|
||||
logCategory = 'AudioLengthTask'
|
||||
description = 'Getting length of audio track'
|
||||
@@ -21,7 +21,7 @@ class AudioLengthTask(ctask.PopenTask):
|
||||
|
||||
def __init__(self, path):
|
||||
"""
|
||||
@type path: unicode
|
||||
:type path: unicode
|
||||
"""
|
||||
assert isinstance(path, unicode), "%r is not unicode" % path
|
||||
|
||||
@@ -35,11 +35,11 @@ class AudioLengthTask(ctask.PopenTask):
|
||||
def commandMissing(self):
|
||||
raise common.MissingDependencyException('soxi')
|
||||
|
||||
def readbytesout(self, bytes):
|
||||
self._output.append(bytes)
|
||||
def readbytesout(self, bytes_stdout):
|
||||
self._output.append(bytes_stdout)
|
||||
|
||||
def readbyteserr(self, bytes):
|
||||
self._error.append(bytes)
|
||||
def readbyteserr(self, bytes_stderr):
|
||||
self._error.append(bytes_stderr)
|
||||
|
||||
def failed(self):
|
||||
self.setException(Exception("soxi failed: %s" % "".join(self._error)))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -9,7 +10,12 @@ def eject_device(device):
|
||||
Eject the given device.
|
||||
"""
|
||||
logger.debug("ejecting device %s", device)
|
||||
os.system('eject %s' % device)
|
||||
try:
|
||||
# `eject device` prints nothing to stdout
|
||||
subprocess.check_output(['eject', device], stderr=subprocess.STDOUT)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.warning("command '%s' returned with exit code '%d' (%s)",
|
||||
' '.join(e.cmd), e.returncode, e.output.rstrip())
|
||||
|
||||
|
||||
def load_device(device):
|
||||
@@ -17,7 +23,13 @@ def load_device(device):
|
||||
Load the given device.
|
||||
"""
|
||||
logger.debug("loading (eject -t) device %s", device)
|
||||
os.system('eject -t %s' % device)
|
||||
try:
|
||||
# `eject -t device` prints nothing to stdout
|
||||
subprocess.check_output(['eject', '-t', device],
|
||||
stderr=subprocess.STDOUT)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.warning("command '%s' returned with exit code '%d' (%s)",
|
||||
' '.join(e.cmd), e.returncode, e.output.rstrip())
|
||||
|
||||
|
||||
def unmount_device(device):
|
||||
@@ -28,7 +40,7 @@ def unmount_device(device):
|
||||
If the given device is a symlink, the target will be checked.
|
||||
"""
|
||||
device = os.path.realpath(device)
|
||||
logger.debug('possibly unmount real path %r' % device)
|
||||
logger.debug('possibly unmount real path %r', device)
|
||||
proc = open('/proc/mounts').read()
|
||||
if device in proc:
|
||||
print('Device %s is mounted, unmounting' % device)
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import time
|
||||
import hashlib
|
||||
import re
|
||||
import ruamel.yaml as yaml
|
||||
from ruamel.yaml.comments import CommentedMap as OrderedDict
|
||||
|
||||
import whipper
|
||||
|
||||
@@ -16,65 +19,57 @@ class WhipperLogger(result.Logger):
|
||||
def log(self, ripResult, epoch=time.time()):
|
||||
"""Returns big str: logfile joined text lines"""
|
||||
|
||||
lines = self.logRip(ripResult, epoch=epoch)
|
||||
return "\n".join(lines)
|
||||
return self.logRip(ripResult, epoch)
|
||||
|
||||
def logRip(self, ripResult, epoch):
|
||||
"""Returns logfile lines list"""
|
||||
|
||||
lines = []
|
||||
riplog = OrderedDict()
|
||||
|
||||
# Ripper version
|
||||
lines.append("Log created by: whipper %s (internal logger)" %
|
||||
whipper.__version__)
|
||||
riplog["Log created by"] = "whipper %s (internal logger)" % (
|
||||
whipper.__version__)
|
||||
|
||||
# Rip date
|
||||
date = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(epoch)).strip()
|
||||
lines.append("Log creation date: %s" % date)
|
||||
lines.append("")
|
||||
riplog["Log creation date"] = date
|
||||
|
||||
# Rip technical settings
|
||||
lines.append("Ripping phase information:")
|
||||
lines.append(" Drive: %s%s (revision %s)" % (
|
||||
ripResult.vendor, ripResult.model, ripResult.release))
|
||||
lines.append(" Extraction engine: cdparanoia %s" %
|
||||
ripResult.cdparanoiaVersion)
|
||||
if ripResult.cdparanoiaDefeatsCache is None:
|
||||
defeat = "Unknown"
|
||||
elif ripResult.cdparanoiaDefeatsCache:
|
||||
defeat = "Yes"
|
||||
else:
|
||||
defeat = "No"
|
||||
lines.append(" Defeat audio cache: %s" % defeat)
|
||||
lines.append(" Read offset correction: %+d" % ripResult.offset)
|
||||
data = OrderedDict()
|
||||
|
||||
data["Drive"] = "%s%s (revision %s)" % (
|
||||
ripResult.vendor, ripResult.model, ripResult.release)
|
||||
data["Extraction engine"] = "cdparanoia %s" % (
|
||||
ripResult.cdparanoiaVersion)
|
||||
data["Defeat audio cache"] = ripResult.cdparanoiaDefeatsCache
|
||||
data["Read offset correction"] = ripResult.offset
|
||||
|
||||
# Currently unsupported by the official cdparanoia package
|
||||
over = "No"
|
||||
# Only implemented in whipper (ripResult.overread)
|
||||
if ripResult.overread:
|
||||
over = "Yes"
|
||||
lines.append(" Overread into lead-out: %s" % over)
|
||||
data["Overread into lead-out"] = True if ripResult.overread else False
|
||||
# Next one fully works only using the patched cdparanoia package
|
||||
# lines.append("Fill up missing offset samples with silence: Yes")
|
||||
lines.append(" Gap detection: cdrdao %s" % ripResult.cdrdaoVersion)
|
||||
if ripResult.isCdr:
|
||||
isCdr = "Yes"
|
||||
else:
|
||||
isCdr = "No"
|
||||
lines.append(" CD-R detected: %s" % isCdr)
|
||||
lines.append("")
|
||||
# lines.append("Fill up missing offset samples with silence: true")
|
||||
data["Gap detection"] = "cdrdao %s" % ripResult.cdrdaoVersion
|
||||
|
||||
data["CD-R detected"] = ripResult.isCdr
|
||||
riplog["Ripping phase information"] = data
|
||||
|
||||
# CD metadata
|
||||
lines.append("CD metadata:")
|
||||
lines.append(" Album: %s - %s" % (ripResult.artist, ripResult.title))
|
||||
lines.append(" CDDB Disc ID: %s" % ripResult. table.getCDDBDiscId())
|
||||
lines.append(" MusicBrainz Disc ID: %s" %
|
||||
ripResult. table.getMusicBrainzDiscId())
|
||||
lines.append(" MusicBrainz lookup url: %s" %
|
||||
ripResult. table.getMusicBrainzSubmitURL())
|
||||
lines.append("")
|
||||
release = OrderedDict()
|
||||
release["Artist"] = ripResult.artist
|
||||
release["Title"] = ripResult.title
|
||||
data = OrderedDict()
|
||||
data["Release"] = release
|
||||
data["CDDB Disc ID"] = ripResult.table.getCDDBDiscId()
|
||||
data["MusicBrainz Disc ID"] = ripResult.table.getMusicBrainzDiscId()
|
||||
data["MusicBrainz lookup URL"] = (
|
||||
ripResult.table.getMusicBrainzSubmitURL())
|
||||
if ripResult.metadata:
|
||||
data["MusicBrainz Release URL"] = ripResult.metadata.url
|
||||
riplog["CD metadata"] = data
|
||||
|
||||
# TOC section
|
||||
lines.append("TOC:")
|
||||
data = OrderedDict()
|
||||
table = ripResult.table
|
||||
|
||||
# Test for HTOA presence
|
||||
@@ -89,154 +84,171 @@ class WhipperLogger(result.Logger):
|
||||
htoastart = htoa.absolute
|
||||
htoaend = table.getTrackEnd(0)
|
||||
htoalength = table.tracks[0].getIndex(1).absolute - htoastart
|
||||
lines.append(" 0:")
|
||||
lines.append(" Start: %s" % common.framesToMSF(htoastart))
|
||||
lines.append(" Length: %s" % common.framesToMSF(htoalength))
|
||||
lines.append(" Start sector: %d" % htoastart)
|
||||
lines.append(" End sector: %d" % htoaend)
|
||||
lines.append("")
|
||||
track = OrderedDict()
|
||||
track["Start"] = common.framesToMSF(htoastart)
|
||||
track["Length"] = common.framesToMSF(htoalength)
|
||||
track["Start sector"] = htoastart
|
||||
track["End sector"] = htoaend
|
||||
data[0] = track
|
||||
|
||||
# For every track include information in the TOC
|
||||
for t in table.tracks:
|
||||
# FIXME: what happens to a track start over 60 minutes ?
|
||||
# Answer: tested empirically, everything seems OK
|
||||
start = t.getIndex(1).absolute
|
||||
length = table.getTrackLength(t.number)
|
||||
end = table.getTrackEnd(t.number)
|
||||
lines.append(" %d:" % t.number)
|
||||
lines.append(" Start: %s" % common.framesToMSF(start))
|
||||
lines.append(" Length: %s" % common.framesToMSF(length))
|
||||
lines.append(" Start sector: %d" % start)
|
||||
lines.append(" End sector: %d" % end)
|
||||
lines.append("")
|
||||
track = OrderedDict()
|
||||
track["Start"] = common.framesToMSF(start)
|
||||
track["Length"] = common.framesToMSF(length)
|
||||
track["Start sector"] = start
|
||||
track["End sector"] = end
|
||||
data[t.number] = track
|
||||
riplog["TOC"] = data
|
||||
|
||||
# Tracks section
|
||||
lines.append("Tracks:")
|
||||
data = OrderedDict()
|
||||
duration = 0.0
|
||||
for t in ripResult.tracks:
|
||||
if not t.filename:
|
||||
continue
|
||||
track_lines, ARDB_entry, ARDB_match = self.trackLog(t)
|
||||
track_dict, ARDB_entry, ARDB_match = self.trackLog(t)
|
||||
self._inARDatabase += int(ARDB_entry)
|
||||
self._accuratelyRipped += int(ARDB_match)
|
||||
lines.extend(track_lines)
|
||||
lines.append("")
|
||||
data[t.number] = track_dict
|
||||
duration += t.testduration + t.copyduration
|
||||
riplog["Tracks"] = data
|
||||
|
||||
# Status report
|
||||
lines.append("Conclusive status report:")
|
||||
arHeading = " AccurateRip summary:"
|
||||
data = OrderedDict()
|
||||
if self._inARDatabase == 0:
|
||||
lines.append("%s None of the tracks are present in the "
|
||||
"AccurateRip database" % arHeading)
|
||||
message = ("None of the tracks are present in the "
|
||||
"AccurateRip database")
|
||||
else:
|
||||
nonHTOA = len(ripResult.tracks)
|
||||
if ripResult.tracks[0].number == 0:
|
||||
nonHTOA -= 1
|
||||
if self._accuratelyRipped == 0:
|
||||
lines.append("%s No tracks could be verified as accurate "
|
||||
"(you may have a different pressing from the "
|
||||
"one(s) in the database)" % arHeading)
|
||||
message = ("No tracks could be verified as accurate "
|
||||
"(you may have a different pressing from the "
|
||||
"one(s) in the database)")
|
||||
elif self._accuratelyRipped < nonHTOA:
|
||||
accurateTracks = nonHTOA - self._accuratelyRipped
|
||||
lines.append("%s Some tracks could not be verified as "
|
||||
"accurate (%d/%d got no match)" % (
|
||||
arHeading, accurateTracks, nonHTOA))
|
||||
message = ("Some tracks could not be verified as "
|
||||
"accurate (%d/%d got no match)") % (
|
||||
accurateTracks, nonHTOA)
|
||||
else:
|
||||
lines.append("%s All tracks accurately ripped" % arHeading)
|
||||
message = "All tracks accurately ripped"
|
||||
data["AccurateRip summary"] = message
|
||||
|
||||
hsHeading = " Health status:"
|
||||
if self._errors:
|
||||
lines.append("%s There were errors" % hsHeading)
|
||||
message = "There were errors"
|
||||
else:
|
||||
lines.append("%s No errors occurred" % hsHeading)
|
||||
lines.append(" EOF: End of status report")
|
||||
lines.append("")
|
||||
message = "No errors occurred"
|
||||
data["Health Status"] = message
|
||||
data["EOF"] = "End of status report"
|
||||
riplog["Conclusive status report"] = data
|
||||
|
||||
riplog = yaml.dump(
|
||||
riplog,
|
||||
default_flow_style=False,
|
||||
width=4000,
|
||||
Dumper=yaml.RoundTripDumper
|
||||
)
|
||||
# Add a newline after the "Log creation date" line
|
||||
riplog = re.sub(
|
||||
r'^(Log creation date: .*)$',
|
||||
"\\1\n",
|
||||
riplog,
|
||||
flags=re.MULTILINE
|
||||
)
|
||||
# Add a newline after a dictionary ends and returns to top-level
|
||||
riplog = re.sub(
|
||||
r"^(\s{2})([^\n]*)\n([A-Z][^\n]+)",
|
||||
"\\1\\2\n\n\\3",
|
||||
riplog,
|
||||
flags=re.MULTILINE
|
||||
)
|
||||
# Add a newline after a track closes
|
||||
riplog = re.sub(
|
||||
r"^(\s{4}[^\n]*)\n(\s{2}[0-9]+)",
|
||||
"\\1\n\n\\2",
|
||||
riplog,
|
||||
flags=re.MULTILINE
|
||||
)
|
||||
# Remove single quotes around the "Log creation date" value
|
||||
riplog = re.sub(
|
||||
r"^(Log creation date: )'(.*)'",
|
||||
"\\1\\2",
|
||||
riplog,
|
||||
flags=re.MULTILINE
|
||||
)
|
||||
|
||||
# Log hash
|
||||
hasher = hashlib.sha256()
|
||||
hasher.update("\n".join(lines).encode("utf-8"))
|
||||
lines.append("SHA-256 hash: %s" % hasher.hexdigest().upper())
|
||||
lines.append("")
|
||||
return lines
|
||||
hasher.update(riplog.encode("utf-8"))
|
||||
riplog += "\nSHA-256 hash: %s\n" % hasher.hexdigest().upper()
|
||||
return riplog
|
||||
|
||||
def trackLog(self, trackResult):
|
||||
"""Returns Tracks section lines: data picked from trackResult"""
|
||||
|
||||
lines = []
|
||||
|
||||
# Track number
|
||||
lines.append(" %d:" % trackResult.number)
|
||||
track = OrderedDict()
|
||||
|
||||
# Filename (including path) of ripped track
|
||||
lines.append(" Filename: %s" % trackResult.filename)
|
||||
track["Filename"] = trackResult.filename
|
||||
|
||||
# Pre-gap length
|
||||
pregap = trackResult.pregap
|
||||
if pregap:
|
||||
lines.append(" Pre-gap length: %s" % common.framesToMSF(pregap))
|
||||
track["Pre-gap length"] = common.framesToMSF(pregap)
|
||||
|
||||
# Peak level
|
||||
peak = trackResult.peak / 32768.0
|
||||
lines.append(" Peak level: %.6f" % peak)
|
||||
track["Peak level"] = float("%.6f" % peak)
|
||||
|
||||
# Pre-emphasis status
|
||||
# Only implemented in whipper (trackResult.pre_emphasis)
|
||||
if trackResult.pre_emphasis:
|
||||
preEmph = "Yes"
|
||||
else:
|
||||
preEmph = "No"
|
||||
lines.append(" Pre-emphasis: %s" % preEmph)
|
||||
track["Pre-emphasis"] = trackResult.pre_emphasis
|
||||
|
||||
# Extraction speed
|
||||
if trackResult.copyspeed:
|
||||
lines.append(" Extraction speed: %.1f X" % (
|
||||
trackResult.copyspeed))
|
||||
track["Extraction speed"] = "%.1f X" % trackResult.copyspeed
|
||||
|
||||
# Extraction quality
|
||||
if trackResult.quality and trackResult.quality > 0.001:
|
||||
lines.append(" Extraction quality: %.2f %%" %
|
||||
(trackResult.quality * 100.0, ))
|
||||
track["Extraction quality"] = "%.2f %%" % (
|
||||
trackResult.quality * 100.0, )
|
||||
|
||||
# Ripper Test CRC
|
||||
if trackResult.testcrc is not None:
|
||||
lines.append(" Test CRC: %08X" % trackResult.testcrc)
|
||||
track["Test CRC"] = "%08X" % trackResult.testcrc
|
||||
|
||||
# Ripper Copy CRC
|
||||
if trackResult.copycrc is not None:
|
||||
lines.append(" Copy CRC: %08X" % trackResult.copycrc)
|
||||
track["Copy CRC"] = "%08X" % trackResult.copycrc
|
||||
|
||||
# AccurateRip track status
|
||||
ARDB_entry = 0
|
||||
ARDB_match = 0
|
||||
for v in ("v1", "v2"):
|
||||
data = OrderedDict()
|
||||
if trackResult.AR[v]["DBCRC"]:
|
||||
lines.append(" AccurateRip %s:" % v)
|
||||
ARDB_entry += 1
|
||||
if trackResult.AR[v]["CRC"] == trackResult.AR[v]["DBCRC"]:
|
||||
lines.append(" Result: Found, exact match")
|
||||
data["Result"] = "Found, exact match"
|
||||
ARDB_match += 1
|
||||
else:
|
||||
lines.append(" Result: Found, NO exact match")
|
||||
lines.append(
|
||||
" Confidence: %d" % trackResult.AR[v]["DBConfidence"]
|
||||
)
|
||||
lines.append(
|
||||
" Local CRC: %s" % trackResult.AR[v]["CRC"].upper()
|
||||
)
|
||||
lines.append(
|
||||
" Remote CRC: %s" % trackResult.AR[v]["DBCRC"].upper()
|
||||
)
|
||||
data["Result"] = "Found, NO exact match"
|
||||
data["Confidence"] = trackResult.AR[v]["DBConfidence"]
|
||||
data["Local CRC"] = trackResult.AR[v]["CRC"].upper()
|
||||
data["Remote CRC"] = trackResult.AR[v]["DBCRC"].upper()
|
||||
elif trackResult.number != 0:
|
||||
lines.append(" AccurateRip %s:" % v)
|
||||
lines.append(
|
||||
" Result: Track not present in AccurateRip database"
|
||||
)
|
||||
data["Result"] = "Track not present in AccurateRip database"
|
||||
track["AccurateRip %s" % v] = data
|
||||
|
||||
# Check if Test & Copy CRCs are equal
|
||||
if trackResult.testcrc == trackResult.copycrc:
|
||||
lines.append(" Status: Copy OK")
|
||||
track["Status"] = "Copy OK"
|
||||
else:
|
||||
self._errors = True
|
||||
lines.append(" Status: Error, CRC mismatch")
|
||||
return lines, bool(ARDB_entry), bool(ARDB_match)
|
||||
track["Status"] = "Error, CRC mismatch"
|
||||
return track, bool(ARDB_entry), bool(ARDB_match)
|
||||
|
||||
@@ -69,16 +69,18 @@ class RipResult:
|
||||
I hold information about the result for rips.
|
||||
I can be used to write log files.
|
||||
|
||||
@ivar offset: sample read offset
|
||||
@ivar table: the full index table
|
||||
@type table: L{whipper.image.table.Table}
|
||||
:cvar offset: sample read offset
|
||||
:cvar table: the full index table
|
||||
:vartype table: whipper.image.table.Table
|
||||
:cvar metadata: disc metadata from MusicBrainz (if available)
|
||||
:vartype metadata: whipper.common.mbngs.DiscMetadata
|
||||
|
||||
@ivar vendor: vendor of the CD drive
|
||||
@ivar model: model of the CD drive
|
||||
@ivar release: release of the CD drive
|
||||
:cvar vendor: vendor of the CD drive
|
||||
:cvar model: model of the CD drive
|
||||
:cvar release: release of the CD drive
|
||||
|
||||
@ivar cdrdaoVersion: version of cdrdao used for the rip
|
||||
@ivar cdparanoiaVersion: version of cdparanoia used for the rip
|
||||
:cvar cdrdaoVersion: version of cdrdao used for the rip
|
||||
:cvar cdparanoiaVersion: version of cdparanoia used for the rip
|
||||
"""
|
||||
|
||||
offset = 0
|
||||
@@ -88,6 +90,7 @@ class RipResult:
|
||||
table = None
|
||||
artist = None
|
||||
title = None
|
||||
metadata = None
|
||||
|
||||
vendor = None
|
||||
model = None
|
||||
@@ -104,10 +107,10 @@ class RipResult:
|
||||
|
||||
def getTrackResult(self, number):
|
||||
"""
|
||||
@param number: the track number (0 for HTOA)
|
||||
:param number: the track number (0 for HTOA)
|
||||
|
||||
@type number: int
|
||||
@rtype: L{TrackResult}
|
||||
:type number: int
|
||||
:rtype: TrackResult
|
||||
"""
|
||||
for t in self.tracks:
|
||||
if t.number == number:
|
||||
@@ -125,11 +128,11 @@ class Logger(object):
|
||||
"""
|
||||
Create a log from the given ripresult.
|
||||
|
||||
@param epoch: when the log file gets generated
|
||||
@type epoch: float
|
||||
@type ripResult: L{RipResult}
|
||||
:param epoch: when the log file gets generated
|
||||
:type epoch: float
|
||||
:type ripResult: RipResult
|
||||
|
||||
@rtype: str
|
||||
:rtype: str
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -140,7 +143,8 @@ class Logger(object):
|
||||
class EntryPoint(object):
|
||||
name = 'whipper'
|
||||
|
||||
def load(self):
|
||||
@staticmethod
|
||||
def load():
|
||||
from whipper.result import logger
|
||||
return logger.WhipperLogger
|
||||
|
||||
@@ -149,7 +153,7 @@ def getLoggers():
|
||||
"""
|
||||
Get all logger plugins with entry point 'whipper.logger'.
|
||||
|
||||
@rtype: dict of C{str} -> C{Logger}
|
||||
:rtype: dict of :class:`str` -> :any:`Logger`
|
||||
"""
|
||||
d = {}
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ class TestCase(unittest.TestCase):
|
||||
# and we'd like to check for the actual exception under TaskException,
|
||||
# so override the way twisted.trial.unittest does, without failure
|
||||
|
||||
# XXX: Pylint, method could be a function (no-self-use)
|
||||
def failUnlessRaises(self, exception, f, *args, **kwargs):
|
||||
try:
|
||||
result = f(*args, **kwargs)
|
||||
@@ -53,7 +54,7 @@ class TestCase(unittest.TestCase):
|
||||
return inst
|
||||
except exception as e:
|
||||
raise Exception('%s raised instead of %s:\n %s' %
|
||||
(sys.exec_info()[0], exception.__name__, str(e))
|
||||
(sys.exc_info()[0], exception.__name__, str(e))
|
||||
)
|
||||
else:
|
||||
raise Exception('%s not raised (%r returned)' %
|
||||
@@ -62,7 +63,8 @@ class TestCase(unittest.TestCase):
|
||||
|
||||
assertRaises = failUnlessRaises
|
||||
|
||||
def readCue(self, name):
|
||||
@staticmethod
|
||||
def readCue(name):
|
||||
"""
|
||||
Read a .cue file, and replace the version comment with the current
|
||||
version so we can use it in comparisons.
|
||||
@@ -71,7 +73,7 @@ class TestCase(unittest.TestCase):
|
||||
ret = open(cuefile).read().decode('utf-8')
|
||||
ret = re.sub(
|
||||
'REM COMMENT "whipper.*',
|
||||
'REM COMMENT "whipper %s"' % (whipper.__version__),
|
||||
'REM COMMENT "whipper %s"' % whipper.__version__,
|
||||
ret, re.MULTILINE)
|
||||
|
||||
return ret
|
||||
|
||||
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].discId2, '00027ef8')
|
||||
self.assertEqual(responses[1].cddbDiscId, '05021002')
|
||||
self.assertEqual(responses[1].confidences[0], 5)
|
||||
self.assertEqual(responses[1].confidences[1], 5)
|
||||
self.assertEqual(responses[1].confidences[0], 6)
|
||||
self.assertEqual(responses[1].confidences[1], 6)
|
||||
self.assertEqual(responses[1].checksums[0], 'dc77f9ab')
|
||||
self.assertEqual(responses[1].checksums[1], 'dd97d2c3')
|
||||
|
||||
@@ -203,7 +203,7 @@ class TestVerifyResult(TestCase):
|
||||
'v2': {
|
||||
'CRC': 'dc77f9ab',
|
||||
'DBCRC': 'dc77f9ab',
|
||||
'DBConfidence': 5,
|
||||
'DBConfidence': 6
|
||||
},
|
||||
'DBMaxConfidence': 12,
|
||||
'DBMaxConfidenceCRC': '284fc705',
|
||||
@@ -217,7 +217,7 @@ class TestVerifyResult(TestCase):
|
||||
'v2': {
|
||||
'CRC': 'dd97d2c3',
|
||||
'DBCRC': 'dd97d2c3',
|
||||
'DBConfidence': 5,
|
||||
'DBConfidence': 6,
|
||||
},
|
||||
'DBMaxConfidence': 20,
|
||||
'DBMaxConfidenceCRC': '9cc1f32e',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- Mode: Python; test-case-name: whipper.test.test_common_mbngs -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
# vi:si:et:sw=4:sts=4:ts=4:set fileencoding=utf-8
|
||||
|
||||
import os
|
||||
import json
|
||||
@@ -12,15 +12,17 @@ from whipper.common import mbngs
|
||||
class MetadataTestCase(unittest.TestCase):
|
||||
|
||||
# Generated with rip -R cd info
|
||||
def testJeffEverybodySingle(self):
|
||||
filename = 'whipper.release.3451f29c-9bb8-4cc5-bfcc-bd50104b94f8.json'
|
||||
def testMissingReleaseDate(self):
|
||||
# Using: The KLF - Space & Chill Out
|
||||
# https://musicbrainz.org/release/c56ff16e-1d81-47de-926f-ba22891bd2bd
|
||||
filename = 'whipper.release.c56ff16e-1d81-47de-926f-ba22891bd2bd.json'
|
||||
path = os.path.join(os.path.dirname(__file__), filename)
|
||||
handle = open(path, "rb")
|
||||
response = json.loads(handle.read())
|
||||
handle.close()
|
||||
discid = "wbjbST2jUHRZaB1inCyxxsL7Eqc-"
|
||||
discid = "b.yqPuCBdsV5hrzDvYrw52iK_jE-"
|
||||
|
||||
metadata = mbngs._getMetadata({}, response['release'], discid)
|
||||
metadata = mbngs._getMetadata(response['release'], discid)
|
||||
|
||||
self.assertFalse(metadata.release)
|
||||
|
||||
@@ -33,21 +35,22 @@ class MetadataTestCase(unittest.TestCase):
|
||||
handle.close()
|
||||
discid = "f7XO36a7n1LCCskkCiulReWbwZA-"
|
||||
|
||||
metadata = mbngs._getMetadata({}, response['release'], discid)
|
||||
metadata = mbngs._getMetadata(response['release'], discid)
|
||||
|
||||
self.assertEqual(metadata.artist, u'Various Artists')
|
||||
self.assertEqual(metadata.release, u'2001-10-15')
|
||||
self.assertEqual(metadata.mbidArtist,
|
||||
u'89ad4ac3-39f7-470e-963a-56509c546377')
|
||||
[u'89ad4ac3-39f7-470e-963a-56509c546377'])
|
||||
|
||||
self.assertEqual(len(metadata.tracks), 18)
|
||||
|
||||
track16 = metadata.tracks[15]
|
||||
|
||||
self.assertEqual(track16.artist, 'Tom Jones & Stereophonics')
|
||||
self.assertEqual(track16.mbidArtist,
|
||||
u'57c6f649-6cde-48a7-8114-2a200247601a'
|
||||
';0bfba3d3-6a04-4779-bb0a-df07df5b0558')
|
||||
self.assertEqual(track16.mbidArtist, [
|
||||
u'57c6f649-6cde-48a7-8114-2a200247601a',
|
||||
u'0bfba3d3-6a04-4779-bb0a-df07df5b0558',
|
||||
])
|
||||
self.assertEqual(track16.sortName,
|
||||
u'Jones, Tom & Stereophonics')
|
||||
|
||||
@@ -60,15 +63,16 @@ class MetadataTestCase(unittest.TestCase):
|
||||
handle.close()
|
||||
discid = "xAq8L4ELMW14.6wI6tt7QAcxiDI-"
|
||||
|
||||
metadata = mbngs._getMetadata({}, response['release'], discid)
|
||||
metadata = mbngs._getMetadata(response['release'], discid)
|
||||
|
||||
self.assertEqual(metadata.artist, u'Isobel Campbell & Mark Lanegan')
|
||||
self.assertEqual(metadata.sortName,
|
||||
u'Campbell, Isobel & Lanegan, Mark')
|
||||
self.assertEqual(metadata.release, u'2006-01-30')
|
||||
self.assertEqual(metadata.mbidArtist,
|
||||
u'd51f3a15-12a2-41a0-acfa-33b5eae71164;'
|
||||
'a9126556-f555-4920-9617-6e013f8228a7')
|
||||
self.assertEqual(metadata.mbidArtist, [
|
||||
u'd51f3a15-12a2-41a0-acfa-33b5eae71164',
|
||||
u'a9126556-f555-4920-9617-6e013f8228a7',
|
||||
])
|
||||
|
||||
self.assertEqual(len(metadata.tracks), 12)
|
||||
|
||||
@@ -78,9 +82,10 @@ class MetadataTestCase(unittest.TestCase):
|
||||
self.assertEqual(track12.sortName,
|
||||
u'Campbell, Isobel'
|
||||
' & Lanegan, Mark')
|
||||
self.assertEqual(track12.mbidArtist,
|
||||
u'd51f3a15-12a2-41a0-acfa-33b5eae71164;'
|
||||
'a9126556-f555-4920-9617-6e013f8228a7')
|
||||
self.assertEqual(track12.mbidArtist, [
|
||||
u'd51f3a15-12a2-41a0-acfa-33b5eae71164',
|
||||
u'a9126556-f555-4920-9617-6e013f8228a7',
|
||||
])
|
||||
|
||||
def testMalaInCuba(self):
|
||||
# single artist disc, but with multiple artists tracks
|
||||
@@ -92,13 +97,13 @@ class MetadataTestCase(unittest.TestCase):
|
||||
handle.close()
|
||||
discid = "u0aKVpO.59JBy6eQRX2vYcoqQZ0-"
|
||||
|
||||
metadata = mbngs._getMetadata({}, response['release'], discid)
|
||||
metadata = mbngs._getMetadata(response['release'], discid)
|
||||
|
||||
self.assertEqual(metadata.artist, u'Mala')
|
||||
self.assertEqual(metadata.sortName, u'Mala')
|
||||
self.assertEqual(metadata.release, u'2012-09-17')
|
||||
self.assertEqual(metadata.mbidArtist,
|
||||
u'09f221eb-c97e-4da5-ac22-d7ab7c555bbb')
|
||||
[u'09f221eb-c97e-4da5-ac22-d7ab7c555bbb'])
|
||||
|
||||
self.assertEqual(len(metadata.tracks), 14)
|
||||
|
||||
@@ -107,49 +112,52 @@ class MetadataTestCase(unittest.TestCase):
|
||||
self.assertEqual(track6.artist, u'Mala feat. Dreiser & Sexto Sentido')
|
||||
self.assertEqual(track6.sortName,
|
||||
u'Mala feat. Dreiser & Sexto Sentido')
|
||||
self.assertEqual(track6.mbidArtist,
|
||||
u'09f221eb-c97e-4da5-ac22-d7ab7c555bbb'
|
||||
';ec07a209-55ff-4084-bc41-9d4d1764e075'
|
||||
';f626b92e-07b1-4a19-ad13-c09d690db66c')
|
||||
self.assertEqual(track6.mbidArtist, [
|
||||
u'09f221eb-c97e-4da5-ac22-d7ab7c555bbb',
|
||||
u'ec07a209-55ff-4084-bc41-9d4d1764e075',
|
||||
u'f626b92e-07b1-4a19-ad13-c09d690db66c',
|
||||
])
|
||||
|
||||
def testNorthernGateway(self):
|
||||
def testUnknownArtist(self):
|
||||
"""
|
||||
check the received metadata for artists tagged with [unknown]
|
||||
and artists tagged with an alias in MusicBrainz
|
||||
|
||||
see https://github.com/whipper-team/whipper/issues/155
|
||||
"""
|
||||
filename = 'whipper.release.38b05c7d-65fe-4dc0-9c10-33a391b86703.json'
|
||||
# Using: CunninLynguists - Sloppy Seconds, Volume 1
|
||||
# https://musicbrainz.org/release/8478d4da-0cda-4e46-ae8c-1eeacfa5cf37
|
||||
filename = 'whipper.release.8478d4da-0cda-4e46-ae8c-1eeacfa5cf37.json'
|
||||
path = os.path.join(os.path.dirname(__file__), filename)
|
||||
handle = open(path, "rb")
|
||||
response = json.loads(handle.read())
|
||||
handle.close()
|
||||
discid = "rzGHHqfPWIq1GsOLhhlBcZuqo.I-"
|
||||
discid = "RhrwgVb0hZNkabQCw1dZIhdbMFg-"
|
||||
|
||||
metadata = mbngs._getMetadata({}, response['release'], discid)
|
||||
self.assertEqual(metadata.artist, u'Various Artists')
|
||||
self.assertEqual(metadata.release, u'2010')
|
||||
metadata = mbngs._getMetadata(response['release'], discid)
|
||||
self.assertEqual(metadata.artist, u'CunninLynguists')
|
||||
self.assertEqual(metadata.release, u'2003')
|
||||
self.assertEqual(metadata.mbidArtist,
|
||||
u'89ad4ac3-39f7-470e-963a-56509c546377')
|
||||
[u'69c4cc43-8163-41c5-ac81-30946d27bb69'])
|
||||
|
||||
self.assertEqual(len(metadata.tracks), 10)
|
||||
self.assertEqual(len(metadata.tracks), 30)
|
||||
|
||||
track2 = metadata.tracks[1]
|
||||
track8 = metadata.tracks[7]
|
||||
|
||||
self.assertEqual(track2.artist, u'Twisted Reaction feat. Danielle')
|
||||
self.assertEqual(track2.sortName,
|
||||
u'Twisted Reaction feat. [unknown]')
|
||||
self.assertEqual(track2.mbidArtist,
|
||||
u'4f69f624-73ea-4a16-b822-bd2ca58032bf'
|
||||
';125ec42a-7229-4250-afc5-e057484327fe')
|
||||
self.assertEqual(track8.artist, u'???')
|
||||
self.assertEqual(track8.sortName, u'[unknown]')
|
||||
self.assertEqual(track8.mbidArtist,
|
||||
[u'125ec42a-7229-4250-afc5-e057484327fe'])
|
||||
|
||||
track4 = metadata.tracks[3]
|
||||
track9 = metadata.tracks[8]
|
||||
|
||||
self.assertEqual(track4.artist, u'BioGenesis')
|
||||
self.assertEqual(track4.sortName,
|
||||
u'Bio Genesis')
|
||||
self.assertEqual(track4.mbidArtist,
|
||||
u'dd61b86c-c015-43e1-9a28-58fceb0975c8')
|
||||
self.assertEqual(track9.artist, u'CunninLynguists feat. Tonedeff')
|
||||
self.assertEqual(track9.sortName,
|
||||
u'CunninLynguists feat. Tonedeff')
|
||||
self.assertEqual(track9.mbidArtist, [
|
||||
u'69c4cc43-8163-41c5-ac81-30946d27bb69',
|
||||
u'b3869d83-9fb5-4eac-b5ca-2d155fcbee12'
|
||||
])
|
||||
|
||||
def testNenaAndKimWildSingle(self):
|
||||
"""
|
||||
@@ -163,12 +171,13 @@ class MetadataTestCase(unittest.TestCase):
|
||||
handle.close()
|
||||
discid = "X2c2IQ5vUy5x6Jh7Xi_DGHtA1X8-"
|
||||
|
||||
metadata = mbngs._getMetadata({}, response['release'], discid)
|
||||
metadata = mbngs._getMetadata(response['release'], discid)
|
||||
self.assertEqual(metadata.artist, u'Nena & Kim Wilde')
|
||||
self.assertEqual(metadata.release, u'2003-05-19')
|
||||
self.assertEqual(metadata.mbidArtist,
|
||||
u'38bfaa7f-ee98-48cb-acd0-946d7aeecd76'
|
||||
';4b462375-c508-432a-8c88-ceeec38b16ae')
|
||||
self.assertEqual(metadata.mbidArtist, [
|
||||
u'38bfaa7f-ee98-48cb-acd0-946d7aeecd76',
|
||||
u'4b462375-c508-432a-8c88-ceeec38b16ae',
|
||||
])
|
||||
|
||||
self.assertEqual(len(metadata.tracks), 4)
|
||||
|
||||
@@ -176,14 +185,85 @@ class MetadataTestCase(unittest.TestCase):
|
||||
|
||||
self.assertEqual(track1.artist, u'Nena & Kim Wilde')
|
||||
self.assertEqual(track1.sortName, u'Nena & Wilde, Kim')
|
||||
self.assertEqual(track1.mbidArtist,
|
||||
u'38bfaa7f-ee98-48cb-acd0-946d7aeecd76'
|
||||
';4b462375-c508-432a-8c88-ceeec38b16ae')
|
||||
self.assertEqual(track1.mbidArtist, [
|
||||
u'38bfaa7f-ee98-48cb-acd0-946d7aeecd76',
|
||||
u'4b462375-c508-432a-8c88-ceeec38b16ae',
|
||||
])
|
||||
self.assertEqual(track1.mbid,
|
||||
u'1cc96e78-28ed-3820-b0b6-614c35b121ac')
|
||||
self.assertEqual(track1.mbidRecording,
|
||||
u'fde5622c-ce23-4ebb-975d-51d4a926f901')
|
||||
|
||||
track2 = metadata.tracks[1]
|
||||
|
||||
self.assertEqual(track2.artist, u'Nena & Kim Wilde')
|
||||
self.assertEqual(track2.sortName, u'Nena & Wilde, Kim')
|
||||
self.assertEqual(track2.mbidArtist,
|
||||
u'38bfaa7f-ee98-48cb-acd0-946d7aeecd76'
|
||||
';4b462375-c508-432a-8c88-ceeec38b16ae')
|
||||
self.assertEqual(track2.mbidArtist, [
|
||||
u'38bfaa7f-ee98-48cb-acd0-946d7aeecd76',
|
||||
u'4b462375-c508-432a-8c88-ceeec38b16ae',
|
||||
])
|
||||
self.assertEqual(track2.mbid,
|
||||
u'f16db4bf-9a34-3d5a-a975-c9375ab7a2ca')
|
||||
self.assertEqual(track2.mbidRecording,
|
||||
u'5f19758e-7421-4c71-a599-9a9575d8e1b0')
|
||||
|
||||
def testMissingReleaseGroupType(self):
|
||||
"""Check that whipper doesn't break if there's no type."""
|
||||
# Using: Gustafsson, Österberg & Cowle - What's Up? 8 (disc 4)
|
||||
# https://musicbrainz.org/release/d8e6153a-2c47-4804-9d73-0aac1081c3b1
|
||||
filename = 'whipper.release.d8e6153a-2c47-4804-9d73-0aac1081c3b1.json'
|
||||
path = os.path.join(os.path.dirname(__file__), filename)
|
||||
handle = open(path, "rb")
|
||||
response = json.loads(handle.read())
|
||||
handle.close()
|
||||
discid = "xu338_M8WukSRi0J.KTlDoflB8Y-" # disc 4
|
||||
|
||||
metadata = mbngs._getMetadata(response['release'], discid)
|
||||
self.assertEqual(metadata.releaseType, None)
|
||||
|
||||
def testAllAvailableMetadata(self):
|
||||
"""Check that all possible metadata gets assigned."""
|
||||
# Using: David Rovics - The Other Side
|
||||
# https://musicbrainz.org/release/6109ceed-7e21-490b-b5ad-3a66b4e4cfbb
|
||||
filename = 'whipper.release.6109ceed-7e21-490b-b5ad-3a66b4e4cfbb.json'
|
||||
path = os.path.join(os.path.dirname(__file__), filename)
|
||||
handle = open(path, "rb")
|
||||
response = json.loads(handle.read())
|
||||
handle.close()
|
||||
discid = "cHW1Uutl_kyWNaLJsLmTGTe4rnE-"
|
||||
|
||||
metadata = mbngs._getMetadata(response['release'], discid)
|
||||
self.assertEqual(metadata.artist, u'David Rovics')
|
||||
self.assertEqual(metadata.sortName, u'Rovics, David')
|
||||
self.assertFalse(metadata.various)
|
||||
self.assertIsInstance(metadata.tracks, list)
|
||||
self.assertEqual(metadata.release, u'2015')
|
||||
self.assertEqual(metadata.releaseTitle, u'The Other Side')
|
||||
self.assertEqual(metadata.releaseType, u'Album')
|
||||
self.assertEqual(metadata.mbid,
|
||||
u'6109ceed-7e21-490b-b5ad-3a66b4e4cfbb')
|
||||
self.assertEqual(metadata.mbidReleaseGroup,
|
||||
u'99850b41-a06e-4fb8-992c-75c191a77803')
|
||||
self.assertEqual(metadata.mbidArtist,
|
||||
[u'4d56eb9f-13b3-4f05-9db7-50195378d49f'])
|
||||
self.assertEqual(metadata.url,
|
||||
u'https://musicbrainz.org/release'
|
||||
'/6109ceed-7e21-490b-b5ad-3a66b4e4cfbb')
|
||||
self.assertEqual(metadata.catalogNumber, u'[none]')
|
||||
self.assertEqual(metadata.barcode, u'700261430249')
|
||||
|
||||
self.assertEqual(len(metadata.tracks), 16)
|
||||
|
||||
track1 = metadata.tracks[0]
|
||||
self.assertEqual(track1.artist, u'David Rovics')
|
||||
self.assertEqual(track1.title, u'Waiting for the Hurricane')
|
||||
self.assertEqual(track1.duration, 176320)
|
||||
self.assertEqual(track1.mbid,
|
||||
u'4116eea3-b9c2-452a-8d63-92f1e585b225')
|
||||
self.assertEqual(track1.sortName, u'Rovics, David')
|
||||
self.assertEqual(track1.mbidArtist,
|
||||
[u'4d56eb9f-13b3-4f05-9db7-50195378d49f'])
|
||||
self.assertEqual(track1.mbidRecording,
|
||||
u'b191794d-b7c6-4d6f-971e-0a543959b5ad')
|
||||
self.assertEqual(track1.mbidWorks,
|
||||
[u'90d5be68-0b29-45a3-ba01-c27ad78e3625'])
|
||||
|
||||
@@ -15,9 +15,8 @@ class PathTestCase(unittest.TestCase):
|
||||
|
||||
path = prog.getPath(u'/tmp', DEFAULT_DISC_TEMPLATE,
|
||||
'mbdiscid', None)
|
||||
self.assertEqual(path,
|
||||
unicode('/tmp/unknown/Unknown Artist - mbdiscid/'
|
||||
'Unknown Artist - mbdiscid'))
|
||||
self.assertEqual(path, (u'/tmp/unknown/Unknown Artist - mbdiscid/'
|
||||
u'Unknown Artist - mbdiscid'))
|
||||
|
||||
def testStandardTemplateFilled(self):
|
||||
prog = program.Program(config.Config())
|
||||
@@ -27,9 +26,8 @@ class PathTestCase(unittest.TestCase):
|
||||
|
||||
path = prog.getPath(u'/tmp', DEFAULT_DISC_TEMPLATE,
|
||||
'mbdiscid', md, 0)
|
||||
self.assertEqual(path,
|
||||
unicode('/tmp/unknown/Jeff Buckley - Grace/'
|
||||
'Jeff Buckley - Grace'))
|
||||
self.assertEqual(path, (u'/tmp/unknown/Jeff Buckley - Grace/'
|
||||
u'Jeff Buckley - Grace'))
|
||||
|
||||
def testIssue66TemplateFilled(self):
|
||||
prog = program.Program(config.Config())
|
||||
|
||||
@@ -59,7 +59,8 @@ class KanyeMixedTestCase(unittest.TestCase):
|
||||
|
||||
class WriteCueFileTestCase(unittest.TestCase):
|
||||
|
||||
def testWrite(self):
|
||||
@staticmethod
|
||||
def testWrite():
|
||||
fd, path = tempfile.mkstemp(suffix=u'.whipper.test.cue')
|
||||
os.close(fd)
|
||||
|
||||
|
||||
@@ -320,6 +320,20 @@ class TOTBLTestCase(common.TestCase):
|
||||
self.assertEqual(self.toc.table.getCDDBDiscId(), '810b7b0b')
|
||||
|
||||
|
||||
class GentlemenTestCase(common.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.path = os.path.join(os.path.dirname(__file__),
|
||||
u'gentlemen.fast.toc')
|
||||
self.toc = toc.TocFile(self.path)
|
||||
self.toc.parse()
|
||||
self.assertEquals(len(self.toc.table.tracks), 11)
|
||||
|
||||
def testCDDBId(self):
|
||||
self.toc.table.absolutize()
|
||||
self.assertEquals(self.toc.table.getCDDBDiscId(), '810b7b0b')
|
||||
|
||||
|
||||
# The Strokes - Someday has a 1 frame SILENCE marked as such in toc
|
||||
|
||||
|
||||
@@ -353,7 +367,8 @@ class StrokesTestCase(common.TestCase):
|
||||
'strokes-someday.eac.cue')).read()).decode('utf-8')
|
||||
common.diffStrings(ref, cue)
|
||||
|
||||
def _filterCue(self, output):
|
||||
@staticmethod
|
||||
def _filterCue(output):
|
||||
# helper to be able to compare our generated .cue with the
|
||||
# EAC-extracted one
|
||||
discard = ['TITLE', 'PERFORMER', 'FLAGS', 'REM']
|
||||
|
||||
@@ -75,8 +75,8 @@ class AnalyzeFileTask(cdparanoia.AnalyzeTask):
|
||||
def __init__(self, path):
|
||||
self.command = ['cat', path]
|
||||
|
||||
def readbytesout(self, bytes):
|
||||
self.readbyteserr(bytes)
|
||||
def readbytesout(self, bytes_stdout):
|
||||
self.readbyteserr(bytes_stdout)
|
||||
|
||||
|
||||
class CacheTestCase(common.TestCase):
|
||||
|
||||
@@ -9,7 +9,7 @@ from whipper.test import common
|
||||
|
||||
class VersionTestCase(common.TestCase):
|
||||
def testGetVersion(self):
|
||||
v = cdrdao.getCDRDAOVersion()
|
||||
v = cdrdao.version()
|
||||
self.assertTrue(v)
|
||||
# make sure it starts with a digit
|
||||
self.assertTrue(int(v[0]))
|
||||
|
||||
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