Merge branch 'release/0.7.3'
This commit is contained in:
19
.github/config.yml
vendored
Normal file
19
.github/config.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# Configuration for new-issue-welcome - https://github.com/behaviorbot/new-issue-welcome
|
||||
|
||||
# Comment to be posted to on first time issues
|
||||
newIssueWelcomeComment: |
|
||||
👋 Thanks for opening your first issue here! If you're reporting a 🐞 bug, please make sure you include steps to reproduce it. We get a lot of issues on this repo, so please be patient and we will get back to you as soon as we can.
|
||||
|
||||
To help make it easier for us to investigate your issue, please follow the [contributing instructions](https://github.com/whipper-team/whipper#bug-reports--feature-requests).
|
||||
|
||||
# Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome
|
||||
|
||||
# Comment to be posted to on PRs from first time contributors in your repository
|
||||
newPRWelcomeComment: >
|
||||
💖 Thanks for opening your first pull request here! 💖
|
||||
|
||||
# Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge
|
||||
|
||||
# Comment to be posted to on pull requests merged by a first time user
|
||||
firstPRMergeComment: >
|
||||
Congrats on merging your first pull request! 🎉🎉🎉
|
||||
42
.github/stale.yml
vendored
Normal file
42
.github/stale.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
# Configuration for probot-stale - https://github.com/probot/stale
|
||||
|
||||
# Number of days of inactivity before an Issue or Pull Request becomes stale
|
||||
daysUntilStale: 30
|
||||
|
||||
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
|
||||
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
|
||||
daysUntilClose: 7
|
||||
|
||||
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
|
||||
exemptLabels:
|
||||
- Accepted
|
||||
|
||||
# Set to true to ignore issues in a project (defaults to false)
|
||||
exemptProjects: false
|
||||
|
||||
# Set to true to ignore issues in a milestone (defaults to false)
|
||||
exemptMilestones:
|
||||
- "2.0"
|
||||
- backlog
|
||||
|
||||
# Label to use when marking as stale
|
||||
staleLabel: "Status: stale"
|
||||
|
||||
# Comment to post when marking as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue/pull request has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed in 7 days if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
|
||||
# Comment to post when removing the stale label.
|
||||
unmarkComment: >
|
||||
Thank you for updating this issue. It is no longer marked as stale.
|
||||
|
||||
# Comment to post when closing a stale Issue or Pull Request.
|
||||
closeComment: |
|
||||
This issue/pull request has been closed due to prolonged inactivity.
|
||||
|
||||
If you think this is an error, please leave a comment and we will gladly reopen it.
|
||||
|
||||
# Limit the number of actions per hour, from 1-30. Default is 30
|
||||
limitPerRun: 30
|
||||
196
CHANGELOG.md
196
CHANGELOG.md
@@ -2,23 +2,59 @@
|
||||
|
||||
## [Unreleased](https://github.com/whipper-team/whipper/tree/HEAD)
|
||||
|
||||
[Full Changelog](https://github.com/whipper-team/whipper/compare/v0.7.2...HEAD)
|
||||
[Full Changelog](https://github.com/whipper-team/whipper/compare/v0.7.3...HEAD)
|
||||
|
||||
## [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)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Error when parsing log file due to left pad track number [\#340](https://github.com/whipper-team/whipper/issues/340)
|
||||
- Failing AccurateRipResponse tests [\#333](https://github.com/whipper-team/whipper/issues/333)
|
||||
- CRITICAL:whipper.command.cd:output directory is a finished rip output directory [\#287](https://github.com/whipper-team/whipper/issues/287)
|
||||
- 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)
|
||||
- Allow plugins from system directories [\#135](https://github.com/whipper-team/whipper/issues/135)
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- 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)
|
||||
- Write musicbrainz\_discid tag when disc is unknown [\#280](https://github.com/whipper-team/whipper/issues/280)
|
||||
- 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:**
|
||||
|
||||
- Discover plugins in system directories too [\#348](https://github.com/whipper-team/whipper/pull/348) ([JoeLametta](https://github.com/JoeLametta))
|
||||
- Avoid zero padding in logger track numbers [\#341](https://github.com/whipper-team/whipper/pull/341) ([itismadness](https://github.com/itismadness))
|
||||
- Update failing AccurateRipResponse tests [\#334](https://github.com/whipper-team/whipper/pull/334) ([JoeLametta](https://github.com/JoeLametta))
|
||||
- Replace sys.std{out,err} statements with logger/print calls [\#331](https://github.com/whipper-team/whipper/pull/331) ([JoeLametta](https://github.com/JoeLametta))
|
||||
- Add Probot apps to improve workflow [\#329](https://github.com/whipper-team/whipper/pull/329) ([JoeLametta](https://github.com/JoeLametta))
|
||||
- Raise exception when cdparanoia can't read any frames [\#328](https://github.com/whipper-team/whipper/pull/328) ([JoeLametta](https://github.com/JoeLametta))
|
||||
- Prevent exception in offset find [\#327](https://github.com/whipper-team/whipper/pull/327) ([JoeLametta](https://github.com/JoeLametta))
|
||||
- Fix template validation error [\#325](https://github.com/whipper-team/whipper/pull/325) ([JoeLametta](https://github.com/JoeLametta))
|
||||
- Fix UnicodeEncodeError with non ASCII MusicBrainz's catalog numbers [\#323](https://github.com/whipper-team/whipper/pull/323) ([JoeLametta](https://github.com/JoeLametta))
|
||||
- Raise exception if template has invalid variables [\#322](https://github.com/whipper-team/whipper/pull/322) ([JoeLametta](https://github.com/JoeLametta))
|
||||
- Preserve ToC file generated by cdrdao [\#321](https://github.com/whipper-team/whipper/pull/321) ([JoeLametta](https://github.com/JoeLametta))
|
||||
|
||||
## [v0.7.2](https://github.com/whipper-team/whipper/tree/v0.7.2) (2018-10-31)
|
||||
[Full Changelog](https://github.com/whipper-team/whipper/compare/v0.7.1...v0.7.2)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Add whipper to Hydrogen Audio wiki's "Comparison of CD rippers" [\#317](https://github.com/whipper-team/whipper/issues/317)
|
||||
- automatically build Docker images [\#301](https://github.com/whipper-team/whipper/issues/301)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- UnicodeEncodeError: 'ascii' codec can't encode characters in position 17-18: ordinal not in range\(128\) [\#315](https://github.com/whipper-team/whipper/issues/315)
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- Add whipper to Hydrogen Audio wiki's "Comparison of CD rippers" [\#317](https://github.com/whipper-team/whipper/issues/317)
|
||||
- Make 0.7.1 release \(before GCI 😅\) [\#312](https://github.com/whipper-team/whipper/issues/312)
|
||||
- automatically build Docker images [\#301](https://github.com/whipper-team/whipper/issues/301)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
@@ -28,24 +64,28 @@
|
||||
## [v0.7.1](https://github.com/whipper-team/whipper/tree/v0.7.1) (2018-10-23)
|
||||
[Full Changelog](https://github.com/whipper-team/whipper/compare/v0.7.0...v0.7.1)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Transfer repository ownership to GitHub organization [\#306](https://github.com/whipper-team/whipper/issues/306)
|
||||
- Add cdparanoia version to log file [\#267](https://github.com/whipper-team/whipper/issues/267)
|
||||
- Remove whipper's retag feature [\#262](https://github.com/whipper-team/whipper/issues/262)
|
||||
- Add a requirements.txt file [\#221](https://github.com/whipper-team/whipper/issues/221)
|
||||
- Limit length of filenames [\#197](https://github.com/whipper-team/whipper/issues/197)
|
||||
- Loggers [\#117](https://github.com/whipper-team/whipper/issues/117)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- TypeError on whipper offset find [\#263](https://github.com/whipper-team/whipper/issues/263)
|
||||
- Remove whipper's retag feature [\#262](https://github.com/whipper-team/whipper/issues/262)
|
||||
- ImportError: libcdio.so.16: cannot open shared object file: No such file or directory [\#229](https://github.com/whipper-team/whipper/issues/229)
|
||||
- Catch DNS error [\#206](https://github.com/whipper-team/whipper/issues/206)
|
||||
- Limit length of filenames [\#197](https://github.com/whipper-team/whipper/issues/197)
|
||||
- Loggers [\#117](https://github.com/whipper-team/whipper/issues/117)
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- Disable eject button when ripping [\#308](https://github.com/whipper-team/whipper/issues/308)
|
||||
- Transfer repository ownership to GitHub organization [\#306](https://github.com/whipper-team/whipper/issues/306)
|
||||
- Variable offset detected [\#295](https://github.com/whipper-team/whipper/issues/295)
|
||||
- Github repo [\#293](https://github.com/whipper-team/whipper/issues/293)
|
||||
- yaml logger [\#292](https://github.com/whipper-team/whipper/issues/292)
|
||||
- Add replaygain processing [\#285](https://github.com/whipper-team/whipper/issues/285)
|
||||
- pre emphasis documentation [\#275](https://github.com/whipper-team/whipper/issues/275)
|
||||
- Add cdparanoia version to log file [\#267](https://github.com/whipper-team/whipper/issues/267)
|
||||
- whipper sometimes creates invalid cue sheets [\#265](https://github.com/whipper-team/whipper/issues/265)
|
||||
- Make .cue and .m3u writing optional [\#259](https://github.com/whipper-team/whipper/issues/259)
|
||||
- Add a requirements.txt file [\#221](https://github.com/whipper-team/whipper/issues/221)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
@@ -66,60 +106,49 @@
|
||||
## [v0.7.0](https://github.com/whipper-team/whipper/tree/v0.7.0) (2018-04-09)
|
||||
[Full Changelog](https://github.com/whipper-team/whipper/compare/v0.6.0...v0.7.0)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Various ripping issues [\#179](https://github.com/whipper-team/whipper/issues/179)
|
||||
- Simple message while reading TOC [\#257](https://github.com/whipper-team/whipper/issues/257)
|
||||
- Small readme cleanups [\#250](https://github.com/whipper-team/whipper/pull/250) ([RecursiveForest](https://github.com/RecursiveForest))
|
||||
- Remove debug commands, add mblookup command [\#249](https://github.com/whipper-team/whipper/pull/249) ([RecursiveForest](https://github.com/RecursiveForest))
|
||||
- remove -T/--toc-pickle [\#245](https://github.com/whipper-team/whipper/pull/245) ([RecursiveForest](https://github.com/RecursiveForest))
|
||||
- credit four major developers by line count [\#243](https://github.com/whipper-team/whipper/pull/243) ([RecursiveForest](https://github.com/RecursiveForest))
|
||||
- Removed reference to unused "profile = flac" config option \(issue \#99\) [\#231](https://github.com/whipper-team/whipper/pull/231) ([calumchisholm](https://github.com/calumchisholm))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- whipper offset find exception [\#208](https://github.com/whipper-team/whipper/issues/208)
|
||||
- cd rip is not able to rip the last track [\#203](https://github.com/whipper-team/whipper/issues/203)
|
||||
- CD-ROM powers off during rip command. [\#189](https://github.com/whipper-team/whipper/issues/189)
|
||||
- Various ripping issues [\#179](https://github.com/whipper-team/whipper/issues/179)
|
||||
- "whipper image verify" abends on FLAC having ID3 tags \("TypeError: %x format: a number is required, not NoneType"\) [\#176](https://github.com/whipper-team/whipper/issues/176)
|
||||
- whipper not picking up all settings in whipper.conf [\#99](https://github.com/whipper-team/whipper/issues/99)
|
||||
- ImportError: libcdio.so.16: cannot open shared object file: No such file or directory [\#229](https://github.com/whipper-team/whipper/issues/229)
|
||||
- fix CI build error with latest pycdio [\#233](https://github.com/whipper-team/whipper/pull/233) ([thomas-mc-work](https://github.com/thomas-mc-work))
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- Simple message while reading TOC [\#257](https://github.com/whipper-team/whipper/issues/257)
|
||||
- Statement to your "only flac" decision [\#247](https://github.com/whipper-team/whipper/issues/247)
|
||||
- How to choose device \(if there are more\)? [\#241](https://github.com/whipper-team/whipper/issues/241)
|
||||
- Import Error No Module Named gobject Fedora 26 and 27 [\#228](https://github.com/whipper-team/whipper/issues/228)
|
||||
- Make a 0.6.0 release [\#219](https://github.com/whipper-team/whipper/issues/219)
|
||||
- CD-ROM powers off during rip command. [\#189](https://github.com/whipper-team/whipper/issues/189)
|
||||
- flac settings [\#184](https://github.com/whipper-team/whipper/issues/184)
|
||||
- Remove connection to parent fork. [\#79](https://github.com/whipper-team/whipper/issues/79)
|
||||
- GUI frontend for whipper [\#40](https://github.com/whipper-team/whipper/issues/40)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Small readme cleanups [\#250](https://github.com/whipper-team/whipper/pull/250) ([RecursiveForest](https://github.com/RecursiveForest))
|
||||
- Remove debug commands, add mblookup command [\#249](https://github.com/whipper-team/whipper/pull/249) ([RecursiveForest](https://github.com/RecursiveForest))
|
||||
- Remove reference to Copr repository [\#248](https://github.com/whipper-team/whipper/pull/248) ([mruszczyk](https://github.com/mruszczyk))
|
||||
- Revert "Convert docstrings to reStructuredText" [\#246](https://github.com/whipper-team/whipper/pull/246) ([RecursiveForest](https://github.com/RecursiveForest))
|
||||
- remove -T/--toc-pickle [\#245](https://github.com/whipper-team/whipper/pull/245) ([RecursiveForest](https://github.com/RecursiveForest))
|
||||
- credit four major developers by line count [\#243](https://github.com/whipper-team/whipper/pull/243) ([RecursiveForest](https://github.com/RecursiveForest))
|
||||
- remove radon reports [\#242](https://github.com/whipper-team/whipper/pull/242) ([RecursiveForest](https://github.com/RecursiveForest))
|
||||
- read command parameters from config sections [\#240](https://github.com/whipper-team/whipper/pull/240) ([RecursiveForest](https://github.com/RecursiveForest))
|
||||
- fix CI build error with latest pycdio [\#233](https://github.com/whipper-team/whipper/pull/233) ([thomas-mc-work](https://github.com/thomas-mc-work))
|
||||
- Removed reference to unused "profile = flac" config option \(issue \#99\) [\#231](https://github.com/whipper-team/whipper/pull/231) ([calumchisholm](https://github.com/calumchisholm))
|
||||
|
||||
## [v0.6.0](https://github.com/whipper-team/whipper/tree/v0.6.0) (2018-02-02)
|
||||
[Full Changelog](https://github.com/whipper-team/whipper/compare/v0.5.1...v0.6.0)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Error: NotFoundException message displayed while ripping an unknown disc [\#198](https://github.com/whipper-team/whipper/issues/198)
|
||||
- rename milestone 101010 to backlog [\#190](https://github.com/whipper-team/whipper/issues/190)
|
||||
- Use 'Artist as credited' in filename instead of 'Artist in MusicBrainz' \(e.g. to solve \[unknown\]\) [\#155](https://github.com/whipper-team/whipper/issues/155)
|
||||
- Declare supported Python version [\#152](https://github.com/whipper-team/whipper/issues/152)
|
||||
- Update doc/release or remove it [\#149](https://github.com/whipper-team/whipper/issues/149)
|
||||
- Test HTOA peak value against 0 \(integer equality\) [\#143](https://github.com/whipper-team/whipper/issues/143)
|
||||
- Identify media type in log file \(ie CD vs CD-R\) [\#137](https://github.com/whipper-team/whipper/issues/137)
|
||||
- Rename the Python module [\#100](https://github.com/whipper-team/whipper/issues/100)
|
||||
- libcdio-paranoia instead of cdparanoia [\#87](https://github.com/whipper-team/whipper/issues/87)
|
||||
- Support both AccurateRip V1 and AccurateRip V2 at the same time [\#18](https://github.com/whipper-team/whipper/issues/18)
|
||||
- Test HTOA peak value against 0 \(integer comparison\) [\#224](https://github.com/whipper-team/whipper/pull/224) ([JoeLametta](https://github.com/JoeLametta))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Error: NotFoundException message displayed while ripping an unknown disc [\#198](https://github.com/whipper-team/whipper/issues/198)
|
||||
- whipper doesn't name files .flac, which leads to it not being able to find ripped files [\#194](https://github.com/whipper-team/whipper/issues/194)
|
||||
- Issues with finding offset [\#182](https://github.com/whipper-team/whipper/issues/182)
|
||||
- failing unittests in systemd-nspawn container [\#157](https://github.com/whipper-team/whipper/issues/157)
|
||||
- Update doc/release or remove it [\#149](https://github.com/whipper-team/whipper/issues/149)
|
||||
- Test HTOA peak value against 0 \(integer equality\) [\#143](https://github.com/whipper-team/whipper/issues/143)
|
||||
- Regression: Unable to resume a failed rip [\#136](https://github.com/whipper-team/whipper/issues/136)
|
||||
- "Catalog Number" incorrectly appended to "artist" instead of the Album name. [\#127](https://github.com/whipper-team/whipper/issues/127)
|
||||
- Track "can't be ripped" but EAC can :\) [\#116](https://github.com/whipper-team/whipper/issues/116)
|
||||
@@ -128,11 +157,26 @@
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- TRACK and FILE order in cue file [\#212](https://github.com/whipper-team/whipper/issues/212)
|
||||
- ImportError - CDDB on Solus. [\#209](https://github.com/whipper-team/whipper/issues/209)
|
||||
- rename milestone 101010 to backlog [\#190](https://github.com/whipper-team/whipper/issues/190)
|
||||
- AttributeError: RipResult instance has no attribute 'profileName' [\#181](https://github.com/whipper-team/whipper/issues/181)
|
||||
- .log, .cue, and .m3u file names [\#180](https://github.com/whipper-team/whipper/issues/180)
|
||||
- Accurip verification step failure [\#178](https://github.com/whipper-team/whipper/issues/178)
|
||||
- Whipper offset find failing [\#177](https://github.com/whipper-team/whipper/issues/177)
|
||||
- using your own MusicBrainz server [\#172](https://github.com/whipper-team/whipper/issues/172)
|
||||
- cdda2wav from cdrtools instead of cdparanoia [\#38](https://github.com/whipper-team/whipper/issues/38)
|
||||
- Use 'Artist as credited' in filename instead of 'Artist in MusicBrainz' \(e.g. to solve \[unknown\]\) [\#155](https://github.com/whipper-team/whipper/issues/155)
|
||||
- Declare supported Python version [\#152](https://github.com/whipper-team/whipper/issues/152)
|
||||
- Identify media type in log file \(ie CD vs CD-R\) [\#137](https://github.com/whipper-team/whipper/issues/137)
|
||||
- Accurate rip failures still exit 0 [\#126](https://github.com/whipper-team/whipper/issues/126)
|
||||
- Rename the Python module [\#100](https://github.com/whipper-team/whipper/issues/100)
|
||||
- libcdio-paranoia instead of cdparanoia [\#87](https://github.com/whipper-team/whipper/issues/87)
|
||||
- Release, Tags, NEWS? [\#63](https://github.com/whipper-team/whipper/issues/63)
|
||||
- Support both AccurateRip V1 and AccurateRip V2 at the same time [\#18](https://github.com/whipper-team/whipper/issues/18)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Test HTOA peak value against 0 \(integer comparison\) [\#224](https://github.com/whipper-team/whipper/pull/224) ([JoeLametta](https://github.com/JoeLametta))
|
||||
- Fix appearance of template description text. [\#223](https://github.com/whipper-team/whipper/pull/223) ([calumchisholm](https://github.com/calumchisholm))
|
||||
- Run whipper without installation [\#222](https://github.com/whipper-team/whipper/pull/222) ([vmx](https://github.com/vmx))
|
||||
- Remove doc/release [\#218](https://github.com/whipper-team/whipper/pull/218) ([MerlijnWajer](https://github.com/MerlijnWajer))
|
||||
@@ -166,21 +210,29 @@
|
||||
## [v0.5.0](https://github.com/whipper-team/whipper/tree/v0.5.0) (2017-04-24)
|
||||
[Full Changelog](https://github.com/whipper-team/whipper/compare/v0.4.2...v0.5.0)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- overly verbose warning logging [\#131](https://github.com/whipper-team/whipper/issues/131)
|
||||
- Check that whipper deals properly with CD pre-emphasis [\#120](https://github.com/whipper-team/whipper/issues/120)
|
||||
- Remove gstreamer dependency [\#29](https://github.com/whipper-team/whipper/issues/29)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Final track rip failure due to file size mismatch [\#146](https://github.com/whipper-team/whipper/issues/146)
|
||||
- Fails to rip if MB Release doesn't have a release date/year [\#133](https://github.com/whipper-team/whipper/issues/133)
|
||||
- overly verbose warning logging [\#131](https://github.com/whipper-team/whipper/issues/131)
|
||||
- fb271f08cdee877795091065c344dcc902d1dcbf breaks HEAD [\#129](https://github.com/whipper-team/whipper/issues/129)
|
||||
- 'whipper drive list' returns a suggestion to run 'rip offset find' [\#112](https://github.com/whipper-team/whipper/issues/112)
|
||||
- EmptyError\('not a single buffer gotten',\) [\#101](https://github.com/whipper-team/whipper/issues/101)
|
||||
- Julie Roberts bug [\#74](https://github.com/whipper-team/whipper/issues/74)
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- `whipper find offset` still requiring gst [\#141](https://github.com/whipper-team/whipper/issues/141)
|
||||
- Burn FLACs 1:1 CD ? [\#125](https://github.com/whipper-team/whipper/issues/125)
|
||||
- whipper offset find -o OFFSET not working [\#123](https://github.com/whipper-team/whipper/issues/123)
|
||||
- Check that whipper deals properly with CD pre-emphasis [\#120](https://github.com/whipper-team/whipper/issues/120)
|
||||
- FreeDB metadata not honored [\#119](https://github.com/whipper-team/whipper/issues/119)
|
||||
- Difficulty getting flac encoding working. [\#118](https://github.com/whipper-team/whipper/issues/118)
|
||||
- enabling external loggers triggers python errors [\#111](https://github.com/whipper-team/whipper/issues/111)
|
||||
- additional tag creation [\#108](https://github.com/whipper-team/whipper/issues/108)
|
||||
- False positive on HTOA [\#82](https://github.com/whipper-team/whipper/issues/82)
|
||||
- Remove gstreamer dependency [\#29](https://github.com/whipper-team/whipper/issues/29)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Remove notes related to GStreamer flacparse [\#140](https://github.com/whipper-team/whipper/pull/140) ([Freso](https://github.com/Freso))
|
||||
@@ -195,13 +247,14 @@
|
||||
## [v0.4.2](https://github.com/whipper-team/whipper/tree/v0.4.2) (2017-01-08)
|
||||
[Full Changelog](https://github.com/whipper-team/whipper/compare/v0.4.1...v0.4.2)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Whipper attempts to rip with no CD inserted [\#81](https://github.com/whipper-team/whipper/issues/81)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- 0.4.1 Release created but version number in code not bumped [\#105](https://github.com/whipper-team/whipper/issues/105)
|
||||
- Whipper attempts to rip with no CD inserted [\#81](https://github.com/whipper-team/whipper/issues/81)
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- Make a 0.4.1 release [\#104](https://github.com/whipper-team/whipper/issues/104)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
@@ -211,10 +264,10 @@
|
||||
## [v0.4.1](https://github.com/whipper-team/whipper/tree/v0.4.1) (2017-01-06)
|
||||
[Full Changelog](https://github.com/whipper-team/whipper/compare/v0.4.0...v0.4.1)
|
||||
|
||||
**Implemented enhancements:**
|
||||
**Closed issues:**
|
||||
|
||||
- Please don't stop - despite the recent events \(ANSWERED\) [\#76](https://github.com/whipper-team/whipper/issues/76)
|
||||
- Migrate away from the "rip" command [\#21](https://github.com/whipper-team/whipper/issues/21)
|
||||
- Fixed README broken links and added a better changelog [\#90](https://github.com/whipper-team/whipper/pull/90) ([JoeLametta](https://github.com/JoeLametta))
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
@@ -226,6 +279,7 @@
|
||||
- cdrdao no-disc ejection & --eject [\#93](https://github.com/whipper-team/whipper/pull/93) ([RecursiveForest](https://github.com/RecursiveForest))
|
||||
- argparse & logging [\#92](https://github.com/whipper-team/whipper/pull/92) ([RecursiveForest](https://github.com/RecursiveForest))
|
||||
- Update README.md [\#91](https://github.com/whipper-team/whipper/pull/91) ([pieqq](https://github.com/pieqq))
|
||||
- Fixed README broken links and added a better changelog [\#90](https://github.com/whipper-team/whipper/pull/90) ([JoeLametta](https://github.com/JoeLametta))
|
||||
- soxi: remove self.\_path unused variable, mark dep as 'soxi' [\#89](https://github.com/whipper-team/whipper/pull/89) ([RecursiveForest](https://github.com/RecursiveForest))
|
||||
- Fix spelling mistake in README.md [\#86](https://github.com/whipper-team/whipper/pull/86) ([takeshibaconsuzuki](https://github.com/takeshibaconsuzuki))
|
||||
- Error reporting enhancements \(conditional-raise-instead-of-assert version\) [\#80](https://github.com/whipper-team/whipper/pull/80) ([chrysn](https://github.com/chrysn))
|
||||
@@ -240,6 +294,11 @@
|
||||
- wrong status code when giving up [\#57](https://github.com/whipper-team/whipper/issues/57)
|
||||
- CD-TEXT issue [\#49](https://github.com/whipper-team/whipper/issues/49)
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- ImportError: No module named log [\#64](https://github.com/whipper-team/whipper/issues/64)
|
||||
- whatlogger no longer recognized [\#56](https://github.com/whipper-team/whipper/issues/56)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Invoke whipper by its name + Readme rewrite [\#70](https://github.com/whipper-team/whipper/pull/70) ([JoeLametta](https://github.com/JoeLametta))
|
||||
@@ -261,6 +320,11 @@
|
||||
- UnicodeEncodeError [\#43](https://github.com/whipper-team/whipper/issues/43)
|
||||
- Use a single standard for config/cache/state files [\#24](https://github.com/whipper-team/whipper/issues/24)
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- offset find fails [\#46](https://github.com/whipper-team/whipper/issues/46)
|
||||
- Error launching rip cd rip command [\#41](https://github.com/whipper-team/whipper/issues/41)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Sox [\#48](https://github.com/whipper-team/whipper/pull/48) ([RecursiveForest](https://github.com/RecursiveForest))
|
||||
@@ -269,14 +333,6 @@
|
||||
## [v0.2.4](https://github.com/whipper-team/whipper/tree/v0.2.4) (2016-10-09)
|
||||
[Full Changelog](https://github.com/whipper-team/whipper/compare/v0.2.3...v0.2.4)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Don't allow ripping without an explicit offset, and make pycdio a required dependency [\#23](https://github.com/whipper-team/whipper/issues/23)
|
||||
- Minimal makedepends for building [\#17](https://github.com/whipper-team/whipper/issues/17)
|
||||
- Delete stale branches [\#7](https://github.com/whipper-team/whipper/issues/7)
|
||||
- get rid of the gstreamer-0.10 dependency [\#2](https://github.com/whipper-team/whipper/issues/2)
|
||||
- Merge 'fork' into 'master' [\#1](https://github.com/whipper-team/whipper/issues/1)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- whipper fails to build on bash-compgen [\#25](https://github.com/whipper-team/whipper/issues/25)
|
||||
@@ -286,6 +342,18 @@
|
||||
- rip offset find seems to fail [\#4](https://github.com/whipper-team/whipper/issues/4)
|
||||
- rip cd info seems to fail [\#3](https://github.com/whipper-team/whipper/issues/3)
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- Error selecting Drive for ripping [\#34](https://github.com/whipper-team/whipper/issues/34)
|
||||
- Offset not saved: could not get device info \(requires pycdio\) [\#33](https://github.com/whipper-team/whipper/issues/33)
|
||||
- On Arch Linux, CDDB does not know how to install morituri. [\#28](https://github.com/whipper-team/whipper/issues/28)
|
||||
- Error reading TOC [\#26](https://github.com/whipper-team/whipper/issues/26)
|
||||
- Don't allow ripping without an explicit offset, and make pycdio a required dependency [\#23](https://github.com/whipper-team/whipper/issues/23)
|
||||
- Minimal makedepends for building [\#17](https://github.com/whipper-team/whipper/issues/17)
|
||||
- Delete stale branches [\#7](https://github.com/whipper-team/whipper/issues/7)
|
||||
- get rid of the gstreamer-0.10 dependency [\#2](https://github.com/whipper-team/whipper/issues/2)
|
||||
- Merge 'fork' into 'master' [\#1](https://github.com/whipper-team/whipper/issues/1)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Issue24 [\#42](https://github.com/whipper-team/whipper/pull/42) ([JoeLametta](https://github.com/JoeLametta))
|
||||
|
||||
42
COVERAGE
42
COVERAGE
@@ -1,4 +1,4 @@
|
||||
Coverage.py 4.5.1 text report against whipper v0.7.2
|
||||
Coverage.py 4.5.2 text report against whipper v0.7.3
|
||||
|
||||
$ coverage run --branch --omit='whipper/test/*' --source=whipper -m unittest discover
|
||||
$ coverage report -m
|
||||
@@ -8,42 +8,42 @@ Name Stmts Miss Branch BrPart Cover Missing
|
||||
whipper/__init__.py 10 2 4 2 71% 9, 11, 8->9, 10->11
|
||||
whipper/__main__.py 7 7 2 0 0% 4-14
|
||||
whipper/command/__init__.py 0 0 0 0 100%
|
||||
whipper/command/accurip.py 44 44 18 0 0% 21-96
|
||||
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 219 181 56 0 14% 71-79, 84-184, 187, 199, 222-276, 283-308, 311-488
|
||||
whipper/command/drive.py 62 62 12 0 0% 21-122
|
||||
whipper/command/cd.py 224 186 58 0 13% 71-79, 84-193, 196, 208, 231-284, 291-318, 321-491
|
||||
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 65 65 20 0 0% 4-109
|
||||
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 111 111 32 0 0% 21-227
|
||||
whipper/command/offset.py 110 110 32 0 0% 21-221
|
||||
whipper/common/__init__.py 0 0 0 0 100%
|
||||
whipper/common/accurip.py 133 5 54 5 95% 123, 134, 143-145, 116->123, 127->134, 160->163, 252->258, 261->267
|
||||
whipper/common/cache.py 105 50 34 6 44% 66-90, 96, 99, 107-112, 115-116, 132, 144-149, 172-179, 203-208, 213-230, 95->96, 98->99, 131->132, 142->153, 143->144, 171->172
|
||||
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/checksum.py 26 14 2 0 43% 41-42, 45-46, 49-64
|
||||
whipper/common/common.py 142 22 32 6 83% 50-51, 118-119, 142-143, 161-168, 180, 274-280, 316-320, 117->118, 130->133, 179->180, 189->196, 270->274, 314->322
|
||||
whipper/common/config.py 92 8 18 4 89% 105-106, 124-125, 131, 142, 144, 146, 130->131, 141->142, 143->144, 145->146
|
||||
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/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, 158-159, 164-165, 209, 212, 215, 238-241, 250, 270-324, 157->158, 163->164, 208->209, 211->212, 214->215, 237->238, 247->250
|
||||
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/path.py 24 0 8 3 91% 42->45, 52->57, 62->67
|
||||
whipper/common/program.py 344 263 108 5 20% 89-91, 97-105, 113-145, 154-159, 162, 166-170, 215, 226-227, 229-233, 249-264, 272-398, 409-459, 467-475, 478-494, 505-545, 557-574, 577-595, 598-608, 611-619, 81->84, 212->215, 225->226, 228->229, 235->239
|
||||
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, 87-88, 91-94, 102, 116-117, 124, 130, 136, 142, 148, 85->87, 99->102
|
||||
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/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/task/__init__.py 0 0 0 0 100%
|
||||
whipper/extern/task/task.py 276 117 54 11 53% 26-27, 56, 60, 86, 152-154, 172-174, 182-198, 216-219, 239-240, 281-282, 285-291, 306-307, 315-317, 326-333, 339-355, 359, 362, 369-386, 397-398, 401-404, 408, 411, 426, 429-431, 447, 459, 504-509, 518-523, 534-542, 545-553, 556-557, 565, 570-572, 55->56, 59->60, 68->70, 151->152, 165->exit, 215->216, 229->231, 234->exit, 494->496, 531->534, 569->570
|
||||
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/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-214
|
||||
whipper/image/table.py 398 22 114 16 93% 237, 346-347, 499, 578, 664-665, 685-686, 695-698, 702-703, 750, 796-797, 799-800, 844-845, 850-852, 180->183, 498->499, 532->536, 555->558, 577->578, 585->592, 684->685, 693->699, 694->695, 723->728, 728->722, 749->750, 795->796, 798->799, 843->844, 849->850
|
||||
whipper/image/toc.py 203 15 60 10 90% 134, 262-263, 279-282, 340-342, 364-366, 386, 410, 130->134, 213->221, 261->262, 278->279, 288->293, 324->331, 339->340, 363->364, 373->377, 405->410
|
||||
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/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-43, 52-58, 22->26, 31->32, 36->37, 45->52
|
||||
whipper/program/cdparanoia.py 315 185 86 3 39% 48-50, 59-60, 124-126, 163-166, 199-200, 241-255, 258-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 51 29 10 2 39% 25-47, 54-60, 70-72, 76-78, 86, 93, 69->70, 75->76
|
||||
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/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
|
||||
@@ -52,4 +52,4 @@ 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
|
||||
-----------------------------------------------------------------------------
|
||||
TOTAL 3950 1900 1090 108 49%
|
||||
TOTAL 3961 1910 1104 108 49%
|
||||
|
||||
@@ -9,7 +9,7 @@ RUN apt-get update \
|
||||
&& pip install pycdio==2.0.0
|
||||
|
||||
# libcdio-paranoia / libcdio-utils are wrongfully packaged in Debian, thus built manually
|
||||
# see https://github.com/JoeLametta/whipper/pull/237#issuecomment-367985625
|
||||
# see https://github.com/whipper-team/whipper/pull/237#issuecomment-367985625
|
||||
RUN curl -o - 'https://ftp.gnu.org/gnu/libcdio/libcdio-2.0.0.tar.gz' | tar zxf - \
|
||||
&& cd libcdio-2.0.0 \
|
||||
&& autoreconf -fi \
|
||||
|
||||
30
README.md
30
README.md
@@ -117,7 +117,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` **10.2+0.94+2-2**
|
||||
- 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).
|
||||
- [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`
|
||||
@@ -126,7 +127,7 @@ 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**. If using `libcdio` **0.83**, which is _too old_ to satisfy all the requirements of whipper, just stick to `pycdio` **0.17**. Altough it needs additional testing, `libcdio` **2.0.0** seems to work fine if used with `pycdio` **2.0.0**. All other combinations aren't guaranteed to work.
|
||||
- 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.
|
||||
- [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
|
||||
@@ -250,7 +251,7 @@ read_offset = 6 ; drive read offset in positive/negative frames (no leading +)
|
||||
unknown = True
|
||||
output_directory = ~/My Music
|
||||
track_template = new/%%A/%%y - %%d/%%t - %%n ; note: the format char '%' must be represented '%%'
|
||||
disc_template = %(track_template)s
|
||||
disc_template = new/%%A/%%y - %%d/%%A - %%d
|
||||
# ...
|
||||
```
|
||||
|
||||
@@ -265,14 +266,31 @@ python2 -m whipper -h
|
||||
|
||||
## Logger plugins
|
||||
|
||||
Whipper supports using external logger plugins to write rip `.log` files.
|
||||
Whipper allows using external logger plugins to customize the template of `.log` files.
|
||||
|
||||
List available plugins with `whipper cd rip -h`. Specify a logger to rip with by passing `-L loggername`:
|
||||
The available plugins can be listed with `whipper cd rip -h`. Specify a logger to rip with by passing `-L loggername`:
|
||||
|
||||
```bash
|
||||
whipper cd rip -L what
|
||||
whipper cd rip -L eac
|
||||
```
|
||||
|
||||
Whipper searches for logger plugins in the following paths:
|
||||
|
||||
- `$XDG_DATA_HOME/whipper/plugins`
|
||||
- Paths returned by the following Python instruction:
|
||||
|
||||
`[x + '/whipper/plugins' for x in site.getsitepackages()]`
|
||||
|
||||
- If whipper is run in a `virtualenv`, it will use these alternative instructions (from `distutils.sysconfig`):
|
||||
- `get_python_lib(plat_specific=False, standard_lib=False, prefix='/usr/local') + '/whipper/plugins'`
|
||||
- `get_python_lib(plat_specific=False, standard_lib=False) + '/whipper/plugins'`
|
||||
|
||||
On a default Debian/Ubuntu installation, the following paths are searched by whipper:
|
||||
|
||||
- `$HOME/.local/share/whipper/plugins`
|
||||
- `/usr/local/lib/python2.7/dist-packages/whipper/plugins`
|
||||
- `/usr/lib/python2.7/dist-packages/whipper/plugins`
|
||||
|
||||
### Official logger plugins
|
||||
|
||||
I suggest using whipper's default logger unless you've got particular requirements.
|
||||
|
||||
2
TODO
2
TODO
@@ -1,6 +1,6 @@
|
||||
TODO:
|
||||
|
||||
Please see https://github.com/JoeLametta/whipper/milestones for further
|
||||
Please see https://github.com/whipper-team/whipper/milestones for further
|
||||
TODO items; this file exists only to have contents individually removed
|
||||
eventually, not to be continually updated.
|
||||
|
||||
|
||||
6
setup.py
6
setup.py
@@ -5,9 +5,9 @@ setup(
|
||||
name="whipper",
|
||||
version=whipper_version,
|
||||
description="a secure cd ripper preferring accuracy over speed",
|
||||
author=['Thomas Vander Stichele', 'Joe Lametta', 'Samantha Baldwin'],
|
||||
maintainer=['Joe Lametta', 'Samantha Baldwin'],
|
||||
url='https://github.com/JoeLametta/whipper',
|
||||
author=['Thomas Vander Stichele', 'The Whipper Team'],
|
||||
maintainer=['The Whipper Team'],
|
||||
url='https://github.com/whipper-team/whipper',
|
||||
license='GPL3',
|
||||
packages=find_packages(),
|
||||
entry_points={
|
||||
|
||||
@@ -2,9 +2,9 @@ import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
__version__ = '0.7.2'
|
||||
__version__ = '0.7.3'
|
||||
|
||||
level = logging.WARNING
|
||||
level = logging.INFO
|
||||
if 'WHIPPER_DEBUG' in os.environ:
|
||||
level = os.environ['WHIPPER_DEBUG'].upper()
|
||||
if 'WHIPPER_LOGFILE' in os.environ:
|
||||
|
||||
@@ -18,8 +18,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with whipper. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
|
||||
from whipper.command.basecommand import BaseCommand
|
||||
from whipper.common.accurip import get_db_entry, ACCURATERIP_URL
|
||||
|
||||
@@ -42,18 +40,16 @@ retrieves and display accuraterip data from the given URL
|
||||
|
||||
count = responses[0].num_tracks
|
||||
|
||||
sys.stdout.write("Found %d responses for %d tracks\n\n" % (
|
||||
len(responses), count))
|
||||
logger.info("found %d responses for %d tracks", len(responses), count)
|
||||
|
||||
for (i, r) in enumerate(responses):
|
||||
if r.num_tracks != count:
|
||||
sys.stdout.write(
|
||||
"Warning: response %d has %d tracks instead of %d\n" % (
|
||||
i, r.num_tracks, count))
|
||||
logger.warning("response %d has %d tracks instead of %d",
|
||||
i, r.num_tracks, count)
|
||||
|
||||
# checksum and confidence by track
|
||||
for track in range(count):
|
||||
sys.stdout.write("Track %d:\n" % (track + 1))
|
||||
print("Track %d:" % (track + 1))
|
||||
checksums = {}
|
||||
|
||||
for (i, r) in enumerate(responses):
|
||||
@@ -82,9 +78,9 @@ retrieves and display accuraterip data from the given URL
|
||||
sortedChecksums.reverse()
|
||||
|
||||
for highest, checksum in sortedChecksums:
|
||||
sys.stdout.write(" %d result(s) for checksum %s: %s\n" % (
|
||||
len(checksums[checksum]), checksum,
|
||||
str(checksums[checksum])))
|
||||
print(" %d result(s) for checksum %s: %s" % (
|
||||
len(checksums[checksum]),
|
||||
checksum, checksums[checksum]))
|
||||
|
||||
|
||||
class AccuRip(BaseCommand):
|
||||
|
||||
@@ -22,12 +22,12 @@ import argparse
|
||||
import cdio
|
||||
import os
|
||||
import glob
|
||||
import sys
|
||||
import logging
|
||||
from whipper.command.basecommand import BaseCommand
|
||||
from whipper.common import (
|
||||
accurip, config, drive, program, task
|
||||
)
|
||||
from whipper.common.common import validate_template
|
||||
from whipper.program import cdrdao, cdparanoia, utils
|
||||
from whipper.result import result
|
||||
|
||||
@@ -83,29 +83,28 @@ class _CD(BaseCommand):
|
||||
def do(self):
|
||||
self.config = config.Config()
|
||||
self.program = program.Program(self.config,
|
||||
record=self.options.record,
|
||||
stdout=sys.stdout)
|
||||
record=self.options.record)
|
||||
self.runner = task.SyncRunner()
|
||||
|
||||
# if the device is mounted (data session), unmount it
|
||||
self.device = self.options.device
|
||||
sys.stdout.write('Checking device %s\n' % self.device)
|
||||
logger.info('checking device %s', self.device)
|
||||
|
||||
utils.load_device(self.device)
|
||||
utils.unmount_device(self.device)
|
||||
|
||||
# first, read the normal TOC, which is fast
|
||||
print("Reading TOC...")
|
||||
logger.info("reading TOC...")
|
||||
self.ittoc = self.program.getFastToc(self.runner, self.device)
|
||||
|
||||
# already show us some info based on this
|
||||
self.program.getRipResult(self.ittoc.getCDDBDiscId())
|
||||
sys.stdout.write("CDDB disc id: %s\n" % self.ittoc.getCDDBDiscId())
|
||||
print("CDDB disc id: %s" % self.ittoc.getCDDBDiscId())
|
||||
self.mbdiscid = self.ittoc.getMusicBrainzDiscId()
|
||||
sys.stdout.write("MusicBrainz disc id %s\n" % self.mbdiscid)
|
||||
print("MusicBrainz disc id %s" % self.mbdiscid)
|
||||
|
||||
sys.stdout.write("MusicBrainz lookup URL %s\n" %
|
||||
self.ittoc.getMusicBrainzSubmitURL())
|
||||
print("MusicBrainz lookup URL %s" %
|
||||
self.ittoc.getMusicBrainzSubmitURL())
|
||||
|
||||
self.program.metadata = (
|
||||
self.program.getMusicBrainz(self.ittoc, self.mbdiscid,
|
||||
@@ -119,12 +118,12 @@ class _CD(BaseCommand):
|
||||
cddbid = self.ittoc.getCDDBValues()
|
||||
cddbmd = self.program.getCDDB(cddbid)
|
||||
if cddbmd:
|
||||
sys.stdout.write('FreeDB identifies disc as %s\n' % cddbmd)
|
||||
logger.info('FreeDB identifies disc as %s', cddbmd)
|
||||
|
||||
# also used by rip cd info
|
||||
if not getattr(self.options, 'unknown', False):
|
||||
logger.critical("unable to retrieve disc metadata, "
|
||||
"--unknown not passed")
|
||||
"--unknown argument not passed")
|
||||
return -1
|
||||
|
||||
self.program.result.isCdr = cdrdao.DetectCdr(self.device)
|
||||
@@ -134,11 +133,21 @@ class _CD(BaseCommand):
|
||||
"--cdr not passed")
|
||||
return -1
|
||||
|
||||
# Change working directory before cdrdao's task
|
||||
if self.options.working_directory is not None:
|
||||
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)
|
||||
# now, read the complete index table, which is slower
|
||||
self.itable = self.program.getTable(self.runner,
|
||||
self.ittoc.getCDDBDiscId(),
|
||||
self.ittoc.getMusicBrainzDiscId(),
|
||||
self.device, self.options.offset)
|
||||
self.device, self.options.offset,
|
||||
out_fpath)
|
||||
|
||||
assert self.itable.getCDDBDiscId() == self.ittoc.getCDDBDiscId(), \
|
||||
"full table's id %s differs from toc id %s" % (
|
||||
@@ -167,7 +176,7 @@ class _CD(BaseCommand):
|
||||
self.program.result.cdparanoiaDefeatsCache = \
|
||||
self.config.getDefeatsCache(*info)
|
||||
except KeyError as e:
|
||||
logger.debug('Got key error: %r' % (e, ))
|
||||
logger.debug('got key error: %r', (e, ))
|
||||
self.program.result.artist = self.program.metadata \
|
||||
and self.program.metadata.artist \
|
||||
or 'Unknown Artist'
|
||||
@@ -225,8 +234,7 @@ Log files will log the path to tracks relative to this directory.
|
||||
if info:
|
||||
try:
|
||||
default_offset = config.Config().getReadOffset(*info)
|
||||
sys.stdout.write("Using configured read offset %d\n" %
|
||||
default_offset)
|
||||
logger.info("using configured read offset %d", default_offset)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@@ -235,8 +243,8 @@ Log files will log the path to tracks relative to this directory.
|
||||
self.parser.add_argument('-L', '--logger',
|
||||
action="store", dest="logger",
|
||||
default='whipper',
|
||||
help="logger to use (choose from '"
|
||||
"', '".join(loggers) + "')")
|
||||
help=("logger to use (choose from: '%s" %
|
||||
"', '".join(loggers) + "')"))
|
||||
# FIXME: get from config
|
||||
self.parser.add_argument('-o', '--offset',
|
||||
action="store", dest="offset",
|
||||
@@ -285,7 +293,9 @@ Log files will log the path to tracks relative to this directory.
|
||||
|
||||
self.options.track_template = self.options.track_template.decode(
|
||||
'utf-8')
|
||||
validate_template(self.options.track_template, 'track')
|
||||
self.options.disc_template = self.options.disc_template.decode('utf-8')
|
||||
validate_template(self.options.disc_template, 'disc')
|
||||
|
||||
if self.options.offset is None:
|
||||
raise ValueError("Drive offset is unconfigured.\n"
|
||||
@@ -324,26 +334,24 @@ Log files will log the path to tracks relative to this directory.
|
||||
if logs:
|
||||
msg = ("output directory %s is a finished rip" %
|
||||
dirname.encode('utf-8'))
|
||||
logger.critical(msg)
|
||||
logger.debug(msg)
|
||||
raise RuntimeError(msg)
|
||||
else:
|
||||
sys.stdout.write("output directory %s already exists\n" %
|
||||
dirname.encode('utf-8'))
|
||||
else:
|
||||
print("creating output directory %s" % dirname.encode('utf-8'))
|
||||
logger.info("creating output directory %s",
|
||||
dirname.encode('utf-8'))
|
||||
os.makedirs(dirname)
|
||||
|
||||
# FIXME: turn this into a method
|
||||
|
||||
def _ripIfNotRipped(number):
|
||||
logger.debug('ripIfNotRipped for track %d' % number)
|
||||
logger.debug('ripIfNotRipped for track %d', number)
|
||||
# we can have a previous result
|
||||
trackResult = self.program.result.getTrackResult(number)
|
||||
if not trackResult:
|
||||
trackResult = result.TrackResult()
|
||||
self.program.result.tracks.append(trackResult)
|
||||
else:
|
||||
logger.debug('ripIfNotRipped have trackresult, path %r' %
|
||||
logger.debug('ripIfNotRipped have trackresult, path %r',
|
||||
trackResult.filename)
|
||||
|
||||
path = self.program.getPath(self.program.outdir,
|
||||
@@ -351,7 +359,7 @@ Log files will log the path to tracks relative to this directory.
|
||||
self.mbdiscid,
|
||||
self.program.metadata,
|
||||
track_number=number) + '.flac'
|
||||
logger.debug('ripIfNotRipped: path %r' % path)
|
||||
logger.debug('ripIfNotRipped: path %r', path)
|
||||
trackResult.number = number
|
||||
|
||||
assert isinstance(path, unicode), "%r is not unicode" % path
|
||||
@@ -368,18 +376,18 @@ Log files will log the path to tracks relative to this directory.
|
||||
if path != trackResult.filename:
|
||||
# the path is different (different name/template ?)
|
||||
# but we can copy it
|
||||
logger.debug('previous result %r, expected %r' % (
|
||||
trackResult.filename, path))
|
||||
logger.debug('previous result %r, expected %r',
|
||||
trackResult.filename, path)
|
||||
|
||||
sys.stdout.write('Verifying track %d of %d: %s\n' % (
|
||||
number, len(self.itable.tracks),
|
||||
os.path.basename(path).encode('utf-8')))
|
||||
logger.info('verifying track %d of %d: %s',
|
||||
number, len(self.itable.tracks),
|
||||
os.path.basename(path).encode('utf-8'))
|
||||
if not self.program.verifyTrack(self.runner, trackResult):
|
||||
sys.stdout.write('Verification failed, reripping...\n')
|
||||
logger.warning('verification failed, reripping...')
|
||||
os.unlink(path)
|
||||
|
||||
if not os.path.exists(path):
|
||||
logger.debug('path %r does not exist, ripping...' % path)
|
||||
logger.debug('path %r does not exist, ripping...', path)
|
||||
tries = 0
|
||||
# we reset durations for test and copy here
|
||||
trackResult.testduration = 0.0
|
||||
@@ -389,9 +397,9 @@ Log files will log the path to tracks relative to this directory.
|
||||
tries += 1
|
||||
if tries > 1:
|
||||
extra = " (try %d)" % tries
|
||||
sys.stdout.write('Ripping track %d of %d%s: %s\n' % (
|
||||
number, len(self.itable.tracks), extra,
|
||||
os.path.basename(path).encode('utf-8')))
|
||||
logger.info('ripping track %d of %d%s: %s',
|
||||
number, len(self.itable.tracks), extra,
|
||||
os.path.basename(path).encode('utf-8'))
|
||||
try:
|
||||
logger.debug('ripIfNotRipped: track %d, try %d',
|
||||
number, tries)
|
||||
@@ -399,7 +407,7 @@ Log files will log the path to tracks relative to this directory.
|
||||
offset=int(self.options.offset),
|
||||
device=self.device,
|
||||
taglist=self.program.getTagList(
|
||||
number),
|
||||
number, self.mbdiscid),
|
||||
overread=self.options.overread,
|
||||
what='track %d of %d%s' % (
|
||||
number,
|
||||
@@ -407,43 +415,37 @@ Log files will log the path to tracks relative to this directory.
|
||||
extra))
|
||||
break
|
||||
except Exception as e:
|
||||
logger.debug('Got exception %r on try %d',
|
||||
e, tries)
|
||||
logger.debug('got exception %r on try %d', e, tries)
|
||||
|
||||
if tries == MAX_TRIES:
|
||||
logger.critical('Giving up on track %d after %d times' % (
|
||||
number, tries))
|
||||
logger.critical('giving up on track %d after %d times',
|
||||
number, tries)
|
||||
raise RuntimeError(
|
||||
"track can't be ripped. "
|
||||
"Rip attempts number is equal to 'MAX_TRIES'")
|
||||
if trackResult.testcrc == trackResult.copycrc:
|
||||
sys.stdout.write('CRCs match for track %d\n' % number)
|
||||
logger.info('CRCs match for track %d', number)
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"CRCs did not match for track %d\n" % number
|
||||
"CRCs did not match for track %d" % number
|
||||
)
|
||||
|
||||
sys.stdout.write(
|
||||
'Peak level: {}\n'.format(trackResult.peak))
|
||||
|
||||
sys.stdout.write(
|
||||
'Rip quality: {:.2%}\n'.format(trackResult.quality))
|
||||
print('Peak level: %.6f' % (trackResult.peak / 32768.0))
|
||||
print('Rip quality: {:.2%}'.format(trackResult.quality))
|
||||
|
||||
# overlay this rip onto the Table
|
||||
if number == 0:
|
||||
# HTOA goes on index 0 of track 1
|
||||
# ignore silence in PREGAP
|
||||
if trackResult.peak == SILENT:
|
||||
logger.debug(
|
||||
'HTOA peak %r is equal to the SILENT '
|
||||
'threshold, disregarding', trackResult.peak)
|
||||
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)
|
||||
logger.debug('Unlinking %r', trackResult.filename)
|
||||
logger.debug('unlinking %r', trackResult.filename)
|
||||
os.unlink(trackResult.filename)
|
||||
trackResult.filename = None
|
||||
sys.stdout.write(
|
||||
'HTOA discarded, contains digital silence\n')
|
||||
logger.info('HTOA discarded, contains digital silence')
|
||||
else:
|
||||
self.itable.setFile(1, 0, trackResult.filename,
|
||||
self.ittoc.getTrackStart(1), number)
|
||||
@@ -457,14 +459,15 @@ Log files will log the path to tracks relative to this directory.
|
||||
htoa = self.program.getHTOA()
|
||||
if htoa:
|
||||
start, stop = htoa
|
||||
print('found Hidden Track One Audio from frame %d to %d' % (
|
||||
start, stop))
|
||||
logger.info('found Hidden Track One Audio from frame %d to %d',
|
||||
start, stop)
|
||||
_ripIfNotRipped(0)
|
||||
|
||||
for i, track in enumerate(self.itable.tracks):
|
||||
# FIXME: rip data tracks differently
|
||||
if not track.audio:
|
||||
print('skipping data track %d, not implemented' % (i + 1))
|
||||
logger.warning('skipping data track %d, not implemented',
|
||||
i + 1)
|
||||
# FIXME: make it work for now
|
||||
track.indexes[1].relative = 0
|
||||
continue
|
||||
@@ -479,7 +482,7 @@ Log files will log the path to tracks relative to this directory.
|
||||
try:
|
||||
self.program.verifyImage(self.runner, self.ittoc)
|
||||
except accurip.EntryNotFound:
|
||||
print('AccurateRip entry not found')
|
||||
logger.warning('AccurateRip entry not found')
|
||||
|
||||
accurip.print_report(self.program.result)
|
||||
|
||||
|
||||
@@ -18,8 +18,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with whipper. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
|
||||
from whipper.command.basecommand import BaseCommand
|
||||
from whipper.common import config, drive
|
||||
from whipper.extern.task import task
|
||||
@@ -40,24 +38,21 @@ class Analyze(BaseCommand):
|
||||
runner.run(t)
|
||||
|
||||
if t.defeatsCache is None:
|
||||
sys.stdout.write(
|
||||
'Cannot analyze the drive. Is there a CD in it?\n')
|
||||
logger.critical('cannot analyze the drive: is there a CD in it?')
|
||||
return
|
||||
if not t.defeatsCache:
|
||||
sys.stdout.write(
|
||||
'cdparanoia cannot defeat the audio cache on this drive.\n')
|
||||
logger.info('cdparanoia cannot defeat the audio cache '
|
||||
'on this drive')
|
||||
else:
|
||||
sys.stdout.write(
|
||||
'cdparanoia can defeat the audio cache on this drive.\n')
|
||||
logger.info('cdparanoia can defeat the audio cache on this drive')
|
||||
|
||||
info = drive.getDeviceInfo(self.options.device)
|
||||
if not info:
|
||||
sys.stdout.write('Drive caching behaviour not saved:'
|
||||
'could not get device info (requires pycdio).\n')
|
||||
logger.error('drive caching behaviour not saved: '
|
||||
'could not get device info')
|
||||
return
|
||||
|
||||
sys.stdout.write(
|
||||
'Adding drive cache behaviour to configuration file.\n')
|
||||
logger.info('adding drive cache behaviour to configuration file')
|
||||
|
||||
config.Config().setDefeatsCache(
|
||||
info[0], info[1], info[2], t.defeatsCache)
|
||||
@@ -72,48 +67,38 @@ class List(BaseCommand):
|
||||
self.config = config.Config()
|
||||
|
||||
if not paths:
|
||||
sys.stdout.write('No drives found.\n')
|
||||
sys.stdout.write('Create /dev/cdrom if you have a CD drive, \n')
|
||||
sys.stdout.write('or install pycdio for better detection.\n')
|
||||
|
||||
logger.critical('no drives found. Create /dev/cdrom '
|
||||
'if you have a CD drive, or install '
|
||||
'pycdio for better detection')
|
||||
return
|
||||
|
||||
try:
|
||||
import cdio as _ # noqa: F401 (TODO: fix it in a separate PR?)
|
||||
except ImportError:
|
||||
sys.stdout.write(
|
||||
'Install pycdio for vendor/model/release detection.\n')
|
||||
logger.error('install pycdio for vendor/model/release detection')
|
||||
return
|
||||
|
||||
for path in paths:
|
||||
vendor, model, release = drive.getDeviceInfo(path)
|
||||
sys.stdout.write(
|
||||
"drive: %s, vendor: %s, model: %s, release: %s\n" % (
|
||||
path, vendor, model, release))
|
||||
print("drive: %s, vendor: %s, model: %s, release: %s" % (
|
||||
path, vendor, model, release))
|
||||
|
||||
try:
|
||||
offset = self.config.getReadOffset(
|
||||
vendor, model, release)
|
||||
sys.stdout.write(
|
||||
" Configured read offset: %d\n" % offset)
|
||||
print(" Configured read offset: %d" % offset)
|
||||
except KeyError:
|
||||
# Note spaces at the beginning for pretty terminal output
|
||||
sys.stdout.write(" "
|
||||
"No read offset found. "
|
||||
"Run 'whipper offset find'\n")
|
||||
logger.warning("no read offset found. "
|
||||
"Run 'whipper offset find'")
|
||||
|
||||
try:
|
||||
defeats = self.config.getDefeatsCache(
|
||||
vendor, model, release)
|
||||
sys.stdout.write(
|
||||
" Can defeat audio cache: %s\n" % defeats)
|
||||
print(" Can defeat audio cache: %s" % defeats)
|
||||
except KeyError:
|
||||
sys.stdout.write(
|
||||
" Unknown whether audio cache can be defeated. "
|
||||
"Run 'whipper drive analyze'\n")
|
||||
|
||||
if not paths:
|
||||
sys.stdout.write('No drives found.\n')
|
||||
logger.warning("unknown whether audio cache can be "
|
||||
"defeated. Run 'whipper drive analyze'")
|
||||
|
||||
|
||||
class Drive(BaseCommand):
|
||||
|
||||
@@ -5,9 +5,9 @@ import os
|
||||
import sys
|
||||
import pkg_resources
|
||||
import musicbrainzngs
|
||||
|
||||
import site
|
||||
import whipper
|
||||
|
||||
from distutils.sysconfig import get_python_lib
|
||||
from whipper.command import cd, offset, drive, image, accurip, mblookup
|
||||
from whipper.command.basecommand import BaseCommand
|
||||
from whipper.common import common, directory, config
|
||||
@@ -19,23 +19,30 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
server = config.Config().get_musicbrainz_server()
|
||||
except KeyError as e:
|
||||
sys.stderr.write('whipper: %s\n' % str(e))
|
||||
sys.exit()
|
||||
|
||||
server = config.Config().get_musicbrainz_server()
|
||||
musicbrainzngs.set_hostname(server)
|
||||
|
||||
# Find whipper's plugins paths (local paths have higher priority)
|
||||
plugins_p = [directory.data_path('plugins')] # local path (in $HOME)
|
||||
if hasattr(sys, 'real_prefix'): # no getsitepackages() in virtualenv
|
||||
plugins_p.append(
|
||||
get_python_lib(plat_specific=False, standard_lib=False,
|
||||
prefix='/usr/local') + '/whipper/plugins')
|
||||
plugins_p.append(get_python_lib(plat_specific=False,
|
||||
standard_lib=False) + '/whipper/plugins')
|
||||
else:
|
||||
plugins_p += [x + '/whipper/plugins' for x in site.getsitepackages()]
|
||||
|
||||
# register plugins with pkg_resources
|
||||
distributions, _ = pkg_resources.working_set.find_plugins(
|
||||
pkg_resources.Environment([directory.data_path('plugins')])
|
||||
pkg_resources.Environment(plugins_p)
|
||||
)
|
||||
list(map(pkg_resources.working_set.add, distributions))
|
||||
try:
|
||||
cmd = Whipper(sys.argv[1:], os.path.basename(sys.argv[0]), None)
|
||||
ret = cmd.do()
|
||||
except SystemError as e:
|
||||
sys.stderr.write('whipper: error: %s\n' % e)
|
||||
logger.critical("SystemError: %s", e)
|
||||
if (isinstance(e, common.EjectError) and
|
||||
cmd.options.eject in ('failure', 'always')):
|
||||
eject_device(e.device)
|
||||
@@ -51,18 +58,17 @@ def main():
|
||||
if isinstance(e.exception, ImportError):
|
||||
raise ImportError(e.exception)
|
||||
elif isinstance(e.exception, common.MissingDependencyException):
|
||||
sys.stderr.write('whipper: error: missing dependency "%s"\n' %
|
||||
e.exception.dependency)
|
||||
logger.critical('missing dependency "%s"', e.exception.dependency)
|
||||
return 255
|
||||
|
||||
if isinstance(e.exception, common.EmptyError):
|
||||
logger.debug("EmptyError: %r", str(e.exception))
|
||||
sys.stderr.write('whipper: error: Could not create encoded file.\n') # noqa: E501
|
||||
logger.debug("EmptyError: %s", e.exception)
|
||||
logger.critical('could not create encoded file')
|
||||
return 255
|
||||
|
||||
# in python3 we can instead do `raise e.exception` as that would show
|
||||
# the exception's original context
|
||||
sys.stderr.write(e.exceptionMessage)
|
||||
logger.critical(e.exceptionMessage)
|
||||
return 255
|
||||
return ret if ret else 0
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import logging
|
||||
from whipper.command.basecommand import BaseCommand
|
||||
@@ -71,7 +70,7 @@ CD in the AccurateRip database."""
|
||||
else:
|
||||
self._offsets.append(int(b))
|
||||
|
||||
logger.debug('Trying with offsets %r', self._offsets)
|
||||
logger.debug('trying with offsets %r', self._offsets)
|
||||
|
||||
def do(self):
|
||||
runner = ctask.SyncRunner()
|
||||
@@ -79,7 +78,7 @@ CD in the AccurateRip database."""
|
||||
device = self.options.device
|
||||
|
||||
# if necessary, load and unmount
|
||||
sys.stdout.write('Checking device %s\n' % device)
|
||||
logger.info('checking device %s', device)
|
||||
|
||||
utils.load_device(device)
|
||||
utils.unmount_device(device)
|
||||
@@ -93,10 +92,12 @@ CD in the AccurateRip database."""
|
||||
try:
|
||||
responses = accurip.get_db_entry(table.accuraterip_path())
|
||||
except accurip.EntryNotFound:
|
||||
print('Accuraterip entry not found')
|
||||
logger.warning("AccurateRip entry not found: drive offset "
|
||||
"can't be determined, try again with another disc")
|
||||
return
|
||||
|
||||
if responses:
|
||||
logger.debug('%d AccurateRip responses found.' % len(responses))
|
||||
logger.debug('%d AccurateRip responses found.', len(responses))
|
||||
if responses[0].cddbDiscId != table.getCDDBDiscId():
|
||||
logger.warning("AccurateRip response discid different: %s",
|
||||
responses[0].cddbDiscId)
|
||||
@@ -114,36 +115,32 @@ CD in the AccurateRip database."""
|
||||
return None, None
|
||||
|
||||
for offset in self._offsets:
|
||||
sys.stdout.write('Trying read offset %d ...\n' % offset)
|
||||
logger.info('trying read offset %d...', offset)
|
||||
try:
|
||||
archecksums = self._arcs(runner, table, 1, offset)
|
||||
except task.TaskException as e:
|
||||
|
||||
# let MissingDependency fall through
|
||||
if isinstance(e.exception,
|
||||
common.MissingDependencyException):
|
||||
if isinstance(e.exception, common.MissingDependencyException):
|
||||
raise e
|
||||
|
||||
if isinstance(e.exception, cdparanoia.FileSizeError):
|
||||
sys.stdout.write(
|
||||
'WARNING: cannot rip with offset %d...\n' % offset)
|
||||
logger.warning('cannot rip with offset %d...', offset)
|
||||
continue
|
||||
|
||||
logger.warning("Unknown task exception for offset %d: %r" % (
|
||||
offset, e))
|
||||
sys.stdout.write(
|
||||
'WARNING: cannot rip with offset %d...\n' % offset)
|
||||
logger.warning("unknown task exception for offset %d: %s",
|
||||
offset, e)
|
||||
logger.warning('cannot rip with offset %d...', offset)
|
||||
continue
|
||||
|
||||
logger.debug('AR checksums calculated: %s %s' % archecksums)
|
||||
logger.debug('AR checksums calculated: %s %s', archecksums)
|
||||
|
||||
c, i = match(archecksums, 1, responses)
|
||||
if c:
|
||||
count = 1
|
||||
logger.debug('MATCHED against response %d' % i)
|
||||
sys.stdout.write(
|
||||
'Offset of device is likely %d, confirming ...\n' %
|
||||
offset)
|
||||
logger.debug('matched against response %d', i)
|
||||
logger.info('offset of device is likely %d, confirming...',
|
||||
offset)
|
||||
|
||||
# now try and rip all other tracks as well, except for the
|
||||
# last one (to avoid readers that can't do overread
|
||||
@@ -152,31 +149,30 @@ CD in the AccurateRip database."""
|
||||
archecksums = self._arcs(runner, table, track, offset)
|
||||
except task.TaskException as e:
|
||||
if isinstance(e.exception, cdparanoia.FileSizeError):
|
||||
sys.stdout.write(
|
||||
'WARNING: cannot rip with offset %d...\n' %
|
||||
offset)
|
||||
logger.warning('cannot rip with offset %d...',
|
||||
offset)
|
||||
continue
|
||||
|
||||
c, i = match(archecksums, track, responses)
|
||||
if c:
|
||||
logger.debug('MATCHED track %d against response %d' % (
|
||||
track, i))
|
||||
logger.debug('matched track %d against response %d',
|
||||
track, i)
|
||||
count += 1
|
||||
|
||||
if count == len(table.tracks) - 1:
|
||||
self._foundOffset(device, offset)
|
||||
return 0
|
||||
else:
|
||||
sys.stdout.write(
|
||||
'Only %d of %d tracks matched, continuing ...\n' % (
|
||||
count, len(table.tracks)))
|
||||
logger.warning('only %d of %d tracks matched, '
|
||||
'continuing...', count,
|
||||
len(table.tracks))
|
||||
|
||||
sys.stdout.write('No matching offset found.\n')
|
||||
sys.stdout.write('Consider trying again with a different disc.\n')
|
||||
logger.error('no matching offset found. '
|
||||
'Consider trying again with a different disc')
|
||||
|
||||
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)
|
||||
logger.debug('ripping track %r with offset %d...', track, offset)
|
||||
|
||||
fd, path = tempfile.mkstemp(
|
||||
suffix=u'.track%02d.offset%d.whipper.wav' % (
|
||||
@@ -203,17 +199,15 @@ CD in the AccurateRip database."""
|
||||
return ("%08x" % v1, "%08x" % v2)
|
||||
|
||||
def _foundOffset(self, device, offset):
|
||||
sys.stdout.write('\nRead offset of device is: %d.\n' %
|
||||
offset)
|
||||
print('\nRead offset of device is: %d.' % offset)
|
||||
|
||||
info = drive.getDeviceInfo(device)
|
||||
if not info:
|
||||
sys.stdout.write(
|
||||
'Offset not saved: could not get '
|
||||
'device info (requires pycdio).\n')
|
||||
logger.error('offset not saved: '
|
||||
'could not get device info (requires pycdio)')
|
||||
return
|
||||
|
||||
sys.stdout.write('Adding read offset to configuration file.\n')
|
||||
logger.info('adding read offset to configuration file')
|
||||
|
||||
config.Config().setReadOffset(info[0], info[1], info[2],
|
||||
offset)
|
||||
|
||||
@@ -107,17 +107,15 @@ def calculate_checksums(track_paths):
|
||||
track_count = len(track_paths)
|
||||
v1_checksums = []
|
||||
v2_checksums = []
|
||||
logger.debug('checksumming %d tracks' % track_count)
|
||||
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:
|
||||
logger.error(
|
||||
'could not calculate AccurateRip v1 checksum for track %d %r' %
|
||||
(i+1, path)
|
||||
)
|
||||
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)
|
||||
@@ -125,10 +123,8 @@ def calculate_checksums(track_paths):
|
||||
path, i+1, track_count, wave=True, v2=True
|
||||
)
|
||||
if not v2_sum:
|
||||
logger.error(
|
||||
'could not calculate AccurateRip v2 checksum for track %d %r' %
|
||||
(i+1, path)
|
||||
)
|
||||
logger.error('could not calculate AccurateRip v2 checksum '
|
||||
'for track %d %r', i + 1, path)
|
||||
v2_checksums.append(None)
|
||||
else:
|
||||
v2_checksums.append("%08x" % v2_sum)
|
||||
@@ -141,12 +137,11 @@ def _download_entry(path):
|
||||
try:
|
||||
resp = requests.get(url)
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
logger.error('error retrieving AccurateRip entry: %r' % e)
|
||||
logger.error('error retrieving AccurateRip entry: %r', e)
|
||||
return None
|
||||
if not resp.ok:
|
||||
logger.error('error retrieving AccurateRip entry: %s %s %r' % (
|
||||
resp.status_code, resp.reason, resp
|
||||
))
|
||||
logger.error('error retrieving AccurateRip entry: %s %s %r',
|
||||
resp.status_code, resp.reason, resp)
|
||||
return None
|
||||
return resp.content
|
||||
|
||||
@@ -158,7 +153,7 @@ def _save_entry(raw_entry, path):
|
||||
makedirs(dirname(path))
|
||||
except OSError as e:
|
||||
if e.errno != EEXIST:
|
||||
logger.error('could not save entry to %s: %r' % (path, str(e)))
|
||||
logger.error('could not save entry to %s: %s', path, e)
|
||||
return
|
||||
open(path, 'wb').write(raw_entry)
|
||||
|
||||
@@ -211,10 +206,9 @@ def _match_responses(tracks, responses):
|
||||
track.AR[v]['DBConfidence'] = r.confidences[i]
|
||||
logger.debug(
|
||||
'track %d matched response %s in AccurateRip'
|
||||
' database: %s crc %s confidence %s' %
|
||||
(i, r.cddbDiscId, v, track.AR[v]['DBCRC'],
|
||||
track.AR[v]['DBConfidence'])
|
||||
)
|
||||
' database: %s crc %s confidence %s',
|
||||
i, r.cddbDiscId, v, track.AR[v]['DBCRC'],
|
||||
track.AR[v]['DBConfidence'])
|
||||
return any((
|
||||
all([t.AR['v1']['DBCRC'] for t in tracks]),
|
||||
all([t.AR['v2']['DBCRC'] for t in tracks])
|
||||
@@ -240,7 +234,7 @@ def verify_result(result, responses, checksums):
|
||||
|
||||
def print_report(result):
|
||||
"""
|
||||
Print AccurateRip verification results to stdout.
|
||||
Print AccurateRip verification results.
|
||||
"""
|
||||
for i, track in enumerate(result.tracks):
|
||||
status = 'rip NOT accurate'
|
||||
@@ -268,9 +262,7 @@ def print_report(result):
|
||||
print('track 0: unknown (not tracked)')
|
||||
continue
|
||||
if not (track.AR['v1']['CRC'] or track.AR['v2']['CRC']):
|
||||
logger.error(
|
||||
'no track AR CRC on non-HTOA track %d' % track.number
|
||||
)
|
||||
logger.error('no track AR CRC on non-HTOA track %d', track.number)
|
||||
print('track %2d: unknown (error)' % track.number)
|
||||
else:
|
||||
print('track %2d: %-16s %-23s v1 [%s], v2 [%s], DB [%s]' % (
|
||||
|
||||
@@ -87,7 +87,7 @@ class Persister:
|
||||
handle.close()
|
||||
# do an atomic move
|
||||
shutil.move(path, self._path)
|
||||
logger.debug('saved persisted object to %r' % self._path)
|
||||
logger.debug('saved persisted object to %r', self._path)
|
||||
|
||||
def _unpickle(self, default=None):
|
||||
self.object = default
|
||||
@@ -103,7 +103,7 @@ class Persister:
|
||||
|
||||
try:
|
||||
self.object = pickle.load(handle)
|
||||
logger.debug('loaded persisted object from %r' % self._path)
|
||||
logger.debug('loaded persisted object from %r', self._path)
|
||||
except Exception as e:
|
||||
# TODO: restrict kind of caught exceptions?
|
||||
# can fail for various reasons; in that case, pretend we didn't
|
||||
@@ -143,9 +143,8 @@ class PersistedCache:
|
||||
if hasattr(persister.object, 'instanceVersion'):
|
||||
o = persister.object
|
||||
if o.instanceVersion < o.__class__.classVersion:
|
||||
logger.debug(
|
||||
'key %r persisted object version %d is outdated',
|
||||
key, o.instanceVersion)
|
||||
logger.debug('key %r persisted object version %d '
|
||||
'is outdated', key, o.instanceVersion)
|
||||
persister.object = None
|
||||
# FIXME: don't delete old objects atm
|
||||
# persister.delete()
|
||||
@@ -216,12 +215,11 @@ class TableCache:
|
||||
ptable = self._pcache.get(cddbdiscid)
|
||||
if ptable.object:
|
||||
if ptable.object.getMusicBrainzDiscId() != mbdiscid:
|
||||
logger.debug('cached table is for different mb id %r' % (
|
||||
ptable.object.getMusicBrainzDiscId()))
|
||||
logger.debug('cached table is for different mb id %r',
|
||||
ptable.object.getMusicBrainzDiscId())
|
||||
ptable.object = None
|
||||
else:
|
||||
logger.debug('no valid cached table found for %r' %
|
||||
cddbdiscid)
|
||||
logger.debug('no valid cached table found for %r', cddbdiscid)
|
||||
|
||||
if not ptable.object:
|
||||
# get an empty persistable from the writable location
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
import os
|
||||
import os.path
|
||||
import math
|
||||
import re
|
||||
import subprocess
|
||||
import unicodedata
|
||||
|
||||
@@ -262,8 +263,8 @@ def getRelativePath(targetPath, collectionPath):
|
||||
|
||||
Used to determine the path to use in .cue/.m3u files
|
||||
"""
|
||||
logger.debug('getRelativePath: target %r, collection %r' % (
|
||||
targetPath, collectionPath))
|
||||
logger.debug('getRelativePath: target %r, collection %r',
|
||||
targetPath, collectionPath)
|
||||
|
||||
targetDir = os.path.dirname(targetPath)
|
||||
collectionDir = os.path.dirname(collectionPath)
|
||||
@@ -274,12 +275,24 @@ def getRelativePath(targetPath, collectionPath):
|
||||
rel = os.path.relpath(
|
||||
targetDir + os.path.sep,
|
||||
collectionDir + os.path.sep)
|
||||
logger.debug(
|
||||
'getRelativePath: target and collection in different dir, %r' % rel
|
||||
)
|
||||
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):
|
||||
"""
|
||||
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)
|
||||
elif kind == 'track':
|
||||
matches = re.findall(r'%[^A,R,S,X,a,d,n,r,s,t,x,y]', template)
|
||||
if '%' in template and matches:
|
||||
raise ValueError(kind + ' template string contains invalid '
|
||||
'variable(s): {}'.format(', '.join(matches)))
|
||||
|
||||
|
||||
class VersionGetter(object):
|
||||
"""
|
||||
I get the version of a program by looking for it in command output
|
||||
|
||||
@@ -47,8 +47,8 @@ class Config:
|
||||
with codecs.open(self._path, 'r', encoding='utf-8') as f:
|
||||
self._parser.readfp(f)
|
||||
|
||||
logger.info('Loaded %d sections from config file' %
|
||||
len(self._parser.sections()))
|
||||
logger.debug('loaded %d sections from config file',
|
||||
len(self._parser.sections()))
|
||||
|
||||
def write(self):
|
||||
fd, path = tempfile.mkstemp(suffix=u'.whipperrc')
|
||||
@@ -130,14 +130,13 @@ class Config:
|
||||
if not name.startswith('drive:'):
|
||||
continue
|
||||
|
||||
logger.debug('Looking at section %r' % name)
|
||||
logger.debug('looking at section %r', name)
|
||||
conf = {}
|
||||
for key in ['vendor', 'model', 'release']:
|
||||
locals()[key] = locals()[key].strip()
|
||||
conf[key] = self._parser.get(name, key)
|
||||
logger.debug("%s: '%s' versus '%s'" % (
|
||||
key, locals()[key], conf[key]
|
||||
))
|
||||
logger.debug("%s: '%s' versus '%s'",
|
||||
key, locals()[key], conf[key])
|
||||
if vendor.strip() != conf['vendor']:
|
||||
continue
|
||||
if model.strip() != conf['model']:
|
||||
|
||||
@@ -36,7 +36,7 @@ def getAllDevicePaths():
|
||||
# see https://savannah.gnu.org/bugs/index.php?38477
|
||||
return [str(dev) for dev in _getAllDevicePathsPyCdio()]
|
||||
except ImportError:
|
||||
logger.info('Cannot import pycdio')
|
||||
logger.info('cannot import pycdio')
|
||||
return _getAllDevicePathsStatic()
|
||||
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ def _record(record, which, name, what):
|
||||
handle = open(filename, 'w')
|
||||
handle.write(json.dumps(what))
|
||||
handle.close()
|
||||
logger.info('Wrote %s %s to %s', which, name, filename)
|
||||
logger.info('wrote %s %s to %s', which, name, filename)
|
||||
|
||||
# credit is of the form [dict, str, dict, ... ]
|
||||
# e.g. [
|
||||
@@ -152,10 +152,9 @@ def _getMetadata(releaseShort, release, discid, country=None):
|
||||
|
||||
@rtype: L{DiscMetadata} or None
|
||||
"""
|
||||
logger.debug('getMetadata for release id %r',
|
||||
release['id'])
|
||||
logger.debug('getMetadata for release id %r', release['id'])
|
||||
if not release['id']:
|
||||
logger.warning('No id for release %r', release)
|
||||
logger.warning('no id for release %r', release)
|
||||
return None
|
||||
|
||||
assert release['id'], 'Release does not have an id'
|
||||
@@ -183,7 +182,7 @@ def _getMetadata(releaseShort, release, discid, country=None):
|
||||
discMD.artist = albumArtistName
|
||||
discMD.sortName = discCredit.getSortName()
|
||||
if 'date' not in release:
|
||||
logger.warning("Release with ID '%s' (%s - %s) does not have a date",
|
||||
logger.warning("release with ID '%s' (%s - %s) does not have a date",
|
||||
release['id'], discMD.artist, release['title'])
|
||||
else:
|
||||
discMD.release = release['date']
|
||||
@@ -235,9 +234,8 @@ def _getMetadata(releaseShort, release, discid, country=None):
|
||||
# FIXME: unit of duration ?
|
||||
track.duration = int(t['recording'].get('length', 0))
|
||||
if not track.duration:
|
||||
logger.warning(
|
||||
'track %r (%r) does not have duration' %
|
||||
(track.title, track.mbid))
|
||||
logger.warning('track %r (%r) does not have duration',
|
||||
track.title, track.mbid)
|
||||
tainted = True
|
||||
else:
|
||||
duration += track.duration
|
||||
@@ -271,7 +269,7 @@ def musicbrainz(discid, country=None, record=False):
|
||||
import musicbrainzngs
|
||||
|
||||
musicbrainzngs.set_useragent("whipper", whipper.__version__,
|
||||
"https://github.com/JoeLametta/whipper")
|
||||
"https://github.com/whipper-team/whipper")
|
||||
ret = []
|
||||
|
||||
try:
|
||||
@@ -297,8 +295,8 @@ def musicbrainz(discid, country=None, record=False):
|
||||
import json
|
||||
for release in result['disc']['release-list']:
|
||||
formatted = json.dumps(release, sort_keys=False, indent=4)
|
||||
logger.debug('result %s: artist %r, title %r' % (
|
||||
formatted, release['artist-credit-phrase'], release['title']))
|
||||
logger.debug('result %s: artist %r, title %r', formatted,
|
||||
release['artist-credit-phrase'], release['title'])
|
||||
|
||||
# to get titles of recordings, we need to query the release with
|
||||
# artist-credits
|
||||
@@ -309,7 +307,7 @@ def musicbrainz(discid, country=None, record=False):
|
||||
_record(record, 'release', release['id'], res)
|
||||
releaseDetail = res['release']
|
||||
formatted = json.dumps(releaseDetail, sort_keys=False, indent=4)
|
||||
logger.debug('release %s' % formatted)
|
||||
logger.debug('release %s', formatted)
|
||||
|
||||
md = _getMetadata(release, releaseDetail, discid, country)
|
||||
if md:
|
||||
|
||||
@@ -25,7 +25,6 @@ Common functionality and class for all programs using whipper.
|
||||
import musicbrainzngs
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from whipper.common import accurip, cache, checksum, common, mbngs, path
|
||||
@@ -59,15 +58,12 @@ class Program:
|
||||
outdir = None
|
||||
result = None
|
||||
|
||||
_stdout = None
|
||||
|
||||
def __init__(self, config, record=False, stdout=sys.stdout):
|
||||
def __init__(self, config, record=False):
|
||||
"""
|
||||
@param record: whether to record results of API calls for playback.
|
||||
"""
|
||||
self._record = record
|
||||
self._cache = cache.ResultCache()
|
||||
self._stdout = stdout
|
||||
self._config = config
|
||||
|
||||
d = {}
|
||||
@@ -87,7 +83,7 @@ class Program:
|
||||
|
||||
def setWorkingDirectory(self, workingDirectory):
|
||||
if workingDirectory:
|
||||
logger.info('Changing to working directory %s' % workingDirectory)
|
||||
logger.info('changing to working directory %s', workingDirectory)
|
||||
os.chdir(workingDirectory)
|
||||
|
||||
def getFastToc(self, runner, device):
|
||||
@@ -97,14 +93,14 @@ class Program:
|
||||
from pkg_resources import parse_version as V
|
||||
version = cdrdao.getCDRDAOVersion()
|
||||
if V(version) < V('1.2.3rc2'):
|
||||
sys.stdout.write('Warning: cdrdao older than 1.2.3 has a '
|
||||
'pre-gap length bug.\n'
|
||||
'See http://sourceforge.net/tracker/?func=detail&aid=604751&group_id=2171&atid=102171\n') # noqa: E501
|
||||
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
|
||||
assert toc.hasTOC()
|
||||
return toc
|
||||
|
||||
def getTable(self, runner, cddbdiscid, mbdiscid, device, offset):
|
||||
def getTable(self, runner, cddbdiscid, mbdiscid, device, offset,
|
||||
out_path):
|
||||
"""
|
||||
Retrieve the Table either from the cache or the drive.
|
||||
|
||||
@@ -123,24 +119,24 @@ class Program:
|
||||
itable = tdict[offset]
|
||||
|
||||
if not itable:
|
||||
logger.debug('getTable: cddbdiscid %s, mbdiscid %s not '
|
||||
'in cache for offset %s, reading table' % (
|
||||
cddbdiscid, mbdiscid, offset))
|
||||
t = cdrdao.ReadTableTask(device)
|
||||
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
|
||||
tdict[offset] = itable
|
||||
ptable.persist(tdict)
|
||||
logger.debug('getTable: read table %r' % itable)
|
||||
logger.debug('getTable: read table %r', itable)
|
||||
else:
|
||||
logger.debug('getTable: cddbdiscid %s, mbdiscid %s in cache '
|
||||
'for offset %s' % (cddbdiscid, mbdiscid, offset))
|
||||
logger.debug('getTable: loaded table %r' % itable)
|
||||
'for offset %s', cddbdiscid, mbdiscid, offset)
|
||||
logger.debug('getTable: loaded table %r', itable)
|
||||
|
||||
assert itable.hasTOC()
|
||||
|
||||
self.result.table = itable
|
||||
|
||||
logger.debug('getTable: returning table with mb id %s' %
|
||||
logger.debug('getTable: returning table with mb id %s',
|
||||
itable.getMusicBrainzDiscId())
|
||||
return itable
|
||||
|
||||
@@ -252,12 +248,12 @@ class Program:
|
||||
return [item['DTITLE'] for item in md if 'DTITLE' in item] or None
|
||||
|
||||
except ValueError as e:
|
||||
self._stdout.write("WARNING: CDDB protocol error: %s\n" % e)
|
||||
logger.warning("CDDB protocol error: %s", e)
|
||||
|
||||
except IOError as e:
|
||||
# FIXME: for some reason errno is a str ?
|
||||
if e.errno == 'socket error':
|
||||
self._stdout.write("WARNING: CDDB network error: %r\n" % (e, ))
|
||||
logger.warning("CDDB network error: %r", (e, ))
|
||||
else:
|
||||
raise
|
||||
|
||||
@@ -269,7 +265,7 @@ class Program:
|
||||
@type ittoc: L{whipper.image.table.Table}
|
||||
"""
|
||||
# look up disc on MusicBrainz
|
||||
self._stdout.write('Disc duration: %s, %d audio tracks\n' % (
|
||||
print('Disc duration: %s, %d audio tracks' % (
|
||||
common.formatTime(ittoc.duration() / 1000.0),
|
||||
ittoc.getAudioTracks()))
|
||||
logger.debug('MusicBrainz submit url: %r',
|
||||
@@ -286,41 +282,37 @@ class Program:
|
||||
record=self._record)
|
||||
break
|
||||
except mbngs.NotFoundException as e:
|
||||
logger.warning("release not found: %r" % (e, ))
|
||||
logger.warning("release not found: %r", (e, ))
|
||||
break
|
||||
except musicbrainzngs.NetworkError as e:
|
||||
logger.warning("network error: %r" % (e, ))
|
||||
logger.warning("network error: %r", (e, ))
|
||||
break
|
||||
except mbngs.MusicBrainzException as e:
|
||||
logger.warning("musicbrainz exception: %r" % (e, ))
|
||||
logger.warning("musicbrainz exception: %r", (e, ))
|
||||
time.sleep(5)
|
||||
continue
|
||||
|
||||
if not metadatas:
|
||||
self._stdout.write('Continuing without metadata\n')
|
||||
logger.warning('continuing without metadata')
|
||||
|
||||
if metadatas:
|
||||
deltas = {}
|
||||
|
||||
self._stdout.write('\nMatching releases:\n')
|
||||
print('\nMatching releases:')
|
||||
|
||||
for metadata in metadatas:
|
||||
self._stdout.write('\n')
|
||||
self._stdout.write('Artist : %s\n' %
|
||||
metadata.artist.encode('utf-8'))
|
||||
self._stdout.write('Title : %s\n' %
|
||||
metadata.title.encode('utf-8'))
|
||||
self._stdout.write('Duration: %s\n' %
|
||||
common.formatTime(metadata.duration /
|
||||
1000.0))
|
||||
self._stdout.write('URL : %s\n' % metadata.url)
|
||||
self._stdout.write('Release : %s\n' % metadata.mbid)
|
||||
self._stdout.write('Type : %s\n' % metadata.releaseType)
|
||||
print('\nArtist : %s' % metadata.artist.encode('utf-8'))
|
||||
print('Title : %s' % metadata.title.encode('utf-8'))
|
||||
print('Duration: %s' % common.formatTime(
|
||||
metadata.duration / 1000.0))
|
||||
print('URL : %s' % metadata.url)
|
||||
print('Release : %s' % metadata.mbid)
|
||||
print('Type : %s' % metadata.releaseType)
|
||||
if metadata.barcode:
|
||||
self._stdout.write("Barcode : %s\n" % metadata.barcode)
|
||||
print("Barcode : %s" % metadata.barcode)
|
||||
if metadata.catalogNumber:
|
||||
self._stdout.write("Cat no : %s\n" %
|
||||
metadata.catalogNumber)
|
||||
print("Cat no : %s" %
|
||||
metadata.catalogNumber.encode('utf-8'))
|
||||
|
||||
delta = abs(metadata.duration - ittoc.duration())
|
||||
if delta not in deltas:
|
||||
@@ -343,20 +335,15 @@ class Program:
|
||||
|
||||
if release:
|
||||
metadatas = [m for m in metadatas if m.url.endswith(release)]
|
||||
logger.debug('Asked for release %r, only kept %r',
|
||||
release, metadatas)
|
||||
logger.debug('asked for release %r, only kept %r', release,
|
||||
metadatas)
|
||||
if len(metadatas) == 1:
|
||||
self._stdout.write('\n')
|
||||
self._stdout.write('Picked requested release id %s\n' %
|
||||
release)
|
||||
self._stdout.write('Artist : %s\n' %
|
||||
metadatas[0].artist.encode('utf-8'))
|
||||
self._stdout.write('Title : %s\n' %
|
||||
metadatas[0].title.encode('utf-8'))
|
||||
logger.info('picked requested release id %s', release)
|
||||
print('Artist: %s' % metadatas[0].artist.encode('utf-8'))
|
||||
print('Title : %s' % metadatas[0].title.encode('utf-8'))
|
||||
elif not metadatas:
|
||||
self._stdout.write(
|
||||
"Requested release id '%s', "
|
||||
"but none of the found releases match\n" % release)
|
||||
logger.warning("requested release id '%s', but none of "
|
||||
"the found releases match", release)
|
||||
return
|
||||
else:
|
||||
if lowest:
|
||||
@@ -368,36 +355,31 @@ class Program:
|
||||
releaseTitle = metadatas[0].releaseTitle
|
||||
for i, metadata in enumerate(metadatas):
|
||||
if not artist == metadata.artist:
|
||||
logger.warning("artist 0: %r and artist %d: %r "
|
||||
"are not the same" % (
|
||||
artist, i, metadata.artist))
|
||||
logger.warning("artist 0: %r and artist %d: %r are "
|
||||
"not the same", artist, i,
|
||||
metadata.artist)
|
||||
if not releaseTitle == metadata.releaseTitle:
|
||||
logger.warning("title 0: %r and title %d: %r "
|
||||
"are not the same" % (
|
||||
releaseTitle, i,
|
||||
metadata.releaseTitle))
|
||||
logger.warning("title 0: %r and title %d: %r are "
|
||||
"not the same", releaseTitle, i,
|
||||
metadata.releaseTitle)
|
||||
|
||||
if (not release and len(list(deltas)) > 1):
|
||||
self._stdout.write('\n')
|
||||
self._stdout.write('Picked closest match in duration.\n')
|
||||
self._stdout.write('Others may be wrong in MusicBrainz, '
|
||||
'please correct.\n')
|
||||
self._stdout.write('Artist : %s\n' %
|
||||
artist.encode('utf-8'))
|
||||
self._stdout.write('Title : %s\n' %
|
||||
metadatas[0].title.encode('utf-8'))
|
||||
logger.warning('picked closest match in duration. '
|
||||
'Others may be wrong in MusicBrainz, '
|
||||
'please correct')
|
||||
print('Artist : %s' % artist.encode('utf-8'))
|
||||
print('Title : %s' % metadatas[0].title.encode('utf-8'))
|
||||
|
||||
# Select one of the returned releases. We just pick the first one.
|
||||
ret = metadatas[0]
|
||||
else:
|
||||
self._stdout.write(
|
||||
'Submit this disc to MusicBrainz at the above URL.\n')
|
||||
print('Submit this disc to MusicBrainz at the above URL.')
|
||||
ret = None
|
||||
|
||||
self._stdout.write('\n')
|
||||
print('')
|
||||
return ret
|
||||
|
||||
def getTagList(self, number):
|
||||
def getTagList(self, number, mbdiscid):
|
||||
"""
|
||||
Based on the metadata, get a dict of tags for the given track.
|
||||
|
||||
@@ -417,7 +399,6 @@ class Program:
|
||||
disc = self.metadata.title
|
||||
mbidAlbum = self.metadata.mbid
|
||||
mbidTrackAlbum = self.metadata.mbidArtist
|
||||
mbDiscId = self.metadata.discid
|
||||
|
||||
if number > 0:
|
||||
try:
|
||||
@@ -427,7 +408,7 @@ class Program:
|
||||
mbidTrack = track.mbid
|
||||
mbidTrackArtist = track.mbidArtist
|
||||
except IndexError as e:
|
||||
print('ERROR: no track %d found, %r' % (number, e))
|
||||
logger.error('no track %d found, %r', number, e)
|
||||
raise
|
||||
else:
|
||||
# htoa defaults to disc's artist
|
||||
@@ -435,6 +416,9 @@ class Program:
|
||||
|
||||
tags = {}
|
||||
|
||||
if number > 0:
|
||||
tags['MUSICBRAINZ_DISCID'] = mbdiscid
|
||||
|
||||
if self.metadata and not self.metadata.various:
|
||||
tags['ALBUMARTIST'] = albumArtist
|
||||
tags['ARTIST'] = trackArtist
|
||||
@@ -452,7 +436,6 @@ class Program:
|
||||
tags['MUSICBRAINZ_ARTISTID'] = mbidTrackArtist
|
||||
tags['MUSICBRAINZ_ALBUMID'] = mbidAlbum
|
||||
tags['MUSICBRAINZ_ALBUMARTISTID'] = mbidTrackAlbum
|
||||
tags['MUSICBRAINZ_DISCID'] = mbDiscId
|
||||
|
||||
# TODO/FIXME: ISRC tag
|
||||
|
||||
@@ -482,15 +465,14 @@ class Program:
|
||||
runner.run(t)
|
||||
except task.TaskException as e:
|
||||
if isinstance(e.exception, common.MissingFrames):
|
||||
logger.warning('missing frames for %r' % trackResult.filename)
|
||||
logger.warning('missing frames for %r', trackResult.filename)
|
||||
return False
|
||||
else:
|
||||
raise
|
||||
|
||||
ret = trackResult.testcrc == t.checksum
|
||||
logger.debug('verifyTrack: track result crc %r, '
|
||||
'file crc %r, result %r',
|
||||
trackResult.testcrc, t.checksum, ret)
|
||||
logger.debug('verifyTrack: track result crc %r, file crc %r, '
|
||||
'result %r', trackResult.testcrc, t.checksum, ret)
|
||||
return ret
|
||||
|
||||
def ripTrack(self, runner, trackResult, offset, device, taglist,
|
||||
@@ -526,10 +508,10 @@ class Program:
|
||||
runner.run(t)
|
||||
|
||||
logger.debug('ripped track')
|
||||
logger.debug('test speed %.3f/%.3f seconds' % (
|
||||
t.testspeed, t.testduration))
|
||||
logger.debug('copy speed %.3f/%.3f seconds' % (
|
||||
t.copyspeed, t.copyduration))
|
||||
logger.debug('test speed %.3f/%.3f seconds',
|
||||
t.testspeed, t.testduration)
|
||||
logger.debug('copy speed %.3f/%.3f seconds',
|
||||
t.copyspeed, t.copyduration)
|
||||
trackResult.testcrc = t.testchecksum
|
||||
trackResult.copycrc = t.copychecksum
|
||||
trackResult.peak = t.peak
|
||||
@@ -542,7 +524,7 @@ class Program:
|
||||
|
||||
if trackResult.filename != t.path:
|
||||
trackResult.filename = t.path
|
||||
logger.info('Filename changed to %r', trackResult.filename)
|
||||
logger.info('filename changed to %r', trackResult.filename)
|
||||
|
||||
def verifyImage(self, runner, table):
|
||||
"""
|
||||
@@ -563,7 +545,7 @@ class Program:
|
||||
return False
|
||||
|
||||
responses = accurip.get_db_entry(table.accuraterip_path())
|
||||
logger.info('%d AccurateRip response(s) found' % len(responses))
|
||||
logger.info('%d AccurateRip response(s) found', len(responses))
|
||||
|
||||
checksums = accurip.calculate_checksums([
|
||||
os.path.join(os.path.dirname(self.cuePath), t.indexes[1].path)
|
||||
|
||||
@@ -51,8 +51,7 @@ class PopenTask(task.Task):
|
||||
|
||||
raise
|
||||
|
||||
logger.debug('Started %r with pid %d', self.command,
|
||||
self._popen.pid)
|
||||
logger.debug('started %r with pid %d', self.command, self._popen.pid)
|
||||
|
||||
self.schedule(1.0, self._read, runner)
|
||||
|
||||
@@ -89,7 +88,7 @@ class PopenTask(task.Task):
|
||||
|
||||
self._done()
|
||||
except Exception as e:
|
||||
logger.debug('exception during _read(): %r', str(e))
|
||||
logger.debug('exception during _read(): %s', e)
|
||||
self.setException(e)
|
||||
self.stop()
|
||||
|
||||
@@ -97,10 +96,9 @@ class PopenTask(task.Task):
|
||||
assert self._popen.returncode is not None, "No returncode"
|
||||
|
||||
if self._popen.returncode >= 0:
|
||||
logger.debug('Return code was %d', self._popen.returncode)
|
||||
logger.debug('return code was %d', self._popen.returncode)
|
||||
else:
|
||||
logger.debug('Terminated with signal %d',
|
||||
-self._popen.returncode)
|
||||
logger.debug('terminated with signal %d', -self._popen.returncode)
|
||||
|
||||
self.setProgress(1.0)
|
||||
|
||||
@@ -113,7 +111,7 @@ class PopenTask(task.Task):
|
||||
return
|
||||
|
||||
def abort(self):
|
||||
logger.debug('Aborting, sending SIGTERM to %d', self._popen.pid)
|
||||
logger.debug('aborting, sending SIGTERM to %d', self._popen.pid)
|
||||
os.kill(self._popen.pid, signal.SIGTERM)
|
||||
# self.stop()
|
||||
|
||||
|
||||
34
whipper/extern/task/task.py
vendored
34
whipper/extern/task/task.py
vendored
@@ -18,6 +18,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with whipper. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import print_function
|
||||
import logging
|
||||
import sys
|
||||
|
||||
@@ -166,7 +167,8 @@ class Task(LogStub):
|
||||
value >= 1.0 or value == 0.0):
|
||||
self.progress = value
|
||||
self._notifyListeners('progressed', value)
|
||||
self.log('notifying progress: %r on %r', value, self.description)
|
||||
self.debug('notifying progress: %r on %r',
|
||||
value, self.description)
|
||||
|
||||
def setDescription(self, description):
|
||||
if description != self.description:
|
||||
@@ -366,23 +368,23 @@ class BaseMultiTask(Task, ITaskListener):
|
||||
Subclasses should chain up to me at the end of their implementation.
|
||||
They should fall through to chaining up if there is an exception.
|
||||
"""
|
||||
self.log('BaseMultiTask.stopped: task %r (%d of %d)',
|
||||
task, self.tasks.index(task) + 1, len(self.tasks))
|
||||
self.debug('BaseMultiTask.stopped: task %r (%d of %d)',
|
||||
task, self.tasks.index(task) + 1, len(self.tasks))
|
||||
if task.exception:
|
||||
self.log('BaseMultiTask.stopped: exception %r',
|
||||
task.exceptionMessage)
|
||||
self.warning('BaseMultiTask.stopped: exception %r',
|
||||
task.exceptionMessage)
|
||||
self.exception = task.exception
|
||||
self.exceptionMessage = task.exceptionMessage
|
||||
self.stop()
|
||||
return
|
||||
|
||||
if self._task == len(self.tasks):
|
||||
self.log('BaseMultiTask.stopped: all tasks done')
|
||||
self.debug('BaseMultiTask.stopped: all tasks done')
|
||||
self.stop()
|
||||
return
|
||||
|
||||
# pick another
|
||||
self.log('BaseMultiTask.stopped: pick next task')
|
||||
self.debug('BaseMultiTask.stopped: pick next task')
|
||||
self.schedule(0, self.next)
|
||||
|
||||
|
||||
@@ -511,8 +513,8 @@ class SyncRunner(TaskRunner, ITaskListener):
|
||||
def schedule(self, task, delta, callable, *args, **kwargs):
|
||||
def c():
|
||||
try:
|
||||
self.log('schedule: calling %r(*args=%r, **kwargs=%r)',
|
||||
callable, args, kwargs)
|
||||
self.debug('schedule: calling %r(*args=%r, **kwargs=%r)',
|
||||
callable, args, kwargs)
|
||||
callable(*args, **kwargs)
|
||||
return False
|
||||
except Exception as e:
|
||||
@@ -521,8 +523,8 @@ class SyncRunner(TaskRunner, ITaskListener):
|
||||
task.setException(e)
|
||||
self.stopped(task)
|
||||
raise
|
||||
self.log('schedule: scheduling %r(*args=%r, **kwargs=%r)',
|
||||
callable, args, kwargs)
|
||||
self.debug('schedule: scheduling %r(*args=%r, **kwargs=%r)',
|
||||
callable, args, kwargs)
|
||||
|
||||
gobject.timeout_add(int(delta * 1000L), c)
|
||||
|
||||
@@ -539,15 +541,15 @@ class SyncRunner(TaskRunner, ITaskListener):
|
||||
self._task.description, 100.0))
|
||||
else:
|
||||
# clear with whitespace
|
||||
sys.stdout.write("%s\r" % (' ' * self._longest, ))
|
||||
print(("%s\r" % (' ' * self._longest, )), end='')
|
||||
|
||||
def _output(self, what, newline=False, ret=True):
|
||||
sys.stdout.write(what)
|
||||
sys.stdout.write(' ' * (self._longest - len(what)))
|
||||
print(what, end='')
|
||||
print((' ' * (self._longest - len(what))), end='')
|
||||
if ret:
|
||||
sys.stdout.write('\r')
|
||||
print('\r', end='')
|
||||
if newline:
|
||||
sys.stdout.write('\n')
|
||||
print('')
|
||||
sys.stdout.flush()
|
||||
if len(what) > self._longest:
|
||||
self._longest = len(what)
|
||||
|
||||
@@ -85,7 +85,7 @@ class CueFile(object):
|
||||
currentTrack = None
|
||||
counter = 0
|
||||
|
||||
logger.info('Parsing .cue file %r', self._path)
|
||||
logger.info('parsing .cue file %r', self._path)
|
||||
handle = codecs.open(self._path, 'r', 'utf-8')
|
||||
|
||||
for number, line in enumerate(handle.readlines()):
|
||||
|
||||
@@ -135,7 +135,7 @@ class ImageVerifyTask(task.MultiSeparateTask):
|
||||
self.addTask(taskk)
|
||||
self._tasks.append((0, track, taskk))
|
||||
except (KeyError, IndexError):
|
||||
logger.debug('no htoa track')
|
||||
logger.debug('no HTOA track')
|
||||
|
||||
for trackIndex, track in enumerate(cue.table.tracks):
|
||||
logger.debug('verifying track %d', trackIndex + 1)
|
||||
@@ -155,8 +155,8 @@ class ImageVerifyTask(task.MultiSeparateTask):
|
||||
def stop(self):
|
||||
for trackIndex, track, taskk in self._tasks:
|
||||
if taskk.exception:
|
||||
logger.debug('subtask %r had exception %r, shutting down' % (
|
||||
taskk, taskk.exception))
|
||||
logger.debug('subtask %r had exception %r, shutting down',
|
||||
taskk, taskk.exception)
|
||||
self.setException(taskk.exception)
|
||||
break
|
||||
|
||||
@@ -195,17 +195,16 @@ class ImageEncodeTask(task.MultiSeparateTask):
|
||||
root, ext = os.path.splitext(os.path.basename(path))
|
||||
outpath = os.path.join(outdir, root + '.' + 'flac')
|
||||
logger.debug('schedule encode to %r', outpath)
|
||||
taskk = encode.FlacEncodeTask(path,
|
||||
os.path.join(outdir,
|
||||
root + '.' + 'flac'))
|
||||
taskk = encode.FlacEncodeTask(
|
||||
path, os.path.join(outdir, root + '.' + 'flac'))
|
||||
self.addTask(taskk)
|
||||
|
||||
try:
|
||||
htoa = cue.table.tracks[0].indexes[0]
|
||||
logger.debug('encoding htoa track')
|
||||
logger.debug('encoding HTOA track')
|
||||
add(htoa)
|
||||
except (KeyError, IndexError):
|
||||
logger.debug('no htoa track')
|
||||
logger.debug('no HTOA track')
|
||||
pass
|
||||
|
||||
for trackIndex, track in enumerate(cue.table.tracks):
|
||||
|
||||
@@ -333,8 +333,8 @@ class Table(object):
|
||||
@returns: the 28-character base64-encoded disc ID
|
||||
"""
|
||||
if self.mbdiscid:
|
||||
logger.debug('getMusicBrainzDiscId: returning cached %r'
|
||||
% self.mbdiscid)
|
||||
logger.debug('getMusicBrainzDiscId: returning cached %r',
|
||||
self.mbdiscid)
|
||||
return self.mbdiscid
|
||||
values = self._getMusicBrainzValues()
|
||||
|
||||
@@ -381,7 +381,7 @@ class Table(object):
|
||||
assert len(result) == 28, \
|
||||
"Result should be 28 characters, not %d" % len(result)
|
||||
|
||||
logger.debug('getMusicBrainzDiscId: returning %r' % result)
|
||||
logger.debug('getMusicBrainzDiscId: returning %r', result)
|
||||
self.mbdiscid = result
|
||||
return result
|
||||
|
||||
@@ -489,7 +489,7 @@ class Table(object):
|
||||
targetPath = common.getRelativePath(path, cuePath)
|
||||
line = 'FILE "%s" WAVE' % targetPath
|
||||
lines.append(line)
|
||||
logger.debug('writeFile: %r' % line)
|
||||
logger.debug('writeFile: %r', line)
|
||||
|
||||
# header
|
||||
main = ['PERFORMER', 'TITLE']
|
||||
@@ -530,11 +530,11 @@ class Table(object):
|
||||
counter = index.counter
|
||||
|
||||
if index.path:
|
||||
logger.debug('counter %d, writeFile' % counter)
|
||||
logger.debug('counter %d, writeFile', counter)
|
||||
writeFile(index.path)
|
||||
|
||||
for i, track in enumerate(self.tracks):
|
||||
logger.debug('track i %r, track %r' % (i, track))
|
||||
logger.debug('track i %r, track %r', i, track)
|
||||
# FIXME: skip data tracks for now
|
||||
if not track.audio:
|
||||
continue
|
||||
@@ -545,7 +545,7 @@ class Table(object):
|
||||
|
||||
for number in indexes:
|
||||
index = track.indexes[number]
|
||||
logger.debug('index %r, %r' % (number, index))
|
||||
logger.debug('index %r, %r', number, index)
|
||||
|
||||
# any time the source counter changes to a higher value,
|
||||
# write a FILE statement
|
||||
@@ -553,9 +553,9 @@ class Table(object):
|
||||
# at counter 0 here
|
||||
if index.counter > counter:
|
||||
if index.path:
|
||||
logger.debug('counter %d, writeFile' % counter)
|
||||
logger.debug('counter %d, writeFile', counter)
|
||||
writeFile(index.path)
|
||||
logger.debug('setting counter to index.counter %r' %
|
||||
logger.debug('setting counter to index.counter %r',
|
||||
index.counter)
|
||||
counter = index.counter
|
||||
|
||||
@@ -564,7 +564,7 @@ class Table(object):
|
||||
wroteTrack = True
|
||||
line = " TRACK %02d %s" % (i + 1, 'AUDIO')
|
||||
lines.append(line)
|
||||
logger.debug('%r' % line)
|
||||
logger.debug('%r', line)
|
||||
|
||||
for key in CDTEXT_FIELDS:
|
||||
if key in track.cdtext:
|
||||
@@ -620,7 +620,7 @@ class Table(object):
|
||||
while True:
|
||||
track = self.tracks[t - 1]
|
||||
index = track.getIndex(i)
|
||||
logger.debug('Clearing path on track %d, index %d', t, i)
|
||||
logger.debug('clearing path on track %d, index %d', t, i)
|
||||
index.path = None
|
||||
index.relative = None
|
||||
try:
|
||||
@@ -639,9 +639,8 @@ class Table(object):
|
||||
@type track: C{int}
|
||||
@type index: C{int}
|
||||
"""
|
||||
logger.debug('setFile: track %d, index %d, path %r, '
|
||||
'length %r, counter %r', track, index, path, length,
|
||||
counter)
|
||||
logger.debug('setFile: track %d, index %d, path %r, length %r, '
|
||||
'counter %r', track, index, path, length, counter)
|
||||
|
||||
t = self.tracks[track - 1]
|
||||
i = t.indexes[index]
|
||||
@@ -654,9 +653,9 @@ class Table(object):
|
||||
i.path = path
|
||||
i.relative = i.absolute - start
|
||||
i.counter = counter
|
||||
logger.debug('Setting path %r, relative %r on '
|
||||
'track %d, index %d, counter %r',
|
||||
path, i.relative, track, index, counter)
|
||||
logger.debug('setting path %r, relative %r on track %d, '
|
||||
'index %d, counter %r', path, i.relative, track,
|
||||
index, counter)
|
||||
try:
|
||||
track, index = self.getNextTrackIndex(track, index)
|
||||
t = self.tracks[track - 1]
|
||||
@@ -682,13 +681,13 @@ class Table(object):
|
||||
assert track.number == t
|
||||
assert index.number == i
|
||||
if index.counter is None:
|
||||
logger.debug('Track %d, index %d has no counter', t, i)
|
||||
logger.debug('track %d, index %d has no counter', t, i)
|
||||
break
|
||||
if index.counter != counter:
|
||||
logger.debug(
|
||||
'Track %d, index %d has a different counter', t, i)
|
||||
logger.debug('track %d, index %d has a different counter',
|
||||
t, i)
|
||||
break
|
||||
logger.debug('Setting absolute offset %d on track %d, index %d',
|
||||
logger.debug('setting absolute offset %d on track %d, index %d',
|
||||
index.relative, t, i)
|
||||
if index.absolute is not None:
|
||||
if index.absolute != index.relative:
|
||||
@@ -722,18 +721,16 @@ class Table(object):
|
||||
for i in list(t.indexes.values()):
|
||||
if i.absolute is not None:
|
||||
i.absolute += self.leadout + gap
|
||||
logger.debug('Fixing track %02d, index %02d, '
|
||||
'absolute %d' % (
|
||||
t.number, i.number, i.absolute))
|
||||
logger.debug('fixing track %02d, index %02d, absolute %d',
|
||||
t.number, i.number, i.absolute)
|
||||
if i.counter is not None:
|
||||
i.counter += sourceCounter
|
||||
logger.debug('Fixing track %02d, index %02d, '
|
||||
'counter %d' % (
|
||||
t.number, i.number, i.counter))
|
||||
logger.debug('fixing track %02d, index %02d, counter %d',
|
||||
t.number, i.number, i.counter)
|
||||
self.tracks.append(t)
|
||||
|
||||
self.leadout += other.leadout + gap # FIXME
|
||||
logger.debug('Fixing leadout, now %d', self.leadout)
|
||||
logger.debug('fixing leadout, now %d', self.leadout)
|
||||
|
||||
def _getSessionGap(self, session):
|
||||
# From cdrecord multi-session info:
|
||||
@@ -841,13 +838,13 @@ class Table(object):
|
||||
Check if this table can be used to generate a .cue file
|
||||
"""
|
||||
if not self.hasTOC():
|
||||
logger.debug('No TOC, cannot cue')
|
||||
logger.debug('no TOC, cannot cue')
|
||||
return False
|
||||
|
||||
for t in self.tracks:
|
||||
for i in list(t.indexes.values()):
|
||||
if i.relative is None:
|
||||
logger.debug('Track %02d, Index %02d does not '
|
||||
logger.debug('track %02d, Index %02d does not '
|
||||
'have relative', t.number, i.number)
|
||||
return False
|
||||
|
||||
|
||||
@@ -109,8 +109,8 @@ class Sources:
|
||||
@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))
|
||||
logger.debug('appending source, counter %d, abs offset %d, '
|
||||
'source %r', counter, offset, source)
|
||||
self._sources.append((counter, offset, source))
|
||||
|
||||
def get(self, offset):
|
||||
@@ -152,8 +152,8 @@ class TocFile(object):
|
||||
absolute = absoluteOffset + trackOffset
|
||||
# this may be in a new source, so calculate relative
|
||||
c, o, s = self._sources.get(absolute)
|
||||
logger.debug('at abs offset %d, we are in source %r' % (
|
||||
absolute, s))
|
||||
logger.debug('at abs offset %d, we are in source %r',
|
||||
absolute, s)
|
||||
counterStart = self._sources.getCounterStart(c)
|
||||
relative = absolute - counterStart
|
||||
|
||||
@@ -161,10 +161,9 @@ class TocFile(object):
|
||||
absolute=absolute,
|
||||
relative=relative,
|
||||
counter=c)
|
||||
logger.debug(
|
||||
'[track %02d index %02d] trackOffset %r, added %r',
|
||||
currentTrack.number, i, trackOffset,
|
||||
currentTrack.getIndex(i))
|
||||
logger.debug('[track %02d index %02d] trackOffset %r, added %r',
|
||||
currentTrack.number, i, trackOffset,
|
||||
currentTrack.getIndex(i))
|
||||
|
||||
def parse(self):
|
||||
currentFile = None
|
||||
@@ -209,11 +208,11 @@ class TocFile(object):
|
||||
# is a limitation of our parser approach
|
||||
if state == 'HEADER':
|
||||
self.table.cdtext[key] = value
|
||||
logger.debug('Found disc CD-Text %s: %r', key, value)
|
||||
logger.debug('found disc CD-Text %s: %r', key, value)
|
||||
elif state == 'TRACK':
|
||||
if key != 'ISRC' or not currentTrack \
|
||||
or currentTrack.isrc is not None:
|
||||
logger.debug('Found track CD-Text %s: %r',
|
||||
logger.debug('found track CD-Text %s: %r',
|
||||
key, value)
|
||||
currentTrack.cdtext[key] = value
|
||||
|
||||
@@ -221,7 +220,7 @@ class TocFile(object):
|
||||
m = _CATALOG_RE.search(line)
|
||||
if m:
|
||||
self.table.catalog = m.group('catalog')
|
||||
logger.debug("Found catalog number %s", self.table.catalog)
|
||||
logger.debug("found catalog number %s", self.table.catalog)
|
||||
|
||||
# look for TRACK lines
|
||||
m = _TRACK_RE.search(line)
|
||||
@@ -260,23 +259,23 @@ class TocFile(object):
|
||||
m = _PRE_EMPHASIS_RE.search(line)
|
||||
if m:
|
||||
currentTrack.pre_emphasis = True
|
||||
logger.debug('Track has PRE_EMPHASIS')
|
||||
logger.debug('track has PRE_EMPHASIS')
|
||||
|
||||
# look for ISRC lines
|
||||
m = _ISRC_RE.search(line)
|
||||
if m:
|
||||
isrc = m.group('isrc')
|
||||
currentTrack.isrc = isrc
|
||||
logger.debug('Found ISRC code %s', isrc)
|
||||
logger.debug('found ISRC code %s', isrc)
|
||||
|
||||
# look for SILENCE lines
|
||||
m = _SILENCE_RE.search(line)
|
||||
if m:
|
||||
length = m.group('length')
|
||||
logger.debug('SILENCE of %r', length)
|
||||
logger.debug('silence of %r', length)
|
||||
self._sources.append(counter, absoluteOffset, None)
|
||||
if currentFile is not None:
|
||||
logger.debug('SILENCE after FILE, increasing counter')
|
||||
logger.debug('silence after file, increasing counter')
|
||||
counter += 1
|
||||
relativeOffset = 0
|
||||
currentFile = None
|
||||
@@ -286,7 +285,7 @@ class TocFile(object):
|
||||
m = _ZERO_RE.search(line)
|
||||
if m:
|
||||
if currentFile is not None:
|
||||
logger.debug('ZERO after FILE, increasing counter')
|
||||
logger.debug('zero after file, increasing counter')
|
||||
counter += 1
|
||||
relativeOffset = 0
|
||||
currentFile = None
|
||||
@@ -299,13 +298,13 @@ class TocFile(object):
|
||||
filePath = m.group('name')
|
||||
start = m.group('start')
|
||||
length = m.group('length')
|
||||
logger.debug('FILE %s, start %r, length %r',
|
||||
logger.debug('file %s, start %r, length %r',
|
||||
filePath, common.msfToFrames(start),
|
||||
common.msfToFrames(length))
|
||||
if not currentFile or filePath != currentFile.path:
|
||||
counter += 1
|
||||
relativeOffset = 0
|
||||
logger.debug('track %d, switched to new FILE, '
|
||||
logger.debug('track %d, switched to new file, '
|
||||
'increased counter to %d',
|
||||
trackNumber, counter)
|
||||
currentFile = File(filePath, common.msfToFrames(start),
|
||||
@@ -319,12 +318,12 @@ class TocFile(object):
|
||||
if m:
|
||||
filePath = m.group('name')
|
||||
length = m.group('length')
|
||||
logger.debug('FILE %s, length %r',
|
||||
logger.debug('file %s, length %r',
|
||||
filePath, common.msfToFrames(length))
|
||||
if not currentFile or filePath != currentFile.path:
|
||||
counter += 1
|
||||
relativeOffset = 0
|
||||
logger.debug('track %d, switched to new FILE, '
|
||||
logger.debug('track %d, switched to new file, '
|
||||
'increased counter to %d',
|
||||
trackNumber, counter)
|
||||
# FIXME: assume that a MODE2_FORM_MIX track always starts at 0
|
||||
@@ -343,8 +342,8 @@ class TocFile(object):
|
||||
|
||||
length = common.msfToFrames(m.group('length'))
|
||||
c, o, s = self._sources.get(absoluteOffset)
|
||||
logger.debug('at abs offset %d, we are in source %r' % (
|
||||
absoluteOffset, s))
|
||||
logger.debug('at abs offset %d, we are in source %r',
|
||||
absoluteOffset, s)
|
||||
counterStart = self._sources.getCounterStart(c)
|
||||
relativeOffset = absoluteOffset - counterStart
|
||||
|
||||
|
||||
@@ -36,17 +36,13 @@ def accuraterip_checksum(f, track_number, total_tracks, wave=False, v2=False):
|
||||
if not wave:
|
||||
flac.wait()
|
||||
if flac.returncode != 0:
|
||||
logger.warning(
|
||||
'ARC calculation failed: flac return code is non zero: %r' %
|
||||
flac.returncode
|
||||
)
|
||||
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
|
||||
)
|
||||
logger.warning('ARC calculation failed: '
|
||||
'arc return code is non zero: %r', arc.returncode)
|
||||
return None
|
||||
|
||||
try:
|
||||
|
||||
@@ -121,8 +121,8 @@ class ProgressParser:
|
||||
|
||||
def _parse_read(self, wordOffset):
|
||||
if wordOffset % common.WORDS_PER_FRAME != 0:
|
||||
logger.debug('THOMAS: not a multiple of %d: %d' % (
|
||||
common.WORDS_PER_FRAME, wordOffset))
|
||||
logger.debug('THOMAS: not a multiple of %d: %d',
|
||||
common.WORDS_PER_FRAME, wordOffset)
|
||||
return
|
||||
|
||||
frameOffset = wordOffset / common.WORDS_PER_FRAME
|
||||
@@ -190,18 +190,19 @@ class ProgressParser:
|
||||
"""
|
||||
frames = self.stop - self.start + 1 # + 1 since stop is inclusive
|
||||
reads = self.reads
|
||||
logger.debug('getTrackQuality: frames %d, reads %d' % (frames, reads))
|
||||
logger.debug('getTrackQuality: frames %d, reads %d', frames, reads)
|
||||
|
||||
# don't go over a 100%; we know cdparanoia reads each frame at least
|
||||
# twice
|
||||
try:
|
||||
# don't go over a 100%
|
||||
# we know that cdparanoia reads each frame at least twice
|
||||
return min(frames * 2.0 / reads, 1.0)
|
||||
except ZeroDivisionError:
|
||||
return 0
|
||||
|
||||
raise RuntimeError("cdparanoia couldn't read any frames "
|
||||
"for the current track")
|
||||
|
||||
# FIXME: handle errors
|
||||
|
||||
|
||||
class ReadTrackTask(task.Task):
|
||||
"""
|
||||
I am a task that reads a track using cdparanoia.
|
||||
@@ -271,12 +272,11 @@ class ReadTrackTask(task.Task):
|
||||
stopTrack = i + 1
|
||||
stopOffset = self._stop - self._table.getTrackStart(i + 1)
|
||||
|
||||
logger.debug('Ripping from %d to %d (inclusive)',
|
||||
self._start, self._stop)
|
||||
logger.debug('Starting at track %d, offset %d',
|
||||
startTrack, startOffset)
|
||||
logger.debug('Stopping at track %d, offset %d',
|
||||
stopTrack, stopOffset)
|
||||
logger.debug('ripping from %d to %d (inclusive)', self._start,
|
||||
self._stop)
|
||||
logger.debug('starting at track %d, offset %d', startTrack,
|
||||
startOffset)
|
||||
logger.debug('stopping at track %d, offset %d', stopTrack, stopOffset)
|
||||
|
||||
bufsize = 1024
|
||||
if self._overread:
|
||||
@@ -291,7 +291,7 @@ class ReadTrackTask(task.Task):
|
||||
startTrack, common.framesToHMSF(startOffset),
|
||||
stopTrack, common.framesToHMSF(stopOffset)),
|
||||
self.path])
|
||||
logger.debug('Running %s' % (" ".join(argv), ))
|
||||
logger.debug('running %s', (" ".join(argv), ))
|
||||
try:
|
||||
self._popen = asyncsub.Popen(argv,
|
||||
bufsize=bufsize,
|
||||
@@ -371,7 +371,7 @@ class ReadTrackTask(task.Task):
|
||||
logger.warning('file size %d did not match expected size %d',
|
||||
size, expected)
|
||||
if (size - expected) % common.BYTES_PER_FRAME == 0:
|
||||
logger.warning('%d frames difference' % (
|
||||
logger.warning('%d frames difference', (
|
||||
(size - expected) / common.BYTES_PER_FRAME))
|
||||
else:
|
||||
logger.warning('non-integral amount of frames difference')
|
||||
@@ -451,7 +451,7 @@ class ReadVerifyTrackTask(task.MultiSeparateTask):
|
||||
"""
|
||||
task.MultiSeparateTask.__init__(self)
|
||||
|
||||
logger.debug('Creating read and verify task on %r', path)
|
||||
logger.debug('creating read and verify task on %r', path)
|
||||
|
||||
if taglist:
|
||||
logger.debug('read and verify with taglist %r', taglist)
|
||||
@@ -520,12 +520,12 @@ class ReadVerifyTrackTask(task.MultiSeparateTask):
|
||||
self.testchecksum = c1 = self.tasks[1].checksum
|
||||
self.copychecksum = c2 = self.tasks[3].checksum
|
||||
if c1 == c2:
|
||||
logger.info('Checksums match, %08x' % c1)
|
||||
logger.info('checksums match, %08x', c1)
|
||||
self.checksum = self.testchecksum
|
||||
else:
|
||||
# FIXME: detect this before encoding
|
||||
logger.info('Checksums do not match, %08x %08x' % (
|
||||
c1, c2))
|
||||
logger.info('checksums do not match, %08x %08x',
|
||||
c1, c2)
|
||||
self.exception = ChecksumException(
|
||||
'read and verify failed: test checksum')
|
||||
|
||||
@@ -538,11 +538,11 @@ class ReadVerifyTrackTask(task.MultiSeparateTask):
|
||||
|
||||
if not self.exception:
|
||||
try:
|
||||
logger.debug('Moving to final path %r', self.path)
|
||||
logger.debug('moving to final path %r', self.path)
|
||||
os.rename(self._tmppath, self.path)
|
||||
except Exception as e:
|
||||
logger.debug('Exception while moving to final '
|
||||
'path %r: %r', self.path, str(e))
|
||||
logger.debug('exception while moving to final '
|
||||
'path %r: %s', self.path, e)
|
||||
self.exception = e
|
||||
else:
|
||||
os.unlink(self._tmppath)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import tempfile
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
from whipper.common.common import EjectError
|
||||
from whipper.common.common import EjectError, truncate_filename
|
||||
from whipper.image.toc import TocFile
|
||||
|
||||
import logging
|
||||
@@ -12,7 +13,7 @@ logger = logging.getLogger(__name__)
|
||||
CDRDAO = 'cdrdao'
|
||||
|
||||
|
||||
def read_toc(device, fast_toc=False):
|
||||
def read_toc(device, fast_toc=False, toc_path=None):
|
||||
"""
|
||||
Return cdrdao-generated table of contents for 'device'.
|
||||
"""
|
||||
@@ -43,6 +44,14 @@ def read_toc(device, fast_toc=False):
|
||||
|
||||
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
|
||||
|
||||
@@ -68,7 +77,7 @@ def version():
|
||||
out, err = cdrdao.communicate()
|
||||
if cdrdao.returncode != 1:
|
||||
logger.warning("cdrdao version detection failed: "
|
||||
"return code is " + str(cdrdao.returncode))
|
||||
"return code is %s", cdrdao.returncode)
|
||||
return None
|
||||
m = re.compile(r'^Cdrdao version (?P<version>.*) - \(C\)').search(
|
||||
err.decode('utf-8'))
|
||||
@@ -86,11 +95,11 @@ def ReadTOCTask(device):
|
||||
return read_toc(device, fast_toc=True)
|
||||
|
||||
|
||||
def ReadTableTask(device):
|
||||
def ReadTableTask(device, toc_path=None):
|
||||
"""
|
||||
stopgap morituri-insanity compatibility layer
|
||||
"""
|
||||
return read_toc(device)
|
||||
return read_toc(device, toc_path=toc_path)
|
||||
|
||||
|
||||
def getCDRDAOVersion():
|
||||
|
||||
@@ -20,7 +20,7 @@ def peak_level(track_path):
|
||||
sox = Popen([SOX, track_path, "-n", "stats", "-b", "16"], stderr=PIPE)
|
||||
out, err = sox.communicate()
|
||||
if sox.returncode:
|
||||
logger.warning("SoX peak detection failed: " + str(sox.returncode))
|
||||
logger.warning("SoX peak detection failed: %s", sox.returncode)
|
||||
return None
|
||||
# relevant captured lines looks like this:
|
||||
# Min level -26215
|
||||
|
||||
@@ -89,7 +89,7 @@ class WhipperLogger(result.Logger):
|
||||
htoastart = htoa.absolute
|
||||
htoaend = table.getTrackEnd(0)
|
||||
htoalength = table.tracks[0].getIndex(1).absolute - htoastart
|
||||
lines.append(" 00:")
|
||||
lines.append(" 0:")
|
||||
lines.append(" Start: %s" % common.framesToMSF(htoastart))
|
||||
lines.append(" Length: %s" % common.framesToMSF(htoalength))
|
||||
lines.append(" Start sector: %d" % htoastart)
|
||||
@@ -103,7 +103,7 @@ class WhipperLogger(result.Logger):
|
||||
start = t.getIndex(1).absolute
|
||||
length = table.getTrackLength(t.number)
|
||||
end = table.getTrackEnd(t.number)
|
||||
lines.append(" %02d:" % 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)
|
||||
@@ -166,7 +166,7 @@ class WhipperLogger(result.Logger):
|
||||
lines = []
|
||||
|
||||
# Track number
|
||||
lines.append(" %02d:" % trackResult.number)
|
||||
lines.append(" %d:" % trackResult.number)
|
||||
|
||||
# Filename (including path) of ripped track
|
||||
lines.append(" Filename: %s" % trackResult.filename)
|
||||
|
||||
Binary file not shown.
@@ -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], 4)
|
||||
self.assertEqual(responses[1].confidences[1], 4)
|
||||
self.assertEqual(responses[1].confidences[0], 5)
|
||||
self.assertEqual(responses[1].confidences[1], 5)
|
||||
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': 4,
|
||||
'DBConfidence': 5,
|
||||
},
|
||||
'DBMaxConfidence': 12,
|
||||
'DBMaxConfidenceCRC': '284fc705',
|
||||
@@ -217,7 +217,7 @@ class TestVerifyResult(TestCase):
|
||||
'v2': {
|
||||
'CRC': 'dd97d2c3',
|
||||
'DBCRC': 'dd97d2c3',
|
||||
'DBConfidence': 4,
|
||||
'DBConfidence': 5,
|
||||
},
|
||||
'DBMaxConfidence': 20,
|
||||
'DBMaxConfidenceCRC': '9cc1f32e',
|
||||
|
||||
@@ -117,7 +117,7 @@ class MetadataTestCase(unittest.TestCase):
|
||||
check the received metadata for artists tagged with [unknown]
|
||||
and artists tagged with an alias in MusicBrainz
|
||||
|
||||
see https://github.com/JoeLametta/whipper/issues/155
|
||||
see https://github.com/whipper-team/whipper/issues/155
|
||||
"""
|
||||
filename = 'whipper.release.38b05c7d-65fe-4dc0-9c10-33a391b86703.json'
|
||||
path = os.path.join(os.path.dirname(__file__), filename)
|
||||
|
||||
Reference in New Issue
Block a user