diff --git a/.github/config.yml b/.github/config.yml deleted file mode 100644 index fa7be91..0000000 --- a/.github/config.yml +++ /dev/null @@ -1,19 +0,0 @@ -# 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 deleted file mode 100644 index bf20bee..0000000 --- a/.github/stale.yml +++ /dev/null @@ -1,42 +0,0 @@ -# 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/.github/workflows/greetings.yml b/.github/workflows/greetings.yml new file mode 100644 index 0000000..021d5fa --- /dev/null +++ b/.github/workflows/greetings.yml @@ -0,0 +1,16 @@ +name: Greetings + +on: [pull_request, issues] + +jobs: + greeting: + runs-on: ubuntu-latest + steps: + - uses: actions/first-interaction@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + issue-message: | + 👋 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). + pr-message: '💖 Thanks for opening your first pull request here! 💖' diff --git a/.travis.yml b/.travis.yml index 8d102eb..7f5eae0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ sudo: required language: python python: - - "2.7" + - "3.5" virtualenv: system_site_packages: false diff --git a/CHANGELOG.md b/CHANGELOG.md index ef10476..73c916f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,38 @@ ## [Unreleased](https://github.com/whipper-team/whipper/tree/HEAD) -[Full Changelog](https://github.com/whipper-team/whipper/compare/v0.8.0...HEAD) +[Full Changelog](https://github.com/whipper-team/whipper/compare/v0.9.0...HEAD) + +## [v0.9.0](https://github.com/whipper-team/whipper/tree/v0.9.0) (2019-11-04) + +[Full Changelog](https://github.com/whipper-team/whipper/compare/v0.8.0...v0.9.0) + + +**Fixed bugs:** + +- Fix regression introduced due to Python 3 port [\#424](https://github.com/whipper-team/whipper/issues/424) +- Properly tagging releases on dockerhub [\#423](https://github.com/whipper-team/whipper/issues/423) +- Test failure when building a release [\#420](https://github.com/whipper-team/whipper/issues/420) +- Dockerfile is missing ruamel.yaml [\#419](https://github.com/whipper-team/whipper/issues/419) +- Port to Python 3 [\#78](https://github.com/whipper-team/whipper/issues/78) + +**Closed issues:** + +- Why is CD-Text if found not used for naming Disk and Tracks? [\#397](https://github.com/whipper-team/whipper/issues/397) + +**Merged pull requests:** + +- Python 3 port [\#411](https://github.com/whipper-team/whipper/pull/411) ([ddevault](https://github.com/ddevault)) ## [v0.8.0](https://github.com/whipper-team/whipper/tree/v0.8.0) (2019-10-27) + [Full Changelog](https://github.com/whipper-team/whipper/compare/v0.7.3...v0.8.0) +**Implemented enhancements:** + +- Include MusicBrainz Release ID in the log file [\#381](https://github.com/whipper-team/whipper/issues/381) +- Specify supported version\(s\) of Python in setup.py [\#378](https://github.com/whipper-team/whipper/pull/378) ([Freso](https://github.com/Freso)) + **Fixed bugs:** - whipper bails out if MusicBrainz release group doesn’t have a type [\#396](https://github.com/whipper-team/whipper/issues/396) @@ -22,10 +49,7 @@ - Separate out Release in log into two value map [\#416](https://github.com/whipper-team/whipper/issues/416) - Network issue [\#412](https://github.com/whipper-team/whipper/issues/412) - RequestsDependencyWarning: urllib3 \(1.25.2\) or chardet \(3.0.4\) doesn't match a supported version [\#400](https://github.com/whipper-team/whipper/issues/400) -- Run script after rip [\#394](https://github.com/whipper-team/whipper/issues/394) - Add git/mercurial dependency to the README [\#386](https://github.com/whipper-team/whipper/issues/386) -- Include MusicBrainz Release ID in the log file [\#381](https://github.com/whipper-team/whipper/issues/381) -- Rip while entering MusicBrainz data [\#360](https://github.com/whipper-team/whipper/issues/360) - Doesn't eject - "eject: unable to eject" \(but manual eject works\) [\#355](https://github.com/whipper-team/whipper/issues/355) - Note in the whipper output/log if development version was used [\#337](https://github.com/whipper-team/whipper/issues/337) - fedora 29, whipper 0.72, Error While Executing Any Command [\#332](https://github.com/whipper-team/whipper/issues/332) @@ -48,7 +72,6 @@ - Use eject value of the class again [\#391](https://github.com/whipper-team/whipper/pull/391) ([gorgobacka](https://github.com/gorgobacka)) - Convert documentation from epydoc to reStructuredText [\#387](https://github.com/whipper-team/whipper/pull/387) ([JoeLametta](https://github.com/JoeLametta)) - Include MusicBrainz Release URL in log output [\#382](https://github.com/whipper-team/whipper/pull/382) ([Freso](https://github.com/Freso)) -- Specify supported version\(s\) of Python in setup.py [\#378](https://github.com/whipper-team/whipper/pull/378) ([Freso](https://github.com/Freso)) - Fix critical regressions introduced in 3e79032 and 16b0d8d [\#371](https://github.com/whipper-team/whipper/pull/371) ([JoeLametta](https://github.com/JoeLametta)) - Use git to get whipper's version [\#370](https://github.com/whipper-team/whipper/pull/370) ([Freso](https://github.com/Freso)) - Handle artist MBIDs as multivalue tags [\#367](https://github.com/whipper-team/whipper/pull/367) ([Freso](https://github.com/Freso)) @@ -64,16 +87,14 @@ - accuraterip-checksum: convert to python C extension [\#274](https://github.com/whipper-team/whipper/pull/274) ([mtdcr](https://github.com/mtdcr)) ## [v0.7.3](https://github.com/whipper-team/whipper/tree/v0.7.3) (2018-12-14) + [Full Changelog](https://github.com/whipper-team/whipper/compare/v0.7.2...v0.7.3) **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) - 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) @@ -104,6 +125,7 @@ - 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) **Fixed bugs:** @@ -122,6 +144,7 @@ - Add AppStream metainfo.xml file [\#318](https://github.com/whipper-team/whipper/pull/318) ([Freso](https://github.com/Freso)) ## [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) **Fixed bugs:** @@ -139,12 +162,8 @@ - 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:** @@ -164,26 +183,25 @@ - Add Dockerfile [\#237](https://github.com/whipper-team/whipper/pull/237) ([thomas-mc-work](https://github.com/thomas-mc-work)) ## [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:** + +- Simple message while reading TOC [\#257](https://github.com/whipper-team/whipper/issues/257) + **Fixed bugs:** - 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) **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) - 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:** @@ -199,13 +217,19 @@ - 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:** + +- Declare supported Python version [\#152](https://github.com/whipper-team/whipper/issues/152) + **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) +- cdparanoia toc does not agree with cdrdao-toc, cd-paranoia also reports different \(but better\) lengths [\#175](https://github.com/whipper-team/whipper/issues/175) - 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) @@ -217,18 +241,12 @@ **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) - 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) @@ -258,8 +276,10 @@ - Add flake8 testing to CI [\#151](https://github.com/whipper-team/whipper/pull/151) ([Freso](https://github.com/Freso)) - Clean up files in misc/ [\#150](https://github.com/whipper-team/whipper/pull/150) ([Freso](https://github.com/Freso)) - Update .gitignore [\#148](https://github.com/whipper-team/whipper/pull/148) ([Freso](https://github.com/Freso)) +- Fix references to morituri. [\#109](https://github.com/whipper-team/whipper/pull/109) ([Freso](https://github.com/Freso)) ## [v0.5.1](https://github.com/whipper-team/whipper/tree/v0.5.1) (2017-04-24) + [Full Changelog](https://github.com/whipper-team/whipper/compare/v0.5.0...v0.5.1) **Fixed bugs:** @@ -267,6 +287,7 @@ - 0.5.0 Release init.py version number not updated [\#147](https://github.com/whipper-team/whipper/issues/147) ## [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) **Fixed bugs:** @@ -283,13 +304,9 @@ - `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:** @@ -304,6 +321,7 @@ - Replace rip command suggestions with 'whipper' [\#114](https://github.com/whipper-team/whipper/pull/114) ([JoeLametta](https://github.com/JoeLametta)) ## [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) **Fixed bugs:** @@ -321,6 +339,7 @@ - Update links to Arch Linux AUR packages in README. [\#103](https://github.com/whipper-team/whipper/pull/103) ([Freso](https://github.com/Freso)) ## [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) **Closed issues:** @@ -330,7 +349,6 @@ **Merged pull requests:** -- Fix references to morituri. [\#109](https://github.com/whipper-team/whipper/pull/109) ([Freso](https://github.com/Freso)) - Small cleanups of setup.py [\#102](https://github.com/whipper-team/whipper/pull/102) ([Freso](https://github.com/Freso)) - Persist False value for defeats\_cache correctly [\#98](https://github.com/whipper-team/whipper/pull/98) ([ribbons](https://github.com/ribbons)) - Update suggested commands given by `drive list` [\#97](https://github.com/whipper-team/whipper/pull/97) ([ribbons](https://github.com/ribbons)) @@ -347,6 +365,7 @@ - Use soxi instead of gstreamer to determine a track's length [\#67](https://github.com/whipper-team/whipper/pull/67) ([chrysn](https://github.com/chrysn)) ## [v0.4.0](https://github.com/whipper-team/whipper/tree/v0.4.0) (2016-11-08) + [Full Changelog](https://github.com/whipper-team/whipper/compare/v0.3.0...v0.4.0) **Fixed bugs:** @@ -354,18 +373,12 @@ - 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)) - do not recalculate musicbrainz disc id for every getMusicBrainzDiscId… [\#69](https://github.com/whipper-team/whipper/pull/69) ([RecursiveForest](https://github.com/RecursiveForest)) - Directory [\#62](https://github.com/whipper-team/whipper/pull/62) ([RecursiveForest](https://github.com/RecursiveForest)) - undelete overzealously removed plugin initialisation [\#61](https://github.com/whipper-team/whipper/pull/61) ([RecursiveForest](https://github.com/RecursiveForest)) -- Readme rewrite [\#60](https://github.com/whipper-team/whipper/pull/60) ([RecursiveForest](https://github.com/RecursiveForest)) - README.md: drop executable flag [\#55](https://github.com/whipper-team/whipper/pull/55) ([chrysn](https://github.com/chrysn)) - nuke-autohell [\#54](https://github.com/whipper-team/whipper/pull/54) ([RecursiveForest](https://github.com/RecursiveForest)) - standardise program/sox.py formatting, add test case, docstring [\#53](https://github.com/whipper-team/whipper/pull/53) ([RecursiveForest](https://github.com/RecursiveForest)) @@ -373,6 +386,7 @@ - use setuptools, remove autohell, use raw make for src/ [\#51](https://github.com/whipper-team/whipper/pull/51) ([RecursiveForest](https://github.com/RecursiveForest)) ## [v0.3.0](https://github.com/whipper-team/whipper/tree/v0.3.0) (2016-10-17) + [Full Changelog](https://github.com/whipper-team/whipper/compare/v0.2.4...v0.3.0) **Fixed bugs:** @@ -380,19 +394,19 @@ - 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)) - Fast accuraterip checksum [\#37](https://github.com/whipper-team/whipper/pull/37) ([MerlijnWajer](https://github.com/MerlijnWajer)) ## [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) + **Fixed bugs:** - whipper fails to build on bash-compgen [\#25](https://github.com/whipper-team/whipper/issues/25) @@ -407,8 +421,6 @@ - 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) @@ -428,15 +440,21 @@ - Fork [\#6](https://github.com/whipper-team/whipper/pull/6) ([abendebury](https://github.com/abendebury)) ## [v0.2.3](https://github.com/whipper-team/whipper/tree/v0.2.3) (2014-07-16) + [Full Changelog](https://github.com/whipper-team/whipper/compare/v0.2.2...v0.2.3) ## [v0.2.2](https://github.com/whipper-team/whipper/tree/v0.2.2) (2013-07-30) + [Full Changelog](https://github.com/whipper-team/whipper/compare/v0.2.1...v0.2.2) ## [v0.2.1](https://github.com/whipper-team/whipper/tree/v0.2.1) (2013-07-15) + [Full Changelog](https://github.com/whipper-team/whipper/compare/v0.2.0...v0.2.1) ## [v0.2.0](https://github.com/whipper-team/whipper/tree/v0.2.0) (2013-01-20) +[Full Changelog](https://github.com/whipper-team/whipper/compare/20421488be8a82606f7ae82a16c9d8bc015b9e01...v0.2.0) -\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* + + +\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* diff --git a/COVERAGE b/COVERAGE index 1014e46..a30a425 100644 --- a/COVERAGE +++ b/COVERAGE @@ -1,4 +1,4 @@ -Coverage.py 4.5.4 text report against whipper v0.8.0 +Coverage.py 4.5.4 text report against whipper v0.9.0 $ coverage run --branch --omit='whipper/test/*' --source=whipper -m unittest discover $ coverage report -m @@ -10,46 +10,46 @@ whipper/__main__.py 7 7 2 0 0% 4-14 whipper/command/__init__.py 0 0 0 0 100% whipper/command/accurip.py 41 41 18 0 0% 21-90 whipper/command/basecommand.py 69 29 30 8 53% 70, 72, 76, 82-88, 98-102, 107-114, 127, 129, 133, 139, 142-145, 68->70, 71->72, 75->76, 80->82, 96->98, 106->107, 126->127, 128->129 -whipper/command/cd.py 227 189 60 0 13% 72-80, 85-196, 199, 212, 236-288, 295-322, 325-496 +whipper/command/cd.py 227 189 60 0 13% 72-80, 85-196, 199, 212, 236-288, 295-321, 324-493 whipper/command/drive.py 57 57 10 0 0% 21-107 -whipper/command/image.py 38 38 6 0 0% 21-76 -whipper/command/main.py 68 68 22 0 0% 4-116 +whipper/command/image.py 37 37 6 0 0% 21-75 +whipper/command/main.py 68 68 24 0 0% 4-116 whipper/command/mblookup.py 29 3 8 2 86% 21-23, 35->37, 37->28 whipper/command/offset.py 110 110 32 0 0% 21-219 whipper/common/__init__.py 0 0 0 0 100% -whipper/common/accurip.py 132 5 54 5 95% 119, 125, 134-136, 114->119, 120->125, 150->153, 241->247, 250->256 -whipper/common/cache.py 104 49 34 6 44% 66-90, 96, 99, 108-111, 114-115, 131, 143-147, 170-177, 201-206, 211-227, 95->96, 98->99, 130->131, 141->151, 142->143, 169->170 +whipper/common/accurip.py 132 5 62 4 95% 118, 124, 133-135, 113->118, 119->124, 241->247, 251->257 +whipper/common/cache.py 100 48 34 5 44% 66-90, 96, 99, 107-110, 113-114, 138-142, 165-172, 196-201, 206-222, 95->96, 98->99, 136->146, 137->138, 164->165 whipper/common/checksum.py 26 14 2 0 43% 41-42, 45-46, 49-64 whipper/common/common.py 150 28 38 6 78% 51-52, 119-120, 143-144, 162-169, 181, 274-279, 286-291, 328-332, 118->119, 131->134, 180->181, 190->197, 271->274, 326->334 -whipper/common/config.py 91 8 18 4 89% 105-106, 124-125, 131, 141, 143, 145, 130->131, 140->141, 142->143, 144->145 -whipper/common/directory.py 21 8 10 2 55% 29, 39, 44-51, 28->29, 38->39 -whipper/common/drive.py 31 20 6 0 35% 35-40, 44-50, 54-60, 64-71 +whipper/common/config.py 90 8 18 4 89% 104-105, 123-124, 130, 140, 142, 144, 129->130, 139->140, 141->142, 143->144 +whipper/common/directory.py 18 5 4 0 68% 42-48 +whipper/common/drive.py 31 20 8 0 33% 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 174 52 66 7 70% 38-39, 45, 93-99, 174-175, 180-181, 227, 233, 258-260, 269, 289-344, 159->158, 173->174, 179->180, 226->227, 232->233, 257->258, 266->269 -whipper/common/path.py 24 0 8 3 91% 42->45, 52->57, 62->67 -whipper/common/program.py 346 268 112 5 19% 85-87, 93-104, 113-147, 156-161, 164, 169-173, 218, 229-230, 232-236, 253-268, 276-387, 398-456, 464-472, 476-491, 502-542, 554-571, 574-592, 595-605, 608-616, 76->79, 215->218, 228->229, 231->232, 238->242 +whipper/common/path.py 24 0 8 3 91% 42->45, 52->56, 60->65 +whipper/common/program.py 345 267 117 5 19% 85-87, 93-104, 113-147, 156-161, 164, 169-173, 218, 229-230, 232-236, 253-268, 276-386, 397-455, 463-471, 475-490, 501-540, 552-569, 572-590, 593-603, 606-614, 76->79, 215->218, 228->229, 231->232, 238->242 whipper/common/renamer.py 102 2 16 1 97% 133, 156, 58->66 -whipper/common/task.py 77 19 14 2 75% 47-52, 86-87, 91-94, 102, 115-116, 123, 129, 135, 141, 147, 84->86, 99->102 +whipper/common/task.py 77 15 14 2 79% 47-52, 86-87, 102, 115-116, 123, 129, 135, 141, 147, 84->86, 99->102 whipper/extern/__init__.py 0 0 0 0 100% -whipper/extern/asyncsub.py 130 71 66 12 40% 15-17, 32, 37-38, 47-84, 89-102, 115, 122, 134, 145, 151, 156-160, 164-176, 14->15, 35->37, 45->47, 110->113, 114->115, 121->122, 133->134, 139->141, 141->152, 144->145, 148->151, 163->164 -whipper/extern/freedb.py 104 83 38 1 17% 49, 57-58, 61, 64, 84-163, 171-223, 56->57 +whipper/extern/asyncsub.py 112 55 58 11 46% 15-17, 32, 37-38, 47-84, 89-102, 115, 122, 134, 145, 151, 14->15, 35->37, 45->47, 110->113, 114->115, 121->122, 133->134, 139->141, 141->152, 144->145, 148->151 +whipper/extern/freedb.py 90 72 42 0 17% 46, 54, 74-153, 160-199 whipper/extern/task/__init__.py 0 0 0 0 100% -whipper/extern/task/task.py 271 115 54 11 53% 54, 58, 79, 87, 153-155, 174-176, 184-200, 218-221, 242-243, 284-285, 288-294, 309-310, 318-320, 329-336, 342-359, 363, 366, 373-390, 401-402, 405-408, 412, 415, 430, 433-435, 451, 463, 509-514, 521-526, 535-543, 546-554, 557-558, 566, 571-573, 53->54, 57->58, 66->68, 152->153, 166->exit, 217->218, 231->233, 236->exit, 498->500, 532->535, 570->571 +whipper/extern/task/task.py 270 115 56 11 53% 53, 59, 78, 86, 152-154, 173-175, 183-199, 217-220, 241-242, 283-284, 287-293, 308-309, 317-319, 328-335, 341-358, 362, 365, 372-389, 400-401, 404-407, 411, 414, 429, 432-434, 450, 462, 508-513, 520-525, 534-542, 545-553, 556-557, 565, 570-572, 52->53, 56->59, 65->67, 151->152, 165->exit, 216->217, 230->232, 235->exit, 497->499, 531->534, 569->570 whipper/image/__init__.py 0 0 0 0 100% -whipper/image/cue.py 91 9 20 3 89% 99, 116-117, 132-134, 159, 187, 205, 98->99, 115->116, 131->132 +whipper/image/cue.py 91 9 20 3 89% 98, 115-116, 131-133, 158, 186, 204, 97->98, 114->115, 130->131 whipper/image/image.py 116 93 18 0 17% 49-57, 65-67, 74-107, 121-154, 157-173, 184-214 -whipper/image/table.py 395 18 114 16 93% 238, 497, 576, 661-662, 682-683, 692-695, 746, 792-793, 795-796, 840-841, 846-848, 181->184, 496->497, 530->534, 553->556, 575->576, 583->590, 681->682, 690->696, 691->692, 720->724, 724->719, 745->746, 791->792, 794->795, 839->840, 845->846 -whipper/image/toc.py 203 16 60 10 90% 134, 261-262, 278-281, 339-341, 363-365, 385, 409, 439, 130->134, 212->220, 260->261, 277->278, 287->292, 323->330, 338->339, 362->363, 372->376, 404->409 +whipper/image/table.py 394 18 120 16 93% 240, 499, 578, 663-664, 684-685, 694-697, 748, 794-795, 797-798, 842-843, 848-850, 180->183, 498->499, 532->536, 555->558, 577->578, 585->592, 683->684, 692->698, 693->694, 722->726, 726->721, 747->748, 793->794, 796->797, 841->842, 847->848 +whipper/image/toc.py 203 16 60 10 90% 133, 260-261, 277-280, 338-340, 362-364, 384, 408, 438, 129->133, 211->219, 259->260, 276->277, 286->291, 322->329, 337->338, 361->362, 371->375, 403->408 whipper/program/__init__.py 0 0 0 0 100% whipper/program/arc.py 3 0 0 0 100% -whipper/program/cdparanoia.py 309 180 78 2 39% 48-50, 59-60, 124-126, 198-199, 239-253, 256-306, 309-347, 350-354, 357-393, 447-500, 505-552, 586-589, 592, 599, 607-612, 123->124, 598->599 -whipper/program/cdrdao.py 114 75 34 2 28% 33-58, 80-86, 90-105, 108-137, 140-144, 147-161, 168-171, 181-183, 187-189, 180->181, 186->187 +whipper/program/cdparanoia.py 307 179 78 2 39% 48-50, 59-60, 124-126, 198-199, 239-253, 256-306, 309-347, 350-354, 357-393, 447-499, 504-551, 585-588, 591, 598, 606-611, 123->124, 597->598 +whipper/program/cdrdao.py 113 74 32 2 28% 33-58, 80-86, 90-105, 108-137, 140-144, 147-160, 167-170, 180-182, 186-188, 179->180, 185->186 whipper/program/flac.py 9 5 0 0 44% 12-19 whipper/program/sox.py 17 4 4 2 71% 18-19, 23-24, 17->18, 22->23 -whipper/program/soxi.py 28 2 2 1 90% 36, 49, 48->49 +whipper/program/soxi.py 28 2 4 1 91% 36, 49, 48->49 whipper/program/utils.py 23 16 2 0 28% 12-17, 25-31, 42-47 whipper/result/__init__.py 0 0 0 0 100% whipper/result/logger.py 144 23 40 16 78% 68, 84-92, 112, 123, 128, 130, 134-135, 143, 202, 240, 244-245, 252-253, 67->68, 83->84, 111->112, 122->123, 127->128, 129->130, 133->134, 142->143, 201->202, 213->217, 217->222, 222->226, 226->230, 234->244, 236->240, 249->252 whipper/result/result.py 57 13 6 0 70% 115-119, 137, 148-149, 158-165 ----------------------------------------------------------------------------- -TOTAL 3997 1766 1108 129 53% +TOTAL 3950 1727 1123 123 53% diff --git a/Dockerfile b/Dockerfile index 998a40e..7a89763 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,53 +1,73 @@ FROM debian:buster -RUN apt-get update \ - && apt-get install -y cdrdao git python-gobject-2 python-musicbrainzngs python-mutagen \ - python-setuptools python-requests libsndfile1-dev flac sox \ - libiso9660-dev python-pip swig make pkgconf \ - eject locales \ - autoconf libtool curl \ - && pip install pycdio==2.1.0 +RUN apt-get update && apt-get install --no-install-recommends -y \ + autoconf \ + automake \ + cdrdao \ + bzip2 \ + curl \ + eject \ + flac \ + gir1.2-glib-2.0 \ + git \ + libiso9660-dev \ + libsndfile1-dev \ + libtool \ + locales \ + make \ + pkgconf \ + python3-dev \ + python3-gi \ + python3-musicbrainzngs \ + python3-mutagen \ + python3-pip \ + python3-requests \ + python3-ruamel.yaml \ + python3-setuptools \ + sox \ + swig \ + && apt-get clean && rm -rf /var/lib/apt/lists/* \ + && pip3 --no-cache-dir install pycdio==2.1.0 # libcdio-paranoia / libcdio-utils are wrongfully packaged in Debian, thus built manually # see https://github.com/whipper-team/whipper/pull/237#issuecomment-367985625 RUN curl -o - 'https://ftp.gnu.org/gnu/libcdio/libcdio-2.1.0.tar.bz2' | tar jxf - \ - && cd libcdio-2.1.0 \ - && autoreconf -fi \ - && ./configure --disable-dependency-tracking --disable-cxx --disable-example-progs --disable-static \ - && make install \ - && cd .. \ - && rm -rf libcdio-2.1.0 + && cd libcdio-2.1.0 \ + && autoreconf -fi \ + && ./configure --disable-dependency-tracking --disable-cxx --disable-example-progs --disable-static \ + && make install \ + && cd .. \ + && rm -rf libcdio-2.1.0 # Install cd-paranoia from tarball RUN curl -o - 'https://ftp.gnu.org/gnu/libcdio/libcdio-paranoia-10.2+2.0.0.tar.bz2' | tar jxf - \ - && cd libcdio-paranoia-10.2+2.0.0 \ - && autoreconf -fi \ - && ./configure --disable-dependency-tracking --disable-example-progs --disable-static \ - && make install \ - && cd .. \ - && rm -rf libcdio-paranoia-10.2+2.0.0 + && cd libcdio-paranoia-10.2+2.0.0 \ + && autoreconf -fi \ + && ./configure --disable-dependency-tracking --disable-example-progs --disable-static \ + && make install \ + && cd .. \ + && rm -rf libcdio-paranoia-10.2+2.0.0 RUN ldconfig # add user RUN useradd -m worker -G cdrom \ - && mkdir -p /output /home/worker/.config/whipper \ - && chown worker: /output /home/worker/.config/whipper + && mkdir -p /output /home/worker/.config/whipper \ + && chown worker: /output /home/worker/.config/whipper VOLUME ["/home/worker/.config/whipper", "/output"] # setup locales + cleanup RUN echo "LC_ALL=en_US.UTF-8" >> /etc/environment \ - && echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen \ - && echo "LANG=en_US.UTF-8" > /etc/locale.conf \ - && locale-gen en_US.UTF-8 \ - && apt-get clean && apt-get autoremove -y + && echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen \ + && echo "LANG=en_US.UTF-8" > /etc/locale.conf \ + && locale-gen en_US.UTF-8 # install whipper RUN mkdir /whipper COPY . /whipper/ -RUN cd /whipper && python2 setup.py install \ - && rm -rf /whipper \ - && whipper -v +RUN cd /whipper && python3 setup.py install \ + && rm -rf /whipper \ + && whipper -v ENV LC_ALL=en_US.UTF-8 ENV LANG=en_US diff --git a/README.md b/README.md index 4c5e599..5c77e5b 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,12 @@ [![GitHub Issues](https://img.shields.io/github/issues/whipper-team/whipper.svg)](https://github.com/whipper-team/whipper/issues) [![GitHub contributors](https://img.shields.io/github/contributors/whipper-team/whipper.svg)](https://github.com/whipper-team/whipper/graphs/contributors) -Whipper is a Python 2.7 CD-DA ripper based on the [morituri project](https://github.com/thomasvs/morituri) (_CDDA ripper for *nix systems aiming for accuracy over speed_). It started just as a fork of morituri - which development seems to have halted - merging old ignored pull requests, improving it with bugfixes and new features. Nowadays whipper's codebase diverges significantly from morituri's one. +Whipper is a Python 3 (3.5+) CD-DA ripper based on the [morituri project](https://github.com/thomasvs/morituri) (_CDDA ripper for *nix systems aiming for accuracy over speed_). It started just as a fork of morituri - which development seems to have halted - merging old ignored pull requests, improving it with bugfixes and new features. Nowadays whipper's codebase diverges significantly from morituri's one. Whipper is currently developed and tested _only_ on Linux distributions but _may_ work fine on other *nix OSes too. In order to track whipper's latest changes it's advised to check its commit history (README and [CHANGELOG](#changelog) files may not be comprehensive). -We've nearly completed porting the codebase to Python 3 (Python 2 won't be supported anymore in future releases). If you would like to follow the progress of the port e/o help us with it, please check [pull request #411](https://github.com/whipper-team/whipper/pull/411). - ## Table of content - [Rationale](#rationale) @@ -27,8 +25,7 @@ We've nearly completed porting the codebase to Python 3 (Python 2 won't be suppo - [Building](#building) 1. [Required dependencies](#required-dependencies) 2. [Fetching the source code](#fetching-the-source-code) - 3. [Building the bundled dependencies](#building-the-bundled-dependencies) - 4. [Finalizing the build](#finalizing-the-build) + 3. [Finalizing the build](#finalizing-the-build) - [Usage](#usage) - [Getting started](#getting-started) - [Configuration file documentation](#configuration-file-documentation) @@ -75,7 +72,7 @@ Whipper still isn't available as an official package in every Linux distribution ### Docker -You can easily install whipper without needing to care about the required dependencies by making use of the automatically built images hosted on Docker Hub: +You can easily install whipper without needing to care about the required dependencies by making use of the automatically built images hosted on [Docker Hub](https://hub.docker.com/r/whipperteam/whipper): `docker pull whipperteam/whipper` @@ -96,7 +93,7 @@ You should put this e.g. into your `.bash_aliases`. Also keep in mind to substit Make sure you create the configuration directory: -`mkdir -p ~/.config/whipper ${PWD}/output` +`mkdir -p ~/.config/whipper "${PWD}"/output` Finally you can test the correct installation: @@ -111,7 +108,7 @@ This is a noncomprehensive summary which shows whipper's packaging status (unoff [![Packaging status](https://repology.org/badge/vertical-allrepos/whipper.svg)](https://repology.org/metapackage/whipper) -Someone also packaged whipper as snap: [unofficial snap on snapcraft](https://snapcraft.io/whipper). +There's also an [unoffical snap package on snapcraft](https://snapcraft.io/whipper). In case you decide to install whipper using an unofficial repository just keep in mind it is your responsibility to verify that the provided content is safe to use. @@ -123,33 +120,34 @@ 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 +- [cd-paranoia](https://github.com/rocky/libcdio-paranoia), for the actual ripping - To avoid bugs it's advised to use `cd-paranoia` versions ≥ **10.2+0.94+2-2** - The package named `libcdio-utils`, available on Debian and Ubuntu, is affected by a bug (except for Debian testing/sid): it doesn't include the `cd-paranoia` binary (needed by whipper). For more details see: [#888053 (Debian)](https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=888053), [#889803 (Debian)](https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=889803) and [#1750264 (Ubuntu)](https://bugs.launchpad.net/ubuntu/+source/libcdio/+bug/1750264). - [cdrdao](http://cdrdao.sourceforge.net/), for session, TOC, pre-gap, and ISRC extraction - [GObject Introspection](https://wiki.gnome.org/Projects/GObjectIntrospection), to provide GLib-2.0 methods used by `task.py` - [PyGObject](https://pypi.org/project/PyGObject/), required by `task.py` -- [python-musicbrainzngs](https://github.com/alastair/python-musicbrainzngs), for metadata lookup -- [python-mutagen](https://pypi.python.org/pypi/mutagen), for tagging support -- [python-setuptools](https://pypi.python.org/pypi/setuptools), for installation, plugins support -- [python-requests](https://pypi.python.org/pypi/requests), for retrieving AccurateRip database entries +- [musicbrainzngs](https://pypi.org/project/musicbrainzngs/), for metadata lookup +- [mutagen](https://pypi.python.org/pypi/mutagen), for tagging support +- [setuptools](https://pypi.python.org/pypi/setuptools), for installation, plugins support +- [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 the most recent `pycdio` version with the corresponding `libcdio` release or, if stuck to old pycdio versions, **0.20**/**0.21** with `libcdio` ≥ **0.90** ≤ **0.94**. All other combinations won't probably work. - [ruamel.yaml](https://pypi.org/project/ruamel.yaml/), for generating well formed YAML report logfiles - [libsndfile](http://www.mega-nerd.com/libsndfile/), for reading wav files - [flac](https://xiph.org/flac/), for reading flac files - [sox](http://sox.sourceforge.net/), for track peak detection +- [git](https://git-scm.com/) or [mercurial](https://www.mercurial-scm.org/) + - Required either when running whipper without installing it or when building it from its source code (code cloned from a git/mercurial repository). Some dependencies aren't available in the PyPI. They can be probably installed using your distribution's package manager: -- [cd-paranoia](https://www.gnu.org/software/libcdio/) +- [cd-paranoia](https://github.com/rocky/libcdio-paranoia) - [cdrdao](http://cdrdao.sourceforge.net/) - [GObject Introspection](https://wiki.gnome.org/Projects/GObjectIntrospection) - [libsndfile](http://www.mega-nerd.com/libsndfile/) - [flac](https://xiph.org/flac/) - [sox](http://sox.sourceforge.net/) - [git](https://git-scm.com/) or [mercurial](https://www.mercurial-scm.org/) - - Required either when running whipper without installing it or when building it from its source code (code cloned from a git/mercurial repository). PyPI installable dependencies are listed in the [requirements.txt](https://github.com/whipper-team/whipper/blob/master/requirements.txt) file and can be installed issuing the following command: @@ -164,22 +162,9 @@ git clone https://github.com/whipper-team/whipper.git cd whipper ``` -### Building the bundled dependencies - -Whipper uses and packages a slightly different version of the `accuraterip-checksum` tool: - -You can edit the install path in `config.mk` - -```bash -cd src -make -sudo make install -cd .. -``` - ### Finalizing the build -Install whipper: `python2 setup.py install` +Install whipper: `python3 setup.py install` Note that, depending on the chosen installation path, this command may require elevated rights. @@ -232,7 +217,7 @@ The configuration file is stored in `$XDG_CONFIG_HOME/whipper/whipper.conf`, or See [XDG Base Directory Specification](http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html) -and [ConfigParser](https://docs.python.org/2/library/configparser.html). +and [ConfigParser](https://docs.python.org/3/library/configparser.html). The configuration file consists of newline-delineated `[sections]` containing `key = value` pairs. The sections `[main]` and @@ -271,7 +256,7 @@ To make it easier for developers, you can run whipper straight from the source checkout: ```bash -python2 -m whipper -h +python3 -m whipper -h ``` ## Logger plugins @@ -298,8 +283,10 @@ Whipper searches for logger plugins in the following paths: 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` +- `/usr/local/lib/python3.X/dist-packages/whipper/plugins` +- `/usr/lib/python3.X/dist-packages/whipper/plugins` + +Where `X` stands for the minor version of the Python 3 release available on the system. ### Official logger plugins @@ -336,7 +323,7 @@ Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Make sure you have the latest copy from our [git repository](https://github.com/whipper-team/whipper). Where possible, please include tests for new or changed functionality. You can run tests -with `python -m unittest discover` from your source checkout. +with `python3 -m unittest discover` from your source checkout. ### Developer Certificate of Origin (DCO) @@ -410,7 +397,7 @@ gzip whipper.log And attach the gzipped log file to your bug report. -Without `WHIPPER_LOGFILE` set, logging messages will go to stderr. `WHIPPER_DEBUG` accepts a string of the [default python logging levels](https://docs.python.org/2/library/logging.html#logging-levels). +Without `WHIPPER_LOGFILE` set, logging messages will go to stderr. `WHIPPER_DEBUG` accepts a string of the [default python logging levels](https://docs.python.org/3/library/logging.html#logging-levels). ## Credits diff --git a/misc/offsets.py b/misc/offsets.py index 626f925..73fd2bc 100644 --- a/misc/offsets.py +++ b/misc/offsets.py @@ -6,13 +6,12 @@ import sys -import BeautifulSoup +from bs4 import BeautifulSoup -handle = open(sys.argv[1]) +with open(sys.argv[1]) as f: + doc = f.read() -doc = handle.read() - -soup = BeautifulSoup.BeautifulSoup(doc) +soup = BeautifulSoup(doc, features='html.parser') offsets = {} # offset -> total count @@ -50,18 +49,18 @@ for count, offset in counts: # now format it for code inclusion lines = [] -line = 'OFFSETS = "' +line = 'OFFSETS = ("' for offset in offsets: - line += offset + ", " + line += offset + ', ' if len(line) > 60: - line += "\" + \\" + line += '"' lines.append(line) - line = ' "' + line = ' "' # get last line too, trimming the comma and adding the quote if len(line) > 11: - line = line[:-2] + '"' + line = line[:-2] + '")' lines.append(line) -print("\n".join(lines)) +print('\n'.join(lines)) diff --git a/setup.py b/setup.py index d107f26..96bebf5 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( maintainer=['The Whipper Team'], url='https://github.com/whipper-team/whipper', license='GPL3', - python_requires='>=2.7,<3', + python_requires='>=3.5', packages=find_packages(), setup_requires=['setuptools_scm'], ext_modules=[ diff --git a/src/accuraterip-checksum.c b/src/accuraterip-checksum.c index ee784d9..652b385 100644 --- a/src/accuraterip-checksum.c +++ b/src/accuraterip-checksum.c @@ -147,7 +147,13 @@ static PyMethodDef accuraterip_methods[] = { { NULL, NULL, 0, NULL }, }; -PyMODINIT_FUNC initaccuraterip(void) +static struct PyModuleDef accuraterip_module = { + .m_base = PyModuleDef_HEAD_INIT, + .m_name = "accuraterip", + .m_methods = accuraterip_methods, +}; + +PyMODINIT_FUNC PyInit_accuraterip(void) { - Py_InitModule("accuraterip", accuraterip_methods); + return PyModule_Create(&accuraterip_module); } diff --git a/whipper/command/cd.py b/whipper/command/cd.py index fbfe3b7..a45fd9e 100644 --- a/whipper/command/cd.py +++ b/whipper/command/cd.py @@ -37,8 +37,8 @@ logger = logging.getLogger(__name__) SILENT = 0 MAX_TRIES = 5 -DEFAULT_TRACK_TEMPLATE = u'%r/%A - %d/%t. %a - %n' -DEFAULT_DISC_TEMPLATE = u'%r/%A - %d/%A - %d' +DEFAULT_TRACK_TEMPLATE = '%r/%A - %d/%t. %a - %n' +DEFAULT_DISC_TEMPLATE = '%r/%A - %d/%A - %d' TEMPLATE_DESCRIPTION = ''' Tracks are named according to the track template, filling in the variables @@ -137,7 +137,7 @@ class _CD(BaseCommand): if getattr(self.options, 'working_directory', False): os.chdir(os.path.expanduser(self.options.working_directory)) if hasattr(self.options, 'output_directory'): - out_bpath = self.options.output_directory.decode('utf-8') + out_bpath = self.options.output_directory # Needed to preserve cdrdao's tocfile out_fpath = self.program.getPath(out_bpath, self.options.disc_template, @@ -295,10 +295,9 @@ Log files will log the path to tracks relative to this directory. self.options.output_directory = os.path.expanduser( self.options.output_directory) - self.options.track_template = self.options.track_template.decode( - 'utf-8') + self.options.track_template = self.options.track_template validate_template(self.options.track_template, 'track') - self.options.disc_template = self.options.disc_template.decode('utf-8') + self.options.disc_template = self.options.disc_template validate_template(self.options.disc_template, 'disc') if self.options.offset is None: @@ -323,7 +322,7 @@ Log files will log the path to tracks relative to this directory. def doCommand(self): self.program.setWorkingDirectory(self.options.working_directory) - self.program.outdir = self.options.output_directory.decode('utf-8') + self.program.outdir = self.options.output_directory self.program.result.offset = int(self.options.offset) self.program.result.overread = self.options.overread self.program.result.logger = self.options.logger @@ -336,13 +335,11 @@ Log files will log the path to tracks relative to this directory. if os.path.exists(dirname): logs = glob.glob(os.path.join(dirname, '*.log')) if logs: - msg = ("output directory %s is a finished rip" % - dirname.encode('utf-8')) + msg = ("output directory %s is a finished rip" % dirname) logger.debug(msg) raise RuntimeError(msg) else: - logger.info("creating output directory %s", - dirname.encode('utf-8')) + logger.info("creating output directory %s", dirname) os.makedirs(dirname) # FIXME: turn this into a method @@ -366,7 +363,7 @@ Log files will log the path to tracks relative to this directory. logger.debug('ripIfNotRipped: path %r', path) trackResult.number = number - assert isinstance(path, unicode), "%r is not unicode" % path + assert isinstance(path, str), "%r is not str" % path trackResult.filename = path if number > 0: trackResult.pregap = self.itable.tracks[number - 1].getPregap() @@ -385,7 +382,7 @@ Log files will log the path to tracks relative to this directory. logger.info('verifying track %d of %d: %s', number, len(self.itable.tracks), - os.path.basename(path).encode('utf-8')) + os.path.basename(path)) if not self.program.verifyTrack(self.runner, trackResult): logger.warning('verification failed, reripping...') os.unlink(path) @@ -403,7 +400,7 @@ Log files will log the path to tracks relative to this directory. extra = " (try %d)" % tries logger.info('ripping track %d of %d%s: %s', number, len(self.itable.tracks), extra, - os.path.basename(path).encode('utf-8')) + os.path.basename(path)) try: logger.debug('ripIfNotRipped: track %d, try %d', number, tries) diff --git a/whipper/command/image.py b/whipper/command/image.py index 89d326e..a49b285 100644 --- a/whipper/command/image.py +++ b/whipper/command/image.py @@ -45,7 +45,6 @@ Verifies the image from the given .cue files against the AccurateRip database. runner = task.SyncRunner() for arg in self.options.cuefile: - arg = arg.decode('utf-8') cueImage = image.Image(arg) cueImage.setup(runner) diff --git a/whipper/command/mblookup.py b/whipper/command/mblookup.py index dc4fcb4..9ecbfba 100644 --- a/whipper/command/mblookup.py +++ b/whipper/command/mblookup.py @@ -17,7 +17,7 @@ Example disc id: KnpGsLhvH.lPrNc1PBL21lb9Bg4-""" def do(self): try: - discId = unicode(self.options.mbdiscid) + discId = str(self.options.mbdiscid) except IndexError: print('Please specify a MusicBrainz disc id.') return 3 @@ -29,7 +29,7 @@ Example disc id: KnpGsLhvH.lPrNc1PBL21lb9Bg4-""" print('- Release %d:' % (i + 1, )) print(' Artist: %s' % md.artist.encode('utf-8')) print(' Title: %s' % md.title.encode('utf-8')) - print(' Type: %s' % unicode(md.releaseType).encode('utf-8')) # noqa: E501 + print(' Type: %s' % str(md.releaseType).encode('utf-8')) # noqa: E501 print(' URL: %s' % md.url) print(' Tracks: %d' % len(md.tracks)) if md.catalogNumber: diff --git a/whipper/command/offset.py b/whipper/command/offset.py index 9e1f61f..bbca660 100644 --- a/whipper/command/offset.py +++ b/whipper/command/offset.py @@ -37,13 +37,13 @@ OFFSETS = ("+6, +667, +48, +102, +12, +30, +103, +618, +96, +594, " "+99, +97, +600, +676, +690, +1292, +702, +686, -24, " "+704, +697, +572, +1182, +688, +91, -491, +145, +689, " "+564, +708, +86, +355, +79, -496, +679, -1164, 0, " - "+1160, -436, +694, +684, +94, +1194, +106, +681, +117, " - "+692, +943, +92, +680, +678, +682, +1268, +1279, +1473, " - "-582, -54, +674, +687, +1272, +1263, +1508, +675, " - "+534, +740, +122, -489, +974, +976, +1303, +108, +1130, " - "+111, +739, +732, -589, -495, -494, +975, +961, +935, " - "+87, +668, +234, +1776, +138, +1364, +1336, +1262, " - "+1127") + "+1160, -436, +694, +684, +94, +1194, +106, +681, " + "+117, +692, +943, +92, +680, +678, +682, +1268, +1279, " + "+1473, -582, -54, +674, +687, +1272, +1263, +1508, " + "+675, +534, +740, +122, -489, +974, +976, +1303, " + "+108, +1130, +111, +739, +732, -589, -495, -494, " + "+975, +961, +935, +87, +668, +234, +1776, +138, +1364, " + "+1336, +1262, +1127") class Find(BaseCommand): @@ -177,7 +177,7 @@ CD in the AccurateRip database.""" logger.debug('ripping track %r with offset %d...', track, offset) fd, path = tempfile.mkstemp( - suffix=u'.track%02d.offset%d.whipper.wav' % ( + suffix='.track%02d.offset%d.whipper.wav' % ( track, offset)) os.close(fd) diff --git a/whipper/common/accurip.py b/whipper/common/accurip.py index 6133ad7..146b3d8 100644 --- a/whipper/common/accurip.py +++ b/whipper/common/accurip.py @@ -21,7 +21,6 @@ import requests import struct -from errno import EEXIST from os import makedirs from os.path import dirname, exists, join @@ -40,7 +39,7 @@ class EntryNotFound(Exception): pass -class _AccurateRipResponse(object): +class _AccurateRipResponse: """ An AccurateRip response contains a collection of metadata identifying a particular digital audio compact disc. @@ -60,7 +59,7 @@ class _AccurateRipResponse(object): position, so track 1 will have array index 0, track 2 will have array index 1, and so forth. HTOA and other hidden tracks are not included. """ - self.num_tracks = struct.unpack("B", data[0])[0] + self.num_tracks = data[0] self.discId1 = "%08x" % struct.unpack(" track.AR[v]['DBConfidence']: + if (track.AR[v]['DBConfidence'] is None or + r.confidences[i] > track.AR[v]['DBConfidence']): track.AR[v]['DBCRC'] = r.checksums[i] track.AR[v]['DBConfidence'] = r.confidences[i] logger.debug( @@ -245,7 +245,8 @@ def print_report(result): track.AR['v2']['DBCRC'] ) if _f]) max_conf = max( - [track.AR[v]['DBConfidence'] for v in ('v1', 'v2')] + [track.AR[v]['DBConfidence'] for v in ('v1', 'v2') + if track.AR[v]['DBConfidence'] is not None], default=None ) if max_conf: if max_conf < track.AR['DBMaxConfidence']: diff --git a/whipper/common/cache.py b/whipper/common/cache.py index 19ed032..a3e32a6 100644 --- a/whipper/common/cache.py +++ b/whipper/common/cache.py @@ -98,17 +98,16 @@ class Persister: if not os.path.exists(self._path): return - handle = open(self._path) - import pickle - - try: - self.object = pickle.load(handle) - logger.debug('loaded persisted object from %r', self._path) - # FIXME: catching too general exception (Exception) - except Exception as e: - # can fail for various reasons; in that case, pretend we didn't - # load it - logger.debug(e) + with open(self._path, 'rb') as handle: + import pickle + try: + self.object = pickle.load(handle) + logger.debug('loaded persisted object from %r', self._path) + # FIXME: catching too general exception (Exception) + except Exception as e: + # can fail for various reasons; in that case, pretend we didn't + # load it + logger.debug(e) def delete(self): self.object = None @@ -124,11 +123,7 @@ class PersistedCache: def __init__(self, path): self.path = path - try: - os.makedirs(self.path) - except OSError as e: - if e.errno != os.errno.EEXIST: # FIXME: errno 17 is 'File Exists' - raise + os.makedirs(self.path, exist_ok=True) def _getPath(self, key): return os.path.join(self.path, '%s.pickle' % key) diff --git a/whipper/common/common.py b/whipper/common/common.py index 310c607..fd545f0 100644 --- a/whipper/common/common.py +++ b/whipper/common/common.py @@ -165,7 +165,7 @@ def truncate_filename(path): fn_lim = os.pathconf(p.encode('utf-8'), 'PC_NAME_MAX') f_max = fn_lim - len(e.encode('utf-8')) f = unicodedata.normalize('NFC', f) - f_trunc = unicode(f.encode('utf-8')[:f_max], 'utf-8', errors='ignore') + f_trunc = f.encode()[:f_max].decode('utf-8', errors='ignore') return os.path.join(p, f_trunc + e) @@ -196,7 +196,7 @@ def shrinkPath(path): name = " ".join(pieces) # ext includes period - parts[-1] = u'%s%s' % (name, ext) + parts[-1] = '%s%s' % (name, ext) path = os.path.join(*parts) return path @@ -209,11 +209,11 @@ def getRealPath(refPath, filePath): :param refPath: path to the file from which the track is referenced; for example, path to the .cue file in the same directory - :type refPath: unicode + :type refPath: str - :type filePath: unicode + :type filePath: str """ - assert isinstance(filePath, unicode), "%r is not unicode" % filePath + assert isinstance(filePath, str), "%r is not str" % filePath if os.path.exists(filePath): return filePath @@ -292,7 +292,7 @@ def validate_template(template, kind): 'variable(s): {}'.format(', '.join(matches))) -class VersionGetter(object): +class VersionGetter: """ I get the version of a program by looking for it in command output according to a regexp. @@ -317,11 +317,11 @@ class VersionGetter(object): version = "(Unknown)" try: - p = asyncsub.Popen(self._args, - stdin=subprocess.PIPE, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, close_fds=True) - p.wait() - output = asyncsub.recv_some(p, e=0, stderr=1) + with asyncsub.Popen(self._args, + stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, close_fds=True) as p: + p.wait() + output = asyncsub.recv_some(p, e=0, stderr=1).decode() vre = self._regexp.search(output) if vre: version = self._expander % vre.groupdict() diff --git a/whipper/common/config.py b/whipper/common/config.py index 9ca386a..8774c86 100644 --- a/whipper/common/config.py +++ b/whipper/common/config.py @@ -18,13 +18,12 @@ # You should have received a copy of the GNU General Public License # along with whipper. If not, see . -import ConfigParser import codecs +import configparser import os.path import shutil import tempfile -import urllib -from urlparse import urlparse +from urllib.parse import urlparse, quote from whipper.common import directory @@ -37,7 +36,7 @@ class Config: def __init__(self, path=None): self._path = path or directory.config_path() - self._parser = ConfigParser.SafeConfigParser() + self._parser = configparser.ConfigParser() self.open() @@ -45,13 +44,13 @@ class Config: # Open the file with the correct encoding if os.path.exists(self._path): with codecs.open(self._path, 'r', encoding='utf-8') as f: - self._parser.readfp(f) + self._parser.read_file(f) logger.debug('loaded %d sections from config file', len(self._parser.sections())) def write(self): - fd, path = tempfile.mkstemp(suffix=u'.whipperrc') + fd, path = tempfile.mkstemp(suffix='.whipperrc') handle = os.fdopen(fd, 'w') self._parser.write(handle) handle.close() @@ -64,7 +63,7 @@ class Config: method = getattr(self._parser, methodName) try: return method(section, option) - except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): + except (configparser.NoSectionError, configparser.NoOptionError): return None def get(self, section, option): @@ -102,7 +101,7 @@ class Config: try: return int(self._parser.get(section, 'read_offset')) - except ConfigParser.NoOptionError: + except configparser.NoOptionError: raise KeyError("Could not find read_offset for %s/%s/%s" % ( vendor, model, release)) @@ -121,7 +120,7 @@ class Config: try: return self._parser.get(section, 'defeats_cache') == 'True' - except ConfigParser.NoOptionError: + except configparser.NoOptionError: raise KeyError("Could not find defeats_cache for %s/%s/%s" % ( vendor, model, release)) @@ -153,7 +152,7 @@ class Config: try: section = self._findDriveSection(vendor, model, release) except KeyError: - section = 'drive:' + urllib.quote('%s:%s:%s' % ( + section = 'drive:' + quote('%s:%s:%s' % ( vendor, model, release)) self._parser.add_section(section) for key in ['vendor', 'model', 'release']: diff --git a/whipper/common/directory.py b/whipper/common/directory.py index f351c02..55b3880 100644 --- a/whipper/common/directory.py +++ b/whipper/common/directory.py @@ -19,33 +19,30 @@ # along with whipper. If not, see . from os import getenv, makedirs -from os.path import join, expanduser, exists +from os.path import join, expanduser def config_path(): - path = join(getenv('XDG_CONFIG_HOME') or join(expanduser('~'), u'.config'), - u'whipper') - if not exists(path): - makedirs(path) - return join(path, u'whipper.conf') + path = join(getenv('XDG_CONFIG_HOME') or join(expanduser('~'), '.config'), + 'whipper') + makedirs(path, exist_ok=True) + return join(path, 'whipper.conf') def cache_path(name=None): - path = join(getenv('XDG_CACHE_HOME') or join(expanduser('~'), u'.cache'), - u'whipper') + path = join(getenv('XDG_CACHE_HOME') or join(expanduser('~'), '.cache'), + 'whipper') if name: path = join(path, name) - if not exists(path): - makedirs(path) + makedirs(path, exist_ok=True) return path def data_path(name=None): path = join(getenv('XDG_DATA_HOME') or - join(expanduser('~'), u'.local/share'), - u'whipper') + join(expanduser('~'), '.local/share'), + 'whipper') if name: path = join(path, name) - if not exists(path): - makedirs(path) + makedirs(path, exist_ok=True) return path diff --git a/whipper/common/mbngs.py b/whipper/common/mbngs.py index d39dc87..f6b014f 100644 --- a/whipper/common/mbngs.py +++ b/whipper/common/mbngs.py @@ -21,7 +21,7 @@ """ Handles communication with the MusicBrainz server using NGS. """ -import urllib2 +from urllib.error import HTTPError import whipper @@ -45,7 +45,7 @@ class NotFoundException(MusicBrainzException): return "Disc not found in MusicBrainz" -class TrackMetadata(object): +class TrackMetadata: artist = None title = None duration = None # in ms @@ -56,12 +56,12 @@ class TrackMetadata(object): mbidWorks = [] -class DiscMetadata(object): +class DiscMetadata: """ :param artist: artist(s) name :param sortName: release artist sort name :param release: earliest release date, in YYYY-MM-DD - :type release: unicode + :type release: str :param title: title of the disc (with disambiguation) :param releaseTitle: title of the release (without disambiguation) :type tracks: list of :any:`TrackMetadata` @@ -152,7 +152,7 @@ def _getWorks(recording): """Get "performance of" works out of a recording.""" works = [] valid_work_rel_types = [ - u'a3005666-a872-32c3-ad06-98af558e99b0', # "Performance" + 'a3005666-a872-32c3-ad06-98af558e99b0', # "Performance" ] if 'work-relation-list' in recording: for work in recording['work-relation-list']: @@ -298,7 +298,7 @@ def musicbrainz(discid, country=None, record=False): result = musicbrainzngs.get_releases_by_discid( discid, includes=["artists", "recordings", "release-groups"]) except musicbrainzngs.ResponseError as e: - if isinstance(e.cause, urllib2.HTTPError): + if isinstance(e.cause, HTTPError): if e.cause.code == 404: raise NotFoundException(e) else: diff --git a/whipper/common/path.py b/whipper/common/path.py index 8d628fb..43c2353 100644 --- a/whipper/common/path.py +++ b/whipper/common/path.py @@ -21,7 +21,7 @@ import re -class PathFilter(object): +class PathFilter: """ I filter path components for safe storage on file systems. """ @@ -50,18 +50,16 @@ class PathFilter(object): # change all fancy single/double quotes to normal quotes if self._quotes: - path = re.sub(ur'[\xc2\xb4\u2018\u2019\u201b]', "'", path, - re.UNICODE) - path = re.sub(ur'[\u201c\u201d\u201f]', '"', path, re.UNICODE) + path = re.sub(r'[\xc2\xb4\u2018\u2019\u201b]', "'", path) + path = re.sub(r'[\u201c\u201d\u201f]', '"', path) if self._special: path = separators(path) - path = re.sub(r'[*?&!\'\"$()`{}\[\]<>]', - '_', path, re.UNICODE) + path = re.sub(r'[*?&!\'\"$()`{}\[\]<>]', '_', path) if self._fat: path = separators(path) # : and | already gone, but leave them here for reference - path = re.sub(r'[:*?"<>|]', '_', path, re.UNICODE) + path = re.sub(r'[:*?"<>|]', '_', path) return path diff --git a/whipper/common/program.py b/whipper/common/program.py index 5cb1e79..1d829ae 100644 --- a/whipper/common/program.py +++ b/whipper/common/program.py @@ -47,7 +47,7 @@ class Program: :vartype metadata: mbngs.DiscMetadata :cvar result: the rip's result :vartype result: result.RipResult - :vartype outdir: unicode + :vartype outdir: str :vartype config: whipper.common.config.Config """ @@ -197,8 +197,8 @@ class Program: - %x: audio extension, lowercase - %X: audio extension, uppercase """ - assert isinstance(outdir, unicode), "%r is not unicode" % outdir - assert isinstance(template, unicode), "%r is not unicode" % template + assert isinstance(outdir, str), "%r is not str" % outdir + assert isinstance(template, str), "%r is not str" % template v = {} v['A'] = 'Unknown Artist' v['d'] = mbdiscid # fallback for title @@ -228,7 +228,7 @@ class Program: if metadata.releaseType: v['R'] = metadata.releaseType v['r'] = metadata.releaseType.lower() - if track_number > 0: + if track_number is not None and track_number > 0: v['a'] = self._filter.filter( metadata.tracks[track_number - 1].artist) v['s'] = self._filter.filter( @@ -307,8 +307,8 @@ class Program: print('\nMatching releases:') for metadata in metadatas: - print('\nArtist : %s' % metadata.artist.encode('utf-8')) - print('Title : %s' % metadata.title.encode('utf-8')) + print('\nArtist : %s' % metadata.artist) + print('Title : %s' % metadata.title) print('Duration: %s' % common.formatTime( metadata.duration / 1000.0)) print('URL : %s' % metadata.url) @@ -318,8 +318,7 @@ class Program: print("Barcode : %s" % metadata.barcode) # TODO: Add test for non ASCII catalog numbers: see issue #215 if metadata.catalogNumber: - print("Cat no : %s" % - metadata.catalogNumber.encode('utf-8')) + print("Cat no : %s" % metadata.catalogNumber) delta = abs(metadata.duration - ittoc.duration()) if delta not in deltas: @@ -334,7 +333,7 @@ class Program: if prompt: guess = (deltas[lowest])[0].mbid - release = raw_input( + release = input( "\nPlease select a release [%s]: " % guess) if not release: @@ -346,8 +345,8 @@ class Program: metadatas) if len(metadatas) == 1: 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')) + print('Artist: %s' % metadatas[0].artist) + print('Title : %s' % metadatas[0].title) elif not metadatas: logger.warning("requested release id '%s', but none of " "the found releases match", release) @@ -374,8 +373,8 @@ class Program: 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')) + print('Artist : %s' % artist) + print('Title : %s' % metadatas[0].title) # Select one of the returned releases. We just pick the first one. ret = metadatas[0] @@ -395,10 +394,10 @@ class Program: :rtype: dict """ - trackArtist = u'Unknown Artist' - releaseArtist = u'Unknown Artist' - disc = u'Unknown Disc' - title = u'Unknown Track' + trackArtist = 'Unknown Artist' + releaseArtist = 'Unknown Artist' + disc = 'Unknown Disc' + title = 'Unknown Track' if self.metadata: trackArtist = self.metadata.artist @@ -435,7 +434,7 @@ class Program: tags['TITLE'] = title tags['ALBUM'] = disc - tags['TRACKNUMBER'] = u'%s' % number + tags['TRACKNUMBER'] = '%s' % number if self.metadata: if self.metadata.release is not None: @@ -506,8 +505,7 @@ class Program: stop = self.result.table.getTrackEnd(trackResult.number) dirname = os.path.dirname(trackResult.filename) - if not os.path.exists(dirname): - os.makedirs(dirname) + os.makedirs(dirname, exist_ok=True) if not what: what = 'track %d' % (trackResult.number, ) @@ -573,7 +571,7 @@ class Program: def write_m3u(self, discname): m3uPath = common.truncate_filename(discname + '.m3u') with open(m3uPath, 'w') as f: - f.write(u'#EXTM3U\n'.encode('utf-8')) + f.write('#EXTM3U\n') for track in self.result.tracks: if not track.filename: # false positive htoa @@ -586,10 +584,10 @@ class Program: common.FRAMES_PER_SECOND) target_path = common.getRelativePath(track.filename, m3uPath) - u = u'#EXTINF:%d,%s\n' % (length, target_path) - f.write(u.encode('utf-8')) + u = '#EXTINF:%d,%s\n' % (length, target_path) + f.write(u) u = '%s\n' % target_path - f.write(u.encode('utf-8')) + f.write(u) def writeCue(self, discName): assert self.result.table.canCue() @@ -597,7 +595,7 @@ class Program: logger.debug('write .cue file to %s', cuePath) handle = open(cuePath, 'w') # FIXME: do we always want utf-8 ? - handle.write(self.result.table.cue(cuePath).encode('utf-8')) + handle.write(self.result.table.cue(cuePath)) handle.close() self.cuePath = cuePath @@ -608,7 +606,7 @@ class Program: logPath = common.truncate_filename(discName + '.log') handle = open(logPath, 'w') log = txt_logger.log(self.result) - handle.write(log.encode('utf-8')) + handle.write(log) handle.close() self.logPath = logPath diff --git a/whipper/common/renamer.py b/whipper/common/renamer.py index 22be22a..941b018 100644 --- a/whipper/common/renamer.py +++ b/whipper/common/renamer.py @@ -24,7 +24,7 @@ import tempfile """Rename files on file system and inside metafiles in a resumable way.""" -class Operator(object): +class Operator: def __init__(self, statePath, key): self._todo = [] @@ -91,7 +91,7 @@ class Operator(object): Execute the operations """ - def next(self): + def __next__(self): operation = self._todo[len(self._done)] if self._resuming: operation.redo() @@ -116,7 +116,7 @@ class FileRenamer(Operator): """ -class Operation(object): +class Operation: def verify(self): """ @@ -199,7 +199,8 @@ class RenameInFile(Operation): (fd, name) = tempfile.mkstemp(suffix='.whipper') for s in handle: - os.write(fd, s.replace(self._source, self._destination)) + os.write(fd, + s.replace(self._source, self._destination).encode()) os.close(fd) os.rename(name, self._path) diff --git a/whipper/extern/asyncsub.py b/whipper/extern/asyncsub.py index 4e4f9a8..767752b 100644 --- a/whipper/extern/asyncsub.py +++ b/whipper/extern/asyncsub.py @@ -11,7 +11,7 @@ import sys PIPE = subprocess.PIPE -if subprocess.mswindows: +if sys.platform == 'win32': from win32file import ReadFile, WriteFile from win32pipe import PeekNamedPipe import msvcrt @@ -42,7 +42,7 @@ class Popen(subprocess.Popen): getattr(self, which).close() setattr(self, which, None) - if subprocess.mswindows: + if sys.platform == 'win32': def send(self, in_put): if not self.stdin: @@ -149,28 +149,4 @@ def recv_some(p, t=.1, e=1, tr=5, stderr=0): y.append(r) else: time.sleep(max((x - time.time()) / tr, 0)) - return ''.join(y) - - -def send_all(p, data): - while data: - sent = p.send(data) - if sent is None: - raise Exception(message) - data = buffer(data, sent) - - -if __name__ == '__main__': - if sys.platform == 'win32': - shell, commands, tail = ('cmd', ('dir /w', 'echo HELLO WORLD'), '\r\n') - else: - shell, commands, tail = ('sh', ('ls', 'echo HELLO WORLD'), '\n') - - a = Popen(shell, stdin=PIPE, stdout=PIPE) - print(recv_some(a)) - for cmd in commands: - send_all(a, cmd + tail) - print(recv_some(a)) - send_all(a, 'exit' + tail) - print(recv_some(a, e=0)) - a.wait() + return ''.join(x.decode() for x in y).encode() diff --git a/whipper/extern/freedb.py b/whipper/extern/freedb.py index 6359a91..4b86de3 100644 --- a/whipper/extern/freedb.py +++ b/whipper/extern/freedb.py @@ -17,16 +17,13 @@ # USA -import sys - - def digit_sum(i): """returns the sum of all digits for the given integer""" return sum(map(int, str(i))) -class DiscID(object): +class DiscID: def __init__(self, offsets, total_length, track_count, playable_length): """offsets is a list of track offsets, in CD frames total_length is the total length of the disc, in seconds @@ -53,15 +50,8 @@ class DiscID(object): "track_count", "playable_length"]])) - if sys.version_info[0] >= 3: - def __str__(self): - return self.__unicode__() - else: - def __str__(self): - return self.__unicode__().encode('ascii') - - def __unicode__(self): - return u"{:08X}".format(int(self)) + def __str__(self): + return "{:08X}".format(int(self)) def __int__(self): digit_sum_ = sum([digit_sum(o // 75) for o in self.offsets]) @@ -90,11 +80,11 @@ def perform_lookup(disc_id, freedb_server, freedb_port): query = freedb_command(freedb_server, freedb_port, - u"query", - *([disc_id.__unicode__(), - u"{:d}".format(disc_id.track_count)] + - [u"{:d}".format(o) for o in disc_id.offsets] + - [u"{:d}".format(disc_id.playable_length)])) + "query", + *([disc_id.__str__(), + "{:d}".format(disc_id.track_count)] + + ["{:d}".format(o) for o in disc_id.offsets] + + ["{:d}".format(disc_id.playable_length)])) line = next(query) response = RESPONSE.match(line) @@ -116,7 +106,7 @@ def perform_lookup(disc_id, freedb_server, freedb_port): elif (code == 211) or (code == 210): # multiple exact or inexact matches line = next(query) - while not line.startswith(u"."): + while not line.startswith("."): match = QUERY_RESULT.match(line) if match is not None: matches.append((match.group(1), @@ -140,7 +130,7 @@ def perform_lookup(disc_id, freedb_server, freedb_port): query = freedb_command(freedb_server, freedb_port, - u"read", + "read", category, disc_id) @@ -149,8 +139,8 @@ def perform_lookup(disc_id, freedb_server, freedb_port): # FIXME: check response code here freedb = {} line = next(query) - while not line.startswith(u"."): - if not line.startswith(u"#"): + while not line.startswith("."): + if not line.startswith("#"): entry = FREEDB_LINE.match(line) if entry is not None: if entry.group(1) in freedb: @@ -165,52 +155,38 @@ def perform_lookup(disc_id, freedb_server, freedb_port): def freedb_command(freedb_server, freedb_port, cmd, *args): """given a freedb_server string, freedb_port int, - command unicode string and argument unicode strings, - yields a list of Unicode strings""" + command string and argument strings, yields a list of strings""" - try: - from urllib.request import urlopen - from urllib.error import URLError - except ImportError: - from urllib2 import urlopen, URLError - try: - from urllib.parse import urlencode - except ImportError: - from urllib import urlencode + from urllib.error import URLError + from urllib.request import urlopen + from urllib.parse import urlencode from socket import getfqdn from whipper import __version__ as VERSION - from sys import version_info - - PY3 = version_info[0] >= 3 # some debug type checking - assert(isinstance(cmd, str if PY3 else unicode)) + assert(isinstance(cmd, str)) for arg in args: - assert(isinstance(arg, str if PY3 else unicode)) + assert(isinstance(arg, str)) POST = [] # generate query to post with arguments in specific order if len(args) > 0: - POST.append((u"cmd", u"cddb {} {}".format(cmd, " ".join(args)))) + POST.append(("cmd", "cddb {} {}".format(cmd, " ".join(args)))) else: - POST.append((u"cmd", u"cddb {}".format(cmd))) + POST.append(("cmd", "cddb {}".format(cmd))) POST.append( - (u"hello", - u"user {} {} {}".format( - getfqdn() if PY3 else getfqdn().decode("UTF-8", "replace"), - u"whipper", - VERSION if PY3 else VERSION.decode("ascii")))) + ("hello", + "user {} {} {}".format(getfqdn(), "whipper", VERSION))) - POST.append((u"proto", u"6")) + POST.append(("proto", "6")) try: # get Request object from post request = urlopen( "http://{}:{:d}/~cddb/cddb.cgi".format(freedb_server, freedb_port), - urlencode(POST).encode("UTF-8") if (version_info[0] >= 3) else - urlencode(POST)) + urlencode(POST).encode()) except URLError as e: raise ValueError(str(e)) try: diff --git a/whipper/extern/task/task.py b/whipper/extern/task/task.py index 3590c75..d739b7b 100644 --- a/whipper/extern/task/task.py +++ b/whipper/extern/task/task.py @@ -18,7 +18,6 @@ # 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 @@ -69,7 +68,7 @@ def _getExceptionMessage(exception, frame=-1, filename=None): % locals() -class LogStub(object): +class LogStub: """ I am a stub for a log interface. """ @@ -244,7 +243,7 @@ class Task(LogStub): # FIXME: should this become a real interface, like in zope ? -class ITaskListener(object): +class ITaskListener: """ I am an interface for objects listening to tasks. """ @@ -484,7 +483,7 @@ class SyncRunner(TaskRunner, ITaskListener): self._task.addListener(self) # only start the task after going into the mainloop, # otherwise the task might complete before we are in it - GLib.timeout_add(0L, self._startWrap, self._task) + GLib.timeout_add(0, self._startWrap, self._task) self.debug('run loop') self._loop.run() @@ -525,7 +524,7 @@ class SyncRunner(TaskRunner, ITaskListener): self.stopped(task) raise - GLib.timeout_add(int(delta * 1000L), c) + GLib.timeout_add(int(delta * 1000), c) # ITaskListener methods def progressed(self, task, value): diff --git a/whipper/image/cue.py b/whipper/image/cue.py index 1a49ca9..0bba8f4 100644 --- a/whipper/image/cue.py +++ b/whipper/image/cue.py @@ -25,7 +25,6 @@ See http://digitalx.org/cuesheetsyntax.php """ import re -import codecs from whipper.common import common from whipper.image import table @@ -58,7 +57,7 @@ _INDEX_RE = re.compile(r""" """, re.VERBOSE) -class CueFile(object): +class CueFile: """ I represent a .cue file as an object. @@ -69,9 +68,9 @@ class CueFile(object): def __init__(self, path): """ - :type path: unicode + :type path: str """ - assert isinstance(path, unicode), "%r is not unicode" % path + assert isinstance(path, str), "%r is not str" % path self._path = path self._rems = {} @@ -86,9 +85,9 @@ class CueFile(object): counter = 0 logger.info('parsing .cue file %r', self._path) - handle = codecs.open(self._path, 'r', 'utf-8') - - for number, line in enumerate(handle.readlines()): + with open(self._path) as f: + content = f.readlines() + for number, line in enumerate(content): line = line.rstrip() m = _REM_RE.search(line) @@ -137,9 +136,9 @@ class CueFile(object): minutes = int(m.expand('\\2')) seconds = int(m.expand('\\3')) frames = int(m.expand('\\4')) - frameOffset = frames \ - + seconds * common.FRAMES_PER_SECOND \ - + minutes * common.FRAMES_PER_SECOND * 60 + frameOffset = int(frames + + seconds * common.FRAMES_PER_SECOND + + minutes * common.FRAMES_PER_SECOND * 60) logger.debug('found index %d of track %r in %r:%d', indexNumber, currentTrack, currentFile.path, @@ -182,7 +181,7 @@ class CueFile(object): """ Translate the .cue's FILE to an existing path. - :type path: unicode + :type path: str """ return common.getRealPath(self._path, path) @@ -194,9 +193,9 @@ class File: def __init__(self, path, file_format): """ - :type path: unicode + :type path: str """ - assert isinstance(path, unicode), "%r is not unicode" % path + assert isinstance(path, str), "%r is not str" % path self.path = path self.format = file_format diff --git a/whipper/image/image.py b/whipper/image/image.py index 83eb4b3..1d2251c 100644 --- a/whipper/image/image.py +++ b/whipper/image/image.py @@ -34,7 +34,7 @@ import logging logger = logging.getLogger(__name__) -class Image(object): +class Image: """ :ivar table: The Table of Contents for this image. :vartype table: table.Table @@ -43,10 +43,10 @@ class Image(object): def __init__(self, path): """ - :type path: unicode + :type path: str :param path: .cue path """ - assert isinstance(path, unicode), "%r is not unicode" % path + assert isinstance(path, str), "%r is not str" % path self._path = path self.cue = cue.CueFile(path) @@ -62,7 +62,7 @@ class Image(object): :param path: .cue path """ - assert isinstance(path, unicode), "%r is not unicode" % path + assert isinstance(path, str), "%r is not str" % path return self.cue.getRealPath(path) @@ -130,7 +130,7 @@ class ImageVerifyTask(task.MultiSeparateTask): htoa = cue.table.tracks[0].indexes[0] track = cue.table.tracks[0] path = image.getRealPath(htoa.path) - assert isinstance(path, unicode), "%r is not unicode" % path + assert isinstance(path, str), "%r is not str" % path logger.debug('schedule scan of audio length of %r', path) taskk = AudioLengthTask(path) self.addTask(taskk) @@ -145,7 +145,7 @@ class ImageVerifyTask(task.MultiSeparateTask): if length == -1: path = image.getRealPath(index.path) - assert isinstance(path, unicode), "%r is not unicode" % path + assert isinstance(path, str), "%r is not str" % path logger.debug('schedule scan of audio length of %r', path) taskk = AudioLengthTask(path) self.addTask(taskk) @@ -167,7 +167,7 @@ class ImageVerifyTask(task.MultiSeparateTask): "in debug log (set RIP_DEBUG=4)") index = track.indexes[1] assert taskk.length % common.SAMPLES_PER_FRAME == 0 - end = taskk.length / common.SAMPLES_PER_FRAME + end = taskk.length // common.SAMPLES_PER_FRAME self.lengths[trackIndex] = end - index.relative task.MultiSeparateTask.stop(self) @@ -192,7 +192,7 @@ class ImageEncodeTask(task.MultiSeparateTask): def add(index): path = image.getRealPath(index.path) - assert isinstance(path, unicode), "%r is not unicode" % path + assert isinstance(path, str), "%r is not str" % path logger.debug('schedule encode of %r', path) root, _ = os.path.splitext(os.path.basename(path)) outpath = os.path.join(outdir, root + '.' + 'flac') diff --git a/whipper/image/table.py b/whipper/image/table.py index 57377a9..131cc47 100644 --- a/whipper/image/table.py +++ b/whipper/image/table.py @@ -23,8 +23,7 @@ Wrap Table of Contents. """ import copy -import urllib -import urlparse +from urllib.parse import urlunparse, urlencode import whipper @@ -66,7 +65,7 @@ class Track: :vartype isrc: str :cvar cdtext: dictionary of CD Text information; :any:`see CDTEXT_KEYS` - :vartype cdtext: str -> unicode + :vartype cdtext: str :cvar pre_emphasis: whether track is pre-emphasised :vartype pre_emphasis: bool """ @@ -91,10 +90,10 @@ class Track: def index(self, number, absolute=None, path=None, relative=None, counter=None): """ - :type path: unicode or None + :type path: str or None """ if path is not None: - assert isinstance(path, unicode), "%r is not unicode" % path + assert isinstance(path, str), "%r is not str" % path i = Index(number, absolute, path, relative, counter) self.indexes[number] = i @@ -133,7 +132,7 @@ class Index: """ :cvar counter: counter for the index source; distinguishes between the matching FILE lines in .cue files for example - :vartype path: unicode or None + :vartype path: str or None """ number = None absolute = None @@ -145,7 +144,7 @@ class Index: counter=None): if path is not None: - assert isinstance(path, unicode), "%r is not unicode" % path + assert isinstance(path, str), "%r is not str" % path self.number = number self.absolute = absolute @@ -158,7 +157,7 @@ class Index: self.number, self.absolute, self.path, self.relative, self.counter) -class Table(object): +class Table: """ I represent a table of indexes on a CD. @@ -221,7 +220,10 @@ class Table(object): # if on a session border, subtract the session leadin thisTrack = self.tracks[number - 1] nextTrack = self.tracks[number] - if nextTrack.session > thisTrack.session: + # The session attribute of a track is None by default (session 1) + # with value > 1 if the track is in another session. Py3 doesn't + # allow NoneType comparisons so we compare against 1 in that case + if int(nextTrack.session or 1) > int(thisTrack.session or 1): gap = self._getSessionGap(nextTrack.session) end -= gap @@ -286,7 +288,7 @@ class Table(object): offset = self.getTrackStart(track.number) + delta offsets.append(offset) debug.append(str(offset)) - seconds = offset / common.FRAMES_PER_SECOND + seconds = offset // common.FRAMES_PER_SECOND n += self._cddbSum(seconds) # the 'real' leadout, not offset by 150 frames @@ -297,8 +299,8 @@ class Table(object): # FIXME: we can't replace these calculations with the getFrameLength # call because the start and leadout in the algorithm get rounded # before making the difference - startSeconds = self.getTrackStart(1) / common.FRAMES_PER_SECOND - leadoutSeconds = leadout / common.FRAMES_PER_SECOND + startSeconds = self.getTrackStart(1) // common.FRAMES_PER_SECOND + leadoutSeconds = leadout // common.FRAMES_PER_SECOND t = leadoutSeconds - startSeconds # durationFrames = self.getFrameLength(data=True) # duration = durationFrames / common.FRAMES_PER_SECOND @@ -348,12 +350,12 @@ class Table(object): sha = sha1() # number of first track - sha.update("%02X" % values[0]) + sha.update(("%02X" % values[0]).encode()) # number of last track - sha.update("%02X" % values[1]) + sha.update(("%02X" % values[1]).encode()) - sha.update("%08X" % values[2]) + sha.update(("%08X" % values[2]).encode()) # offsets of tracks for i in range(1, 100): @@ -361,7 +363,7 @@ class Table(object): offset = values[2 + i] except IndexError: offset = 0 - sha.update("%08X" % offset) + sha.update(("%08X" % offset).encode()) digest = sha.digest() assert len(digest) == 20, \ @@ -372,10 +374,10 @@ class Table(object): # (Rob) used ., _, and - # base64 altchars specify replacements for + and / - result = base64.b64encode(digest, '._') + result = base64.b64encode(digest, b'._').decode() # now replace = - result = "-".join(result.split("=")) + result = result.replace("=", "-") assert len(result) == 28, \ "Result should be 28 characters, not %d" % len(result) @@ -389,13 +391,13 @@ class Table(object): discid = self.getMusicBrainzDiscId() values = self._getMusicBrainzValues() - query = urllib.urlencode({ - 'id': discid, - 'toc': ' '.join([str(v) for v in values]), - 'tracks': self.getAudioTracks(), - }) + query = urlencode([ + ('toc', ' '.join([str(v) for v in values])), + ('tracks', self.getAudioTracks()), + ('id', discid), + ]) - return urlparse.urlunparse(( + return urlunparse(( 'https', host, '/cdtoc/attach', '', query, '')) def getFrameLength(self, data=False): @@ -477,7 +479,7 @@ class Table(object): Dump our internal representation to a .cue file content. - :rtype: unicode + :rtype: str """ logger.debug('generating .cue for cuePath %r', cuePath) diff --git a/whipper/image/toc.py b/whipper/image/toc.py index da175c1..ed2e484 100644 --- a/whipper/image/toc.py +++ b/whipper/image/toc.py @@ -25,7 +25,6 @@ The .toc file format is described in the man page of cdrdao """ import re -import codecs from whipper.common import common from whipper.image import table @@ -134,13 +133,13 @@ class Sources: return self._sources[-1][1] -class TocFile(object): +class TocFile: def __init__(self, path): """ - :type path: unicode + :type path: str """ - assert isinstance(path, unicode), "%r is not unicode" % path + assert isinstance(path, str), "%r is not str" % path self._path = path self._messages = [] self.table = table.Table() @@ -189,9 +188,9 @@ class TocFile(object): # the first track's INDEX 1 can only be gotten from the .toc # file once the first pregap is calculated; so we add INDEX 1 # at the end of each parsed TRACK record - handle = codecs.open(self._path, "r", "utf-8") - - for number, line in enumerate(handle.readlines()): + with open(self._path) as f: + content = f.readlines() + for number, line in enumerate(content): line = line.rstrip() # look for CDTEXT stuff in either header or tracks @@ -202,7 +201,7 @@ class TocFile(object): # usually, value is encoded with octal escapes and in latin-1 # FIXME: other encodings are possible, does cdrdao handle # them ? - value = value.decode('string-escape').decode('latin-1') + value = value.encode().decode('unicode_escape') if key in table.CDTEXT_FIELDS: # FIXME: consider ISRC separate for now, but this # is a limitation of our parser approach @@ -412,7 +411,7 @@ class TocFile(object): """ Translate the .toc's FILE to an existing path. - :type path: unicode + :type path: str """ return common.getRealPath(self._path, path) @@ -424,12 +423,12 @@ class File: def __init__(self, path, start, length): """ - :type path: unicode + :type path: str :type start: int :param start: starting point for the track in this file, in frames :param length: length for the track in this file, in frames """ - assert isinstance(path, unicode), "%r is not unicode" % path + assert isinstance(path, str), "%r is not str" % path self.path = path self.start = start diff --git a/whipper/program/arc.py b/whipper/program/arc.py index 9e6134a..3f20763 100644 --- a/whipper/program/arc.py +++ b/whipper/program/arc.py @@ -2,4 +2,4 @@ import accuraterip def accuraterip_checksum(f, track_number, total_tracks): - return accuraterip.compute(f.encode('utf-8'), track_number, total_tracks) + return accuraterip.compute(f, track_number, total_tracks) diff --git a/whipper/program/cdparanoia.py b/whipper/program/cdparanoia.py index 160646e..636e6f5 100644 --- a/whipper/program/cdparanoia.py +++ b/whipper/program/cdparanoia.py @@ -220,7 +220,7 @@ class ReadTrackTask(task.Task): Read the given track. :param path: where to store the ripped track - :type path: unicode + :type path: str :param table: table of contents of CD :type table: table.Table :param start: first frame to rip @@ -236,7 +236,7 @@ class ReadTrackTask(task.Task): :param what: a string representing what's being read; e.g. Track :type what: str """ - assert isinstance(path, unicode), "%r is not unicode" % path + assert isinstance(path, str), "%r is not str" % path self.path = path self._table = table @@ -314,7 +314,7 @@ class ReadTrackTask(task.Task): self.schedule(0.01, self._read, runner) return - self._buffer += ret + self._buffer += ret.decode() # parse buffer into lines if possible, and parse them if "\n" in self._buffer: @@ -452,8 +452,7 @@ class ReadVerifyTrackTask(task.MultiSeparateTask): logger.debug('read and verify with taglist %r', taglist) # FIXME: choose a dir on the same disk/dir as the final path fd, tmppath = tempfile.mkstemp(suffix='.whipper.wav') - tmppath = unicode(tmppath) - os.fchmod(fd, 0644) + os.fchmod(fd, 0o644) os.close(fd) self._tmpwavpath = tmppath @@ -472,13 +471,13 @@ class ReadVerifyTrackTask(task.MultiSeparateTask): # encode to the final path + '.part' try: - tmpoutpath = path + u'.part' + tmpoutpath = path + '.part' open(tmpoutpath, 'wb').close() except IOError as e: if errno.ENAMETOOLONG != e.errno: raise path = common.truncate_filename(common.shrinkPath(path)) - tmpoutpath = common.truncate_filename(path + u'.part') + tmpoutpath = common.truncate_filename(path + '.part') open(tmpoutpath, 'wb').close() self._tmppath = tmpoutpath self.path = path @@ -597,7 +596,7 @@ class AnalyzeTask(ctask.PopenTask): def done(self): if self.cwd: shutil.rmtree(self.cwd) - output = "".join(self._output) + output = "".join(o.decode() for o in self._output) m = _OK_RE.search(output) self.defeatsCache = bool(m) diff --git a/whipper/program/cdrdao.py b/whipper/program/cdrdao.py index 9b36ada..9bf3399 100644 --- a/whipper/program/cdrdao.py +++ b/whipper/program/cdrdao.py @@ -84,7 +84,7 @@ class ReadTOCTask(task.Task): self._parser = ProgressParser() self.fd, self.tocfile = tempfile.mkstemp( - suffix=u'.cdrdao.read-toc.whipper.task') + suffix='.cdrdao.read-toc.whipper.task') def start(self, runner): task.Task.start(self, runner) @@ -112,7 +112,7 @@ class ReadTOCTask(task.Task): return self.schedule(0.01, self._read, runner) return - self._buffer += ret + self._buffer += ret.decode() # parse buffer into lines if possible, and parse them if "\n" in self._buffer: @@ -151,8 +151,7 @@ class ReadTOCTask(task.Task): t_comp = os.path.abspath(self.toc_path).split(os.sep) t_dirn = os.sep.join(t_comp[:-1]) # If the output path doesn't exist, make it recursively - if not os.path.isdir(t_dirn): - os.makedirs(t_dirn) + os.makedirs(t_dirn, exist_ok=True) t_dst = truncate_filename( os.path.join(t_dirn, t_comp[-1] + '.toc')) shutil.copy(self.tocfile, os.path.join(t_dirn, t_dst)) @@ -168,7 +167,7 @@ def DetectCdr(device): cmd = [CDRDAO, 'disk-info', '-v1', '--device', device] logger.debug("executing %r", cmd) p = Popen(cmd, stdout=PIPE, stderr=PIPE) - return 'CD-R medium : n/a' not in p.stdout.read() + return 'CD-R medium : n/a' not in p.stdout.read().decode() def version(): diff --git a/whipper/program/soxi.py b/whipper/program/soxi.py index 86a1823..387dfcc 100644 --- a/whipper/program/soxi.py +++ b/whipper/program/soxi.py @@ -21,11 +21,11 @@ class AudioLengthTask(ctask.PopenTask): def __init__(self, path): """ - :type path: unicode + :type path: str """ - assert isinstance(path, unicode), "%r is not unicode" % path + assert isinstance(path, str), "%r is not str" % path - self.logName = os.path.basename(path).encode('utf-8') + self.logName = os.path.basename(path) self.command = [SOXI, '-s', path] @@ -47,4 +47,4 @@ class AudioLengthTask(ctask.PopenTask): def done(self): if self._error: logger.warning("soxi reported on stderr: %s", "".join(self._error)) - self.length = int("".join(self._output)) + self.length = int("".join(o.decode() for o in self._output)) diff --git a/whipper/result/result.py b/whipper/result/result.py index 7947730..51a3d6a 100644 --- a/whipper/result/result.py +++ b/whipper/result/result.py @@ -119,7 +119,7 @@ class RipResult: return None -class Logger(object): +class Logger: """ I log the result of a rip. """ @@ -140,7 +140,7 @@ class Logger(object): # A setuptools-like entry point -class EntryPoint(object): +class EntryPoint: name = 'whipper' @staticmethod diff --git a/whipper/test/common.py b/whipper/test/common.py index a35b96e..45f83ad 100644 --- a/whipper/test/common.py +++ b/whipper/test/common.py @@ -70,7 +70,8 @@ class TestCase(unittest.TestCase): version so we can use it in comparisons. """ cuefile = os.path.join(os.path.dirname(__file__), name) - ret = open(cuefile).read().decode('utf-8') + with open(cuefile) as f: + ret = f.read() ret = re.sub( 'REM COMMENT "whipper.*', 'REM COMMENT "whipper %s"' % whipper.__version__, @@ -83,7 +84,7 @@ class UnicodeTestMixin: # A helper mixin to skip tests if we're not in a UTF-8 locale try: - os.stat(u'whipper.test.B\xeate Noire.empty') + os.stat('whipper.test.B\xeate Noire.empty') except UnicodeEncodeError: skip = 'No UTF-8 locale' except OSError: diff --git a/whipper/test/test_command_mblookup.py b/whipper/test/test_command_mblookup.py index c358ca8..c13cb93 100644 --- a/whipper/test/test_command_mblookup.py +++ b/whipper/test/test_command_mblookup.py @@ -1,5 +1,5 @@ # vi:si:et:sw=4:sts=4:ts=4:set fileencoding=utf-8 -u"""Tests for whipper.command.mblookup""" +"""Tests for whipper.command.mblookup""" import os import pickle @@ -9,22 +9,22 @@ from whipper.command import mblookup class MBLookupTestCase(unittest.TestCase): - u"""Test cases for whipper.command.mblookup.MBLookup""" + """Test cases for whipper.command.mblookup.MBLookup""" @staticmethod def _mock_musicbrainz(discid, country=None, record=False): - u"""Mock function for whipper.common.mbngs.musicbrainz function.""" - filename = u"whipper.discid.{}.pickle".format(discid) + """Mock function for whipper.common.mbngs.musicbrainz function.""" + filename = "whipper.discid.{}.pickle".format(discid) path = os.path.join(os.path.dirname(__file__), filename) - with open(path) as p: + with open(path, "rb") as p: return pickle.load(p) def testMissingReleaseType(self): - u"""Test that lookup for release without a type set doesn't fail.""" + """Test that lookup for release without a type set doesn't fail.""" # Using: Gustafsson, Österberg & Cowle - What's Up? 8 (disc 4) # https://musicbrainz.org/release/d8e6153a-2c47-4804-9d73-0aac1081c3b1 mblookup.musicbrainz = self._mock_musicbrainz - discid = u"xu338_M8WukSRi0J.KTlDoflB8Y-" + discid = "xu338_M8WukSRi0J.KTlDoflB8Y-" # https://musicbrainz.org/cdtoc/xu338_M8WukSRi0J.KTlDoflB8Y- - lookup = mblookup.MBLookup([discid], u'whipper mblookup', None) + lookup = mblookup.MBLookup([discid], 'whipper mblookup', None) lookup.do() diff --git a/whipper/test/test_common_accurip.py b/whipper/test/test_common_accurip.py index 9cb1ef2..79c6f1c 100644 --- a/whipper/test/test_common_accurip.py +++ b/whipper/test/test_common_accurip.py @@ -2,7 +2,7 @@ # vi:si:et:sw=4:sts=4:ts=4 import sys -from StringIO import StringIO +from io import StringIO from os import chmod, makedirs from os.path import dirname, exists, join from shutil import copy, rmtree @@ -21,9 +21,8 @@ class TestAccurateRipResponse(TestCase): @classmethod def setUpClass(cls): cls.path = 'c/1/2/dBAR-002-0000f21c-00027ef8-05021002.bin' - cls.entry = _split_responses( - open(join(dirname(__file__), cls.path[6:])).read() - ) + with open(join(dirname(__file__), cls.path[6:]), 'rb') as f: + cls.entry = _split_responses(f.read()) cls.other_path = '4/8/2/dBAR-011-0010e284-009228a3-9809ff0b.bin' def setUp(self): @@ -100,9 +99,8 @@ class TestVerifyResult(TestCase): @classmethod def setUpClass(cls): path = 'c/1/2/dBAR-002-0000f21c-00027ef8-05021002.bin' - cls.responses = _split_responses( - open(join(dirname(__file__), path[6:])).read() - ) + with open(join(dirname(__file__), path[6:]), 'rb') as f: + cls.responses = _split_responses(f.read()) cls.checksums = { 'v1': ['284fc705', '9cc1f32e'], 'v2': ['dc77f9ab', 'dd97d2c3'], diff --git a/whipper/test/test_common_common.py b/whipper/test/test_common_common.py index a6f6e57..3cf1c40 100644 --- a/whipper/test/test_common_common.py +++ b/whipper/test/test_common_common.py @@ -12,7 +12,7 @@ from whipper.test import common as tcommon class ShrinkTestCase(tcommon.TestCase): def testSufjan(self): - path = (u'whipper/Sufjan Stevens - Illinois/02. Sufjan Stevens - ' + path = ('whipper/Sufjan Stevens - Illinois/02. Sufjan Stevens - ' 'The Black Hawk War, or, How to Demolish an Entire ' 'Civilization and Still Feel Good About Yourself in the ' 'Morning, or, We Apologize for the Inconvenience but ' @@ -52,7 +52,7 @@ class GetRelativePathTestCase(tcommon.TestCase): class GetRealPathTestCase(tcommon.TestCase): def testRealWithBackslash(self): - fd, path = tempfile.mkstemp(suffix=u'back\\slash.flac') + fd, path = tempfile.mkstemp(suffix='back\\slash.flac') refPath = os.path.join(os.path.dirname(path), 'fake.cue') self.assertEqual(common.getRealPath(refPath, path), path) diff --git a/whipper/test/test_common_config.py b/whipper/test/test_common_config.py index 0653aad..d0bcb44 100644 --- a/whipper/test/test_common_config.py +++ b/whipper/test/test_common_config.py @@ -12,7 +12,7 @@ from whipper.test import common as tcommon class ConfigTestCase(tcommon.TestCase): def setUp(self): - fd, self._path = tempfile.mkstemp(suffix=u'.whipper.test.config') + fd, self._path = tempfile.mkstemp(suffix='.whipper.test.config') os.close(fd) self._config = config.Config(self._path) diff --git a/whipper/test/test_common_mbngs.py b/whipper/test/test_common_mbngs.py index 6cc89d8..492f466 100644 --- a/whipper/test/test_common_mbngs.py +++ b/whipper/test/test_common_mbngs.py @@ -18,7 +18,7 @@ class MetadataTestCase(unittest.TestCase): filename = 'whipper.release.c56ff16e-1d81-47de-926f-ba22891bd2bd.json' path = os.path.join(os.path.dirname(__file__), filename) handle = open(path, "rb") - response = json.loads(handle.read()) + response = json.loads(handle.read().decode('utf-8')) handle.close() discid = "b.yqPuCBdsV5hrzDvYrw52iK_jE-" @@ -31,16 +31,16 @@ class MetadataTestCase(unittest.TestCase): filename = 'whipper.release.a76714e0-32b1-4ed4-b28e-f86d99642193.json' path = os.path.join(os.path.dirname(__file__), filename) handle = open(path, "rb") - response = json.loads(handle.read()) + response = json.loads(handle.read().decode('utf-8')) handle.close() discid = "f7XO36a7n1LCCskkCiulReWbwZA-" metadata = mbngs._getMetadata(response['release'], discid) - self.assertEqual(metadata.artist, u'Various Artists') - self.assertEqual(metadata.release, u'2001-10-15') + self.assertEqual(metadata.artist, 'Various Artists') + self.assertEqual(metadata.release, '2001-10-15') self.assertEqual(metadata.mbidArtist, - [u'89ad4ac3-39f7-470e-963a-56509c546377']) + ['89ad4ac3-39f7-470e-963a-56509c546377']) self.assertEqual(len(metadata.tracks), 18) @@ -48,43 +48,43 @@ class MetadataTestCase(unittest.TestCase): self.assertEqual(track16.artist, 'Tom Jones & Stereophonics') self.assertEqual(track16.mbidArtist, [ - u'57c6f649-6cde-48a7-8114-2a200247601a', - u'0bfba3d3-6a04-4779-bb0a-df07df5b0558', + '57c6f649-6cde-48a7-8114-2a200247601a', + '0bfba3d3-6a04-4779-bb0a-df07df5b0558', ]) self.assertEqual(track16.sortName, - u'Jones, Tom & Stereophonics') + 'Jones, Tom & Stereophonics') def testBalladOfTheBrokenSeas(self): # various artists disc filename = 'whipper.release.e32ae79a-336e-4d33-945c-8c5e8206dbd3.json' path = os.path.join(os.path.dirname(__file__), filename) handle = open(path, "rb") - response = json.loads(handle.read()) + response = json.loads(handle.read().decode('utf-8')) handle.close() discid = "xAq8L4ELMW14.6wI6tt7QAcxiDI-" metadata = mbngs._getMetadata(response['release'], discid) - self.assertEqual(metadata.artist, u'Isobel Campbell & Mark Lanegan') + self.assertEqual(metadata.artist, 'Isobel Campbell & Mark Lanegan') self.assertEqual(metadata.sortName, - u'Campbell, Isobel & Lanegan, Mark') - self.assertEqual(metadata.release, u'2006-01-30') + 'Campbell, Isobel & Lanegan, Mark') + self.assertEqual(metadata.release, '2006-01-30') self.assertEqual(metadata.mbidArtist, [ - u'd51f3a15-12a2-41a0-acfa-33b5eae71164', - u'a9126556-f555-4920-9617-6e013f8228a7', + 'd51f3a15-12a2-41a0-acfa-33b5eae71164', + 'a9126556-f555-4920-9617-6e013f8228a7', ]) self.assertEqual(len(metadata.tracks), 12) track12 = metadata.tracks[11] - self.assertEqual(track12.artist, u'Isobel Campbell & Mark Lanegan') + self.assertEqual(track12.artist, 'Isobel Campbell & Mark Lanegan') self.assertEqual(track12.sortName, - u'Campbell, Isobel' + 'Campbell, Isobel' ' & Lanegan, Mark') self.assertEqual(track12.mbidArtist, [ - u'd51f3a15-12a2-41a0-acfa-33b5eae71164', - u'a9126556-f555-4920-9617-6e013f8228a7', + 'd51f3a15-12a2-41a0-acfa-33b5eae71164', + 'a9126556-f555-4920-9617-6e013f8228a7', ]) def testMalaInCuba(self): @@ -93,29 +93,29 @@ class MetadataTestCase(unittest.TestCase): filename = 'whipper.release.61c6fd9b-18f8-4a45-963a-ba3c5d990cae.json' path = os.path.join(os.path.dirname(__file__), filename) handle = open(path, "rb") - response = json.loads(handle.read()) + response = json.loads(handle.read().decode('utf-8')) handle.close() discid = "u0aKVpO.59JBy6eQRX2vYcoqQZ0-" metadata = mbngs._getMetadata(response['release'], discid) - self.assertEqual(metadata.artist, u'Mala') - self.assertEqual(metadata.sortName, u'Mala') - self.assertEqual(metadata.release, u'2012-09-17') + self.assertEqual(metadata.artist, 'Mala') + self.assertEqual(metadata.sortName, 'Mala') + self.assertEqual(metadata.release, '2012-09-17') self.assertEqual(metadata.mbidArtist, - [u'09f221eb-c97e-4da5-ac22-d7ab7c555bbb']) + ['09f221eb-c97e-4da5-ac22-d7ab7c555bbb']) self.assertEqual(len(metadata.tracks), 14) track6 = metadata.tracks[5] - self.assertEqual(track6.artist, u'Mala feat. Dreiser & Sexto Sentido') + self.assertEqual(track6.artist, 'Mala feat. Dreiser & Sexto Sentido') self.assertEqual(track6.sortName, - u'Mala feat. Dreiser & Sexto Sentido') + 'Mala feat. Dreiser & Sexto Sentido') self.assertEqual(track6.mbidArtist, [ - u'09f221eb-c97e-4da5-ac22-d7ab7c555bbb', - u'ec07a209-55ff-4084-bc41-9d4d1764e075', - u'f626b92e-07b1-4a19-ad13-c09d690db66c', + '09f221eb-c97e-4da5-ac22-d7ab7c555bbb', + 'ec07a209-55ff-4084-bc41-9d4d1764e075', + 'f626b92e-07b1-4a19-ad13-c09d690db66c', ]) def testUnknownArtist(self): @@ -130,33 +130,33 @@ class MetadataTestCase(unittest.TestCase): filename = 'whipper.release.8478d4da-0cda-4e46-ae8c-1eeacfa5cf37.json' path = os.path.join(os.path.dirname(__file__), filename) handle = open(path, "rb") - response = json.loads(handle.read()) + response = json.loads(handle.read().decode('utf-8')) handle.close() discid = "RhrwgVb0hZNkabQCw1dZIhdbMFg-" metadata = mbngs._getMetadata(response['release'], discid) - self.assertEqual(metadata.artist, u'CunninLynguists') - self.assertEqual(metadata.release, u'2003') + self.assertEqual(metadata.artist, 'CunninLynguists') + self.assertEqual(metadata.release, '2003') self.assertEqual(metadata.mbidArtist, - [u'69c4cc43-8163-41c5-ac81-30946d27bb69']) + ['69c4cc43-8163-41c5-ac81-30946d27bb69']) self.assertEqual(len(metadata.tracks), 30) track8 = metadata.tracks[7] - self.assertEqual(track8.artist, u'???') - self.assertEqual(track8.sortName, u'[unknown]') + self.assertEqual(track8.artist, '???') + self.assertEqual(track8.sortName, '[unknown]') self.assertEqual(track8.mbidArtist, - [u'125ec42a-7229-4250-afc5-e057484327fe']) + ['125ec42a-7229-4250-afc5-e057484327fe']) track9 = metadata.tracks[8] - self.assertEqual(track9.artist, u'CunninLynguists feat. Tonedeff') + self.assertEqual(track9.artist, 'CunninLynguists feat. Tonedeff') self.assertEqual(track9.sortName, - u'CunninLynguists feat. Tonedeff') + 'CunninLynguists feat. Tonedeff') self.assertEqual(track9.mbidArtist, [ - u'69c4cc43-8163-41c5-ac81-30946d27bb69', - u'b3869d83-9fb5-4eac-b5ca-2d155fcbee12' + '69c4cc43-8163-41c5-ac81-30946d27bb69', + 'b3869d83-9fb5-4eac-b5ca-2d155fcbee12' ]) def testNenaAndKimWildSingle(self): @@ -167,45 +167,45 @@ class MetadataTestCase(unittest.TestCase): filename = 'whipper.release.f484a9fc-db21-4106-9408-bcd105c90047.json' path = os.path.join(os.path.dirname(__file__), filename) handle = open(path, "rb") - response = json.loads(handle.read()) + response = json.loads(handle.read().decode('utf-8')) handle.close() discid = "X2c2IQ5vUy5x6Jh7Xi_DGHtA1X8-" metadata = mbngs._getMetadata(response['release'], discid) - self.assertEqual(metadata.artist, u'Nena & Kim Wilde') - self.assertEqual(metadata.release, u'2003-05-19') + self.assertEqual(metadata.artist, 'Nena & Kim Wilde') + self.assertEqual(metadata.release, '2003-05-19') self.assertEqual(metadata.mbidArtist, [ - u'38bfaa7f-ee98-48cb-acd0-946d7aeecd76', - u'4b462375-c508-432a-8c88-ceeec38b16ae', + '38bfaa7f-ee98-48cb-acd0-946d7aeecd76', + '4b462375-c508-432a-8c88-ceeec38b16ae', ]) self.assertEqual(len(metadata.tracks), 4) track1 = metadata.tracks[0] - self.assertEqual(track1.artist, u'Nena & Kim Wilde') - self.assertEqual(track1.sortName, u'Nena & Wilde, Kim') + self.assertEqual(track1.artist, 'Nena & Kim Wilde') + self.assertEqual(track1.sortName, 'Nena & Wilde, Kim') self.assertEqual(track1.mbidArtist, [ - u'38bfaa7f-ee98-48cb-acd0-946d7aeecd76', - u'4b462375-c508-432a-8c88-ceeec38b16ae', + '38bfaa7f-ee98-48cb-acd0-946d7aeecd76', + '4b462375-c508-432a-8c88-ceeec38b16ae', ]) self.assertEqual(track1.mbid, - u'1cc96e78-28ed-3820-b0b6-614c35b121ac') + '1cc96e78-28ed-3820-b0b6-614c35b121ac') self.assertEqual(track1.mbidRecording, - u'fde5622c-ce23-4ebb-975d-51d4a926f901') + 'fde5622c-ce23-4ebb-975d-51d4a926f901') track2 = metadata.tracks[1] - self.assertEqual(track2.artist, u'Nena & Kim Wilde') - self.assertEqual(track2.sortName, u'Nena & Wilde, Kim') + self.assertEqual(track2.artist, 'Nena & Kim Wilde') + self.assertEqual(track2.sortName, 'Nena & Wilde, Kim') self.assertEqual(track2.mbidArtist, [ - u'38bfaa7f-ee98-48cb-acd0-946d7aeecd76', - u'4b462375-c508-432a-8c88-ceeec38b16ae', + '38bfaa7f-ee98-48cb-acd0-946d7aeecd76', + '4b462375-c508-432a-8c88-ceeec38b16ae', ]) self.assertEqual(track2.mbid, - u'f16db4bf-9a34-3d5a-a975-c9375ab7a2ca') + 'f16db4bf-9a34-3d5a-a975-c9375ab7a2ca') self.assertEqual(track2.mbidRecording, - u'5f19758e-7421-4c71-a599-9a9575d8e1b0') + '5f19758e-7421-4c71-a599-9a9575d8e1b0') def testMissingReleaseGroupType(self): """Check that whipper doesn't break if there's no type.""" @@ -214,7 +214,7 @@ class MetadataTestCase(unittest.TestCase): filename = 'whipper.release.d8e6153a-2c47-4804-9d73-0aac1081c3b1.json' path = os.path.join(os.path.dirname(__file__), filename) handle = open(path, "rb") - response = json.loads(handle.read()) + response = json.loads(handle.read().decode('utf-8')) handle.close() discid = "xu338_M8WukSRi0J.KTlDoflB8Y-" # disc 4 @@ -228,42 +228,42 @@ class MetadataTestCase(unittest.TestCase): filename = 'whipper.release.6109ceed-7e21-490b-b5ad-3a66b4e4cfbb.json' path = os.path.join(os.path.dirname(__file__), filename) handle = open(path, "rb") - response = json.loads(handle.read()) + response = json.loads(handle.read().decode('utf-8')) handle.close() discid = "cHW1Uutl_kyWNaLJsLmTGTe4rnE-" metadata = mbngs._getMetadata(response['release'], discid) - self.assertEqual(metadata.artist, u'David Rovics') - self.assertEqual(metadata.sortName, u'Rovics, David') + self.assertEqual(metadata.artist, 'David Rovics') + self.assertEqual(metadata.sortName, 'Rovics, David') self.assertFalse(metadata.various) self.assertIsInstance(metadata.tracks, list) - self.assertEqual(metadata.release, u'2015') - self.assertEqual(metadata.releaseTitle, u'The Other Side') - self.assertEqual(metadata.releaseType, u'Album') + self.assertEqual(metadata.release, '2015') + self.assertEqual(metadata.releaseTitle, 'The Other Side') + self.assertEqual(metadata.releaseType, 'Album') self.assertEqual(metadata.mbid, - u'6109ceed-7e21-490b-b5ad-3a66b4e4cfbb') + '6109ceed-7e21-490b-b5ad-3a66b4e4cfbb') self.assertEqual(metadata.mbidReleaseGroup, - u'99850b41-a06e-4fb8-992c-75c191a77803') + '99850b41-a06e-4fb8-992c-75c191a77803') self.assertEqual(metadata.mbidArtist, - [u'4d56eb9f-13b3-4f05-9db7-50195378d49f']) + ['4d56eb9f-13b3-4f05-9db7-50195378d49f']) self.assertEqual(metadata.url, - u'https://musicbrainz.org/release' + 'https://musicbrainz.org/release' '/6109ceed-7e21-490b-b5ad-3a66b4e4cfbb') - self.assertEqual(metadata.catalogNumber, u'[none]') - self.assertEqual(metadata.barcode, u'700261430249') + self.assertEqual(metadata.catalogNumber, '[none]') + self.assertEqual(metadata.barcode, '700261430249') self.assertEqual(len(metadata.tracks), 16) track1 = metadata.tracks[0] - self.assertEqual(track1.artist, u'David Rovics') - self.assertEqual(track1.title, u'Waiting for the Hurricane') + self.assertEqual(track1.artist, 'David Rovics') + self.assertEqual(track1.title, 'Waiting for the Hurricane') self.assertEqual(track1.duration, 176320) self.assertEqual(track1.mbid, - u'4116eea3-b9c2-452a-8d63-92f1e585b225') - self.assertEqual(track1.sortName, u'Rovics, David') + '4116eea3-b9c2-452a-8d63-92f1e585b225') + self.assertEqual(track1.sortName, 'Rovics, David') self.assertEqual(track1.mbidArtist, - [u'4d56eb9f-13b3-4f05-9db7-50195378d49f']) + ['4d56eb9f-13b3-4f05-9db7-50195378d49f']) self.assertEqual(track1.mbidRecording, - u'b191794d-b7c6-4d6f-971e-0a543959b5ad') + 'b191794d-b7c6-4d6f-971e-0a543959b5ad') self.assertEqual(track1.mbidWorks, - [u'90d5be68-0b29-45a3-ba01-c27ad78e3625']) + ['90d5be68-0b29-45a3-ba01-c27ad78e3625']) diff --git a/whipper/test/test_common_path.py b/whipper/test/test_common_path.py index 41b7cbe..0f59678 100644 --- a/whipper/test/test_common_path.py +++ b/whipper/test/test_common_path.py @@ -12,19 +12,19 @@ class FilterTestCase(common.TestCase): self._filter = path.PathFilter(special=True) def testSlash(self): - part = u'A Charm/A Blade' - self.assertEqual(self._filter.filter(part), u'A Charm-A Blade') + part = 'A Charm/A Blade' + self.assertEqual(self._filter.filter(part), 'A Charm-A Blade') def testFat(self): - part = u'A Word: F**k you?' - self.assertEqual(self._filter.filter(part), u'A Word - F__k you_') + part = 'A Word: F**k you?' + self.assertEqual(self._filter.filter(part), 'A Word - F__k you_') def testSpecial(self): - part = u'<<< $&*!\' "()`{}[]spaceship>>>' + part = '<<< $&*!\' "()`{}[]spaceship>>>' self.assertEqual(self._filter.filter(part), - u'___ _____ ________spaceship___') + '___ _____ ________spaceship___') def testGreatest(self): - part = u'Greatest Ever! Soul: The Definitive Collection' + part = 'Greatest Ever! Soul: The Definitive Collection' self.assertEqual(self._filter.filter(part), - u'Greatest Ever_ Soul - The Definitive Collection') + 'Greatest Ever_ Soul - The Definitive Collection') diff --git a/whipper/test/test_common_program.py b/whipper/test/test_common_program.py index 4fba987..36cc7a6 100644 --- a/whipper/test/test_common_program.py +++ b/whipper/test/test_common_program.py @@ -13,10 +13,10 @@ class PathTestCase(unittest.TestCase): def testStandardTemplateEmpty(self): prog = program.Program(config.Config()) - path = prog.getPath(u'/tmp', DEFAULT_DISC_TEMPLATE, + path = prog.getPath('/tmp', DEFAULT_DISC_TEMPLATE, 'mbdiscid', None) - self.assertEqual(path, (u'/tmp/unknown/Unknown Artist - mbdiscid/' - u'Unknown Artist - mbdiscid')) + self.assertEqual(path, ('/tmp/unknown/Unknown Artist - mbdiscid/' + 'Unknown Artist - mbdiscid')) def testStandardTemplateFilled(self): prog = program.Program(config.Config()) @@ -24,10 +24,10 @@ class PathTestCase(unittest.TestCase): md.artist = md.sortName = 'Jeff Buckley' md.title = 'Grace' - path = prog.getPath(u'/tmp', DEFAULT_DISC_TEMPLATE, + path = prog.getPath('/tmp', DEFAULT_DISC_TEMPLATE, 'mbdiscid', md, 0) - self.assertEqual(path, (u'/tmp/unknown/Jeff Buckley - Grace/' - u'Jeff Buckley - Grace')) + self.assertEqual(path, ('/tmp/unknown/Jeff Buckley - Grace/' + 'Jeff Buckley - Grace')) def testIssue66TemplateFilled(self): prog = program.Program(config.Config()) @@ -35,6 +35,6 @@ class PathTestCase(unittest.TestCase): md.artist = md.sortName = 'Jeff Buckley' md.title = 'Grace' - path = prog.getPath(u'/tmp', u'%A/%d', 'mbdiscid', md, 0) + path = prog.getPath('/tmp', '%A/%d', 'mbdiscid', md, 0) self.assertEqual(path, - u'/tmp/Jeff Buckley/Grace') + '/tmp/Jeff Buckley/Grace') diff --git a/whipper/test/test_common_renamer.py b/whipper/test/test_common_renamer.py index bcfb7dc..d776e3f 100644 --- a/whipper/test/test_common_renamer.py +++ b/whipper/test/test_common_renamer.py @@ -13,7 +13,7 @@ class RenameInFileTestcase(unittest.TestCase): def setUp(self): (fd, self._path) = tempfile.mkstemp(suffix='.whipper.renamer.infile') - os.write(fd, 'This is a test\nThis is another\n') + os.write(fd, 'This is a test\nThis is another\n'.encode()) os.close(fd) def testVerify(self): @@ -25,7 +25,8 @@ class RenameInFileTestcase(unittest.TestCase): def testDo(self): o = renamer.RenameInFile(self._path, 'is is a', 'at was some') o.do() - output = open(self._path).read() + with open(self._path) as f: + output = f.read() self.assertEqual(output, 'That was some test\nThat was somenother\n') os.unlink(self._path) @@ -34,7 +35,8 @@ class RenameInFileTestcase(unittest.TestCase): data = o.serialize() o2 = renamer.RenameInFile.deserialize(data) o2.do() - output = open(self._path).read() + with open(self._path) as f: + output = f.read() self.assertEqual(output, 'That was some test\nThat was somenother\n') os.unlink(self._path) @@ -43,7 +45,7 @@ class RenameFileTestcase(unittest.TestCase): def setUp(self): (fd, self._source) = tempfile.mkstemp(suffix='.whipper.renamer.file') - os.write(fd, 'This is a test\nThis is another\n') + os.write(fd, 'This is a test\nThis is another\n'.encode()) os.close(fd) (fd, self._destination) = tempfile.mkstemp( suffix='.whipper.renamer.file') @@ -66,7 +68,8 @@ class RenameFileTestcase(unittest.TestCase): def testDo(self): self._operation.do() - output = open(self._destination).read() + with open(self._destination) as f: + output = f.read() self.assertEqual(output, 'This is a test\nThis is another\n') os.unlink(self._destination) @@ -74,7 +77,8 @@ class RenameFileTestcase(unittest.TestCase): data = self._operation.serialize() o = renamer.RenameFile.deserialize(data) o.do() - output = open(self._destination).read() + with open(self._destination) as f: + output = f.read() self.assertEqual(output, 'This is a test\nThis is another\n') os.unlink(self._destination) @@ -87,7 +91,7 @@ class OperatorTestCase(unittest.TestCase): (fd, self._source) = tempfile.mkstemp( suffix='.whipper.renamer.operator') - os.write(fd, 'This is a test\nThis is another\n') + os.write(fd, 'This is a test\nThis is another\n'.encode()) os.close(fd) (fd, self._destination) = tempfile.mkstemp( suffix='.whipper.renamer.operator') diff --git a/whipper/test/test_image_cue.py b/whipper/test/test_image_cue.py index d73333f..ea4066a 100644 --- a/whipper/test/test_image_cue.py +++ b/whipper/test/test_image_cue.py @@ -16,7 +16,7 @@ class KingsSingleTestCase(unittest.TestCase): def setUp(self): self.cue = cue.CueFile(os.path.join(os.path.dirname(__file__), - u'kings-single.cue')) + 'kings-single.cue')) self.cue.parse() self.assertEqual(len(self.cue.table.tracks), 11) @@ -32,7 +32,7 @@ class KingsSeparateTestCase(unittest.TestCase): def setUp(self): self.cue = cue.CueFile(os.path.join(os.path.dirname(__file__), - u'kings-separate.cue')) + 'kings-separate.cue')) self.cue.parse() self.assertEqual(len(self.cue.table.tracks), 11) @@ -48,7 +48,7 @@ class KanyeMixedTestCase(unittest.TestCase): def setUp(self): self.cue = cue.CueFile(os.path.join(os.path.dirname(__file__), - u'kanye.cue')) + 'kanye.cue')) self.cue.parse() self.assertEqual(len(self.cue.table.tracks), 13) @@ -61,24 +61,24 @@ class WriteCueFileTestCase(unittest.TestCase): @staticmethod def testWrite(): - fd, path = tempfile.mkstemp(suffix=u'.whipper.test.cue') + fd, path = tempfile.mkstemp(suffix='.whipper.test.cue') os.close(fd) it = table.Table() t = table.Track(1) - t.index(1, absolute=0, path=u'track01.wav', relative=0, counter=1) + t.index(1, absolute=0, path='track01.wav', relative=0, counter=1) it.tracks.append(t) t = table.Track(2) - t.index(0, absolute=1000, path=u'track01.wav', + t.index(0, absolute=1000, path='track01.wav', relative=1000, counter=1) - t.index(1, absolute=2000, path=u'track02.wav', relative=0, counter=2) + t.index(1, absolute=2000, path='track02.wav', relative=0, counter=2) it.tracks.append(t) it.absolutize() it.leadout = 3000 - common.diffStrings(u"""REM DISCID 0C002802 + common.diffStrings("""REM DISCID 0C002802 REM COMMENT "whipper %s" FILE "track01.wav" WAVE TRACK 01 AUDIO diff --git a/whipper/test/test_image_toc.py b/whipper/test/test_image_toc.py index 4786662..79fcfc4 100644 --- a/whipper/test/test_image_toc.py +++ b/whipper/test/test_image_toc.py @@ -14,7 +14,7 @@ from whipper.test import common class CureTestCase(common.TestCase): def setUp(self): - self.path = os.path.join(os.path.dirname(__file__), u'cure.toc') + self.path = os.path.join(os.path.dirname(__file__), 'cure.toc') self.toc = toc.TocFile(self.path) self.toc.parse() self.assertEqual(len(self.toc.table.tracks), 13) @@ -93,8 +93,8 @@ class CureTestCase(common.TestCase): '3/c/4/dBAR-013-0019d4c3-00fe8924-b90c650d.bin') def testGetRealPath(self): - self.assertRaises(KeyError, self.toc.getRealPath, u'track01.wav') - (fd, path) = tempfile.mkstemp(suffix=u'.whipper.test.wav') + self.assertRaises(KeyError, self.toc.getRealPath, 'track01.wav') + (fd, path) = tempfile.mkstemp(suffix='.whipper.test.wav') self.assertEqual(self.toc.getRealPath(path), path) winpath = path.replace('/', '\\') @@ -108,7 +108,7 @@ class CureTestCase(common.TestCase): class BlocTestCase(common.TestCase): def setUp(self): - self.path = os.path.join(os.path.dirname(__file__), u'bloc.toc') + self.path = os.path.join(os.path.dirname(__file__), 'bloc.toc') self.toc = toc.TocFile(self.path) self.toc.parse() self.assertEqual(len(self.toc.table.tracks), 13) @@ -173,7 +173,7 @@ class BlocTestCase(common.TestCase): class BreedersTestCase(common.TestCase): def setUp(self): - self.path = os.path.join(os.path.dirname(__file__), u'breeders.toc') + self.path = os.path.join(os.path.dirname(__file__), 'breeders.toc') self.toc = toc.TocFile(self.path) self.toc.parse() self.assertEqual(len(self.toc.table.tracks), 13) @@ -200,7 +200,7 @@ class BreedersTestCase(common.TestCase): class LadyhawkeTestCase(common.TestCase): def setUp(self): - self.path = os.path.join(os.path.dirname(__file__), u'ladyhawke.toc') + self.path = os.path.join(os.path.dirname(__file__), 'ladyhawke.toc') self.toc = toc.TocFile(self.path) self.toc.parse() self.assertEqual(len(self.toc.table.tracks), 13) @@ -237,13 +237,13 @@ class CapitalMergeTestCase(common.TestCase): def setUp(self): self.toc1 = toc.TocFile(os.path.join(os.path.dirname(__file__), - u'capital.1.toc')) + 'capital.1.toc')) self.toc1.parse() self.assertEqual(len(self.toc1.table.tracks), 11) self.assertTrue(self.toc1.table.tracks[-1].audio) self.toc2 = toc.TocFile(os.path.join(os.path.dirname(__file__), - u'capital.2.toc')) + 'capital.2.toc')) self.toc2.parse() self.assertEqual(len(self.toc2.table.tracks), 1) self.assertFalse(self.toc2.table.tracks[-1].audio) @@ -278,8 +278,8 @@ class UnicodeTestCase(common.TestCase, common.UnicodeTestMixin): # we copy the normal non-utf8 filename to a utf-8 filename # in this test because builds with LANG=C fail if we include # utf-8 filenames in the dist - path = u'Jos\xe9Gonz\xe1lez.toc' - self._performer = u'Jos\xe9 Gonz\xe1lez' + path = 'Jos\xe9Gonz\xe1lez.toc' + self._performer = 'Jos\xe9 Gonz\xe1lez' source = os.path.join(os.path.dirname(__file__), 'jose.toc') (fd, self.dest) = tempfile.mkstemp(suffix=path) os.close(fd) @@ -311,7 +311,7 @@ class UnicodeTestCase(common.TestCase, common.UnicodeTestMixin): class TOTBLTestCase(common.TestCase): def setUp(self): - self.path = os.path.join(os.path.dirname(__file__), u'totbl.fast.toc') + self.path = os.path.join(os.path.dirname(__file__), 'totbl.fast.toc') self.toc = toc.TocFile(self.path) self.toc.parse() self.assertEqual(len(self.toc.table.tracks), 11) @@ -324,7 +324,7 @@ class GentlemenTestCase(common.TestCase): def setUp(self): self.path = os.path.join(os.path.dirname(__file__), - u'gentlemen.fast.toc') + 'gentlemen.fast.toc') self.toc = toc.TocFile(self.path) self.toc.parse() self.assertEquals(len(self.toc.table.tracks), 11) @@ -341,7 +341,7 @@ class StrokesTestCase(common.TestCase): def setUp(self): self.path = os.path.join(os.path.dirname(__file__), - u'strokes-someday.toc') + 'strokes-someday.toc') self.toc = toc.TocFile(self.path) self.toc.parse() self.assertEqual(len(self.toc.table.tracks), 1) @@ -358,13 +358,12 @@ class StrokesTestCase(common.TestCase): self.assertEqual(i1.relative, 0) self.assertEqual(i1.absolute, 1) self.assertEqual(i1.counter, 1) - self.assertEqual(i1.path, u'data.wav') + self.assertEqual(i1.path, 'data.wav') cue = self._filterCue(self.toc.table.cue()) - ref = self._filterCue( - open(os.path.join( - os.path.dirname(__file__), - 'strokes-someday.eac.cue')).read()).decode('utf-8') + with open(os.path.join(os.path.dirname(__file__), + 'strokes-someday.eac.cue')) as f: + ref = self._filterCue(f.read()) common.diffStrings(ref, cue) @staticmethod @@ -400,7 +399,7 @@ class StrokesTestCase(common.TestCase): class SurferRosaTestCase(common.TestCase): def setUp(self): - self.path = os.path.join(os.path.dirname(__file__), u'surferrosa.toc') + self.path = os.path.join(os.path.dirname(__file__), 'surferrosa.toc') self.toc = toc.TocFile(self.path) self.toc.parse() self.assertEqual(len(self.toc.table.tracks), 21) diff --git a/whipper/test/test_program_soxi.py b/whipper/test/test_program_soxi.py index 4ebf62f..0bd0153 100644 --- a/whipper/test/test_program_soxi.py +++ b/whipper/test/test_program_soxi.py @@ -8,7 +8,7 @@ from whipper.extern.task import task from whipper.program.soxi import AudioLengthTask from whipper.test import common as tcommon -base_track_file = os.path.join(os.path.dirname(__file__), u'track.flac') +base_track_file = os.path.join(os.path.dirname(__file__), 'track.flac') base_track_length = 10 * common.SAMPLES_PER_FRAME @@ -27,7 +27,8 @@ class AudioLengthPathTestCase(tcommon.TestCase): def _testSuffix(self, suffix): fd, path = tempfile.mkstemp(suffix=suffix) with os.fdopen(fd, "wb") as temptrack: - temptrack.write(open(base_track_file, "rb").read()) + with open(base_track_file, "rb") as f: + temptrack.write(f.read()) t = AudioLengthTask(path) runner = task.SyncRunner() @@ -39,26 +40,18 @@ class AudioLengthPathTestCase(tcommon.TestCase): class NormalAudioLengthPathTestCase(AudioLengthPathTestCase): def testSingleQuote(self): - self._testSuffix(u"whipper.test.Guns 'N Roses.flac") + self._testSuffix("whipper.test.Guns 'N Roses.flac") def testDoubleQuote(self): # This test makes sure we can checksum files with double quote in # their name - self._testSuffix(u'whipper.test.12" edit.flac') - - -class UnicodeAudioLengthPathTestCase(AudioLengthPathTestCase, - tcommon.UnicodeTestMixin): - - def testUnicodePath(self): - # this test makes sure we can checksum a unicode path - self._testSuffix(u'whipper.test.B\xeate Noire.empty.flac') + self._testSuffix('whipper.test.12" edit.flac') class AbsentFileAudioLengthPathTestCase(AudioLengthPathTestCase): def testAbsentFile(self): tempdir = tempfile.mkdtemp() - path = os.path.join(tempdir, u"nonexistent.flac") + path = os.path.join(tempdir, "nonexistent.flac") t = AudioLengthTask(path) runner = task.SyncRunner() diff --git a/whipper/test/test_result_logger.py b/whipper/test/test_result_logger.py index a665058..8b7f214 100644 --- a/whipper/test/test_result_logger.py +++ b/whipper/test/test_result_logger.py @@ -131,20 +131,23 @@ class LoggerTestCase(unittest.TestCase): logger = WhipperLogger() actual = logger.log(ripResult) actualLines = actual.splitlines() - expectedLines = open( - os.path.join(self.path, 'test_result_logger.log'), 'r' - ).read().splitlines() + with open(os.path.join(self.path, + 'test_result_logger.log'), 'r') as f: + expectedLines = f.read().splitlines() # do not test on version line, date line, or SHA-256 hash line self.assertListEqual(actualLines[2:-1], expectedLines[2:-1]) - self.assertRegexpMatches( + # RegEX updated to support all the 4 cases of the versioning scheme: + # https://github.com/pypa/setuptools_scm/#default-versioning-scheme + self.assertRegex( actualLines[0], re.compile(( r'Log created by: whipper ' - r'[\d]+\.[\d]+.[\d]+\.dev[\w\.\+]+ \(internal logger\)' + r'[\d]+\.[\d]+\.[\d]+(\+d\d{8}|\.dev[\w\.\+]+)? ' + r'\(internal logger\)' )) ) - self.assertRegexpMatches( + self.assertRegex( actualLines[1], re.compile(( r'Log creation date: ' @@ -163,7 +166,8 @@ class LoggerTestCase(unittest.TestCase): Dumper=ruamel.yaml.RoundTripDumper ) ) + log_body = "\n".join(actualLines[:-1]).encode() self.assertEqual( parsedLog['SHA-256 hash'], - hashlib.sha256("\n".join(actualLines[:-1])).hexdigest().upper() + hashlib.sha256(log_body).hexdigest().upper() )