diff --git a/.github/config.yml b/.github/config.yml new file mode 100644 index 0000000..fa7be91 --- /dev/null +++ b/.github/config.yml @@ -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! 🎉🎉🎉 diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000..bf20bee --- /dev/null +++ b/.github/stale.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index ef0e599..63d66a7 100644 --- a/CHANGELOG.md +++ b/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)) diff --git a/COVERAGE b/COVERAGE index 9bc7dcb..c51cd12 100644 --- a/COVERAGE +++ b/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% diff --git a/Dockerfile b/Dockerfile index 0d79d0d..ae6509b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 \ diff --git a/README.md b/README.md index e7f2ff3..ec68d98 100644 --- a/README.md +++ b/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. diff --git a/TODO b/TODO index 9814e7d..3e2b835 100644 --- a/TODO +++ b/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. diff --git a/setup.py b/setup.py index 3ea6a12..8d891d3 100644 --- a/setup.py +++ b/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={ diff --git a/whipper/__init__.py b/whipper/__init__.py index d795e3f..e06df53 100644 --- a/whipper/__init__.py +++ b/whipper/__init__.py @@ -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: diff --git a/whipper/command/accurip.py b/whipper/command/accurip.py index 1859fdb..e12f6c8 100644 --- a/whipper/command/accurip.py +++ b/whipper/command/accurip.py @@ -18,8 +18,6 @@ # You should have received a copy of the GNU General Public License # along with whipper. If not, see . -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): diff --git a/whipper/command/cd.py b/whipper/command/cd.py index 502709f..e47b80e 100644 --- a/whipper/command/cd.py +++ b/whipper/command/cd.py @@ -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) diff --git a/whipper/command/drive.py b/whipper/command/drive.py index 0046034..0860cec 100644 --- a/whipper/command/drive.py +++ b/whipper/command/drive.py @@ -18,8 +18,6 @@ # You should have received a copy of the GNU General Public License # along with whipper. If not, see . -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): diff --git a/whipper/command/main.py b/whipper/command/main.py index 50b3f60..adaec7b 100644 --- a/whipper/command/main.py +++ b/whipper/command/main.py @@ -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 diff --git a/whipper/command/offset.py b/whipper/command/offset.py index d7f7e24..9fe620c 100644 --- a/whipper/command/offset.py +++ b/whipper/command/offset.py @@ -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) diff --git a/whipper/common/accurip.py b/whipper/common/accurip.py index 3b64c85..9085bd5 100644 --- a/whipper/common/accurip.py +++ b/whipper/common/accurip.py @@ -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]' % ( diff --git a/whipper/common/cache.py b/whipper/common/cache.py index d57cb04..5305ed3 100644 --- a/whipper/common/cache.py +++ b/whipper/common/cache.py @@ -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 diff --git a/whipper/common/common.py b/whipper/common/common.py index 9bd20de..1b46d4e 100644 --- a/whipper/common/common.py +++ b/whipper/common/common.py @@ -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 diff --git a/whipper/common/config.py b/whipper/common/config.py index 2b7f7cb..8d10935 100644 --- a/whipper/common/config.py +++ b/whipper/common/config.py @@ -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']: diff --git a/whipper/common/drive.py b/whipper/common/drive.py index ef5281c..37950fd 100644 --- a/whipper/common/drive.py +++ b/whipper/common/drive.py @@ -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() diff --git a/whipper/common/mbngs.py b/whipper/common/mbngs.py index d0d18e0..aaab4c1 100644 --- a/whipper/common/mbngs.py +++ b/whipper/common/mbngs.py @@ -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: diff --git a/whipper/common/program.py b/whipper/common/program.py index 78790b4..08d892a 100644 --- a/whipper/common/program.py +++ b/whipper/common/program.py @@ -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) diff --git a/whipper/common/task.py b/whipper/common/task.py index f9c39cc..5e61933 100644 --- a/whipper/common/task.py +++ b/whipper/common/task.py @@ -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() diff --git a/whipper/extern/task/task.py b/whipper/extern/task/task.py index 3c2b4a6..250ccd6 100644 --- a/whipper/extern/task/task.py +++ b/whipper/extern/task/task.py @@ -18,6 +18,7 @@ # You should have received a copy of the GNU General Public License # along with whipper. If not, see . +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) diff --git a/whipper/image/cue.py b/whipper/image/cue.py index 2edee6f..4088c14 100644 --- a/whipper/image/cue.py +++ b/whipper/image/cue.py @@ -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()): diff --git a/whipper/image/image.py b/whipper/image/image.py index be71127..93120cc 100644 --- a/whipper/image/image.py +++ b/whipper/image/image.py @@ -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): diff --git a/whipper/image/table.py b/whipper/image/table.py index 387a5e1..1d03f96 100644 --- a/whipper/image/table.py +++ b/whipper/image/table.py @@ -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 diff --git a/whipper/image/toc.py b/whipper/image/toc.py index f327b5c..be5e521 100644 --- a/whipper/image/toc.py +++ b/whipper/image/toc.py @@ -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 diff --git a/whipper/program/arc.py b/whipper/program/arc.py index b5f41ad..e112d31 100644 --- a/whipper/program/arc.py +++ b/whipper/program/arc.py @@ -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: diff --git a/whipper/program/cdparanoia.py b/whipper/program/cdparanoia.py index 07f2e69..74871a4 100644 --- a/whipper/program/cdparanoia.py +++ b/whipper/program/cdparanoia.py @@ -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) diff --git a/whipper/program/cdrdao.py b/whipper/program/cdrdao.py index e0da15c..148c9aa 100644 --- a/whipper/program/cdrdao.py +++ b/whipper/program/cdrdao.py @@ -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.*) - \(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(): diff --git a/whipper/program/sox.py b/whipper/program/sox.py index 1d40875..1ec54d3 100644 --- a/whipper/program/sox.py +++ b/whipper/program/sox.py @@ -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 diff --git a/whipper/result/logger.py b/whipper/result/logger.py index 9950363..eda4af0 100644 --- a/whipper/result/logger.py +++ b/whipper/result/logger.py @@ -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) diff --git a/whipper/test/dBAR-002-0000f21c-00027ef8-05021002.bin b/whipper/test/dBAR-002-0000f21c-00027ef8-05021002.bin index 6ff761c..8c804e6 100644 Binary files a/whipper/test/dBAR-002-0000f21c-00027ef8-05021002.bin and b/whipper/test/dBAR-002-0000f21c-00027ef8-05021002.bin differ diff --git a/whipper/test/test_common_accurip.py b/whipper/test/test_common_accurip.py index b28193c..ec6fe8a 100644 --- a/whipper/test/test_common_accurip.py +++ b/whipper/test/test_common_accurip.py @@ -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', diff --git a/whipper/test/test_common_mbngs.py b/whipper/test/test_common_mbngs.py index 8ca401c..231fba7 100644 --- a/whipper/test/test_common_mbngs.py +++ b/whipper/test/test_common_mbngs.py @@ -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)